tempest-db-js 0.4.0 → 0.6.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.
package/dist/bin.cjs CHANGED
@@ -5,8 +5,218 @@ var fs = require('fs');
5
5
  var path = require('path');
6
6
  var promises = require('readline/promises');
7
7
  var url = require('url');
8
+ var module$1 = require('module');
8
9
 
9
10
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
11
+ // src/expressions.ts
12
+ function renderPortableToken(token, dialect) {
13
+ switch (token) {
14
+ case "now":
15
+ return dialect === "postgresql" ? "now()" : "CURRENT_TIMESTAMP";
16
+ case "current_date":
17
+ return "CURRENT_DATE";
18
+ case "current_time":
19
+ return "CURRENT_TIME";
20
+ case "uuidv4":
21
+ if (dialect === "postgresql") return "gen_random_uuid()";
22
+ if (dialect === "mysql") return "(UUID())";
23
+ return "(lower(hex(randomblob(16))))";
24
+ }
25
+ }
26
+
27
+ // src/index.ts
28
+ function isDefaultValue(value) {
29
+ return typeof value === "object" && value !== null && "kind" in value && (value.kind === "literal" || value.kind === "expression");
30
+ }
31
+ function bindsParameters(value) {
32
+ return value.kind === "expression" && typeof value.expression === "object" && "parts" in value.expression;
33
+ }
34
+ function parseReference(ref, options) {
35
+ const dot = ref.lastIndexOf(".");
36
+ if (dot <= 0 || dot === ref.length - 1) {
37
+ throw new Error(`Invalid foreign key reference "${ref}"; expected "table.column".`);
38
+ }
39
+ return {
40
+ table: ref.slice(0, dot),
41
+ column: ref.slice(dot + 1),
42
+ onDelete: options?.onDelete,
43
+ onUpdate: options?.onUpdate
44
+ };
45
+ }
46
+ var Column = class _Column {
47
+ constructor(type, flags, defaultValue = null, onUpdateValue = null, reference = null, dbName = null) {
48
+ this.type = type;
49
+ this.flags = flags;
50
+ this.defaultValue = defaultValue;
51
+ this.onUpdateValue = onUpdateValue;
52
+ this.reference = reference;
53
+ this.dbName = dbName;
54
+ }
55
+ type;
56
+ flags;
57
+ defaultValue;
58
+ onUpdateValue;
59
+ reference;
60
+ dbName;
61
+ /** Clone this column with one facet replaced, carrying every other over. */
62
+ derive(patch) {
63
+ return new _Column(
64
+ this.type,
65
+ patch.flags ?? this.flags,
66
+ patch.defaultValue !== void 0 ? patch.defaultValue : this.defaultValue,
67
+ patch.onUpdateValue !== void 0 ? patch.onUpdateValue : this.onUpdateValue,
68
+ patch.reference !== void 0 ? patch.reference : this.reference,
69
+ patch.dbName !== void 0 ? patch.dbName : this.dbName
70
+ );
71
+ }
72
+ primaryKey() {
73
+ return this.derive({ flags: { ...this.flags, primaryKey: true, hasDefault: true } });
74
+ }
75
+ notNull() {
76
+ return this.derive({ flags: { ...this.flags, notNull: true } });
77
+ }
78
+ /**
79
+ * Add a `UNIQUE` constraint to the column (mirrors SQLAlchemy's
80
+ * `mapped_column(unique=True)`). DDL-only — does not change the inferred type.
81
+ */
82
+ unique() {
83
+ return this.derive({ flags: { ...this.flags, unique: true } });
84
+ }
85
+ /**
86
+ * Map this property to a differently-named database column, à la SQLAlchemy's
87
+ * `mapped_column("consumer_name")` (Django's `db_column`, Prisma's `@map`).
88
+ *
89
+ * The override applies everywhere the name reaches SQL — select, insert,
90
+ * update, delete, where, order by, group by, returning, conflict targets, the
91
+ * migration IR and the drift check — while the TypeScript row keeps the
92
+ * property name. Use it to keep a `snake_case` schema behind a `camelCase`
93
+ * model; {@link Model.naming} does the same for a whole table at once.
94
+ *
95
+ * @param dbName The real column name in the database.
96
+ * @returns A new column bound to that name.
97
+ * @throws Error When `dbName` is empty.
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * class ApiKey extends Model {
102
+ * static tablename = "api_keys";
103
+ * consumerName = column.text().name("consumer_name").notNull();
104
+ * }
105
+ * ```
106
+ */
107
+ name(dbName) {
108
+ if (dbName.length === 0) {
109
+ throw new Error("column.name() requires a non-empty database column name.");
110
+ }
111
+ return this.derive({ dbName });
112
+ }
113
+ /**
114
+ * Declare a foreign-key reference to another table's column, à la SQLAlchemy's
115
+ * `mapped_column(ForeignKey("table.column", ondelete=...))`. DDL-only — does
116
+ * not change the inferred type.
117
+ *
118
+ * @param ref The target as `"table.column"` (e.g. `"users.id"`).
119
+ * @param options Optional `onDelete` / `onUpdate` referential actions.
120
+ * @returns A new column carrying the reference.
121
+ * @throws Error When `ref` is not a valid `"table.column"` string.
122
+ */
123
+ references(ref, options) {
124
+ return this.derive({ reference: parseReference(ref, options) });
125
+ }
126
+ /**
127
+ * Set the insert-time default: a constant value of type `T`, or a portable
128
+ * server-side expression from {@link sql} (e.g. `sql.now()`, `sql.uuidv4()`).
129
+ *
130
+ * @param value The literal default, or a {@link sql} expression.
131
+ * @returns A new column carrying the default.
132
+ * @throws Error When given a `sql.expr` fragment — a `DEFAULT` clause has
133
+ * nowhere to bind parameters; use `sql.raw()` for a verbatim expression.
134
+ */
135
+ default(value) {
136
+ const resolved = isDefaultValue(value) ? value : { kind: "literal", value };
137
+ if (bindsParameters(resolved)) {
138
+ throw new Error(
139
+ "sql.expr`...` binds parameters and cannot be a column default \u2014 use sql.raw() for a verbatim DEFAULT expression."
140
+ );
141
+ }
142
+ return this.derive({
143
+ flags: { ...this.flags, hasDefault: true },
144
+ defaultValue: resolved
145
+ });
146
+ }
147
+ /**
148
+ * Re-apply a value whenever the row is updated (e.g. an `updated_at` column
149
+ * with `sql.now()`). Mirrors SQLAlchemy's `onupdate`.
150
+ *
151
+ * @param value The literal value, or a {@link sql} expression.
152
+ * @returns A new column carrying the on-update value.
153
+ * @throws Error When given a `sql.expr` fragment (see {@link Column.default}).
154
+ */
155
+ onUpdate(value) {
156
+ const resolved = isDefaultValue(value) ? value : { kind: "literal", value };
157
+ if (bindsParameters(resolved)) {
158
+ throw new Error(
159
+ "sql.expr`...` binds parameters and cannot be an onUpdate default \u2014 use sql.raw() for a verbatim expression."
160
+ );
161
+ }
162
+ return this.derive({ onUpdateValue: resolved });
163
+ }
164
+ };
165
+ function toSnakeCase(name) {
166
+ return name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
167
+ }
168
+ var columnsCache = /* @__PURE__ */ new WeakMap();
169
+ function columnsOf(model) {
170
+ const cached = columnsCache.get(model);
171
+ if (cached) return cached;
172
+ const instance = new model();
173
+ const out = {};
174
+ for (const [key, value] of Object.entries(instance)) {
175
+ if (value instanceof Column) {
176
+ out[key] = value;
177
+ }
178
+ }
179
+ columnsCache.set(model, out);
180
+ return out;
181
+ }
182
+ var nameMapCache = /* @__PURE__ */ new WeakMap();
183
+ function columnNamesOf(model) {
184
+ const cached = nameMapCache.get(model);
185
+ if (cached !== void 0) return cached;
186
+ const strategy = model.naming ?? "preserve";
187
+ const map = {};
188
+ const seen = /* @__PURE__ */ new Map();
189
+ let renamed = false;
190
+ for (const [prop, col] of Object.entries(columnsOf(model))) {
191
+ const dbName = col.dbName ?? (strategy === "snake_case" ? toSnakeCase(prop) : prop);
192
+ const collision = seen.get(dbName);
193
+ if (collision !== void 0) {
194
+ throw new Error(
195
+ `${model.tablename}: properties "${collision}" and "${prop}" both map to column "${dbName}".`
196
+ );
197
+ }
198
+ seen.set(dbName, prop);
199
+ map[prop] = dbName;
200
+ if (dbName !== prop) renamed = true;
201
+ }
202
+ const result = renamed ? map : null;
203
+ nameMapCache.set(model, result);
204
+ return result;
205
+ }
206
+
207
+ // src/engine.ts
208
+ module$1.createRequire((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('bin.cjs', document.baseURI).href)));
209
+ function toAsyncDriver(driver) {
210
+ return {
211
+ async execute(sql, params) {
212
+ return await driver.execute(sql, params);
213
+ },
214
+ async close() {
215
+ await driver.close();
216
+ }
217
+ };
218
+ }
219
+
10
220
  // src/migrations/operations.ts
11
221
  var IrreversibleMigration = class extends Error {
12
222
  constructor(message) {
@@ -123,6 +333,8 @@ function renderColumnType(type, dialect) {
123
333
  return "NUMERIC";
124
334
  case "blob":
125
335
  return "BLOB";
336
+ case "array":
337
+ throw new Error(unsupportedArray("sqlite"));
126
338
  default:
127
339
  return "TEXT";
128
340
  }
@@ -164,6 +376,8 @@ function renderColumnType(type, dialect) {
164
376
  return "CHAR(36)";
165
377
  case "enum":
166
378
  return `ENUM(${(meta.values ?? []).map(quoteLiteral).join(", ")})`;
379
+ case "array":
380
+ throw new Error(unsupportedArray("mysql"));
167
381
  }
168
382
  }
169
383
  switch (kind) {
@@ -202,27 +416,39 @@ function renderColumnType(type, dialect) {
202
416
  return "UUID";
203
417
  case "enum":
204
418
  return "TEXT";
419
+ case "array":
420
+ return `${renderColumnType(arrayElement(meta.element), dialect)}[]`;
421
+ }
422
+ }
423
+ function unsupportedArray(dialect) {
424
+ return `column.array() is PostgreSQL-only \u2014 ${dialect} has no native array type. Model the column as JSON there, accepting that array operators will not work.`;
425
+ }
426
+ function arrayElement(element) {
427
+ if (!element) {
428
+ throw new Error(
429
+ "An array column has no element type \u2014 build it with column.array()."
430
+ );
205
431
  }
432
+ return element;
206
433
  }
207
- function renderDefault(def, dialect) {
434
+ function renderDefault(def, dialect, type) {
208
435
  if (def.kind === "expression") {
209
436
  const expr = def.expression;
210
- if (typeof expr === "object") return expr.raw;
211
- switch (expr) {
212
- case "now":
213
- return dialect === "postgresql" ? "now()" : "CURRENT_TIMESTAMP";
214
- case "current_date":
215
- return "CURRENT_DATE";
216
- case "current_time":
217
- return "CURRENT_TIME";
218
- case "uuidv4":
219
- if (dialect === "postgresql") return "gen_random_uuid()";
220
- if (dialect === "mysql") return "(UUID())";
221
- return "(lower(hex(randomblob(16))))";
437
+ if (typeof expr === "object") {
438
+ if ("raw" in expr) return expr.raw;
439
+ throw new Error(
440
+ "sql.expr`...` binds parameters and cannot be rendered as a DEFAULT \u2014 use sql.raw()."
441
+ );
222
442
  }
443
+ return renderPortableToken(expr, dialect);
223
444
  }
224
445
  const value = def.value;
225
446
  if (value === null) return "NULL";
447
+ if (Array.isArray(value) && type?.kind === "array") {
448
+ const elementType = renderColumnType(arrayElement(type.meta.element), dialect);
449
+ const items = value.map((v) => renderDefault({ kind: "literal", value: v }, dialect));
450
+ return `ARRAY[${items.join(", ")}]::${elementType}[]`;
451
+ }
226
452
  if (typeof value === "boolean") {
227
453
  return dialect === "postgresql" ? value ? "TRUE" : "FALSE" : value ? "1" : "0";
228
454
  }
@@ -268,7 +494,9 @@ function tableConstraintClauses(table, dialect) {
268
494
  function renderColumnDef(col, dialect) {
269
495
  let sql = `${quoteId(col.name, dialect)} ${renderColumnType(col.type, dialect)}`;
270
496
  if (col.notNull) sql += " NOT NULL";
271
- if (col.default !== null) sql += ` DEFAULT ${renderDefault(col.default, dialect)}`;
497
+ if (col.default !== null) {
498
+ sql += ` DEFAULT ${renderDefault(col.default, dialect, col.type)}`;
499
+ }
272
500
  sql += columnConstraintSuffix(col, dialect);
273
501
  return sql;
274
502
  }
@@ -292,7 +520,9 @@ function renderCreateTable(table, dialect) {
292
520
  typeStmts.push(`CREATE TYPE ${quoteId(typeName, dialect)} AS ENUM (${values})`);
293
521
  let def = `${quoteId(c.name, dialect)} ${quoteId(typeName, dialect)}`;
294
522
  if (c.notNull) def += " NOT NULL";
295
- if (c.default !== null) def += ` DEFAULT ${renderDefault(c.default, dialect)}`;
523
+ if (c.default !== null) {
524
+ def += ` DEFAULT ${renderDefault(c.default, dialect, c.type)}`;
525
+ }
296
526
  return def + columnConstraintSuffix(c, dialect);
297
527
  }
298
528
  if (dialect === "postgresql" && isAutoIncrementPk(table, c)) {
@@ -436,7 +666,7 @@ function renderAlterColumn(table, to, dialect) {
436
666
  to.notNull ? `ALTER TABLE ${t} ALTER COLUMN ${c} SET NOT NULL` : `ALTER TABLE ${t} ALTER COLUMN ${c} DROP NOT NULL`
437
667
  );
438
668
  stmts.push(
439
- to.default !== null ? `ALTER TABLE ${t} ALTER COLUMN ${c} SET DEFAULT ${renderDefault(to.default, dialect)}` : `ALTER TABLE ${t} ALTER COLUMN ${c} DROP DEFAULT`
669
+ to.default !== null ? `ALTER TABLE ${t} ALTER COLUMN ${c} SET DEFAULT ${renderDefault(to.default, dialect, to.type)}` : `ALTER TABLE ${t} ALTER COLUMN ${c} DROP DEFAULT`
440
670
  );
441
671
  return stmts;
442
672
  }
@@ -607,132 +837,18 @@ function heads(migrations) {
607
837
  return migrations.map((m) => m.revision).filter((r) => !parents.has(r)).sort();
608
838
  }
609
839
 
610
- // src/index.ts
611
- function isDefaultValue(value) {
612
- return typeof value === "object" && value !== null && "kind" in value && (value.kind === "literal" || value.kind === "expression");
613
- }
614
- function parseReference(ref, options) {
615
- const dot = ref.lastIndexOf(".");
616
- if (dot <= 0 || dot === ref.length - 1) {
617
- throw new Error(`Invalid foreign key reference "${ref}"; expected "table.column".`);
618
- }
619
- return {
620
- table: ref.slice(0, dot),
621
- column: ref.slice(dot + 1),
622
- onDelete: options?.onDelete,
623
- onUpdate: options?.onUpdate
624
- };
625
- }
626
- var Column = class _Column {
627
- constructor(type, flags, defaultValue = null, onUpdateValue = null, reference = null) {
628
- this.type = type;
629
- this.flags = flags;
630
- this.defaultValue = defaultValue;
631
- this.onUpdateValue = onUpdateValue;
632
- this.reference = reference;
633
- }
634
- type;
635
- flags;
636
- defaultValue;
637
- onUpdateValue;
638
- reference;
639
- primaryKey() {
640
- return new _Column(
641
- this.type,
642
- { ...this.flags, primaryKey: true, hasDefault: true },
643
- this.defaultValue,
644
- this.onUpdateValue,
645
- this.reference
646
- );
647
- }
648
- notNull() {
649
- return new _Column(
650
- this.type,
651
- { ...this.flags, notNull: true },
652
- this.defaultValue,
653
- this.onUpdateValue,
654
- this.reference
655
- );
656
- }
657
- /**
658
- * Add a `UNIQUE` constraint to the column (mirrors SQLAlchemy's
659
- * `mapped_column(unique=True)`). DDL-only — does not change the inferred type.
660
- */
661
- unique() {
662
- return new _Column(
663
- this.type,
664
- { ...this.flags, unique: true },
665
- this.defaultValue,
666
- this.onUpdateValue,
667
- this.reference
668
- );
669
- }
670
- /**
671
- * Declare a foreign-key reference to another table's column, à la SQLAlchemy's
672
- * `mapped_column(ForeignKey("table.column", ondelete=...))`. DDL-only — does
673
- * not change the inferred type.
674
- *
675
- * @param ref The target as `"table.column"` (e.g. `"users.id"`).
676
- * @param options Optional `onDelete` / `onUpdate` referential actions.
677
- * @returns A new column carrying the reference.
678
- * @throws Error When `ref` is not a valid `"table.column"` string.
679
- */
680
- references(ref, options) {
681
- return new _Column(
682
- this.type,
683
- this.flags,
684
- this.defaultValue,
685
- this.onUpdateValue,
686
- parseReference(ref, options)
687
- );
688
- }
689
- /**
690
- * Set the insert-time default: a constant value of type `T`, or a portable
691
- * server-side expression from {@link sql} (e.g. `sql.now()`, `sql.uuidv4()`).
692
- */
693
- default(value) {
694
- const resolved = isDefaultValue(value) ? value : { kind: "literal", value };
695
- return new _Column(
696
- this.type,
697
- { ...this.flags, hasDefault: true },
698
- resolved,
699
- this.onUpdateValue,
700
- this.reference
701
- );
702
- }
703
- /**
704
- * Re-apply a value whenever the row is updated (e.g. an `updated_at` column
705
- * with `sql.now()`). Mirrors SQLAlchemy's `onupdate`.
706
- */
707
- onUpdate(value) {
708
- const resolved = isDefaultValue(value) ? value : { kind: "literal", value };
709
- return new _Column(this.type, this.flags, this.defaultValue, resolved, this.reference);
710
- }
711
- };
712
- var columnsCache = /* @__PURE__ */ new WeakMap();
713
- function columnsOf(model) {
714
- const cached = columnsCache.get(model);
715
- if (cached) return cached;
716
- const instance = new model();
717
- const out = {};
718
- for (const [key, value] of Object.entries(instance)) {
719
- if (value instanceof Column) {
720
- out[key] = value;
721
- }
722
- }
723
- columnsCache.set(model, out);
724
- return out;
725
- }
726
-
727
840
  // src/migrations/ir.ts
728
841
  function constraintName(prefix, table, columns) {
729
842
  return `${prefix}_${table}_${columns.join("_")}`;
730
843
  }
731
844
  function reflectTable(model) {
845
+ const names = columnNamesOf(model);
846
+ const toColumn = (prop) => names?.[prop] ?? prop;
732
847
  const columns = {};
733
848
  const primaryKey = [];
734
- for (const [name, col] of Object.entries(columnsOf(model))) {
849
+ for (const [prop, col] of Object.entries(columnsOf(model))) {
735
850
  const isPk = col.flags.primaryKey;
851
+ const name = toColumn(prop);
736
852
  columns[name] = {
737
853
  name,
738
854
  type: col.type,
@@ -747,15 +863,16 @@ function reflectTable(model) {
747
863
  const uniqueConstraints = [];
748
864
  const foreignKeys = [];
749
865
  for (const c of model.tableArgs?.() ?? []) {
866
+ const cols = c.columns.map(toColumn);
750
867
  if (c.kind === "unique") {
751
868
  uniqueConstraints.push({
752
- name: c.name ?? constraintName("uq", model.tablename, c.columns),
753
- columns: c.columns
869
+ name: c.name ?? constraintName("uq", model.tablename, cols),
870
+ columns: cols
754
871
  });
755
872
  } else {
756
873
  foreignKeys.push({
757
- name: c.name ?? constraintName("fk", model.tablename, c.columns),
758
- columns: c.columns,
874
+ name: c.name ?? constraintName("fk", model.tablename, cols),
875
+ columns: cols,
759
876
  refTable: c.refTable,
760
877
  refColumns: c.refColumns,
761
878
  onDelete: c.onDelete,
@@ -800,74 +917,6 @@ function affinityToKind(affinity) {
800
917
  return "text";
801
918
  }
802
919
  }
803
- function sqliteForeignKeys(driver, table) {
804
- const rows = driver.execute(`PRAGMA foreign_key_list(${JSON.stringify(table)})`, []).rows;
805
- const byId = /* @__PURE__ */ new Map();
806
- for (const r of rows) {
807
- const list = byId.get(r.id) ?? [];
808
- list.push(r);
809
- byId.set(r.id, list);
810
- }
811
- const fks = [];
812
- for (const group of byId.values()) {
813
- const ordered = [...group].sort((a, b) => a.seq - b.seq);
814
- const columns = ordered.map((r) => r.from);
815
- fks.push({
816
- name: `fk_${table}_${columns.join("_")}`,
817
- columns,
818
- refTable: ordered[0]?.table ?? "",
819
- refColumns: ordered.map((r) => r.to)
820
- });
821
- }
822
- return fks;
823
- }
824
- function sqliteUniques(driver, table) {
825
- const indexes = driver.execute(`PRAGMA index_list(${JSON.stringify(table)})`, []).rows;
826
- const uniques = [];
827
- for (const idx of indexes) {
828
- if (Number(idx.unique) !== 1 || idx.origin === "pk") continue;
829
- const cols = driver.execute(`PRAGMA index_info(${JSON.stringify(idx.name)})`, []).rows.sort((a, b) => a.seqno - b.seqno).map((c) => c.name);
830
- if (cols.length > 0) {
831
- uniques.push({ name: `uq_${table}_${cols.join("_")}`, columns: cols });
832
- }
833
- }
834
- return uniques;
835
- }
836
- function introspectSqlite(driver) {
837
- const tablesRows = driver.execute(
838
- "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name != 'tempest_db_js_migrations'",
839
- []
840
- ).rows;
841
- const tables = {};
842
- for (const row of tablesRows) {
843
- const tableName = String(row.name);
844
- const info = driver.execute(`PRAGMA table_info(${JSON.stringify(tableName)})`, []).rows;
845
- const columns = {};
846
- const primaryKey = [];
847
- for (const col of info) {
848
- const isPk = Number(col.pk) > 0;
849
- const affinity = sqliteAffinity(col.type);
850
- columns[col.name] = {
851
- name: col.name,
852
- type: { kind: affinityToKind(affinity), meta: {} },
853
- notNull: Number(col.notnull) === 1 || isPk,
854
- primaryKey: isPk,
855
- default: null,
856
- unique: false,
857
- references: null
858
- };
859
- if (isPk) primaryKey.push(col.name);
860
- }
861
- tables[tableName] = {
862
- name: tableName,
863
- columns,
864
- primaryKey,
865
- uniqueConstraints: sqliteUniques(driver, tableName),
866
- foreignKeys: sqliteForeignKeys(driver, tableName)
867
- };
868
- }
869
- return { tables };
870
- }
871
920
  function constraintKeys(table) {
872
921
  const fks = /* @__PURE__ */ new Set();
873
922
  const uniques = /* @__PURE__ */ new Set();
@@ -885,9 +934,7 @@ function constraintKeys(table) {
885
934
  }
886
935
  return { fks, uniques };
887
936
  }
888
- function checkDrift(driver, models) {
889
- const actual = introspectSqlite(driver);
890
- const expected = reflectSchema(models);
937
+ function compareSqliteSchemas(actual, expected) {
891
938
  const issues = [];
892
939
  for (const [tableName, expectedTable] of Object.entries(expected.tables)) {
893
940
  const actualTable = actual.tables[tableName];
@@ -958,6 +1005,299 @@ function checkDrift(driver, models) {
958
1005
  }
959
1006
  return issues;
960
1007
  }
1008
+ function sqliteColumnsFromPragma(info) {
1009
+ const columns = {};
1010
+ const primaryKey = [];
1011
+ for (const col of info) {
1012
+ const isPk = Number(col.pk) > 0;
1013
+ columns[col.name] = {
1014
+ name: col.name,
1015
+ type: { kind: affinityToKind(sqliteAffinity(col.type)), meta: {} },
1016
+ notNull: Number(col.notnull) === 1 || isPk,
1017
+ primaryKey: isPk,
1018
+ default: null,
1019
+ unique: false,
1020
+ references: null
1021
+ };
1022
+ if (isPk) primaryKey.push(col.name);
1023
+ }
1024
+ return { columns, primaryKey };
1025
+ }
1026
+ function sqliteForeignKeysFromPragma(table, rows) {
1027
+ const byId = /* @__PURE__ */ new Map();
1028
+ for (const row of rows) {
1029
+ const group = byId.get(row.id) ?? [];
1030
+ group.push(row);
1031
+ byId.set(row.id, group);
1032
+ }
1033
+ const fks = [];
1034
+ for (const group of byId.values()) {
1035
+ const ordered = [...group].sort((a, b) => a.seq - b.seq);
1036
+ fks.push({
1037
+ name: `fk_${table}_${ordered.map((r) => r.from).join("_")}`,
1038
+ columns: ordered.map((r) => r.from),
1039
+ refTable: ordered[0]?.table ?? "",
1040
+ refColumns: ordered.map((r) => r.to)
1041
+ });
1042
+ }
1043
+ return fks;
1044
+ }
1045
+ function sqliteUniqueFromPragma(table, rows) {
1046
+ const cols = [...rows].sort((a, b) => a.seqno - b.seqno).map((c) => c.name);
1047
+ if (cols.length === 0) return null;
1048
+ return { name: `uq_${table}_${cols.join("_")}`, columns: cols };
1049
+ }
1050
+ function isUniqueIndex(idx) {
1051
+ return Number(idx.unique) === 1 && idx.origin !== "pk";
1052
+ }
1053
+ var SQLITE_TABLES_SQL = "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name != 'tempest_db_js_migrations'";
1054
+ async function introspectSqliteAsync(driver) {
1055
+ const tablesRows = (await driver.execute(SQLITE_TABLES_SQL, [])).rows;
1056
+ const tables = {};
1057
+ for (const row of tablesRows) {
1058
+ const tableName = String(row.name);
1059
+ const info = (await driver.execute(`PRAGMA table_info(${JSON.stringify(tableName)})`, [])).rows;
1060
+ const { columns, primaryKey } = sqliteColumnsFromPragma(info);
1061
+ const fkRows = (await driver.execute(`PRAGMA foreign_key_list(${JSON.stringify(tableName)})`, [])).rows;
1062
+ const indexes = (await driver.execute(`PRAGMA index_list(${JSON.stringify(tableName)})`, [])).rows;
1063
+ const uniqueConstraints = [];
1064
+ for (const idx of indexes) {
1065
+ if (!isUniqueIndex(idx)) continue;
1066
+ const cols = (await driver.execute(`PRAGMA index_info(${JSON.stringify(idx.name)})`, [])).rows;
1067
+ const unique = sqliteUniqueFromPragma(tableName, cols);
1068
+ if (unique) uniqueConstraints.push(unique);
1069
+ }
1070
+ tables[tableName] = {
1071
+ name: tableName,
1072
+ columns,
1073
+ primaryKey,
1074
+ uniqueConstraints,
1075
+ foreignKeys: sqliteForeignKeysFromPragma(tableName, fkRows)
1076
+ };
1077
+ }
1078
+ return { tables };
1079
+ }
1080
+ async function checkDriftAsync(driver, dialect, models) {
1081
+ if (dialect === "postgresql") return checkDriftPostgres(driver, models);
1082
+ if (dialect === "sqlite") {
1083
+ return compareSqliteSchemas(
1084
+ await introspectSqliteAsync(driver),
1085
+ reflectSchema(models)
1086
+ );
1087
+ }
1088
+ return [
1089
+ "drift checking is not implemented for MySQL \u2014 its information_schema introspection is still missing"
1090
+ ];
1091
+ }
1092
+ function pgTypeToColumnType(dataType, udtName) {
1093
+ if (dataType.toLowerCase() === "array") {
1094
+ return {
1095
+ kind: "array",
1096
+ meta: { element: { kind: pgUdtToKind(udtName.replace(/^_/, "")), meta: {} } }
1097
+ };
1098
+ }
1099
+ return { kind: pgTypeToKind(dataType, udtName), meta: {} };
1100
+ }
1101
+ function pgUdtToKind(udtName) {
1102
+ switch (udtName) {
1103
+ case "int2":
1104
+ return "smallint";
1105
+ case "int4":
1106
+ return "integer";
1107
+ case "int8":
1108
+ return "bigint";
1109
+ case "float4":
1110
+ return "real";
1111
+ case "float8":
1112
+ return "double";
1113
+ case "numeric":
1114
+ return "numeric";
1115
+ case "varchar":
1116
+ return "varchar";
1117
+ case "bpchar":
1118
+ return "char";
1119
+ case "bool":
1120
+ return "boolean";
1121
+ case "date":
1122
+ return "date";
1123
+ case "time":
1124
+ case "timetz":
1125
+ return "time";
1126
+ case "timestamp":
1127
+ case "timestamptz":
1128
+ return "timestamp";
1129
+ case "bytea":
1130
+ return "blob";
1131
+ case "json":
1132
+ case "jsonb":
1133
+ return "json";
1134
+ case "uuid":
1135
+ return "uuid";
1136
+ default:
1137
+ return "text";
1138
+ }
1139
+ }
1140
+ function pgTypeToKind(dataType, udtName) {
1141
+ const t = dataType.toLowerCase();
1142
+ if (t === "user-defined") return "enum";
1143
+ if (t === "smallint") return "smallint";
1144
+ if (t === "integer") return "integer";
1145
+ if (t === "bigint") return "bigint";
1146
+ if (t === "numeric") return "numeric";
1147
+ if (t === "real") return "real";
1148
+ if (t === "double precision") return "double";
1149
+ if (t === "character varying") return "varchar";
1150
+ if (t === "character") return "char";
1151
+ if (t === "text") return "text";
1152
+ if (t === "boolean") return "boolean";
1153
+ if (t === "date") return "date";
1154
+ if (t.startsWith("time")) return t.startsWith("timestamp") ? "timestamp" : "time";
1155
+ if (t === "bytea") return "blob";
1156
+ if (t === "json") return "json";
1157
+ if (t === "jsonb") return "json";
1158
+ if (t === "uuid") return "uuid";
1159
+ return udtName === "jsonb" ? "json" : "text";
1160
+ }
1161
+ async function introspectPostgres(driver) {
1162
+ const tablesResult = await driver.execute(
1163
+ "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE' AND table_name != 'tempest_db_js_migrations'",
1164
+ []
1165
+ );
1166
+ const tables = {};
1167
+ for (const row of tablesResult.rows) {
1168
+ const tableName = String(row.table_name);
1169
+ const colsResult = await driver.execute(
1170
+ "SELECT column_name, data_type, udt_name, is_nullable FROM information_schema.columns WHERE table_name = $1",
1171
+ [tableName]
1172
+ );
1173
+ const pkResult = await driver.execute(
1174
+ `SELECT a.attname AS name FROM pg_index i
1175
+ JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
1176
+ WHERE i.indrelid = $1::regclass AND i.indisprimary`,
1177
+ [tableName]
1178
+ );
1179
+ const pkSet = new Set(pkResult.rows.map((r) => String(r.name)));
1180
+ const columns = {};
1181
+ const primaryKey = [];
1182
+ for (const col of colsResult.rows) {
1183
+ const name = String(col.column_name);
1184
+ const isPk = pkSet.has(name);
1185
+ columns[name] = {
1186
+ name,
1187
+ type: pgTypeToColumnType(String(col.data_type), String(col.udt_name)),
1188
+ notNull: col.is_nullable === "NO" || isPk,
1189
+ primaryKey: isPk,
1190
+ default: null,
1191
+ unique: false,
1192
+ references: null
1193
+ };
1194
+ if (isPk) primaryKey.push(name);
1195
+ }
1196
+ tables[tableName] = {
1197
+ name: tableName,
1198
+ columns,
1199
+ primaryKey,
1200
+ uniqueConstraints: await postgresUniques(driver, tableName),
1201
+ foreignKeys: await postgresForeignKeys(driver, tableName)
1202
+ };
1203
+ }
1204
+ return { tables };
1205
+ }
1206
+ async function postgresForeignKeys(driver, table) {
1207
+ const result = await driver.execute(
1208
+ `SELECT c.conname AS name,
1209
+ (SELECT array_agg(a.attname ORDER BY k.ord)
1210
+ FROM unnest(c.conkey) WITH ORDINALITY AS k(attnum, ord)
1211
+ JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum) AS cols,
1212
+ cf.relname AS ref_table,
1213
+ (SELECT array_agg(a.attname ORDER BY k.ord)
1214
+ FROM unnest(c.confkey) WITH ORDINALITY AS k(attnum, ord)
1215
+ JOIN pg_attribute a ON a.attrelid = c.confrelid AND a.attnum = k.attnum) AS ref_cols
1216
+ FROM pg_constraint c
1217
+ JOIN pg_class cf ON cf.oid = c.confrelid
1218
+ WHERE c.contype = 'f' AND c.conrelid = $1::regclass`,
1219
+ [table]
1220
+ );
1221
+ return result.rows.map((r) => ({
1222
+ name: String(r.name),
1223
+ columns: r.cols ?? [],
1224
+ refTable: String(r.ref_table),
1225
+ refColumns: r.ref_cols ?? []
1226
+ }));
1227
+ }
1228
+ async function postgresUniques(driver, table) {
1229
+ const result = await driver.execute(
1230
+ `SELECT c.conname AS name,
1231
+ (SELECT array_agg(a.attname ORDER BY k.ord)
1232
+ FROM unnest(c.conkey) WITH ORDINALITY AS k(attnum, ord)
1233
+ JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum) AS cols
1234
+ FROM pg_constraint c
1235
+ WHERE c.contype = 'u' AND c.conrelid = $1::regclass`,
1236
+ [table]
1237
+ );
1238
+ return result.rows.map((r) => ({
1239
+ name: String(r.name),
1240
+ columns: r.cols ?? []
1241
+ }));
1242
+ }
1243
+ function describeKind(type) {
1244
+ if (type.kind !== "array") return type.kind;
1245
+ return `${type.meta.element ? describeKind(type.meta.element) : "unknown"}[]`;
1246
+ }
1247
+ async function checkDriftPostgres(driver, models) {
1248
+ const actual = await introspectPostgres(driver);
1249
+ const expected = reflectSchema(models);
1250
+ const issues = [];
1251
+ for (const [tableName, expectedTable] of Object.entries(expected.tables)) {
1252
+ const actualTable = actual.tables[tableName];
1253
+ if (!actualTable) {
1254
+ issues.push(`table "${tableName}" is missing from the database`);
1255
+ continue;
1256
+ }
1257
+ for (const [colName, expectedCol] of Object.entries(expectedTable.columns)) {
1258
+ const actualCol = actualTable.columns[colName];
1259
+ if (!actualCol) {
1260
+ issues.push(`column "${tableName}.${colName}" is missing from the database`);
1261
+ continue;
1262
+ }
1263
+ if (describeKind(expectedCol.type) !== describeKind(actualCol.type)) {
1264
+ issues.push(
1265
+ `column "${tableName}.${colName}" type differs: model ${describeKind(expectedCol.type)}, db ${describeKind(actualCol.type)}`
1266
+ );
1267
+ }
1268
+ if (expectedCol.notNull !== actualCol.notNull) {
1269
+ issues.push(`column "${tableName}.${colName}" nullability differs`);
1270
+ }
1271
+ }
1272
+ for (const colName of Object.keys(actualTable.columns)) {
1273
+ if (!expectedTable.columns[colName]) {
1274
+ issues.push(
1275
+ `column "${tableName}.${colName}" exists in the database but not in the model`
1276
+ );
1277
+ }
1278
+ }
1279
+ const expectedKeys = constraintKeys(expectedTable);
1280
+ const actualKeys = constraintKeys(actualTable);
1281
+ for (const fk of expectedKeys.fks) {
1282
+ if (!actualKeys.fks.has(fk)) {
1283
+ issues.push(`foreign key "${tableName}: ${fk}" is missing from the database`);
1284
+ }
1285
+ }
1286
+ for (const uq of expectedKeys.uniques) {
1287
+ if (!actualKeys.uniques.has(uq)) {
1288
+ issues.push(
1289
+ `unique constraint "${tableName}: (${uq})" is missing from the database`
1290
+ );
1291
+ }
1292
+ }
1293
+ }
1294
+ for (const tableName of Object.keys(actual.tables)) {
1295
+ if (!expected.tables[tableName]) {
1296
+ issues.push(`table "${tableName}" exists in the database but not in the models`);
1297
+ }
1298
+ }
1299
+ return issues;
1300
+ }
961
1301
 
962
1302
  // src/migrations/renames.ts
963
1303
  function columnShape(col) {
@@ -1100,62 +1440,77 @@ var Op = class {
1100
1440
  }
1101
1441
  };
1102
1442
  var VERSION_TABLE = "tempest_db_js_migrations";
1103
- var MigrationRunner = class {
1443
+ function quoteIdent(name, dialect) {
1444
+ return dialect === "mysql" ? `\`${name.replace(/`/g, "``")}\`` : `"${name.replace(/"/g, '""')}"`;
1445
+ }
1446
+ function placeholder(index, dialect) {
1447
+ return dialect === "postgresql" ? `$${index}` : "?";
1448
+ }
1449
+ var AsyncMigrationRunner = class {
1104
1450
  constructor(driver, dialect) {
1105
1451
  this.driver = driver;
1106
1452
  this.dialect = dialect;
1453
+ this.vt = quoteIdent(VERSION_TABLE, dialect);
1107
1454
  }
1108
1455
  driver;
1109
1456
  dialect;
1457
+ vt;
1110
1458
  /** Create the version-tracking table if it does not exist. */
1111
- ensureVersionTable() {
1112
- this.driver.execute(
1113
- `CREATE TABLE IF NOT EXISTS "${VERSION_TABLE}" (revision TEXT PRIMARY KEY, applied_at TEXT NOT NULL, down_revision TEXT NOT NULL)`,
1459
+ async ensureVersionTable() {
1460
+ await this.driver.execute(
1461
+ `CREATE TABLE IF NOT EXISTS ${this.vt} (revision ${this.textType()} PRIMARY KEY, applied_at ${this.textType()} NOT NULL, down_revision ${this.textType()} NOT NULL)`,
1114
1462
  []
1115
1463
  );
1116
1464
  }
1465
+ /** A portable "text" column type for the version table. */
1466
+ textType() {
1467
+ return this.dialect === "mysql" ? "VARCHAR(255)" : "TEXT";
1468
+ }
1117
1469
  /** The set of applied revision ids. */
1118
- applied() {
1119
- this.ensureVersionTable();
1120
- const { rows } = this.driver.execute(`SELECT revision FROM "${VERSION_TABLE}"`, []);
1470
+ async applied() {
1471
+ await this.ensureVersionTable();
1472
+ const { rows } = await this.driver.execute(`SELECT revision FROM ${this.vt}`, []);
1121
1473
  return new Set(rows.map((r) => String(r.revision)));
1122
1474
  }
1123
- runOps(ops) {
1475
+ async runOps(ops) {
1124
1476
  for (const op of ops) {
1125
1477
  for (const stmt of renderOperation(op, this.dialect)) {
1126
1478
  const trimmed = stmt.trim();
1127
1479
  if (trimmed.length === 0 || trimmed.startsWith("--")) continue;
1128
- this.driver.execute(stmt, []);
1480
+ await this.driver.execute(stmt, []);
1129
1481
  }
1130
1482
  }
1131
1483
  }
1132
- record(migration, appliedAt) {
1133
- this.driver.execute(
1134
- `INSERT INTO "${VERSION_TABLE}" (revision, applied_at, down_revision) VALUES (?, ?, ?)`,
1484
+ async record(migration, appliedAt) {
1485
+ const p = (i) => placeholder(i, this.dialect);
1486
+ await this.driver.execute(
1487
+ `INSERT INTO ${this.vt} (revision, applied_at, down_revision) VALUES (${p(1)}, ${p(2)}, ${p(3)})`,
1135
1488
  [migration.revision, appliedAt, migration.downRevision.join(",")]
1136
1489
  );
1137
1490
  }
1138
- forget(revision) {
1139
- this.driver.execute(`DELETE FROM "${VERSION_TABLE}" WHERE revision = ?`, [revision]);
1491
+ async forget(revision) {
1492
+ await this.driver.execute(
1493
+ `DELETE FROM ${this.vt} WHERE revision = ${placeholder(1, this.dialect)}`,
1494
+ [revision]
1495
+ );
1140
1496
  }
1141
1497
  /**
1142
1498
  * Apply all pending migrations up to the head(s), in DAG order.
1143
1499
  *
1144
1500
  * @param migrations All known migrations.
1145
- * @param appliedAt Timestamp string to stamp (pass one in — the runtime has no
1146
- * wall clock of its own here).
1501
+ * @param appliedAt Timestamp string to stamp on each applied revision.
1147
1502
  * @returns The revision ids that were applied this run.
1148
1503
  */
1149
- upgrade(migrations, appliedAt) {
1150
- const done = this.applied();
1504
+ async upgrade(migrations, appliedAt) {
1505
+ const done = await this.applied();
1151
1506
  const ordered = topoOrder(migrations);
1152
1507
  const ran = [];
1153
1508
  for (const migration of ordered) {
1154
1509
  if (done.has(migration.revision)) continue;
1155
1510
  const op = new Op();
1156
1511
  migration.up(op);
1157
- this.runOps(op.operations);
1158
- this.record(migration, appliedAt);
1512
+ await this.runOps(op.operations);
1513
+ await this.record(migration, appliedAt);
1159
1514
  ran.push(migration.revision);
1160
1515
  }
1161
1516
  return ran;
@@ -1167,27 +1522,25 @@ var MigrationRunner = class {
1167
1522
  * @param steps How many applied revisions to roll back.
1168
1523
  * @returns The revision ids that were reverted.
1169
1524
  */
1170
- downgrade(migrations, steps = 1) {
1171
- const done = this.applied();
1525
+ async downgrade(migrations, steps = 1) {
1526
+ const done = await this.applied();
1172
1527
  const ordered = topoOrder(migrations).filter((m) => done.has(m.revision));
1173
1528
  const toRevert = ordered.slice(-steps).reverse();
1174
1529
  const reverted = [];
1175
1530
  for (const migration of toRevert) {
1176
1531
  const op = new Op();
1177
- if (migration.down.length >= 0) {
1178
- try {
1179
- migration.down(op);
1180
- } catch {
1181
- op.operations.length = 0;
1182
- }
1532
+ try {
1533
+ migration.down(op);
1534
+ } catch {
1535
+ op.operations.length = 0;
1183
1536
  }
1184
1537
  if (op.operations.length === 0) {
1185
1538
  const upOp = new Op();
1186
1539
  migration.up(upOp);
1187
1540
  op.operations.push(...invertAll(upOp.operations));
1188
1541
  }
1189
- this.runOps(op.operations);
1190
- this.forget(migration.revision);
1542
+ await this.runOps(op.operations);
1543
+ await this.forget(migration.revision);
1191
1544
  reverted.push(migration.revision);
1192
1545
  }
1193
1546
  return reverted;
@@ -1326,23 +1679,24 @@ function parseRenameFlags(rest) {
1326
1679
  }
1327
1680
  return out;
1328
1681
  }
1329
- function pending(config, runner) {
1330
- const done = runner.applied();
1682
+ async function pending(config, runner) {
1683
+ const done = await runner.applied();
1331
1684
  return topoOrder(config.migrations).filter((m) => !done.has(m.revision));
1332
1685
  }
1333
- function runMigrationCli(argv, config) {
1686
+ async function runMigrationCli(argv, config) {
1334
1687
  const [command, ...rest] = argv;
1335
- const runner = new MigrationRunner(config.driver, config.dialect);
1688
+ const driver = toAsyncDriver(config.driver);
1689
+ const runner = new AsyncMigrationRunner(driver, config.dialect);
1336
1690
  const appliedAt = config.appliedAt ?? "1970-01-01T00:00:00.000Z";
1337
1691
  switch (command) {
1338
1692
  case "current": {
1339
- const applied = [...runner.applied()].sort();
1693
+ const applied = [...await runner.applied()].sort();
1340
1694
  return ok(applied.length > 0 ? applied : ["(no migrations applied)"]);
1341
1695
  }
1342
1696
  case "heads":
1343
1697
  return ok(heads(config.migrations));
1344
1698
  case "history": {
1345
- const done = runner.applied();
1699
+ const done = await runner.applied();
1346
1700
  return ok(
1347
1701
  topoOrder(config.migrations).map(
1348
1702
  (m) => `${done.has(m.revision) ? "\u2713" : "\xB7"} ${m.revision}${m.label ? ` \u2014 ${m.label}` : ""}`
@@ -1352,7 +1706,7 @@ function runMigrationCli(argv, config) {
1352
1706
  case "upgrade": {
1353
1707
  if (rest.includes("--sql")) {
1354
1708
  const lines = [];
1355
- for (const migration of pending(config, runner)) {
1709
+ for (const migration of await pending(config, runner)) {
1356
1710
  const op = new Op();
1357
1711
  migration.up(op);
1358
1712
  lines.push(`-- ${migration.revision}`);
@@ -1363,19 +1717,19 @@ function runMigrationCli(argv, config) {
1363
1717
  }
1364
1718
  return ok(lines.length > 0 ? lines : ["-- nothing to upgrade"]);
1365
1719
  }
1366
- const ran = runner.upgrade(config.migrations, appliedAt);
1720
+ const ran = await runner.upgrade(config.migrations, appliedAt);
1367
1721
  return ok(ran.length > 0 ? ran.map((r) => `applied ${r}`) : ["nothing to upgrade"]);
1368
1722
  }
1369
1723
  case "downgrade": {
1370
1724
  const steps = rest[0] ? Number(rest[0]) : 1;
1371
- const reverted = runner.downgrade(config.migrations, steps);
1725
+ const reverted = await runner.downgrade(config.migrations, steps);
1372
1726
  return ok(
1373
1727
  reverted.length > 0 ? reverted.map((r) => `reverted ${r}`) : ["nothing to downgrade"]
1374
1728
  );
1375
1729
  }
1376
1730
  case "check": {
1377
1731
  if (!config.models) return fail(["check requires models in the config"]);
1378
- const drift = config.dialect === "sqlite" ? checkDrift(config.driver, config.models) : [];
1732
+ const drift = await checkDriftAsync(driver, config.dialect, config.models);
1379
1733
  const undiffed = diffSchema(
1380
1734
  replaySchema(config.migrations),
1381
1735
  reflectSchema(config.models)
@@ -1504,7 +1858,7 @@ async function main(argv) {
1504
1858
  appliedAt: config.appliedAt ?? (/* @__PURE__ */ new Date()).toISOString()
1505
1859
  };
1506
1860
  const renameFlags = await promptRenames(withClock, rest);
1507
- const result = runMigrationCli([...rest, ...renameFlags], withClock);
1861
+ const result = await runMigrationCli([...rest, ...renameFlags], withClock);
1508
1862
  const sink = result.code === 0 ? process.stdout : process.stderr;
1509
1863
  for (const line of result.lines) sink.write(`${line}
1510
1864
  `);