tempest-db-js 0.5.0 → 0.7.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) {
@@ -100,22 +310,6 @@ function makeRevisionId(label, parents) {
100
310
  return hash.toString(16).padStart(8, "0");
101
311
  }
102
312
 
103
- // src/expressions.ts
104
- function renderPortableToken(token, dialect) {
105
- switch (token) {
106
- case "now":
107
- return dialect === "postgresql" ? "now()" : "CURRENT_TIMESTAMP";
108
- case "current_date":
109
- return "CURRENT_DATE";
110
- case "current_time":
111
- return "CURRENT_TIME";
112
- case "uuidv4":
113
- if (dialect === "postgresql") return "gen_random_uuid()";
114
- if (dialect === "mysql") return "(UUID())";
115
- return "(lower(hex(randomblob(16))))";
116
- }
117
- }
118
-
119
313
  // src/migrations/ddl.ts
120
314
  function quoteId(name, dialect) {
121
315
  return dialect === "mysql" ? `\`${name.replace(/`/g, "``")}\`` : `"${name.replace(/"/g, '""')}"`;
@@ -643,208 +837,28 @@ function heads(migrations) {
643
837
  return migrations.map((m) => m.revision).filter((r) => !parents.has(r)).sort();
644
838
  }
645
839
 
646
- // src/index.ts
647
- function isDefaultValue(value) {
648
- return typeof value === "object" && value !== null && "kind" in value && (value.kind === "literal" || value.kind === "expression");
649
- }
650
- function bindsParameters(value) {
651
- return value.kind === "expression" && typeof value.expression === "object" && "parts" in value.expression;
840
+ // src/migrations/ir.ts
841
+ function constraintName(prefix, table, columns) {
842
+ return `${prefix}_${table}_${columns.join("_")}`;
652
843
  }
653
- function parseReference(ref, options) {
654
- const dot = ref.lastIndexOf(".");
655
- if (dot <= 0 || dot === ref.length - 1) {
656
- throw new Error(`Invalid foreign key reference "${ref}"; expected "table.column".`);
657
- }
658
- return {
659
- table: ref.slice(0, dot),
660
- column: ref.slice(dot + 1),
661
- onDelete: options?.onDelete,
662
- onUpdate: options?.onUpdate
663
- };
664
- }
665
- var Column = class _Column {
666
- constructor(type, flags, defaultValue = null, onUpdateValue = null, reference = null, dbName = null) {
667
- this.type = type;
668
- this.flags = flags;
669
- this.defaultValue = defaultValue;
670
- this.onUpdateValue = onUpdateValue;
671
- this.reference = reference;
672
- this.dbName = dbName;
673
- }
674
- type;
675
- flags;
676
- defaultValue;
677
- onUpdateValue;
678
- reference;
679
- dbName;
680
- /** Clone this column with one facet replaced, carrying every other over. */
681
- derive(patch) {
682
- return new _Column(
683
- this.type,
684
- patch.flags ?? this.flags,
685
- patch.defaultValue !== void 0 ? patch.defaultValue : this.defaultValue,
686
- patch.onUpdateValue !== void 0 ? patch.onUpdateValue : this.onUpdateValue,
687
- patch.reference !== void 0 ? patch.reference : this.reference,
688
- patch.dbName !== void 0 ? patch.dbName : this.dbName
689
- );
690
- }
691
- primaryKey() {
692
- return this.derive({ flags: { ...this.flags, primaryKey: true, hasDefault: true } });
693
- }
694
- notNull() {
695
- return this.derive({ flags: { ...this.flags, notNull: true } });
696
- }
697
- /**
698
- * Add a `UNIQUE` constraint to the column (mirrors SQLAlchemy's
699
- * `mapped_column(unique=True)`). DDL-only — does not change the inferred type.
700
- */
701
- unique() {
702
- return this.derive({ flags: { ...this.flags, unique: true } });
703
- }
704
- /**
705
- * Map this property to a differently-named database column, à la SQLAlchemy's
706
- * `mapped_column("consumer_name")` (Django's `db_column`, Prisma's `@map`).
707
- *
708
- * The override applies everywhere the name reaches SQL — select, insert,
709
- * update, delete, where, order by, group by, returning, conflict targets, the
710
- * migration IR and the drift check — while the TypeScript row keeps the
711
- * property name. Use it to keep a `snake_case` schema behind a `camelCase`
712
- * model; {@link Model.naming} does the same for a whole table at once.
713
- *
714
- * @param dbName The real column name in the database.
715
- * @returns A new column bound to that name.
716
- * @throws Error When `dbName` is empty.
717
- *
718
- * @example
719
- * ```ts
720
- * class ApiKey extends Model {
721
- * static tablename = "api_keys";
722
- * consumerName = column.text().name("consumer_name").notNull();
723
- * }
724
- * ```
725
- */
726
- name(dbName) {
727
- if (dbName.length === 0) {
728
- throw new Error("column.name() requires a non-empty database column name.");
729
- }
730
- return this.derive({ dbName });
731
- }
732
- /**
733
- * Declare a foreign-key reference to another table's column, à la SQLAlchemy's
734
- * `mapped_column(ForeignKey("table.column", ondelete=...))`. DDL-only — does
735
- * not change the inferred type.
736
- *
737
- * @param ref The target as `"table.column"` (e.g. `"users.id"`).
738
- * @param options Optional `onDelete` / `onUpdate` referential actions.
739
- * @returns A new column carrying the reference.
740
- * @throws Error When `ref` is not a valid `"table.column"` string.
741
- */
742
- references(ref, options) {
743
- return this.derive({ reference: parseReference(ref, options) });
744
- }
745
- /**
746
- * Set the insert-time default: a constant value of type `T`, or a portable
747
- * server-side expression from {@link sql} (e.g. `sql.now()`, `sql.uuidv4()`).
748
- *
749
- * @param value The literal default, or a {@link sql} expression.
750
- * @returns A new column carrying the default.
751
- * @throws Error When given a `sql.expr` fragment — a `DEFAULT` clause has
752
- * nowhere to bind parameters; use `sql.raw()` for a verbatim expression.
753
- */
754
- default(value) {
755
- const resolved = isDefaultValue(value) ? value : { kind: "literal", value };
756
- if (bindsParameters(resolved)) {
757
- throw new Error(
758
- "sql.expr`...` binds parameters and cannot be a column default \u2014 use sql.raw() for a verbatim DEFAULT expression."
759
- );
760
- }
761
- return this.derive({
762
- flags: { ...this.flags, hasDefault: true },
763
- defaultValue: resolved
764
- });
765
- }
766
- /**
767
- * Re-apply a value whenever the row is updated (e.g. an `updated_at` column
768
- * with `sql.now()`). Mirrors SQLAlchemy's `onupdate`.
769
- *
770
- * @param value The literal value, or a {@link sql} expression.
771
- * @returns A new column carrying the on-update value.
772
- * @throws Error When given a `sql.expr` fragment (see {@link Column.default}).
773
- */
774
- onUpdate(value) {
775
- const resolved = isDefaultValue(value) ? value : { kind: "literal", value };
776
- if (bindsParameters(resolved)) {
777
- throw new Error(
778
- "sql.expr`...` binds parameters and cannot be an onUpdate default \u2014 use sql.raw() for a verbatim expression."
779
- );
780
- }
781
- return this.derive({ onUpdateValue: resolved });
782
- }
783
- };
784
- function toSnakeCase(name) {
785
- return name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
786
- }
787
- var columnsCache = /* @__PURE__ */ new WeakMap();
788
- function columnsOf(model) {
789
- const cached = columnsCache.get(model);
790
- if (cached) return cached;
791
- const instance = new model();
792
- const out = {};
793
- for (const [key, value] of Object.entries(instance)) {
794
- if (value instanceof Column) {
795
- out[key] = value;
796
- }
797
- }
798
- columnsCache.set(model, out);
799
- return out;
800
- }
801
- var nameMapCache = /* @__PURE__ */ new WeakMap();
802
- function columnNamesOf(model) {
803
- const cached = nameMapCache.get(model);
804
- if (cached !== void 0) return cached;
805
- const strategy = model.naming ?? "preserve";
806
- const map = {};
807
- const seen = /* @__PURE__ */ new Map();
808
- let renamed = false;
809
- for (const [prop, col] of Object.entries(columnsOf(model))) {
810
- const dbName = col.dbName ?? (strategy === "snake_case" ? toSnakeCase(prop) : prop);
811
- const collision = seen.get(dbName);
812
- if (collision !== void 0) {
813
- throw new Error(
814
- `${model.tablename}: properties "${collision}" and "${prop}" both map to column "${dbName}".`
815
- );
816
- }
817
- seen.set(dbName, prop);
818
- map[prop] = dbName;
819
- if (dbName !== prop) renamed = true;
820
- }
821
- const result = renamed ? map : null;
822
- nameMapCache.set(model, result);
823
- return result;
824
- }
825
-
826
- // src/migrations/ir.ts
827
- function constraintName(prefix, table, columns) {
828
- return `${prefix}_${table}_${columns.join("_")}`;
829
- }
830
- function reflectTable(model) {
831
- const names = columnNamesOf(model);
832
- const toColumn = (prop) => names?.[prop] ?? prop;
833
- const columns = {};
834
- const primaryKey = [];
835
- for (const [prop, col] of Object.entries(columnsOf(model))) {
836
- const isPk = col.flags.primaryKey;
837
- const name = toColumn(prop);
838
- columns[name] = {
839
- name,
840
- type: col.type,
841
- notNull: col.flags.notNull || isPk,
842
- primaryKey: isPk,
843
- default: col.defaultValue,
844
- unique: col.flags.unique,
845
- references: col.reference
846
- };
847
- if (isPk) primaryKey.push(name);
844
+ function reflectTable(model) {
845
+ const names = columnNamesOf(model);
846
+ const toColumn = (prop) => names?.[prop] ?? prop;
847
+ const columns = {};
848
+ const primaryKey = [];
849
+ for (const [prop, col] of Object.entries(columnsOf(model))) {
850
+ const isPk = col.flags.primaryKey;
851
+ const name = toColumn(prop);
852
+ columns[name] = {
853
+ name,
854
+ type: col.type,
855
+ notNull: col.flags.notNull || isPk,
856
+ primaryKey: isPk,
857
+ default: col.defaultValue,
858
+ unique: col.flags.unique,
859
+ references: col.reference
860
+ };
861
+ if (isPk) primaryKey.push(name);
848
862
  }
849
863
  const uniqueConstraints = [];
850
864
  const foreignKeys = [];
@@ -903,74 +917,6 @@ function affinityToKind(affinity) {
903
917
  return "text";
904
918
  }
905
919
  }
906
- function sqliteForeignKeys(driver, table) {
907
- const rows = driver.execute(`PRAGMA foreign_key_list(${JSON.stringify(table)})`, []).rows;
908
- const byId = /* @__PURE__ */ new Map();
909
- for (const r of rows) {
910
- const list = byId.get(r.id) ?? [];
911
- list.push(r);
912
- byId.set(r.id, list);
913
- }
914
- const fks = [];
915
- for (const group of byId.values()) {
916
- const ordered = [...group].sort((a, b) => a.seq - b.seq);
917
- const columns = ordered.map((r) => r.from);
918
- fks.push({
919
- name: `fk_${table}_${columns.join("_")}`,
920
- columns,
921
- refTable: ordered[0]?.table ?? "",
922
- refColumns: ordered.map((r) => r.to)
923
- });
924
- }
925
- return fks;
926
- }
927
- function sqliteUniques(driver, table) {
928
- const indexes = driver.execute(`PRAGMA index_list(${JSON.stringify(table)})`, []).rows;
929
- const uniques = [];
930
- for (const idx of indexes) {
931
- if (Number(idx.unique) !== 1 || idx.origin === "pk") continue;
932
- const cols = driver.execute(`PRAGMA index_info(${JSON.stringify(idx.name)})`, []).rows.sort((a, b) => a.seqno - b.seqno).map((c) => c.name);
933
- if (cols.length > 0) {
934
- uniques.push({ name: `uq_${table}_${cols.join("_")}`, columns: cols });
935
- }
936
- }
937
- return uniques;
938
- }
939
- function introspectSqlite(driver) {
940
- const tablesRows = driver.execute(
941
- "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name != 'tempest_db_js_migrations'",
942
- []
943
- ).rows;
944
- const tables = {};
945
- for (const row of tablesRows) {
946
- const tableName = String(row.name);
947
- const info = driver.execute(`PRAGMA table_info(${JSON.stringify(tableName)})`, []).rows;
948
- const columns = {};
949
- const primaryKey = [];
950
- for (const col of info) {
951
- const isPk = Number(col.pk) > 0;
952
- const affinity = sqliteAffinity(col.type);
953
- columns[col.name] = {
954
- name: col.name,
955
- type: { kind: affinityToKind(affinity), meta: {} },
956
- notNull: Number(col.notnull) === 1 || isPk,
957
- primaryKey: isPk,
958
- default: null,
959
- unique: false,
960
- references: null
961
- };
962
- if (isPk) primaryKey.push(col.name);
963
- }
964
- tables[tableName] = {
965
- name: tableName,
966
- columns,
967
- primaryKey,
968
- uniqueConstraints: sqliteUniques(driver, tableName),
969
- foreignKeys: sqliteForeignKeys(driver, tableName)
970
- };
971
- }
972
- return { tables };
973
- }
974
920
  function constraintKeys(table) {
975
921
  const fks = /* @__PURE__ */ new Set();
976
922
  const uniques = /* @__PURE__ */ new Set();
@@ -988,9 +934,7 @@ function constraintKeys(table) {
988
934
  }
989
935
  return { fks, uniques };
990
936
  }
991
- function checkDrift(driver, models) {
992
- const actual = introspectSqlite(driver);
993
- const expected = reflectSchema(models);
937
+ function compareSqliteSchemas(actual, expected) {
994
938
  const issues = [];
995
939
  for (const [tableName, expectedTable] of Object.entries(expected.tables)) {
996
940
  const actualTable = actual.tables[tableName];
@@ -1061,6 +1005,299 @@ function checkDrift(driver, models) {
1061
1005
  }
1062
1006
  return issues;
1063
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
+ }
1064
1301
 
1065
1302
  // src/migrations/renames.ts
1066
1303
  function columnShape(col) {
@@ -1203,62 +1440,77 @@ var Op = class {
1203
1440
  }
1204
1441
  };
1205
1442
  var VERSION_TABLE = "tempest_db_js_migrations";
1206
- 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 {
1207
1450
  constructor(driver, dialect) {
1208
1451
  this.driver = driver;
1209
1452
  this.dialect = dialect;
1453
+ this.vt = quoteIdent(VERSION_TABLE, dialect);
1210
1454
  }
1211
1455
  driver;
1212
1456
  dialect;
1457
+ vt;
1213
1458
  /** Create the version-tracking table if it does not exist. */
1214
- ensureVersionTable() {
1215
- this.driver.execute(
1216
- `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)`,
1217
1462
  []
1218
1463
  );
1219
1464
  }
1465
+ /** A portable "text" column type for the version table. */
1466
+ textType() {
1467
+ return this.dialect === "mysql" ? "VARCHAR(255)" : "TEXT";
1468
+ }
1220
1469
  /** The set of applied revision ids. */
1221
- applied() {
1222
- this.ensureVersionTable();
1223
- 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}`, []);
1224
1473
  return new Set(rows.map((r) => String(r.revision)));
1225
1474
  }
1226
- runOps(ops) {
1475
+ async runOps(ops) {
1227
1476
  for (const op of ops) {
1228
1477
  for (const stmt of renderOperation(op, this.dialect)) {
1229
1478
  const trimmed = stmt.trim();
1230
1479
  if (trimmed.length === 0 || trimmed.startsWith("--")) continue;
1231
- this.driver.execute(stmt, []);
1480
+ await this.driver.execute(stmt, []);
1232
1481
  }
1233
1482
  }
1234
1483
  }
1235
- record(migration, appliedAt) {
1236
- this.driver.execute(
1237
- `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)})`,
1238
1488
  [migration.revision, appliedAt, migration.downRevision.join(",")]
1239
1489
  );
1240
1490
  }
1241
- forget(revision) {
1242
- 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
+ );
1243
1496
  }
1244
1497
  /**
1245
1498
  * Apply all pending migrations up to the head(s), in DAG order.
1246
1499
  *
1247
1500
  * @param migrations All known migrations.
1248
- * @param appliedAt Timestamp string to stamp (pass one in — the runtime has no
1249
- * wall clock of its own here).
1501
+ * @param appliedAt Timestamp string to stamp on each applied revision.
1250
1502
  * @returns The revision ids that were applied this run.
1251
1503
  */
1252
- upgrade(migrations, appliedAt) {
1253
- const done = this.applied();
1504
+ async upgrade(migrations, appliedAt) {
1505
+ const done = await this.applied();
1254
1506
  const ordered = topoOrder(migrations);
1255
1507
  const ran = [];
1256
1508
  for (const migration of ordered) {
1257
1509
  if (done.has(migration.revision)) continue;
1258
1510
  const op = new Op();
1259
1511
  migration.up(op);
1260
- this.runOps(op.operations);
1261
- this.record(migration, appliedAt);
1512
+ await this.runOps(op.operations);
1513
+ await this.record(migration, appliedAt);
1262
1514
  ran.push(migration.revision);
1263
1515
  }
1264
1516
  return ran;
@@ -1270,27 +1522,25 @@ var MigrationRunner = class {
1270
1522
  * @param steps How many applied revisions to roll back.
1271
1523
  * @returns The revision ids that were reverted.
1272
1524
  */
1273
- downgrade(migrations, steps = 1) {
1274
- const done = this.applied();
1525
+ async downgrade(migrations, steps = 1) {
1526
+ const done = await this.applied();
1275
1527
  const ordered = topoOrder(migrations).filter((m) => done.has(m.revision));
1276
1528
  const toRevert = ordered.slice(-steps).reverse();
1277
1529
  const reverted = [];
1278
1530
  for (const migration of toRevert) {
1279
1531
  const op = new Op();
1280
- if (migration.down.length >= 0) {
1281
- try {
1282
- migration.down(op);
1283
- } catch {
1284
- op.operations.length = 0;
1285
- }
1532
+ try {
1533
+ migration.down(op);
1534
+ } catch {
1535
+ op.operations.length = 0;
1286
1536
  }
1287
1537
  if (op.operations.length === 0) {
1288
1538
  const upOp = new Op();
1289
1539
  migration.up(upOp);
1290
1540
  op.operations.push(...invertAll(upOp.operations));
1291
1541
  }
1292
- this.runOps(op.operations);
1293
- this.forget(migration.revision);
1542
+ await this.runOps(op.operations);
1543
+ await this.forget(migration.revision);
1294
1544
  reverted.push(migration.revision);
1295
1545
  }
1296
1546
  return reverted;
@@ -1429,23 +1679,24 @@ function parseRenameFlags(rest) {
1429
1679
  }
1430
1680
  return out;
1431
1681
  }
1432
- function pending(config, runner) {
1433
- const done = runner.applied();
1682
+ async function pending(config, runner) {
1683
+ const done = await runner.applied();
1434
1684
  return topoOrder(config.migrations).filter((m) => !done.has(m.revision));
1435
1685
  }
1436
- function runMigrationCli(argv, config) {
1686
+ async function runMigrationCli(argv, config) {
1437
1687
  const [command, ...rest] = argv;
1438
- const runner = new MigrationRunner(config.driver, config.dialect);
1688
+ const driver = toAsyncDriver(config.driver);
1689
+ const runner = new AsyncMigrationRunner(driver, config.dialect);
1439
1690
  const appliedAt = config.appliedAt ?? "1970-01-01T00:00:00.000Z";
1440
1691
  switch (command) {
1441
1692
  case "current": {
1442
- const applied = [...runner.applied()].sort();
1693
+ const applied = [...await runner.applied()].sort();
1443
1694
  return ok(applied.length > 0 ? applied : ["(no migrations applied)"]);
1444
1695
  }
1445
1696
  case "heads":
1446
1697
  return ok(heads(config.migrations));
1447
1698
  case "history": {
1448
- const done = runner.applied();
1699
+ const done = await runner.applied();
1449
1700
  return ok(
1450
1701
  topoOrder(config.migrations).map(
1451
1702
  (m) => `${done.has(m.revision) ? "\u2713" : "\xB7"} ${m.revision}${m.label ? ` \u2014 ${m.label}` : ""}`
@@ -1455,7 +1706,7 @@ function runMigrationCli(argv, config) {
1455
1706
  case "upgrade": {
1456
1707
  if (rest.includes("--sql")) {
1457
1708
  const lines = [];
1458
- for (const migration of pending(config, runner)) {
1709
+ for (const migration of await pending(config, runner)) {
1459
1710
  const op = new Op();
1460
1711
  migration.up(op);
1461
1712
  lines.push(`-- ${migration.revision}`);
@@ -1466,19 +1717,19 @@ function runMigrationCli(argv, config) {
1466
1717
  }
1467
1718
  return ok(lines.length > 0 ? lines : ["-- nothing to upgrade"]);
1468
1719
  }
1469
- const ran = runner.upgrade(config.migrations, appliedAt);
1720
+ const ran = await runner.upgrade(config.migrations, appliedAt);
1470
1721
  return ok(ran.length > 0 ? ran.map((r) => `applied ${r}`) : ["nothing to upgrade"]);
1471
1722
  }
1472
1723
  case "downgrade": {
1473
1724
  const steps = rest[0] ? Number(rest[0]) : 1;
1474
- const reverted = runner.downgrade(config.migrations, steps);
1725
+ const reverted = await runner.downgrade(config.migrations, steps);
1475
1726
  return ok(
1476
1727
  reverted.length > 0 ? reverted.map((r) => `reverted ${r}`) : ["nothing to downgrade"]
1477
1728
  );
1478
1729
  }
1479
1730
  case "check": {
1480
1731
  if (!config.models) return fail(["check requires models in the config"]);
1481
- const drift = config.dialect === "sqlite" ? checkDrift(config.driver, config.models) : [];
1732
+ const drift = await checkDriftAsync(driver, config.dialect, config.models);
1482
1733
  const undiffed = diffSchema(
1483
1734
  replaySchema(config.migrations),
1484
1735
  reflectSchema(config.models)
@@ -1607,7 +1858,7 @@ async function main(argv) {
1607
1858
  appliedAt: config.appliedAt ?? (/* @__PURE__ */ new Date()).toISOString()
1608
1859
  };
1609
1860
  const renameFlags = await promptRenames(withClock, rest);
1610
- const result = runMigrationCli([...rest, ...renameFlags], withClock);
1861
+ const result = await runMigrationCli([...rest, ...renameFlags], withClock);
1611
1862
  const sink = result.code === 0 ? process.stdout : process.stderr;
1612
1863
  for (const line of result.lines) sink.write(`${line}
1613
1864
  `);