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/README.md +13 -3
- package/dist/bin.cjs +604 -250
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +3 -3
- package/dist/bin.js.map +1 -1
- package/dist/{chunk-JR4MLFQN.js → chunk-G7O5DCCC.js} +1347 -289
- package/dist/chunk-G7O5DCCC.js.map +1 -0
- package/dist/{chunk-43XL66JG.js → chunk-KOW3LSWP.js} +209 -86
- package/dist/chunk-KOW3LSWP.js.map +1 -0
- package/dist/index.cjs +1357 -286
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +897 -42
- package/dist/index.d.ts +897 -42
- package/dist/index.js +1 -1
- package/dist/migrations/index.cjs +336 -117
- package/dist/migrations/index.cjs.map +1 -1
- package/dist/migrations/index.d.cts +58 -5
- package/dist/migrations/index.d.ts +58 -5
- package/dist/migrations/index.js +2 -2
- package/package.json +6 -1
- package/dist/chunk-43XL66JG.js.map +0 -1
- package/dist/chunk-JR4MLFQN.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) {
|
|
@@ -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")
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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)
|
|
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)
|
|
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 [
|
|
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,
|
|
753
|
-
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,
|
|
758
|
-
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
1134
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
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
|
|
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 =
|
|
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
|
`);
|