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/README.md +8 -4
- package/dist/bin.cjs +580 -329
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +3 -3
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-5QQMVTS5.js → chunk-4AWUP7BM.js} +540 -57
- package/dist/chunk-4AWUP7BM.js.map +1 -0
- package/dist/{chunk-EPMLFNFK.js → chunk-SI4CLSF7.js} +109 -59
- package/dist/chunk-SI4CLSF7.js.map +1 -0
- package/dist/index.cjs +544 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +430 -22
- package/dist/index.d.ts +430 -22
- package/dist/index.js +1 -1
- package/dist/migrations/index.cjs +122 -55
- package/dist/migrations/index.cjs.map +1 -1
- package/dist/migrations/index.d.cts +46 -3
- package/dist/migrations/index.d.ts +46 -3
- package/dist/migrations/index.js +2 -2
- package/package.json +6 -1
- package/dist/chunk-5QQMVTS5.js.map +0 -1
- package/dist/chunk-EPMLFNFK.js.map +0 -1
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/
|
|
647
|
-
function
|
|
648
|
-
return
|
|
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
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
1237
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
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
|
|
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 =
|
|
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
|
`);
|