tempest-db-js 0.2.0 → 0.4.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 +3 -2
- package/dist/bin.cjs +429 -42
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +2 -2
- package/dist/{chunk-QMW4NKMH.js → chunk-43XL66JG.js} +546 -43
- package/dist/chunk-43XL66JG.js.map +1 -0
- package/dist/{chunk-AGDD7K3F.js → chunk-JR4MLFQN.js} +217 -22
- package/dist/chunk-JR4MLFQN.js.map +1 -0
- package/dist/index.cjs +217 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +129 -8
- package/dist/index.d.ts +129 -8
- package/dist/index.js +1 -1
- package/dist/migrations/index.cjs +597 -44
- package/dist/migrations/index.cjs.map +1 -1
- package/dist/migrations/index.d.cts +91 -15
- package/dist/migrations/index.d.ts +91 -15
- package/dist/migrations/index.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-AGDD7K3F.js.map +0 -1
- package/dist/chunk-QMW4NKMH.js.map +0 -1
|
@@ -4,23 +4,38 @@
|
|
|
4
4
|
function isDefaultValue(value) {
|
|
5
5
|
return typeof value === "object" && value !== null && "kind" in value && (value.kind === "literal" || value.kind === "expression");
|
|
6
6
|
}
|
|
7
|
+
function parseReference(ref, options) {
|
|
8
|
+
const dot = ref.lastIndexOf(".");
|
|
9
|
+
if (dot <= 0 || dot === ref.length - 1) {
|
|
10
|
+
throw new Error(`Invalid foreign key reference "${ref}"; expected "table.column".`);
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
table: ref.slice(0, dot),
|
|
14
|
+
column: ref.slice(dot + 1),
|
|
15
|
+
onDelete: options?.onDelete,
|
|
16
|
+
onUpdate: options?.onUpdate
|
|
17
|
+
};
|
|
18
|
+
}
|
|
7
19
|
var Column = class _Column {
|
|
8
|
-
constructor(type, flags, defaultValue = null, onUpdateValue = null) {
|
|
20
|
+
constructor(type, flags, defaultValue = null, onUpdateValue = null, reference = null) {
|
|
9
21
|
this.type = type;
|
|
10
22
|
this.flags = flags;
|
|
11
23
|
this.defaultValue = defaultValue;
|
|
12
24
|
this.onUpdateValue = onUpdateValue;
|
|
25
|
+
this.reference = reference;
|
|
13
26
|
}
|
|
14
27
|
type;
|
|
15
28
|
flags;
|
|
16
29
|
defaultValue;
|
|
17
30
|
onUpdateValue;
|
|
31
|
+
reference;
|
|
18
32
|
primaryKey() {
|
|
19
33
|
return new _Column(
|
|
20
34
|
this.type,
|
|
21
35
|
{ ...this.flags, primaryKey: true, hasDefault: true },
|
|
22
36
|
this.defaultValue,
|
|
23
|
-
this.onUpdateValue
|
|
37
|
+
this.onUpdateValue,
|
|
38
|
+
this.reference
|
|
24
39
|
);
|
|
25
40
|
}
|
|
26
41
|
notNull() {
|
|
@@ -28,7 +43,40 @@ var Column = class _Column {
|
|
|
28
43
|
this.type,
|
|
29
44
|
{ ...this.flags, notNull: true },
|
|
30
45
|
this.defaultValue,
|
|
31
|
-
this.onUpdateValue
|
|
46
|
+
this.onUpdateValue,
|
|
47
|
+
this.reference
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Add a `UNIQUE` constraint to the column (mirrors SQLAlchemy's
|
|
52
|
+
* `mapped_column(unique=True)`). DDL-only — does not change the inferred type.
|
|
53
|
+
*/
|
|
54
|
+
unique() {
|
|
55
|
+
return new _Column(
|
|
56
|
+
this.type,
|
|
57
|
+
{ ...this.flags, unique: true },
|
|
58
|
+
this.defaultValue,
|
|
59
|
+
this.onUpdateValue,
|
|
60
|
+
this.reference
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Declare a foreign-key reference to another table's column, à la SQLAlchemy's
|
|
65
|
+
* `mapped_column(ForeignKey("table.column", ondelete=...))`. DDL-only — does
|
|
66
|
+
* not change the inferred type.
|
|
67
|
+
*
|
|
68
|
+
* @param ref The target as `"table.column"` (e.g. `"users.id"`).
|
|
69
|
+
* @param options Optional `onDelete` / `onUpdate` referential actions.
|
|
70
|
+
* @returns A new column carrying the reference.
|
|
71
|
+
* @throws Error When `ref` is not a valid `"table.column"` string.
|
|
72
|
+
*/
|
|
73
|
+
references(ref, options) {
|
|
74
|
+
return new _Column(
|
|
75
|
+
this.type,
|
|
76
|
+
this.flags,
|
|
77
|
+
this.defaultValue,
|
|
78
|
+
this.onUpdateValue,
|
|
79
|
+
parseReference(ref, options)
|
|
32
80
|
);
|
|
33
81
|
}
|
|
34
82
|
/**
|
|
@@ -41,7 +89,8 @@ var Column = class _Column {
|
|
|
41
89
|
this.type,
|
|
42
90
|
{ ...this.flags, hasDefault: true },
|
|
43
91
|
resolved,
|
|
44
|
-
this.onUpdateValue
|
|
92
|
+
this.onUpdateValue,
|
|
93
|
+
this.reference
|
|
45
94
|
);
|
|
46
95
|
}
|
|
47
96
|
/**
|
|
@@ -50,7 +99,7 @@ var Column = class _Column {
|
|
|
50
99
|
*/
|
|
51
100
|
onUpdate(value) {
|
|
52
101
|
const resolved = isDefaultValue(value) ? value : { kind: "literal", value };
|
|
53
|
-
return new _Column(this.type, this.flags, this.defaultValue, resolved);
|
|
102
|
+
return new _Column(this.type, this.flags, this.defaultValue, resolved, this.reference);
|
|
54
103
|
}
|
|
55
104
|
};
|
|
56
105
|
var columnsCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -69,6 +118,9 @@ function columnsOf(model) {
|
|
|
69
118
|
}
|
|
70
119
|
|
|
71
120
|
// src/migrations/ir.ts
|
|
121
|
+
function constraintName(prefix, table, columns) {
|
|
122
|
+
return `${prefix}_${table}_${columns.join("_")}`;
|
|
123
|
+
}
|
|
72
124
|
function reflectTable(model) {
|
|
73
125
|
const columns = {};
|
|
74
126
|
const primaryKey = [];
|
|
@@ -79,11 +131,32 @@ function reflectTable(model) {
|
|
|
79
131
|
type: col.type,
|
|
80
132
|
notNull: col.flags.notNull || isPk,
|
|
81
133
|
primaryKey: isPk,
|
|
82
|
-
default: col.defaultValue
|
|
134
|
+
default: col.defaultValue,
|
|
135
|
+
unique: col.flags.unique,
|
|
136
|
+
references: col.reference
|
|
83
137
|
};
|
|
84
138
|
if (isPk) primaryKey.push(name);
|
|
85
139
|
}
|
|
86
|
-
|
|
140
|
+
const uniqueConstraints = [];
|
|
141
|
+
const foreignKeys = [];
|
|
142
|
+
for (const c of model.tableArgs?.() ?? []) {
|
|
143
|
+
if (c.kind === "unique") {
|
|
144
|
+
uniqueConstraints.push({
|
|
145
|
+
name: c.name ?? constraintName("uq", model.tablename, c.columns),
|
|
146
|
+
columns: c.columns
|
|
147
|
+
});
|
|
148
|
+
} else {
|
|
149
|
+
foreignKeys.push({
|
|
150
|
+
name: c.name ?? constraintName("fk", model.tablename, c.columns),
|
|
151
|
+
columns: c.columns,
|
|
152
|
+
refTable: c.refTable,
|
|
153
|
+
refColumns: c.refColumns,
|
|
154
|
+
onDelete: c.onDelete,
|
|
155
|
+
onUpdate: c.onUpdate
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return { name: model.tablename, columns, primaryKey, uniqueConstraints, foreignKeys };
|
|
87
160
|
}
|
|
88
161
|
function reflectSchema(models) {
|
|
89
162
|
const tables = {};
|
|
@@ -128,6 +201,10 @@ function invert(op) {
|
|
|
128
201
|
return { kind: "rename_column", table: op.table, from: op.to, to: op.from };
|
|
129
202
|
case "recreate_table":
|
|
130
203
|
return { kind: "recreate_table", from: op.to, to: op.from };
|
|
204
|
+
case "add_constraint":
|
|
205
|
+
return { kind: "drop_constraint", table: op.table, constraint: op.constraint };
|
|
206
|
+
case "drop_constraint":
|
|
207
|
+
return { kind: "add_constraint", table: op.table, constraint: op.constraint };
|
|
131
208
|
case "execute":
|
|
132
209
|
if (op.down === null) {
|
|
133
210
|
throw new IrreversibleMigration("execute() operation has no down SQL");
|
|
@@ -140,8 +217,8 @@ function invertAll(ops) {
|
|
|
140
217
|
}
|
|
141
218
|
|
|
142
219
|
// src/migrations/ddl.ts
|
|
143
|
-
function quoteId(name) {
|
|
144
|
-
return `"${name.replace(/"/g, '""')}"`;
|
|
220
|
+
function quoteId(name, dialect) {
|
|
221
|
+
return dialect === "mysql" ? `\`${name.replace(/`/g, "``")}\`` : `"${name.replace(/"/g, '""')}"`;
|
|
145
222
|
}
|
|
146
223
|
function quoteLiteral(value) {
|
|
147
224
|
return `'${value.replace(/'/g, "''")}'`;
|
|
@@ -166,6 +243,45 @@ function renderColumnType(type, dialect) {
|
|
|
166
243
|
return "TEXT";
|
|
167
244
|
}
|
|
168
245
|
}
|
|
246
|
+
if (dialect === "mysql") {
|
|
247
|
+
switch (kind) {
|
|
248
|
+
case "smallint":
|
|
249
|
+
return "SMALLINT";
|
|
250
|
+
case "integer":
|
|
251
|
+
return "INT";
|
|
252
|
+
case "bigint":
|
|
253
|
+
return "BIGINT";
|
|
254
|
+
case "numeric":
|
|
255
|
+
return meta.precision !== void 0 ? `DECIMAL(${meta.precision}${meta.scale !== void 0 ? `, ${meta.scale}` : ""})` : "DECIMAL";
|
|
256
|
+
case "real":
|
|
257
|
+
return "FLOAT";
|
|
258
|
+
case "double":
|
|
259
|
+
return "DOUBLE";
|
|
260
|
+
case "varchar":
|
|
261
|
+
return `VARCHAR(${meta.length ?? 255})`;
|
|
262
|
+
case "char":
|
|
263
|
+
return `CHAR(${meta.length ?? 255})`;
|
|
264
|
+
case "text":
|
|
265
|
+
return "TEXT";
|
|
266
|
+
case "boolean":
|
|
267
|
+
return "TINYINT(1)";
|
|
268
|
+
case "date":
|
|
269
|
+
return "DATE";
|
|
270
|
+
case "time":
|
|
271
|
+
return "TIME";
|
|
272
|
+
case "datetime":
|
|
273
|
+
case "timestamp":
|
|
274
|
+
return "DATETIME";
|
|
275
|
+
case "blob":
|
|
276
|
+
return "BLOB";
|
|
277
|
+
case "json":
|
|
278
|
+
return "JSON";
|
|
279
|
+
case "uuid":
|
|
280
|
+
return "CHAR(36)";
|
|
281
|
+
case "enum":
|
|
282
|
+
return `ENUM(${(meta.values ?? []).map(quoteLiteral).join(", ")})`;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
169
285
|
switch (kind) {
|
|
170
286
|
case "smallint":
|
|
171
287
|
return "SMALLINT";
|
|
@@ -210,29 +326,66 @@ function renderDefault(def, dialect) {
|
|
|
210
326
|
if (typeof expr === "object") return expr.raw;
|
|
211
327
|
switch (expr) {
|
|
212
328
|
case "now":
|
|
213
|
-
return dialect === "
|
|
329
|
+
return dialect === "postgresql" ? "now()" : "CURRENT_TIMESTAMP";
|
|
214
330
|
case "current_date":
|
|
215
331
|
return "CURRENT_DATE";
|
|
216
332
|
case "current_time":
|
|
217
333
|
return "CURRENT_TIME";
|
|
218
334
|
case "uuidv4":
|
|
219
|
-
|
|
335
|
+
if (dialect === "postgresql") return "gen_random_uuid()";
|
|
336
|
+
if (dialect === "mysql") return "(UUID())";
|
|
337
|
+
return "(lower(hex(randomblob(16))))";
|
|
220
338
|
}
|
|
221
339
|
}
|
|
222
340
|
const value = def.value;
|
|
223
341
|
if (value === null) return "NULL";
|
|
224
342
|
if (typeof value === "boolean") {
|
|
225
|
-
return dialect === "
|
|
343
|
+
return dialect === "postgresql" ? value ? "TRUE" : "FALSE" : value ? "1" : "0";
|
|
226
344
|
}
|
|
227
345
|
if (typeof value === "number" || typeof value === "bigint") return String(value);
|
|
228
346
|
if (value instanceof Date) return quoteLiteral(value.toISOString());
|
|
229
347
|
if (typeof value === "object") return quoteLiteral(JSON.stringify(value));
|
|
230
348
|
return quoteLiteral(String(value));
|
|
231
349
|
}
|
|
350
|
+
function renderFkAction(action) {
|
|
351
|
+
return action.toUpperCase();
|
|
352
|
+
}
|
|
353
|
+
function renderFkActions(fk) {
|
|
354
|
+
let sql = "";
|
|
355
|
+
if (fk.onDelete) sql += ` ON DELETE ${renderFkAction(fk.onDelete)}`;
|
|
356
|
+
if (fk.onUpdate) sql += ` ON UPDATE ${renderFkAction(fk.onUpdate)}`;
|
|
357
|
+
return sql;
|
|
358
|
+
}
|
|
359
|
+
function columnConstraintSuffix(col, dialect) {
|
|
360
|
+
let sql = "";
|
|
361
|
+
if (col.unique) sql += " UNIQUE";
|
|
362
|
+
if (col.references) {
|
|
363
|
+
const ref = col.references;
|
|
364
|
+
sql += ` REFERENCES ${quoteId(ref.table, dialect)} (${quoteId(ref.column, dialect)})`;
|
|
365
|
+
sql += renderFkActions(ref);
|
|
366
|
+
}
|
|
367
|
+
return sql;
|
|
368
|
+
}
|
|
369
|
+
function renderUniqueConstraint(uc, dialect) {
|
|
370
|
+
const cols = uc.columns.map((c) => quoteId(c, dialect)).join(", ");
|
|
371
|
+
return `CONSTRAINT ${quoteId(uc.name, dialect)} UNIQUE (${cols})`;
|
|
372
|
+
}
|
|
373
|
+
function renderForeignKeyConstraint(fk, dialect) {
|
|
374
|
+
const cols = fk.columns.map((c) => quoteId(c, dialect)).join(", ");
|
|
375
|
+
const refCols = fk.refColumns.map((c) => quoteId(c, dialect)).join(", ");
|
|
376
|
+
return `CONSTRAINT ${quoteId(fk.name, dialect)} FOREIGN KEY (${cols}) REFERENCES ${quoteId(fk.refTable, dialect)} (${refCols})${renderFkActions(fk)}`;
|
|
377
|
+
}
|
|
378
|
+
function tableConstraintClauses(table, dialect) {
|
|
379
|
+
return [
|
|
380
|
+
...table.uniqueConstraints.map((uc) => renderUniqueConstraint(uc, dialect)),
|
|
381
|
+
...table.foreignKeys.map((fk) => renderForeignKeyConstraint(fk, dialect))
|
|
382
|
+
];
|
|
383
|
+
}
|
|
232
384
|
function renderColumnDef(col, dialect) {
|
|
233
|
-
let sql = `${quoteId(col.name)} ${renderColumnType(col.type, dialect)}`;
|
|
385
|
+
let sql = `${quoteId(col.name, dialect)} ${renderColumnType(col.type, dialect)}`;
|
|
234
386
|
if (col.notNull) sql += " NOT NULL";
|
|
235
387
|
if (col.default !== null) sql += ` DEFAULT ${renderDefault(col.default, dialect)}`;
|
|
388
|
+
sql += columnConstraintSuffix(col, dialect);
|
|
236
389
|
return sql;
|
|
237
390
|
}
|
|
238
391
|
function enumTypeName(table, column) {
|
|
@@ -252,23 +405,29 @@ function renderCreateTable(table, dialect) {
|
|
|
252
405
|
if (dialect === "postgresql" && c.type.kind === "enum") {
|
|
253
406
|
const typeName = enumTypeName(table.name, c.name);
|
|
254
407
|
const values = (c.type.meta.values ?? []).map(quoteLiteral).join(", ");
|
|
255
|
-
typeStmts.push(`CREATE TYPE ${quoteId(typeName)} AS ENUM (${values})`);
|
|
256
|
-
let def = `${quoteId(c.name)} ${quoteId(typeName)}`;
|
|
408
|
+
typeStmts.push(`CREATE TYPE ${quoteId(typeName, dialect)} AS ENUM (${values})`);
|
|
409
|
+
let def = `${quoteId(c.name, dialect)} ${quoteId(typeName, dialect)}`;
|
|
257
410
|
if (c.notNull) def += " NOT NULL";
|
|
258
411
|
if (c.default !== null) def += ` DEFAULT ${renderDefault(c.default, dialect)}`;
|
|
259
|
-
return def;
|
|
412
|
+
return def + columnConstraintSuffix(c, dialect);
|
|
260
413
|
}
|
|
261
414
|
if (dialect === "postgresql" && isAutoIncrementPk(table, c)) {
|
|
262
|
-
return `${quoteId(c.name)} ${postgresSerialType(c.type.kind)}`;
|
|
415
|
+
return `${quoteId(c.name, dialect)} ${postgresSerialType(c.type.kind)}${columnConstraintSuffix(c, dialect)}`;
|
|
416
|
+
}
|
|
417
|
+
if (dialect === "mysql" && isAutoIncrementPk(table, c)) {
|
|
418
|
+
return `${quoteId(c.name, dialect)} ${renderColumnType(c.type, dialect)} NOT NULL AUTO_INCREMENT${columnConstraintSuffix(c, dialect)}`;
|
|
263
419
|
}
|
|
264
420
|
return renderColumnDef(c, dialect);
|
|
265
421
|
});
|
|
266
422
|
if (table.primaryKey.length > 0) {
|
|
267
|
-
cols.push(
|
|
423
|
+
cols.push(
|
|
424
|
+
`PRIMARY KEY (${table.primaryKey.map((c) => quoteId(c, dialect)).join(", ")})`
|
|
425
|
+
);
|
|
268
426
|
}
|
|
427
|
+
cols.push(...tableConstraintClauses(table, dialect));
|
|
269
428
|
return [
|
|
270
429
|
...typeStmts,
|
|
271
|
-
`CREATE TABLE ${quoteId(table.name)} (
|
|
430
|
+
`CREATE TABLE ${quoteId(table.name, dialect)} (
|
|
272
431
|
${cols.join(",\n ")}
|
|
273
432
|
)`
|
|
274
433
|
];
|
|
@@ -278,60 +437,97 @@ function renderOperation(op, dialect) {
|
|
|
278
437
|
case "create_table":
|
|
279
438
|
return renderCreateTable(op.table, dialect);
|
|
280
439
|
case "drop_table":
|
|
281
|
-
return [`DROP TABLE ${quoteId(op.table.name)}`];
|
|
440
|
+
return [`DROP TABLE ${quoteId(op.table.name, dialect)}`];
|
|
282
441
|
case "rename_table":
|
|
283
|
-
return [`
|
|
442
|
+
return dialect === "mysql" ? [`RENAME TABLE ${quoteId(op.from, dialect)} TO ${quoteId(op.to, dialect)}`] : [
|
|
443
|
+
`ALTER TABLE ${quoteId(op.from, dialect)} RENAME TO ${quoteId(op.to, dialect)}`
|
|
444
|
+
];
|
|
284
445
|
case "add_column":
|
|
285
446
|
return [
|
|
286
|
-
`ALTER TABLE ${quoteId(op.table)} ADD COLUMN ${renderColumnDef(op.column, dialect)}`
|
|
447
|
+
`ALTER TABLE ${quoteId(op.table, dialect)} ADD COLUMN ${renderColumnDef(op.column, dialect)}`
|
|
287
448
|
];
|
|
288
449
|
case "drop_column":
|
|
289
|
-
return [
|
|
450
|
+
return [
|
|
451
|
+
`ALTER TABLE ${quoteId(op.table, dialect)} DROP COLUMN ${quoteId(op.column.name, dialect)}`
|
|
452
|
+
];
|
|
290
453
|
case "rename_column":
|
|
291
454
|
return [
|
|
292
|
-
`ALTER TABLE ${quoteId(op.table)} RENAME COLUMN ${quoteId(op.from)} TO ${quoteId(op.to)}`
|
|
455
|
+
`ALTER TABLE ${quoteId(op.table, dialect)} RENAME COLUMN ${quoteId(op.from, dialect)} TO ${quoteId(op.to, dialect)}`
|
|
293
456
|
];
|
|
294
457
|
case "alter_column":
|
|
295
458
|
return renderAlterColumn(op.table, op.to, dialect);
|
|
296
459
|
case "recreate_table":
|
|
297
|
-
return dialect === "sqlite" ? renderSqliteRebuild(op.from, op.to) :
|
|
460
|
+
return dialect === "sqlite" ? renderSqliteRebuild(op.from, op.to) : renderTableDiff(op.from, op.to, dialect);
|
|
461
|
+
case "add_constraint":
|
|
462
|
+
return renderAddConstraint(op.table, op.constraint, dialect);
|
|
463
|
+
case "drop_constraint":
|
|
464
|
+
return renderDropConstraint(op.table, op.constraint, dialect);
|
|
298
465
|
case "execute":
|
|
299
466
|
return [op.up];
|
|
300
467
|
}
|
|
301
468
|
}
|
|
469
|
+
function renderAddConstraint(table, constraint, dialect) {
|
|
470
|
+
if (dialect === "sqlite") {
|
|
471
|
+
throw new Error(
|
|
472
|
+
`add_constraint on SQLite needs a table-rebuild (recreate_table); constraint ${constraint.constraint.name} on ${table}`
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
const clause = constraint.type === "unique" ? renderUniqueConstraint(constraint.constraint, dialect) : renderForeignKeyConstraint(constraint.constraint, dialect);
|
|
476
|
+
return [`ALTER TABLE ${quoteId(table, dialect)} ADD ${clause}`];
|
|
477
|
+
}
|
|
478
|
+
function renderDropConstraint(table, constraint, dialect) {
|
|
479
|
+
if (dialect === "sqlite") {
|
|
480
|
+
throw new Error(
|
|
481
|
+
`drop_constraint on SQLite needs a table-rebuild (recreate_table); constraint ${constraint.constraint.name} on ${table}`
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
const t = quoteId(table, dialect);
|
|
485
|
+
const name = quoteId(constraint.constraint.name, dialect);
|
|
486
|
+
if (dialect === "mysql") {
|
|
487
|
+
return [
|
|
488
|
+
constraint.type === "unique" ? `ALTER TABLE ${t} DROP INDEX ${name}` : `ALTER TABLE ${t} DROP FOREIGN KEY ${name}`
|
|
489
|
+
];
|
|
490
|
+
}
|
|
491
|
+
return [`ALTER TABLE ${t} DROP CONSTRAINT ${name}`];
|
|
492
|
+
}
|
|
302
493
|
function renderSqliteRebuild(from, to) {
|
|
303
494
|
const tmp = `__new_${to.name}`;
|
|
304
495
|
const common = Object.keys(to.columns).filter((c) => c in from.columns);
|
|
305
496
|
const cols = Object.values(to.columns).map((c) => renderColumnDef(c, "sqlite"));
|
|
306
497
|
if (to.primaryKey.length > 0) {
|
|
307
|
-
cols.push(
|
|
498
|
+
cols.push(
|
|
499
|
+
`PRIMARY KEY (${to.primaryKey.map((c) => quoteId(c, "sqlite")).join(", ")})`
|
|
500
|
+
);
|
|
308
501
|
}
|
|
309
|
-
|
|
502
|
+
cols.push(...tableConstraintClauses(to, "sqlite"));
|
|
503
|
+
const commonSql = common.map((c) => quoteId(c, "sqlite")).join(", ");
|
|
310
504
|
return [
|
|
311
505
|
"PRAGMA foreign_keys=off",
|
|
312
|
-
`CREATE TABLE ${quoteId(tmp)} (
|
|
506
|
+
`CREATE TABLE ${quoteId(tmp, "sqlite")} (
|
|
313
507
|
${cols.join(",\n ")}
|
|
314
508
|
)`,
|
|
315
|
-
common.length > 0 ? `INSERT INTO ${quoteId(tmp)} (${commonSql}) SELECT ${commonSql} FROM ${quoteId(from.name)}` : `-- no common columns to copy from ${from.name}`,
|
|
316
|
-
`DROP TABLE ${quoteId(from.name)}`,
|
|
317
|
-
`ALTER TABLE ${quoteId(tmp)} RENAME TO ${quoteId(to.name)}`,
|
|
509
|
+
common.length > 0 ? `INSERT INTO ${quoteId(tmp, "sqlite")} (${commonSql}) SELECT ${commonSql} FROM ${quoteId(from.name, "sqlite")}` : `-- no common columns to copy from ${from.name}`,
|
|
510
|
+
`DROP TABLE ${quoteId(from.name, "sqlite")}`,
|
|
511
|
+
`ALTER TABLE ${quoteId(tmp, "sqlite")} RENAME TO ${quoteId(to.name, "sqlite")}`,
|
|
318
512
|
"PRAGMA foreign_keys=on"
|
|
319
513
|
];
|
|
320
514
|
}
|
|
321
|
-
function
|
|
515
|
+
function renderTableDiff(from, to, dialect) {
|
|
322
516
|
const stmts = [];
|
|
323
517
|
for (const [name, col] of Object.entries(to.columns)) {
|
|
324
518
|
if (!(name in from.columns)) {
|
|
325
519
|
stmts.push(
|
|
326
|
-
`ALTER TABLE ${quoteId(to.name)} ADD COLUMN ${renderColumnDef(col,
|
|
520
|
+
`ALTER TABLE ${quoteId(to.name, dialect)} ADD COLUMN ${renderColumnDef(col, dialect)}`
|
|
327
521
|
);
|
|
328
522
|
} else {
|
|
329
|
-
stmts.push(...renderAlterColumn(to.name, col,
|
|
523
|
+
stmts.push(...renderAlterColumn(to.name, col, dialect));
|
|
330
524
|
}
|
|
331
525
|
}
|
|
332
526
|
for (const name of Object.keys(from.columns)) {
|
|
333
527
|
if (!(name in to.columns)) {
|
|
334
|
-
stmts.push(
|
|
528
|
+
stmts.push(
|
|
529
|
+
`ALTER TABLE ${quoteId(to.name, dialect)} DROP COLUMN ${quoteId(name, dialect)}`
|
|
530
|
+
);
|
|
335
531
|
}
|
|
336
532
|
}
|
|
337
533
|
return stmts;
|
|
@@ -339,11 +535,16 @@ function renderPostgresTableDiff(from, to) {
|
|
|
339
535
|
function renderAlterColumn(table, to, dialect) {
|
|
340
536
|
if (dialect === "sqlite") {
|
|
341
537
|
throw new Error(
|
|
342
|
-
`alter_column on SQLite needs
|
|
538
|
+
`alter_column on SQLite needs a table-rebuild (recreate_table); column ${table}.${to.name}`
|
|
343
539
|
);
|
|
344
540
|
}
|
|
345
|
-
|
|
346
|
-
|
|
541
|
+
if (dialect === "mysql") {
|
|
542
|
+
return [
|
|
543
|
+
`ALTER TABLE ${quoteId(table, dialect)} MODIFY COLUMN ${renderColumnDef(to, dialect)}`
|
|
544
|
+
];
|
|
545
|
+
}
|
|
546
|
+
const t = quoteId(table, dialect);
|
|
547
|
+
const c = quoteId(to.name, dialect);
|
|
347
548
|
const stmts = [
|
|
348
549
|
`ALTER TABLE ${t} ALTER COLUMN ${c} TYPE ${renderColumnType(to.type, dialect)}`
|
|
349
550
|
];
|
|
@@ -362,9 +563,74 @@ function columnSignature(col) {
|
|
|
362
563
|
type: col.type,
|
|
363
564
|
notNull: col.notNull,
|
|
364
565
|
primaryKey: col.primaryKey,
|
|
365
|
-
default: col.default
|
|
566
|
+
default: col.default,
|
|
567
|
+
unique: col.unique,
|
|
568
|
+
references: col.references
|
|
366
569
|
});
|
|
367
570
|
}
|
|
571
|
+
function uniqueSignature(uc) {
|
|
572
|
+
return JSON.stringify({ columns: uc.columns });
|
|
573
|
+
}
|
|
574
|
+
function foreignKeySignature(fk) {
|
|
575
|
+
return JSON.stringify({
|
|
576
|
+
columns: fk.columns,
|
|
577
|
+
refTable: fk.refTable,
|
|
578
|
+
refColumns: fk.refColumns,
|
|
579
|
+
onDelete: fk.onDelete ?? null,
|
|
580
|
+
onUpdate: fk.onUpdate ?? null
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
function diffConstraints(current, target) {
|
|
584
|
+
const ops = [];
|
|
585
|
+
const table = target.name;
|
|
586
|
+
const currentUq = new Map(current.uniqueConstraints.map((u) => [u.name, u]));
|
|
587
|
+
const targetUq = new Map(target.uniqueConstraints.map((u) => [u.name, u]));
|
|
588
|
+
for (const [name, cur] of currentUq) {
|
|
589
|
+
const tgt = targetUq.get(name);
|
|
590
|
+
if (!tgt || uniqueSignature(cur) !== uniqueSignature(tgt)) {
|
|
591
|
+
ops.push(dropUnique(table, cur));
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
for (const [name, tgt] of targetUq) {
|
|
595
|
+
const cur = currentUq.get(name);
|
|
596
|
+
if (!cur || uniqueSignature(cur) !== uniqueSignature(tgt)) {
|
|
597
|
+
ops.push(addUnique(table, tgt));
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
const currentFk = new Map(current.foreignKeys.map((f) => [f.name, f]));
|
|
601
|
+
const targetFk = new Map(target.foreignKeys.map((f) => [f.name, f]));
|
|
602
|
+
for (const [name, cur] of currentFk) {
|
|
603
|
+
const tgt = targetFk.get(name);
|
|
604
|
+
if (!tgt || foreignKeySignature(cur) !== foreignKeySignature(tgt)) {
|
|
605
|
+
ops.push(dropForeignKey(table, cur));
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
for (const [name, tgt] of targetFk) {
|
|
609
|
+
const cur = currentFk.get(name);
|
|
610
|
+
if (!cur || foreignKeySignature(cur) !== foreignKeySignature(tgt)) {
|
|
611
|
+
ops.push(addForeignKey(table, tgt));
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return ops;
|
|
615
|
+
}
|
|
616
|
+
function uniqueNamed(uc) {
|
|
617
|
+
return { type: "unique", constraint: uc };
|
|
618
|
+
}
|
|
619
|
+
function foreignKeyNamed(fk) {
|
|
620
|
+
return { type: "foreignKey", constraint: fk };
|
|
621
|
+
}
|
|
622
|
+
function addUnique(table, uc) {
|
|
623
|
+
return { kind: "add_constraint", table, constraint: uniqueNamed(uc) };
|
|
624
|
+
}
|
|
625
|
+
function dropUnique(table, uc) {
|
|
626
|
+
return { kind: "drop_constraint", table, constraint: uniqueNamed(uc) };
|
|
627
|
+
}
|
|
628
|
+
function addForeignKey(table, fk) {
|
|
629
|
+
return { kind: "add_constraint", table, constraint: foreignKeyNamed(fk) };
|
|
630
|
+
}
|
|
631
|
+
function dropForeignKey(table, fk) {
|
|
632
|
+
return { kind: "drop_constraint", table, constraint: foreignKeyNamed(fk) };
|
|
633
|
+
}
|
|
368
634
|
function diffSchema(current, target) {
|
|
369
635
|
const ops = [];
|
|
370
636
|
const drops = [];
|
|
@@ -393,6 +659,7 @@ function diffSchema(current, target) {
|
|
|
393
659
|
ops.push({ kind: "drop_column", table: name, column: currentCol });
|
|
394
660
|
}
|
|
395
661
|
}
|
|
662
|
+
ops.push(...diffConstraints(currentTable, targetTable));
|
|
396
663
|
}
|
|
397
664
|
for (const [name, currentTable] of Object.entries(current.tables)) {
|
|
398
665
|
if (!target.tables[name]) {
|
|
@@ -535,6 +802,14 @@ var Op = class {
|
|
|
535
802
|
recreateTable(from, to) {
|
|
536
803
|
this.run({ kind: "recreate_table", from, to });
|
|
537
804
|
}
|
|
805
|
+
/** Add a table-level unique / foreign-key constraint. */
|
|
806
|
+
addConstraint(table, constraint) {
|
|
807
|
+
this.run({ kind: "add_constraint", table, constraint });
|
|
808
|
+
}
|
|
809
|
+
/** Drop a table-level unique / foreign-key constraint. */
|
|
810
|
+
dropConstraint(table, constraint) {
|
|
811
|
+
this.run({ kind: "drop_constraint", table, constraint });
|
|
812
|
+
}
|
|
538
813
|
/** Raw SQL escape hatch (e.g. a data migration). `down` may be `null`. */
|
|
539
814
|
execute(up, down = null) {
|
|
540
815
|
this.run({ kind: "execute", up, down });
|
|
@@ -634,6 +909,112 @@ var MigrationRunner = class {
|
|
|
634
909
|
return reverted;
|
|
635
910
|
}
|
|
636
911
|
};
|
|
912
|
+
function quoteIdent(name, dialect) {
|
|
913
|
+
return dialect === "mysql" ? `\`${name.replace(/`/g, "``")}\`` : `"${name.replace(/"/g, '""')}"`;
|
|
914
|
+
}
|
|
915
|
+
function placeholder(index, dialect) {
|
|
916
|
+
return dialect === "postgresql" ? `$${index}` : "?";
|
|
917
|
+
}
|
|
918
|
+
var AsyncMigrationRunner = class {
|
|
919
|
+
constructor(driver, dialect) {
|
|
920
|
+
this.driver = driver;
|
|
921
|
+
this.dialect = dialect;
|
|
922
|
+
this.vt = quoteIdent(VERSION_TABLE, dialect);
|
|
923
|
+
}
|
|
924
|
+
driver;
|
|
925
|
+
dialect;
|
|
926
|
+
vt;
|
|
927
|
+
/** Create the version-tracking table if it does not exist. */
|
|
928
|
+
async ensureVersionTable() {
|
|
929
|
+
await this.driver.execute(
|
|
930
|
+
`CREATE TABLE IF NOT EXISTS ${this.vt} (revision ${this.textType()} PRIMARY KEY, applied_at ${this.textType()} NOT NULL, down_revision ${this.textType()} NOT NULL)`,
|
|
931
|
+
[]
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
/** A portable "text" column type for the version table. */
|
|
935
|
+
textType() {
|
|
936
|
+
return this.dialect === "mysql" ? "VARCHAR(255)" : "TEXT";
|
|
937
|
+
}
|
|
938
|
+
/** The set of applied revision ids. */
|
|
939
|
+
async applied() {
|
|
940
|
+
await this.ensureVersionTable();
|
|
941
|
+
const { rows } = await this.driver.execute(`SELECT revision FROM ${this.vt}`, []);
|
|
942
|
+
return new Set(rows.map((r) => String(r.revision)));
|
|
943
|
+
}
|
|
944
|
+
async runOps(ops) {
|
|
945
|
+
for (const op of ops) {
|
|
946
|
+
for (const stmt of renderOperation(op, this.dialect)) {
|
|
947
|
+
const trimmed = stmt.trim();
|
|
948
|
+
if (trimmed.length === 0 || trimmed.startsWith("--")) continue;
|
|
949
|
+
await this.driver.execute(stmt, []);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
async record(migration, appliedAt) {
|
|
954
|
+
const p = (i) => placeholder(i, this.dialect);
|
|
955
|
+
await this.driver.execute(
|
|
956
|
+
`INSERT INTO ${this.vt} (revision, applied_at, down_revision) VALUES (${p(1)}, ${p(2)}, ${p(3)})`,
|
|
957
|
+
[migration.revision, appliedAt, migration.downRevision.join(",")]
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
async forget(revision) {
|
|
961
|
+
await this.driver.execute(
|
|
962
|
+
`DELETE FROM ${this.vt} WHERE revision = ${placeholder(1, this.dialect)}`,
|
|
963
|
+
[revision]
|
|
964
|
+
);
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* Apply all pending migrations up to the head(s), in DAG order.
|
|
968
|
+
*
|
|
969
|
+
* @param migrations All known migrations.
|
|
970
|
+
* @param appliedAt Timestamp string to stamp on each applied revision.
|
|
971
|
+
* @returns The revision ids that were applied this run.
|
|
972
|
+
*/
|
|
973
|
+
async upgrade(migrations, appliedAt) {
|
|
974
|
+
const done = await this.applied();
|
|
975
|
+
const ordered = topoOrder(migrations);
|
|
976
|
+
const ran = [];
|
|
977
|
+
for (const migration of ordered) {
|
|
978
|
+
if (done.has(migration.revision)) continue;
|
|
979
|
+
const op = new Op();
|
|
980
|
+
migration.up(op);
|
|
981
|
+
await this.runOps(op.operations);
|
|
982
|
+
await this.record(migration, appliedAt);
|
|
983
|
+
ran.push(migration.revision);
|
|
984
|
+
}
|
|
985
|
+
return ran;
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Revert the last `steps` applied migrations (default 1), newest first.
|
|
989
|
+
*
|
|
990
|
+
* @param migrations All known migrations.
|
|
991
|
+
* @param steps How many applied revisions to roll back.
|
|
992
|
+
* @returns The revision ids that were reverted.
|
|
993
|
+
*/
|
|
994
|
+
async downgrade(migrations, steps = 1) {
|
|
995
|
+
const done = await this.applied();
|
|
996
|
+
const ordered = topoOrder(migrations).filter((m) => done.has(m.revision));
|
|
997
|
+
const toRevert = ordered.slice(-steps).reverse();
|
|
998
|
+
const reverted = [];
|
|
999
|
+
for (const migration of toRevert) {
|
|
1000
|
+
const op = new Op();
|
|
1001
|
+
try {
|
|
1002
|
+
migration.down(op);
|
|
1003
|
+
} catch {
|
|
1004
|
+
op.operations.length = 0;
|
|
1005
|
+
}
|
|
1006
|
+
if (op.operations.length === 0) {
|
|
1007
|
+
const upOp = new Op();
|
|
1008
|
+
migration.up(upOp);
|
|
1009
|
+
op.operations.push(...invertAll(upOp.operations));
|
|
1010
|
+
}
|
|
1011
|
+
await this.runOps(op.operations);
|
|
1012
|
+
await this.forget(migration.revision);
|
|
1013
|
+
reverted.push(migration.revision);
|
|
1014
|
+
}
|
|
1015
|
+
return reverted;
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
637
1018
|
|
|
638
1019
|
// src/migrations/introspect.ts
|
|
639
1020
|
function sqliteAffinity(declared) {
|
|
@@ -658,6 +1039,39 @@ function affinityToKind(affinity) {
|
|
|
658
1039
|
return "text";
|
|
659
1040
|
}
|
|
660
1041
|
}
|
|
1042
|
+
function sqliteForeignKeys(driver, table) {
|
|
1043
|
+
const rows = driver.execute(`PRAGMA foreign_key_list(${JSON.stringify(table)})`, []).rows;
|
|
1044
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1045
|
+
for (const r of rows) {
|
|
1046
|
+
const list = byId.get(r.id) ?? [];
|
|
1047
|
+
list.push(r);
|
|
1048
|
+
byId.set(r.id, list);
|
|
1049
|
+
}
|
|
1050
|
+
const fks = [];
|
|
1051
|
+
for (const group of byId.values()) {
|
|
1052
|
+
const ordered = [...group].sort((a, b) => a.seq - b.seq);
|
|
1053
|
+
const columns = ordered.map((r) => r.from);
|
|
1054
|
+
fks.push({
|
|
1055
|
+
name: `fk_${table}_${columns.join("_")}`,
|
|
1056
|
+
columns,
|
|
1057
|
+
refTable: ordered[0]?.table ?? "",
|
|
1058
|
+
refColumns: ordered.map((r) => r.to)
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
return fks;
|
|
1062
|
+
}
|
|
1063
|
+
function sqliteUniques(driver, table) {
|
|
1064
|
+
const indexes = driver.execute(`PRAGMA index_list(${JSON.stringify(table)})`, []).rows;
|
|
1065
|
+
const uniques = [];
|
|
1066
|
+
for (const idx of indexes) {
|
|
1067
|
+
if (Number(idx.unique) !== 1 || idx.origin === "pk") continue;
|
|
1068
|
+
const cols = driver.execute(`PRAGMA index_info(${JSON.stringify(idx.name)})`, []).rows.sort((a, b) => a.seqno - b.seqno).map((c) => c.name);
|
|
1069
|
+
if (cols.length > 0) {
|
|
1070
|
+
uniques.push({ name: `uq_${table}_${cols.join("_")}`, columns: cols });
|
|
1071
|
+
}
|
|
1072
|
+
}
|
|
1073
|
+
return uniques;
|
|
1074
|
+
}
|
|
661
1075
|
function introspectSqlite(driver) {
|
|
662
1076
|
const tablesRows = driver.execute(
|
|
663
1077
|
"SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name != 'tempest_db_js_migrations'",
|
|
@@ -677,14 +1091,39 @@ function introspectSqlite(driver) {
|
|
|
677
1091
|
type: { kind: affinityToKind(affinity), meta: {} },
|
|
678
1092
|
notNull: Number(col.notnull) === 1 || isPk,
|
|
679
1093
|
primaryKey: isPk,
|
|
680
|
-
default: null
|
|
1094
|
+
default: null,
|
|
1095
|
+
unique: false,
|
|
1096
|
+
references: null
|
|
681
1097
|
};
|
|
682
1098
|
if (isPk) primaryKey.push(col.name);
|
|
683
1099
|
}
|
|
684
|
-
tables[tableName] = {
|
|
1100
|
+
tables[tableName] = {
|
|
1101
|
+
name: tableName,
|
|
1102
|
+
columns,
|
|
1103
|
+
primaryKey,
|
|
1104
|
+
uniqueConstraints: sqliteUniques(driver, tableName),
|
|
1105
|
+
foreignKeys: sqliteForeignKeys(driver, tableName)
|
|
1106
|
+
};
|
|
685
1107
|
}
|
|
686
1108
|
return { tables };
|
|
687
1109
|
}
|
|
1110
|
+
function constraintKeys(table) {
|
|
1111
|
+
const fks = /* @__PURE__ */ new Set();
|
|
1112
|
+
const uniques = /* @__PURE__ */ new Set();
|
|
1113
|
+
for (const col of Object.values(table.columns)) {
|
|
1114
|
+
if (col.unique) uniques.add(col.name);
|
|
1115
|
+
if (col.references) {
|
|
1116
|
+
fks.add(`${col.name}=>${col.references.table}(${col.references.column})`);
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
for (const uc of table.uniqueConstraints) {
|
|
1120
|
+
uniques.add([...uc.columns].sort().join(","));
|
|
1121
|
+
}
|
|
1122
|
+
for (const fk of table.foreignKeys) {
|
|
1123
|
+
fks.add(`${fk.columns.join(",")}=>${fk.refTable}(${fk.refColumns.join(",")})`);
|
|
1124
|
+
}
|
|
1125
|
+
return { fks, uniques };
|
|
1126
|
+
}
|
|
688
1127
|
function checkDrift(driver, models) {
|
|
689
1128
|
const actual = introspectSqlite(driver);
|
|
690
1129
|
const expected = reflectSchema(models);
|
|
@@ -722,6 +1161,34 @@ function checkDrift(driver, models) {
|
|
|
722
1161
|
);
|
|
723
1162
|
}
|
|
724
1163
|
}
|
|
1164
|
+
const expectedKeys = constraintKeys(expectedTable);
|
|
1165
|
+
const actualKeys = constraintKeys(actualTable);
|
|
1166
|
+
for (const fk of expectedKeys.fks) {
|
|
1167
|
+
if (!actualKeys.fks.has(fk)) {
|
|
1168
|
+
issues.push(`foreign key "${tableName}: ${fk}" is missing from the database`);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
for (const fk of actualKeys.fks) {
|
|
1172
|
+
if (!expectedKeys.fks.has(fk)) {
|
|
1173
|
+
issues.push(
|
|
1174
|
+
`foreign key "${tableName}: ${fk}" exists in the database but not in the model`
|
|
1175
|
+
);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
for (const uq of expectedKeys.uniques) {
|
|
1179
|
+
if (!actualKeys.uniques.has(uq)) {
|
|
1180
|
+
issues.push(
|
|
1181
|
+
`unique constraint "${tableName}: (${uq})" is missing from the database`
|
|
1182
|
+
);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
for (const uq of actualKeys.uniques) {
|
|
1186
|
+
if (!expectedKeys.uniques.has(uq)) {
|
|
1187
|
+
issues.push(
|
|
1188
|
+
`unique constraint "${tableName}: (${uq})" exists in the database but not in the model`
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
725
1192
|
}
|
|
726
1193
|
for (const tableName of Object.keys(actual.tables)) {
|
|
727
1194
|
if (!expected.tables[tableName]) {
|
|
@@ -783,14 +1250,59 @@ async function introspectPostgres(driver) {
|
|
|
783
1250
|
},
|
|
784
1251
|
notNull: col.is_nullable === "NO" || isPk,
|
|
785
1252
|
primaryKey: isPk,
|
|
786
|
-
default: null
|
|
1253
|
+
default: null,
|
|
1254
|
+
unique: false,
|
|
1255
|
+
references: null
|
|
787
1256
|
};
|
|
788
1257
|
if (isPk) primaryKey.push(name);
|
|
789
1258
|
}
|
|
790
|
-
tables[tableName] = {
|
|
1259
|
+
tables[tableName] = {
|
|
1260
|
+
name: tableName,
|
|
1261
|
+
columns,
|
|
1262
|
+
primaryKey,
|
|
1263
|
+
uniqueConstraints: await postgresUniques(driver, tableName),
|
|
1264
|
+
foreignKeys: await postgresForeignKeys(driver, tableName)
|
|
1265
|
+
};
|
|
791
1266
|
}
|
|
792
1267
|
return { tables };
|
|
793
1268
|
}
|
|
1269
|
+
async function postgresForeignKeys(driver, table) {
|
|
1270
|
+
const result = await driver.execute(
|
|
1271
|
+
`SELECT c.conname AS name,
|
|
1272
|
+
(SELECT array_agg(a.attname ORDER BY k.ord)
|
|
1273
|
+
FROM unnest(c.conkey) WITH ORDINALITY AS k(attnum, ord)
|
|
1274
|
+
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum) AS cols,
|
|
1275
|
+
cf.relname AS ref_table,
|
|
1276
|
+
(SELECT array_agg(a.attname ORDER BY k.ord)
|
|
1277
|
+
FROM unnest(c.confkey) WITH ORDINALITY AS k(attnum, ord)
|
|
1278
|
+
JOIN pg_attribute a ON a.attrelid = c.confrelid AND a.attnum = k.attnum) AS ref_cols
|
|
1279
|
+
FROM pg_constraint c
|
|
1280
|
+
JOIN pg_class cf ON cf.oid = c.confrelid
|
|
1281
|
+
WHERE c.contype = 'f' AND c.conrelid = $1::regclass`,
|
|
1282
|
+
[table]
|
|
1283
|
+
);
|
|
1284
|
+
return result.rows.map((r) => ({
|
|
1285
|
+
name: String(r.name),
|
|
1286
|
+
columns: r.cols ?? [],
|
|
1287
|
+
refTable: String(r.ref_table),
|
|
1288
|
+
refColumns: r.ref_cols ?? []
|
|
1289
|
+
}));
|
|
1290
|
+
}
|
|
1291
|
+
async function postgresUniques(driver, table) {
|
|
1292
|
+
const result = await driver.execute(
|
|
1293
|
+
`SELECT c.conname AS name,
|
|
1294
|
+
(SELECT array_agg(a.attname ORDER BY k.ord)
|
|
1295
|
+
FROM unnest(c.conkey) WITH ORDINALITY AS k(attnum, ord)
|
|
1296
|
+
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum) AS cols
|
|
1297
|
+
FROM pg_constraint c
|
|
1298
|
+
WHERE c.contype = 'u' AND c.conrelid = $1::regclass`,
|
|
1299
|
+
[table]
|
|
1300
|
+
);
|
|
1301
|
+
return result.rows.map((r) => ({
|
|
1302
|
+
name: String(r.name),
|
|
1303
|
+
columns: r.cols ?? []
|
|
1304
|
+
}));
|
|
1305
|
+
}
|
|
794
1306
|
async function checkDriftPostgres(driver, models) {
|
|
795
1307
|
const actual = await introspectPostgres(driver);
|
|
796
1308
|
const expected = reflectSchema(models);
|
|
@@ -823,6 +1335,20 @@ async function checkDriftPostgres(driver, models) {
|
|
|
823
1335
|
);
|
|
824
1336
|
}
|
|
825
1337
|
}
|
|
1338
|
+
const expectedKeys = constraintKeys(expectedTable);
|
|
1339
|
+
const actualKeys = constraintKeys(actualTable);
|
|
1340
|
+
for (const fk of expectedKeys.fks) {
|
|
1341
|
+
if (!actualKeys.fks.has(fk)) {
|
|
1342
|
+
issues.push(`foreign key "${tableName}: ${fk}" is missing from the database`);
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
for (const uq of expectedKeys.uniques) {
|
|
1346
|
+
if (!actualKeys.uniques.has(uq)) {
|
|
1347
|
+
issues.push(
|
|
1348
|
+
`unique constraint "${tableName}: (${uq})" is missing from the database`
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
826
1352
|
}
|
|
827
1353
|
for (const tableName of Object.keys(actual.tables)) {
|
|
828
1354
|
if (!expected.tables[tableName]) {
|
|
@@ -894,6 +1420,30 @@ function applyOperation(schema, op) {
|
|
|
894
1420
|
}
|
|
895
1421
|
break;
|
|
896
1422
|
}
|
|
1423
|
+
case "add_constraint": {
|
|
1424
|
+
const t = tables[op.table];
|
|
1425
|
+
if (t) {
|
|
1426
|
+
tables[op.table] = op.constraint.type === "unique" ? {
|
|
1427
|
+
...t,
|
|
1428
|
+
uniqueConstraints: [...t.uniqueConstraints, op.constraint.constraint]
|
|
1429
|
+
} : { ...t, foreignKeys: [...t.foreignKeys, op.constraint.constraint] };
|
|
1430
|
+
}
|
|
1431
|
+
break;
|
|
1432
|
+
}
|
|
1433
|
+
case "drop_constraint": {
|
|
1434
|
+
const t = tables[op.table];
|
|
1435
|
+
if (t) {
|
|
1436
|
+
const dropName = op.constraint.constraint.name;
|
|
1437
|
+
tables[op.table] = op.constraint.type === "unique" ? {
|
|
1438
|
+
...t,
|
|
1439
|
+
uniqueConstraints: t.uniqueConstraints.filter((u) => u.name !== dropName)
|
|
1440
|
+
} : {
|
|
1441
|
+
...t,
|
|
1442
|
+
foreignKeys: t.foreignKeys.filter((f) => f.name !== dropName)
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
break;
|
|
1446
|
+
}
|
|
897
1447
|
}
|
|
898
1448
|
return { tables };
|
|
899
1449
|
}
|
|
@@ -915,7 +1465,9 @@ function columnShape(col) {
|
|
|
915
1465
|
type: col.type,
|
|
916
1466
|
notNull: col.notNull,
|
|
917
1467
|
primaryKey: col.primaryKey,
|
|
918
|
-
default: col.default
|
|
1468
|
+
default: col.default,
|
|
1469
|
+
unique: col.unique,
|
|
1470
|
+
references: col.references
|
|
919
1471
|
});
|
|
920
1472
|
}
|
|
921
1473
|
function tableShape(columns) {
|
|
@@ -1123,6 +1675,7 @@ function runMigrationCli(argv, config) {
|
|
|
1123
1675
|
}
|
|
1124
1676
|
}
|
|
1125
1677
|
|
|
1678
|
+
exports.AsyncMigrationRunner = AsyncMigrationRunner;
|
|
1126
1679
|
exports.CyclicMigrationGraph = CyclicMigrationGraph;
|
|
1127
1680
|
exports.IrreversibleMigration = IrreversibleMigration;
|
|
1128
1681
|
exports.MigrationRunner = MigrationRunner;
|