tempest-db-js 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,925 @@
1
+ import { columnsOf } from '../chunk-F36ZSQAN.js';
2
+
3
+ // src/migrations/ir.ts
4
+ function reflectTable(model) {
5
+ const columns = {};
6
+ const primaryKey = [];
7
+ for (const [name, col] of Object.entries(columnsOf(model))) {
8
+ const isPk = col.flags.primaryKey;
9
+ columns[name] = {
10
+ name,
11
+ type: col.type,
12
+ notNull: col.flags.notNull || isPk,
13
+ primaryKey: isPk,
14
+ default: col.defaultValue
15
+ };
16
+ if (isPk) primaryKey.push(name);
17
+ }
18
+ return { name: model.tablename, columns, primaryKey };
19
+ }
20
+ function reflectSchema(models) {
21
+ const tables = {};
22
+ for (const model of models) {
23
+ const table = reflectTable(model);
24
+ tables[table.name] = table;
25
+ }
26
+ return { tables };
27
+ }
28
+ function emptySchema() {
29
+ return { tables: {} };
30
+ }
31
+
32
+ // src/migrations/operations.ts
33
+ var IrreversibleMigration = class extends Error {
34
+ constructor(message) {
35
+ super(message);
36
+ this.name = "IrreversibleMigration";
37
+ }
38
+ };
39
+ function invert(op) {
40
+ switch (op.kind) {
41
+ case "create_table":
42
+ return { kind: "drop_table", table: op.table };
43
+ case "drop_table":
44
+ return { kind: "create_table", table: op.table };
45
+ case "rename_table":
46
+ return { kind: "rename_table", from: op.to, to: op.from };
47
+ case "add_column":
48
+ return { kind: "drop_column", table: op.table, column: op.column };
49
+ case "drop_column":
50
+ return { kind: "add_column", table: op.table, column: op.column };
51
+ case "alter_column":
52
+ return {
53
+ kind: "alter_column",
54
+ table: op.table,
55
+ name: op.name,
56
+ from: op.to,
57
+ to: op.from
58
+ };
59
+ case "rename_column":
60
+ return { kind: "rename_column", table: op.table, from: op.to, to: op.from };
61
+ case "recreate_table":
62
+ return { kind: "recreate_table", from: op.to, to: op.from };
63
+ case "execute":
64
+ if (op.down === null) {
65
+ throw new IrreversibleMigration("execute() operation has no down SQL");
66
+ }
67
+ return { kind: "execute", up: op.down, down: op.up };
68
+ }
69
+ }
70
+ function invertAll(ops) {
71
+ return [...ops].reverse().map(invert);
72
+ }
73
+
74
+ // src/migrations/ddl.ts
75
+ function quoteId(name) {
76
+ return `"${name.replace(/"/g, '""')}"`;
77
+ }
78
+ function quoteLiteral(value) {
79
+ return `'${value.replace(/'/g, "''")}'`;
80
+ }
81
+ function renderColumnType(type, dialect) {
82
+ const { kind, meta } = type;
83
+ if (dialect === "sqlite") {
84
+ switch (kind) {
85
+ case "smallint":
86
+ case "integer":
87
+ case "bigint":
88
+ case "boolean":
89
+ return "INTEGER";
90
+ case "real":
91
+ case "double":
92
+ return "REAL";
93
+ case "numeric":
94
+ return "NUMERIC";
95
+ case "blob":
96
+ return "BLOB";
97
+ default:
98
+ return "TEXT";
99
+ }
100
+ }
101
+ switch (kind) {
102
+ case "smallint":
103
+ return "SMALLINT";
104
+ case "integer":
105
+ return "INTEGER";
106
+ case "bigint":
107
+ return "BIGINT";
108
+ case "numeric":
109
+ return meta.precision !== void 0 ? `NUMERIC(${meta.precision}${meta.scale !== void 0 ? `, ${meta.scale}` : ""})` : "NUMERIC";
110
+ case "real":
111
+ return "REAL";
112
+ case "double":
113
+ return "DOUBLE PRECISION";
114
+ case "varchar":
115
+ return meta.length !== void 0 ? `VARCHAR(${meta.length})` : "VARCHAR";
116
+ case "char":
117
+ return meta.length !== void 0 ? `CHAR(${meta.length})` : "CHAR";
118
+ case "text":
119
+ return "TEXT";
120
+ case "boolean":
121
+ return "BOOLEAN";
122
+ case "date":
123
+ return "DATE";
124
+ case "time":
125
+ return meta.withTimezone ? "TIME WITH TIME ZONE" : "TIME";
126
+ case "datetime":
127
+ case "timestamp":
128
+ return meta.withTimezone ? "TIMESTAMP WITH TIME ZONE" : "TIMESTAMP";
129
+ case "blob":
130
+ return "BYTEA";
131
+ case "json":
132
+ return meta.jsonb ? "JSONB" : "JSON";
133
+ case "uuid":
134
+ return "UUID";
135
+ case "enum":
136
+ return "TEXT";
137
+ }
138
+ }
139
+ function renderDefault(def, dialect) {
140
+ if (def.kind === "expression") {
141
+ const expr = def.expression;
142
+ if (typeof expr === "object") return expr.raw;
143
+ switch (expr) {
144
+ case "now":
145
+ return dialect === "sqlite" ? "CURRENT_TIMESTAMP" : "now()";
146
+ case "current_date":
147
+ return "CURRENT_DATE";
148
+ case "current_time":
149
+ return "CURRENT_TIME";
150
+ case "uuidv4":
151
+ return dialect === "sqlite" ? "(lower(hex(randomblob(16))))" : "gen_random_uuid()";
152
+ }
153
+ }
154
+ const value = def.value;
155
+ if (value === null) return "NULL";
156
+ if (typeof value === "boolean") {
157
+ return dialect === "sqlite" ? value ? "1" : "0" : value ? "TRUE" : "FALSE";
158
+ }
159
+ if (typeof value === "number" || typeof value === "bigint") return String(value);
160
+ if (value instanceof Date) return quoteLiteral(value.toISOString());
161
+ if (typeof value === "object") return quoteLiteral(JSON.stringify(value));
162
+ return quoteLiteral(String(value));
163
+ }
164
+ function renderColumnDef(col, dialect) {
165
+ let sql = `${quoteId(col.name)} ${renderColumnType(col.type, dialect)}`;
166
+ if (col.notNull) sql += " NOT NULL";
167
+ if (col.default !== null) sql += ` DEFAULT ${renderDefault(col.default, dialect)}`;
168
+ return sql;
169
+ }
170
+ function enumTypeName(table, column) {
171
+ return `${table}_${column}`;
172
+ }
173
+ function renderCreateTable(table, dialect) {
174
+ const typeStmts = [];
175
+ const cols = Object.values(table.columns).map((c) => {
176
+ if (dialect === "postgresql" && c.type.kind === "enum") {
177
+ const typeName = enumTypeName(table.name, c.name);
178
+ const values = (c.type.meta.values ?? []).map(quoteLiteral).join(", ");
179
+ typeStmts.push(`CREATE TYPE ${quoteId(typeName)} AS ENUM (${values})`);
180
+ let def = `${quoteId(c.name)} ${quoteId(typeName)}`;
181
+ if (c.notNull) def += " NOT NULL";
182
+ if (c.default !== null) def += ` DEFAULT ${renderDefault(c.default, dialect)}`;
183
+ return def;
184
+ }
185
+ return renderColumnDef(c, dialect);
186
+ });
187
+ if (table.primaryKey.length > 0) {
188
+ cols.push(`PRIMARY KEY (${table.primaryKey.map(quoteId).join(", ")})`);
189
+ }
190
+ return [
191
+ ...typeStmts,
192
+ `CREATE TABLE ${quoteId(table.name)} (
193
+ ${cols.join(",\n ")}
194
+ )`
195
+ ];
196
+ }
197
+ function renderOperation(op, dialect) {
198
+ switch (op.kind) {
199
+ case "create_table":
200
+ return renderCreateTable(op.table, dialect);
201
+ case "drop_table":
202
+ return [`DROP TABLE ${quoteId(op.table.name)}`];
203
+ case "rename_table":
204
+ return [`ALTER TABLE ${quoteId(op.from)} RENAME TO ${quoteId(op.to)}`];
205
+ case "add_column":
206
+ return [
207
+ `ALTER TABLE ${quoteId(op.table)} ADD COLUMN ${renderColumnDef(op.column, dialect)}`
208
+ ];
209
+ case "drop_column":
210
+ return [`ALTER TABLE ${quoteId(op.table)} DROP COLUMN ${quoteId(op.column.name)}`];
211
+ case "rename_column":
212
+ return [
213
+ `ALTER TABLE ${quoteId(op.table)} RENAME COLUMN ${quoteId(op.from)} TO ${quoteId(op.to)}`
214
+ ];
215
+ case "alter_column":
216
+ return renderAlterColumn(op.table, op.to, dialect);
217
+ case "recreate_table":
218
+ return dialect === "sqlite" ? renderSqliteRebuild(op.from, op.to) : renderPostgresTableDiff(op.from, op.to);
219
+ case "execute":
220
+ return [op.up];
221
+ }
222
+ }
223
+ function renderSqliteRebuild(from, to) {
224
+ const tmp = `__new_${to.name}`;
225
+ const common = Object.keys(to.columns).filter((c) => c in from.columns);
226
+ const cols = Object.values(to.columns).map((c) => renderColumnDef(c, "sqlite"));
227
+ if (to.primaryKey.length > 0) {
228
+ cols.push(`PRIMARY KEY (${to.primaryKey.map(quoteId).join(", ")})`);
229
+ }
230
+ const commonSql = common.map(quoteId).join(", ");
231
+ return [
232
+ "PRAGMA foreign_keys=off",
233
+ `CREATE TABLE ${quoteId(tmp)} (
234
+ ${cols.join(",\n ")}
235
+ )`,
236
+ common.length > 0 ? `INSERT INTO ${quoteId(tmp)} (${commonSql}) SELECT ${commonSql} FROM ${quoteId(from.name)}` : `-- no common columns to copy from ${from.name}`,
237
+ `DROP TABLE ${quoteId(from.name)}`,
238
+ `ALTER TABLE ${quoteId(tmp)} RENAME TO ${quoteId(to.name)}`,
239
+ "PRAGMA foreign_keys=on"
240
+ ];
241
+ }
242
+ function renderPostgresTableDiff(from, to) {
243
+ const stmts = [];
244
+ for (const [name, col] of Object.entries(to.columns)) {
245
+ if (!(name in from.columns)) {
246
+ stmts.push(
247
+ `ALTER TABLE ${quoteId(to.name)} ADD COLUMN ${renderColumnDef(col, "postgresql")}`
248
+ );
249
+ } else {
250
+ stmts.push(...renderAlterColumn(to.name, col, "postgresql"));
251
+ }
252
+ }
253
+ for (const name of Object.keys(from.columns)) {
254
+ if (!(name in to.columns)) {
255
+ stmts.push(`ALTER TABLE ${quoteId(to.name)} DROP COLUMN ${quoteId(name)}`);
256
+ }
257
+ }
258
+ return stmts;
259
+ }
260
+ function renderAlterColumn(table, to, dialect) {
261
+ if (dialect === "sqlite") {
262
+ throw new Error(
263
+ `alter_column on SQLite needs batch/table-rebuild (Phase 6e); column ${table}.${to.name}`
264
+ );
265
+ }
266
+ const t = quoteId(table);
267
+ const c = quoteId(to.name);
268
+ const stmts = [
269
+ `ALTER TABLE ${t} ALTER COLUMN ${c} TYPE ${renderColumnType(to.type, dialect)}`
270
+ ];
271
+ stmts.push(
272
+ to.notNull ? `ALTER TABLE ${t} ALTER COLUMN ${c} SET NOT NULL` : `ALTER TABLE ${t} ALTER COLUMN ${c} DROP NOT NULL`
273
+ );
274
+ stmts.push(
275
+ to.default !== null ? `ALTER TABLE ${t} ALTER COLUMN ${c} SET DEFAULT ${renderDefault(to.default, dialect)}` : `ALTER TABLE ${t} ALTER COLUMN ${c} DROP DEFAULT`
276
+ );
277
+ return stmts;
278
+ }
279
+
280
+ // src/migrations/diff.ts
281
+ function columnSignature(col) {
282
+ return JSON.stringify({
283
+ type: col.type,
284
+ notNull: col.notNull,
285
+ primaryKey: col.primaryKey,
286
+ default: col.default
287
+ });
288
+ }
289
+ function diffSchema(current, target) {
290
+ const ops = [];
291
+ const drops = [];
292
+ for (const [name, targetTable] of Object.entries(target.tables)) {
293
+ const currentTable = current.tables[name];
294
+ if (!currentTable) {
295
+ ops.push({ kind: "create_table", table: targetTable });
296
+ continue;
297
+ }
298
+ for (const [colName, targetCol] of Object.entries(targetTable.columns)) {
299
+ const currentCol = currentTable.columns[colName];
300
+ if (!currentCol) {
301
+ ops.push({ kind: "add_column", table: name, column: targetCol });
302
+ } else if (columnSignature(currentCol) !== columnSignature(targetCol)) {
303
+ ops.push({
304
+ kind: "alter_column",
305
+ table: name,
306
+ name: colName,
307
+ from: currentCol,
308
+ to: targetCol
309
+ });
310
+ }
311
+ }
312
+ for (const [colName, currentCol] of Object.entries(currentTable.columns)) {
313
+ if (!targetTable.columns[colName]) {
314
+ ops.push({ kind: "drop_column", table: name, column: currentCol });
315
+ }
316
+ }
317
+ }
318
+ for (const [name, currentTable] of Object.entries(current.tables)) {
319
+ if (!target.tables[name]) {
320
+ drops.push({ kind: "drop_table", table: currentTable });
321
+ }
322
+ }
323
+ return [...ops, ...drops];
324
+ }
325
+
326
+ // src/migrations/graph.ts
327
+ var CyclicMigrationGraph = class extends Error {
328
+ constructor(remaining) {
329
+ super(`migration graph has a cycle among: ${remaining.join(", ")}`);
330
+ this.name = "CyclicMigrationGraph";
331
+ }
332
+ };
333
+ var UnknownRevision = class extends Error {
334
+ constructor(revision, parent) {
335
+ super(`revision ${revision} references unknown parent ${parent}`);
336
+ this.name = "UnknownRevision";
337
+ }
338
+ };
339
+ function topoOrder(migrations) {
340
+ const byId = /* @__PURE__ */ new Map();
341
+ for (const m of migrations) byId.set(m.revision, m);
342
+ const indegree = /* @__PURE__ */ new Map();
343
+ const children = /* @__PURE__ */ new Map();
344
+ for (const m of migrations) {
345
+ indegree.set(m.revision, m.downRevision.length);
346
+ for (const parent of m.downRevision) {
347
+ if (!byId.has(parent)) throw new UnknownRevision(m.revision, parent);
348
+ const list = children.get(parent) ?? [];
349
+ list.push(m.revision);
350
+ children.set(parent, list);
351
+ }
352
+ }
353
+ const ready = migrations.filter((m) => (indegree.get(m.revision) ?? 0) === 0).map((m) => m.revision).sort();
354
+ const ordered = [];
355
+ while (ready.length > 0) {
356
+ const id = ready.shift();
357
+ ordered.push(byId.get(id));
358
+ const next = (children.get(id) ?? []).sort();
359
+ for (const child of next) {
360
+ const deg = (indegree.get(child) ?? 0) - 1;
361
+ indegree.set(child, deg);
362
+ if (deg === 0) {
363
+ ready.push(child);
364
+ ready.sort();
365
+ }
366
+ }
367
+ }
368
+ if (ordered.length !== migrations.length) {
369
+ const remaining = migrations.map((m) => m.revision).filter((r) => !ordered.some((o) => o.revision === r));
370
+ throw new CyclicMigrationGraph(remaining);
371
+ }
372
+ return ordered;
373
+ }
374
+ function heads(migrations) {
375
+ const parents = /* @__PURE__ */ new Set();
376
+ for (const m of migrations) for (const p of m.downRevision) parents.add(p);
377
+ return migrations.map((m) => m.revision).filter((r) => !parents.has(r)).sort();
378
+ }
379
+
380
+ // src/migrations/codegen.ts
381
+ function renderOps(ops) {
382
+ if (ops.length === 0) return "[]";
383
+ const body = ops.map((op) => ` ${JSON.stringify(op)},`).join("\n");
384
+ return `[
385
+ ${body}
386
+ ]`;
387
+ }
388
+ function generateMigration(draft) {
389
+ let down;
390
+ try {
391
+ down = renderOps(invertAll(draft.operations));
392
+ } catch (error) {
393
+ down = `(() => { throw new Error(${JSON.stringify(
394
+ `irreversible migration: ${error.message}`
395
+ )}); })()`;
396
+ }
397
+ return `import type { Migration, Op } from "tempest-db-js/migrations";
398
+
399
+ export const revision = ${JSON.stringify(draft.revision)};
400
+ export const downRevision: string[] = ${JSON.stringify(draft.downRevision)};
401
+ export const label = ${JSON.stringify(draft.label)};
402
+
403
+ const upOps = ${renderOps(draft.operations)};
404
+
405
+ const downOps = ${down};
406
+
407
+ export const up = (op: Op): void => {
408
+ for (const operation of upOps) op.run(operation);
409
+ };
410
+
411
+ export const down = (op: Op): void => {
412
+ for (const operation of downOps) op.run(operation);
413
+ };
414
+
415
+ export default { revision, downRevision, label, up, down } satisfies Migration;
416
+ `;
417
+ }
418
+ function makeRevisionId(label, parents) {
419
+ let hash = 2166136261;
420
+ for (const ch of `${label}|${[...parents].sort().join(",")}`) {
421
+ hash ^= ch.charCodeAt(0);
422
+ hash = Math.imul(hash, 16777619) >>> 0;
423
+ }
424
+ return hash.toString(16).padStart(8, "0");
425
+ }
426
+
427
+ // src/migrations/runner.ts
428
+ var Op = class {
429
+ operations = [];
430
+ /** Record a raw operation (used by autogenerated migrations). */
431
+ run(operation) {
432
+ this.operations.push(operation);
433
+ }
434
+ createTable(table) {
435
+ this.run({ kind: "create_table", table });
436
+ }
437
+ dropTable(table) {
438
+ this.run({ kind: "drop_table", table });
439
+ }
440
+ renameTable(from, to) {
441
+ this.run({ kind: "rename_table", from, to });
442
+ }
443
+ addColumn(table, column) {
444
+ this.run({ kind: "add_column", table, column });
445
+ }
446
+ dropColumn(table, column) {
447
+ this.run({ kind: "drop_column", table, column });
448
+ }
449
+ alterColumn(table, name, from, to) {
450
+ this.run({ kind: "alter_column", table, name, from, to });
451
+ }
452
+ renameColumn(table, from, to) {
453
+ this.run({ kind: "rename_column", table, from, to });
454
+ }
455
+ /** Rebuild a table (SQLite batch-mode / PostgreSQL per-column alters). */
456
+ recreateTable(from, to) {
457
+ this.run({ kind: "recreate_table", from, to });
458
+ }
459
+ /** Raw SQL escape hatch (e.g. a data migration). `down` may be `null`. */
460
+ execute(up, down = null) {
461
+ this.run({ kind: "execute", up, down });
462
+ }
463
+ };
464
+ var VERSION_TABLE = "tempest_db_js_migrations";
465
+ var MigrationRunner = class {
466
+ constructor(driver, dialect) {
467
+ this.driver = driver;
468
+ this.dialect = dialect;
469
+ }
470
+ driver;
471
+ dialect;
472
+ /** Create the version-tracking table if it does not exist. */
473
+ ensureVersionTable() {
474
+ this.driver.execute(
475
+ `CREATE TABLE IF NOT EXISTS "${VERSION_TABLE}" (revision TEXT PRIMARY KEY, applied_at TEXT NOT NULL, down_revision TEXT NOT NULL)`,
476
+ []
477
+ );
478
+ }
479
+ /** The set of applied revision ids. */
480
+ applied() {
481
+ this.ensureVersionTable();
482
+ const { rows } = this.driver.execute(`SELECT revision FROM "${VERSION_TABLE}"`, []);
483
+ return new Set(rows.map((r) => String(r.revision)));
484
+ }
485
+ runOps(ops) {
486
+ for (const op of ops) {
487
+ for (const stmt of renderOperation(op, this.dialect)) {
488
+ const trimmed = stmt.trim();
489
+ if (trimmed.length === 0 || trimmed.startsWith("--")) continue;
490
+ this.driver.execute(stmt, []);
491
+ }
492
+ }
493
+ }
494
+ record(migration, appliedAt) {
495
+ this.driver.execute(
496
+ `INSERT INTO "${VERSION_TABLE}" (revision, applied_at, down_revision) VALUES (?, ?, ?)`,
497
+ [migration.revision, appliedAt, migration.downRevision.join(",")]
498
+ );
499
+ }
500
+ forget(revision) {
501
+ this.driver.execute(`DELETE FROM "${VERSION_TABLE}" WHERE revision = ?`, [revision]);
502
+ }
503
+ /**
504
+ * Apply all pending migrations up to the head(s), in DAG order.
505
+ *
506
+ * @param migrations All known migrations.
507
+ * @param appliedAt Timestamp string to stamp (pass one in — the runtime has no
508
+ * wall clock of its own here).
509
+ * @returns The revision ids that were applied this run.
510
+ */
511
+ upgrade(migrations, appliedAt) {
512
+ const done = this.applied();
513
+ const ordered = topoOrder(migrations);
514
+ const ran = [];
515
+ for (const migration of ordered) {
516
+ if (done.has(migration.revision)) continue;
517
+ const op = new Op();
518
+ migration.up(op);
519
+ this.runOps(op.operations);
520
+ this.record(migration, appliedAt);
521
+ ran.push(migration.revision);
522
+ }
523
+ return ran;
524
+ }
525
+ /**
526
+ * Revert the last `steps` applied migrations (default 1), newest first.
527
+ *
528
+ * @param migrations All known migrations.
529
+ * @param steps How many applied revisions to roll back.
530
+ * @returns The revision ids that were reverted.
531
+ */
532
+ downgrade(migrations, steps = 1) {
533
+ const done = this.applied();
534
+ const ordered = topoOrder(migrations).filter((m) => done.has(m.revision));
535
+ const toRevert = ordered.slice(-steps).reverse();
536
+ const reverted = [];
537
+ for (const migration of toRevert) {
538
+ const op = new Op();
539
+ if (migration.down.length >= 0) {
540
+ try {
541
+ migration.down(op);
542
+ } catch {
543
+ op.operations.length = 0;
544
+ }
545
+ }
546
+ if (op.operations.length === 0) {
547
+ const upOp = new Op();
548
+ migration.up(upOp);
549
+ op.operations.push(...invertAll(upOp.operations));
550
+ }
551
+ this.runOps(op.operations);
552
+ this.forget(migration.revision);
553
+ reverted.push(migration.revision);
554
+ }
555
+ return reverted;
556
+ }
557
+ };
558
+
559
+ // src/migrations/introspect.ts
560
+ function sqliteAffinity(declared) {
561
+ const t = declared.toUpperCase();
562
+ if (t.includes("INT")) return "INTEGER";
563
+ if (t.includes("CHAR") || t.includes("CLOB") || t.includes("TEXT")) return "TEXT";
564
+ if (t.includes("BLOB") || t === "") return "BLOB";
565
+ if (t.includes("REAL") || t.includes("FLOA") || t.includes("DOUB")) return "REAL";
566
+ return "NUMERIC";
567
+ }
568
+ function affinityToKind(affinity) {
569
+ switch (affinity) {
570
+ case "INTEGER":
571
+ return "integer";
572
+ case "REAL":
573
+ return "real";
574
+ case "BLOB":
575
+ return "blob";
576
+ case "NUMERIC":
577
+ return "numeric";
578
+ default:
579
+ return "text";
580
+ }
581
+ }
582
+ function introspectSqlite(driver) {
583
+ const tablesRows = driver.execute(
584
+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name != 'tempest_db_js_migrations'",
585
+ []
586
+ ).rows;
587
+ const tables = {};
588
+ for (const row of tablesRows) {
589
+ const tableName = String(row.name);
590
+ const info = driver.execute(`PRAGMA table_info(${JSON.stringify(tableName)})`, []).rows;
591
+ const columns = {};
592
+ const primaryKey = [];
593
+ for (const col of info) {
594
+ const isPk = Number(col.pk) > 0;
595
+ const affinity = sqliteAffinity(col.type);
596
+ columns[col.name] = {
597
+ name: col.name,
598
+ type: { kind: affinityToKind(affinity), meta: {} },
599
+ notNull: Number(col.notnull) === 1 || isPk,
600
+ primaryKey: isPk,
601
+ default: null
602
+ };
603
+ if (isPk) primaryKey.push(col.name);
604
+ }
605
+ tables[tableName] = { name: tableName, columns, primaryKey };
606
+ }
607
+ return { tables };
608
+ }
609
+ function checkDrift(driver, models) {
610
+ const actual = introspectSqlite(driver);
611
+ const expected = reflectSchema(models);
612
+ const issues = [];
613
+ for (const [tableName, expectedTable] of Object.entries(expected.tables)) {
614
+ const actualTable = actual.tables[tableName];
615
+ if (!actualTable) {
616
+ issues.push(`table "${tableName}" is missing from the database`);
617
+ continue;
618
+ }
619
+ for (const [colName, expectedCol] of Object.entries(expectedTable.columns)) {
620
+ const actualCol = actualTable.columns[colName];
621
+ if (!actualCol) {
622
+ issues.push(`column "${tableName}.${colName}" is missing from the database`);
623
+ continue;
624
+ }
625
+ const expAff = sqliteAffinity(renderColumnType(expectedCol.type, "sqlite"));
626
+ const actAff = sqliteAffinity(renderColumnType(actualCol.type, "sqlite"));
627
+ if (expAff !== actAff) {
628
+ issues.push(
629
+ `column "${tableName}.${colName}" affinity differs: model ${expAff}, db ${actAff}`
630
+ );
631
+ }
632
+ if (expectedCol.notNull !== actualCol.notNull) {
633
+ issues.push(`column "${tableName}.${colName}" nullability differs`);
634
+ }
635
+ if (expectedCol.primaryKey !== actualCol.primaryKey) {
636
+ issues.push(`column "${tableName}.${colName}" primary-key flag differs`);
637
+ }
638
+ }
639
+ for (const colName of Object.keys(actualTable.columns)) {
640
+ if (!expectedTable.columns[colName]) {
641
+ issues.push(
642
+ `column "${tableName}.${colName}" exists in the database but not in the model`
643
+ );
644
+ }
645
+ }
646
+ }
647
+ for (const tableName of Object.keys(actual.tables)) {
648
+ if (!expected.tables[tableName]) {
649
+ issues.push(`table "${tableName}" exists in the database but not in the models`);
650
+ }
651
+ }
652
+ return issues;
653
+ }
654
+ function pgTypeToKind(dataType, udtName) {
655
+ const t = dataType.toLowerCase();
656
+ if (t === "user-defined") return "enum";
657
+ if (t === "smallint") return "smallint";
658
+ if (t === "integer") return "integer";
659
+ if (t === "bigint") return "bigint";
660
+ if (t === "numeric") return "numeric";
661
+ if (t === "real") return "real";
662
+ if (t === "double precision") return "double";
663
+ if (t === "character varying") return "varchar";
664
+ if (t === "character") return "char";
665
+ if (t === "text") return "text";
666
+ if (t === "boolean") return "boolean";
667
+ if (t === "date") return "date";
668
+ if (t.startsWith("time")) return t.startsWith("timestamp") ? "timestamp" : "time";
669
+ if (t === "bytea") return "blob";
670
+ if (t === "json") return "json";
671
+ if (t === "jsonb") return "json";
672
+ if (t === "uuid") return "uuid";
673
+ return udtName === "jsonb" ? "json" : "text";
674
+ }
675
+ async function introspectPostgres(driver) {
676
+ const tablesResult = await driver.execute(
677
+ "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE' AND table_name != 'tempest_db_js_migrations'",
678
+ []
679
+ );
680
+ const tables = {};
681
+ for (const row of tablesResult.rows) {
682
+ const tableName = String(row.table_name);
683
+ const colsResult = await driver.execute(
684
+ "SELECT column_name, data_type, udt_name, is_nullable FROM information_schema.columns WHERE table_name = $1",
685
+ [tableName]
686
+ );
687
+ const pkResult = await driver.execute(
688
+ `SELECT a.attname AS name FROM pg_index i
689
+ JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
690
+ WHERE i.indrelid = $1::regclass AND i.indisprimary`,
691
+ [tableName]
692
+ );
693
+ const pkSet = new Set(pkResult.rows.map((r) => String(r.name)));
694
+ const columns = {};
695
+ const primaryKey = [];
696
+ for (const col of colsResult.rows) {
697
+ const name = String(col.column_name);
698
+ const isPk = pkSet.has(name);
699
+ columns[name] = {
700
+ name,
701
+ type: {
702
+ kind: pgTypeToKind(String(col.data_type), String(col.udt_name)),
703
+ meta: {}
704
+ },
705
+ notNull: col.is_nullable === "NO" || isPk,
706
+ primaryKey: isPk,
707
+ default: null
708
+ };
709
+ if (isPk) primaryKey.push(name);
710
+ }
711
+ tables[tableName] = { name: tableName, columns, primaryKey };
712
+ }
713
+ return { tables };
714
+ }
715
+ async function checkDriftPostgres(driver, models) {
716
+ const actual = await introspectPostgres(driver);
717
+ const expected = reflectSchema(models);
718
+ const issues = [];
719
+ for (const [tableName, expectedTable] of Object.entries(expected.tables)) {
720
+ const actualTable = actual.tables[tableName];
721
+ if (!actualTable) {
722
+ issues.push(`table "${tableName}" is missing from the database`);
723
+ continue;
724
+ }
725
+ for (const [colName, expectedCol] of Object.entries(expectedTable.columns)) {
726
+ const actualCol = actualTable.columns[colName];
727
+ if (!actualCol) {
728
+ issues.push(`column "${tableName}.${colName}" is missing from the database`);
729
+ continue;
730
+ }
731
+ if (expectedCol.type.kind !== actualCol.type.kind) {
732
+ issues.push(
733
+ `column "${tableName}.${colName}" type differs: model ${expectedCol.type.kind}, db ${actualCol.type.kind}`
734
+ );
735
+ }
736
+ if (expectedCol.notNull !== actualCol.notNull) {
737
+ issues.push(`column "${tableName}.${colName}" nullability differs`);
738
+ }
739
+ }
740
+ for (const colName of Object.keys(actualTable.columns)) {
741
+ if (!expectedTable.columns[colName]) {
742
+ issues.push(
743
+ `column "${tableName}.${colName}" exists in the database but not in the model`
744
+ );
745
+ }
746
+ }
747
+ }
748
+ for (const tableName of Object.keys(actual.tables)) {
749
+ if (!expected.tables[tableName]) {
750
+ issues.push(`table "${tableName}" exists in the database but not in the models`);
751
+ }
752
+ }
753
+ return issues;
754
+ }
755
+
756
+ // src/migrations/replay.ts
757
+ function applyOperation(schema, op) {
758
+ const tables = { ...schema.tables };
759
+ switch (op.kind) {
760
+ case "create_table":
761
+ tables[op.table.name] = op.table;
762
+ break;
763
+ case "drop_table":
764
+ delete tables[op.table.name];
765
+ break;
766
+ case "rename_table": {
767
+ const t = tables[op.from];
768
+ if (t) {
769
+ delete tables[op.from];
770
+ tables[op.to] = { ...t, name: op.to };
771
+ }
772
+ break;
773
+ }
774
+ case "recreate_table":
775
+ delete tables[op.from.name];
776
+ tables[op.to.name] = op.to;
777
+ break;
778
+ case "add_column": {
779
+ const t = tables[op.table];
780
+ if (t) {
781
+ tables[op.table] = {
782
+ ...t,
783
+ columns: { ...t.columns, [op.column.name]: op.column }
784
+ };
785
+ }
786
+ break;
787
+ }
788
+ case "drop_column": {
789
+ const t = tables[op.table];
790
+ if (t) {
791
+ const columns = { ...t.columns };
792
+ delete columns[op.column.name];
793
+ tables[op.table] = { ...t, columns };
794
+ }
795
+ break;
796
+ }
797
+ case "alter_column": {
798
+ const t = tables[op.table];
799
+ if (t) {
800
+ tables[op.table] = {
801
+ ...t,
802
+ columns: { ...t.columns, [op.name]: op.to }
803
+ };
804
+ }
805
+ break;
806
+ }
807
+ case "rename_column": {
808
+ const t = tables[op.table];
809
+ const col = t?.columns[op.from];
810
+ if (t && col) {
811
+ const columns = { ...t.columns };
812
+ delete columns[op.from];
813
+ columns[op.to] = { ...col, name: op.to };
814
+ tables[op.table] = { ...t, columns };
815
+ }
816
+ break;
817
+ }
818
+ }
819
+ return { tables };
820
+ }
821
+ function replaySchema(migrations) {
822
+ let schema = emptySchema();
823
+ for (const migration of topoOrder(migrations)) {
824
+ const op = new Op();
825
+ migration.up(op);
826
+ for (const operation of op.operations) {
827
+ schema = applyOperation(schema, operation);
828
+ }
829
+ }
830
+ return schema;
831
+ }
832
+
833
+ // src/migrations/cli.ts
834
+ function ok(lines) {
835
+ return { code: 0, lines };
836
+ }
837
+ function fail(lines) {
838
+ return { code: 1, lines };
839
+ }
840
+ function pending(config, runner) {
841
+ const done = runner.applied();
842
+ return topoOrder(config.migrations).filter((m) => !done.has(m.revision));
843
+ }
844
+ function runMigrationCli(argv, config) {
845
+ const [command, ...rest] = argv;
846
+ const runner = new MigrationRunner(config.driver, config.dialect);
847
+ const appliedAt = config.appliedAt ?? "1970-01-01T00:00:00.000Z";
848
+ switch (command) {
849
+ case "current": {
850
+ const applied = [...runner.applied()].sort();
851
+ return ok(applied.length > 0 ? applied : ["(no migrations applied)"]);
852
+ }
853
+ case "heads":
854
+ return ok(heads(config.migrations));
855
+ case "history": {
856
+ const done = runner.applied();
857
+ return ok(
858
+ topoOrder(config.migrations).map(
859
+ (m) => `${done.has(m.revision) ? "\u2713" : "\xB7"} ${m.revision}${m.label ? ` \u2014 ${m.label}` : ""}`
860
+ )
861
+ );
862
+ }
863
+ case "upgrade": {
864
+ if (rest.includes("--sql")) {
865
+ const lines = [];
866
+ for (const migration of pending(config, runner)) {
867
+ const op = new Op();
868
+ migration.up(op);
869
+ lines.push(`-- ${migration.revision}`);
870
+ for (const operation of op.operations) {
871
+ for (const stmt of renderOperation(operation, config.dialect))
872
+ lines.push(`${stmt};`);
873
+ }
874
+ }
875
+ return ok(lines.length > 0 ? lines : ["-- nothing to upgrade"]);
876
+ }
877
+ const ran = runner.upgrade(config.migrations, appliedAt);
878
+ return ok(ran.length > 0 ? ran.map((r) => `applied ${r}`) : ["nothing to upgrade"]);
879
+ }
880
+ case "downgrade": {
881
+ const steps = rest[0] ? Number(rest[0]) : 1;
882
+ const reverted = runner.downgrade(config.migrations, steps);
883
+ return ok(
884
+ reverted.length > 0 ? reverted.map((r) => `reverted ${r}`) : ["nothing to downgrade"]
885
+ );
886
+ }
887
+ case "check": {
888
+ if (!config.models) return fail(["check requires models in the config"]);
889
+ const drift = config.dialect === "sqlite" ? checkDrift(config.driver, config.models) : [];
890
+ const undiffed = diffSchema(
891
+ replaySchema(config.migrations),
892
+ reflectSchema(config.models)
893
+ );
894
+ const issues = [
895
+ ...drift.map((d) => `drift: ${d}`),
896
+ ...undiffed.map((o) => `uncaptured: ${o.kind}`)
897
+ ];
898
+ return issues.length > 0 ? fail(issues) : ok(["no drift; models match migrations"]);
899
+ }
900
+ case "revision": {
901
+ if (!config.models)
902
+ return fail(["revision --autogenerate requires models in the config"]);
903
+ const msgIndex = rest.indexOf("-m");
904
+ const label = msgIndex >= 0 ? rest[msgIndex + 1] ?? "revision" : "revision";
905
+ const parents = heads(config.migrations);
906
+ const ops = rest.includes("--autogenerate") ? diffSchema(replaySchema(config.migrations), reflectSchema(config.models)) : [];
907
+ const source = generateMigration({
908
+ revision: makeRevisionId(label, parents),
909
+ downRevision: parents,
910
+ label,
911
+ operations: ops
912
+ });
913
+ return ok(source.split("\n"));
914
+ }
915
+ default:
916
+ return fail([
917
+ `unknown command ${JSON.stringify(command)}`,
918
+ "commands: current | history | heads | upgrade [--sql] | downgrade [N] | check | revision -m <msg> [--autogenerate]"
919
+ ]);
920
+ }
921
+ }
922
+
923
+ export { CyclicMigrationGraph, IrreversibleMigration, MigrationRunner, Op, UnknownRevision, applyOperation, checkDrift, checkDriftPostgres, diffSchema, emptySchema, generateMigration, heads, introspectPostgres, introspectSqlite, invert, invertAll, makeRevisionId, reflectSchema, reflectTable, renderColumnDef, renderColumnType, renderDefault, renderOperation, replaySchema, runMigrationCli, sqliteAffinity, topoOrder };
924
+ //# sourceMappingURL=index.js.map
925
+ //# sourceMappingURL=index.js.map