cwist
Modern C web framework with libttak runtime
Loading...
Searching...
No Matches
migrate.c
Go to the documentation of this file.
2#include <stdio.h>
3#include <stdlib.h>
4#include <string.h>
5
16static int migrate_ensure_table(sqlite3 *db) {
17 const char *sql = "CREATE TABLE IF NOT EXISTS _cwist_migrations ("
18 " version INTEGER PRIMARY KEY,"
19 " name TEXT NOT NULL,"
20 " applied_at TEXT NOT NULL DEFAULT (datetime('now','utc'))"
21 ");";
22 char *errmsg = NULL;
23 int rc = sqlite3_exec(db, sql, NULL, NULL, &errmsg);
24 if (rc != SQLITE_OK) {
25 fprintf(stderr, "[migrate] ensure table: %s\n", errmsg ? errmsg : "?");
26 sqlite3_free(errmsg);
28 }
29 return CWIST_MIGRATE_OK;
30}
31
40static int migrate_exec_sql(sqlite3 *db, const char *sql, int version, const char *direction) {
41 char *errmsg = NULL;
42 int rc = sqlite3_exec(db, "BEGIN;", NULL, NULL, &errmsg);
43 if (rc != SQLITE_OK) {
44 fprintf(stderr, "[migrate] BEGIN failed (v%d %s): %s\n", version, direction,
45 errmsg ? errmsg : "?");
46 sqlite3_free(errmsg);
48 }
49
50 rc = sqlite3_exec(db, sql, NULL, NULL, &errmsg);
51 if (rc != SQLITE_OK) {
52 fprintf(stderr, "[migrate] SQL failed (v%d %s): %s\n", version, direction,
53 errmsg ? errmsg : "?");
54 sqlite3_free(errmsg);
55 sqlite3_exec(db, "ROLLBACK;", NULL, NULL, NULL);
57 }
58
59 rc = sqlite3_exec(db, "COMMIT;", NULL, NULL, &errmsg);
60 if (rc != SQLITE_OK) {
61 fprintf(stderr, "[migrate] COMMIT failed (v%d %s): %s\n", version, direction,
62 errmsg ? errmsg : "?");
63 sqlite3_free(errmsg);
64 sqlite3_exec(db, "ROLLBACK;", NULL, NULL, NULL);
66 }
67 return CWIST_MIGRATE_OK;
68}
69
76static int migration_cmp_asc(const void *a, const void *b) {
77 const cwist_migration_t *ma = (const cwist_migration_t *)a;
78 const cwist_migration_t *mb = (const cwist_migration_t *)b;
79 return ma->version - mb->version;
80}
81
88static int migration_cmp_desc(const void *a, const void *b) {
89 const cwist_migration_t *ma = (const cwist_migration_t *)a;
90 const cwist_migration_t *mb = (const cwist_migration_t *)b;
91 return mb->version - ma->version;
92}
93
99int cwist_migrate_version(sqlite3 *db) {
100 if (!db) return -1;
101 if (migrate_ensure_table(db) != CWIST_MIGRATE_OK) return -1;
102
103 sqlite3_stmt *stmt = NULL;
104 int rc = sqlite3_prepare_v2(db, "SELECT COALESCE(MAX(version), 0) FROM _cwist_migrations;", -1,
105 &stmt, NULL);
106 if (rc != SQLITE_OK) return -1;
107
108 int version = 0;
109 if (sqlite3_step(stmt) == SQLITE_ROW) {
110 version = sqlite3_column_int(stmt, 0);
111 }
112 sqlite3_finalize(stmt);
113 return version;
114}
115
123int cwist_migrate_up(sqlite3 *db, const cwist_migration_t *migrations, int count) {
124 if (!db || !migrations || count <= 0) return CWIST_MIGRATE_ERR_ARGS;
125
126 if (migrate_ensure_table(db) != CWIST_MIGRATE_OK) return CWIST_MIGRATE_ERR_SQL;
127
128 int current = cwist_migrate_version(db);
129 if (current < 0) return CWIST_MIGRATE_ERR_SQL;
130
131 /* Work on a sorted copy so callers need not pre-sort. */
132 cwist_migration_t *sorted =
133 (cwist_migration_t *)malloc(sizeof(cwist_migration_t) * (size_t)count);
134 if (!sorted) return CWIST_MIGRATE_ERR_GENERIC;
135 memcpy(sorted, migrations, sizeof(cwist_migration_t) * (size_t)count);
136 qsort(sorted, (size_t)count, sizeof(cwist_migration_t), migration_cmp_asc);
137
138 for (int i = 0; i < count; i++) {
139 if (sorted[i].version <= current) continue;
140 if (!sorted[i].up_sql) continue;
141
142 int rc = migrate_exec_sql(db, sorted[i].up_sql, sorted[i].version, "up");
143 if (rc != CWIST_MIGRATE_OK) {
144 free(sorted);
145 return rc;
146 }
147
148 /* Record in history. */
149 {
150 sqlite3_stmt *ins = NULL;
151 if (sqlite3_prepare_v2(db,
152 "INSERT INTO _cwist_migrations (version, name) VALUES (?, ?);",
153 -1, &ins, NULL) == SQLITE_OK) {
154 sqlite3_bind_int(ins, 1, sorted[i].version);
155 sqlite3_bind_text(ins, 2, sorted[i].name ? sorted[i].name : "", -1, SQLITE_STATIC);
156 sqlite3_step(ins);
157 sqlite3_finalize(ins);
158 }
159 }
160 }
161
162 free(sorted);
163 return CWIST_MIGRATE_OK;
164}
165
174int cwist_migrate_down(sqlite3 *db, const cwist_migration_t *migrations, int count, int steps) {
175 if (!db || !migrations || count <= 0) return CWIST_MIGRATE_ERR_ARGS;
176
177 if (migrate_ensure_table(db) != CWIST_MIGRATE_OK) return CWIST_MIGRATE_ERR_SQL;
178
179 int current = cwist_migrate_version(db);
180 if (current <= 0) return CWIST_MIGRATE_OK; /* nothing to roll back */
181
182 /* Sort descending so we undo in reverse order. */
183 cwist_migration_t *sorted =
184 (cwist_migration_t *)malloc(sizeof(cwist_migration_t) * (size_t)count);
185 if (!sorted) return CWIST_MIGRATE_ERR_GENERIC;
186 memcpy(sorted, migrations, sizeof(cwist_migration_t) * (size_t)count);
187 qsort(sorted, (size_t)count, sizeof(cwist_migration_t), migration_cmp_desc);
188
189 int rolled = 0;
190 int limit = (steps <= 0) ? count : steps;
191
192 for (int i = 0; i < count && rolled < limit; i++) {
193 if (sorted[i].version > current) continue;
194
195 /* Check it's actually in the history table. */
196 sqlite3_stmt *chk = NULL;
197 int found = 0;
198 if (sqlite3_prepare_v2(db, "SELECT 1 FROM _cwist_migrations WHERE version = ?;", -1, &chk,
199 NULL) == SQLITE_OK) {
200 sqlite3_bind_int(chk, 1, sorted[i].version);
201 if (sqlite3_step(chk) == SQLITE_ROW) found = 1;
202 sqlite3_finalize(chk);
203 }
204 if (!found) continue;
205
206 /* Irreversible migration: keep the history row so a later up does
207 * not re-apply up_sql against a database that already carries it. */
208 if (!sorted[i].down_sql) continue;
209
210 int rc = migrate_exec_sql(db, sorted[i].down_sql, sorted[i].version, "down");
211 if (rc != CWIST_MIGRATE_OK) {
212 free(sorted);
213 return rc;
214 }
215
216 /* Remove from history. */
217 {
218 sqlite3_stmt *del = NULL;
219 if (sqlite3_prepare_v2(db, "DELETE FROM _cwist_migrations WHERE version = ?;", -1, &del,
220 NULL) == SQLITE_OK) {
221 sqlite3_bind_int(del, 1, sorted[i].version);
222 sqlite3_step(del);
223 sqlite3_finalize(del);
224 }
225 }
226
227 rolled++;
228 }
229
230 free(sorted);
231 return CWIST_MIGRATE_OK;
232}
int cwist_migrate_version(sqlite3 *db)
Query the highest applied migration version recorded in the database.
Definition migrate.c:99
int cwist_migrate_up(sqlite3 *db, const cwist_migration_t *migrations, int count)
Apply all pending up migrations after sorting them by version.
Definition migrate.c:123
int cwist_migrate_down(sqlite3 *db, const cwist_migration_t *migrations, int count, int steps)
Roll back applied migrations in reverse version order.
Definition migrate.c:174
Schema migration system with version history and rollback.
#define CWIST_MIGRATE_ERR_GENERIC
Definition migrate.h:18
#define CWIST_MIGRATE_OK
Return codes.
Definition migrate.h:17
#define CWIST_MIGRATE_ERR_SQL
Definition migrate.h:19
#define CWIST_MIGRATE_ERR_ARGS
Definition migrate.h:20
size_t count
Definition otel.c:58
A single migration step.
Definition migrate.h:31