tempest-db-js 0.3.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 +342 -14
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +2 -2
- package/dist/{chunk-OP7FRDI5.js → chunk-43XL66JG.js} +352 -14
- package/dist/chunk-43XL66JG.js.map +1 -0
- package/dist/{chunk-Q32CBI2A.js → chunk-JR4MLFQN.js} +87 -9
- package/dist/chunk-JR4MLFQN.js.map +1 -0
- package/dist/index.cjs +86 -6
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +102 -3
- package/dist/index.d.ts +102 -3
- package/dist/index.js +1 -1
- package/dist/migrations/index.cjs +403 -16
- package/dist/migrations/index.cjs.map +1 -1
- package/dist/migrations/index.d.cts +51 -12
- package/dist/migrations/index.d.ts +51 -12
- package/dist/migrations/index.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-OP7FRDI5.js.map +0 -1
- package/dist/chunk-Q32CBI2A.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");
|
|
@@ -270,10 +347,45 @@ function renderDefault(def, dialect) {
|
|
|
270
347
|
if (typeof value === "object") return quoteLiteral(JSON.stringify(value));
|
|
271
348
|
return quoteLiteral(String(value));
|
|
272
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
|
+
}
|
|
273
384
|
function renderColumnDef(col, dialect) {
|
|
274
385
|
let sql = `${quoteId(col.name, dialect)} ${renderColumnType(col.type, dialect)}`;
|
|
275
386
|
if (col.notNull) sql += " NOT NULL";
|
|
276
387
|
if (col.default !== null) sql += ` DEFAULT ${renderDefault(col.default, dialect)}`;
|
|
388
|
+
sql += columnConstraintSuffix(col, dialect);
|
|
277
389
|
return sql;
|
|
278
390
|
}
|
|
279
391
|
function enumTypeName(table, column) {
|
|
@@ -297,13 +409,13 @@ function renderCreateTable(table, dialect) {
|
|
|
297
409
|
let def = `${quoteId(c.name, dialect)} ${quoteId(typeName, dialect)}`;
|
|
298
410
|
if (c.notNull) def += " NOT NULL";
|
|
299
411
|
if (c.default !== null) def += ` DEFAULT ${renderDefault(c.default, dialect)}`;
|
|
300
|
-
return def;
|
|
412
|
+
return def + columnConstraintSuffix(c, dialect);
|
|
301
413
|
}
|
|
302
414
|
if (dialect === "postgresql" && isAutoIncrementPk(table, c)) {
|
|
303
|
-
return `${quoteId(c.name, dialect)} ${postgresSerialType(c.type.kind)}`;
|
|
415
|
+
return `${quoteId(c.name, dialect)} ${postgresSerialType(c.type.kind)}${columnConstraintSuffix(c, dialect)}`;
|
|
304
416
|
}
|
|
305
417
|
if (dialect === "mysql" && isAutoIncrementPk(table, c)) {
|
|
306
|
-
return `${quoteId(c.name, dialect)} ${renderColumnType(c.type, dialect)} NOT NULL AUTO_INCREMENT`;
|
|
418
|
+
return `${quoteId(c.name, dialect)} ${renderColumnType(c.type, dialect)} NOT NULL AUTO_INCREMENT${columnConstraintSuffix(c, dialect)}`;
|
|
307
419
|
}
|
|
308
420
|
return renderColumnDef(c, dialect);
|
|
309
421
|
});
|
|
@@ -312,6 +424,7 @@ function renderCreateTable(table, dialect) {
|
|
|
312
424
|
`PRIMARY KEY (${table.primaryKey.map((c) => quoteId(c, dialect)).join(", ")})`
|
|
313
425
|
);
|
|
314
426
|
}
|
|
427
|
+
cols.push(...tableConstraintClauses(table, dialect));
|
|
315
428
|
return [
|
|
316
429
|
...typeStmts,
|
|
317
430
|
`CREATE TABLE ${quoteId(table.name, dialect)} (
|
|
@@ -345,10 +458,38 @@ function renderOperation(op, dialect) {
|
|
|
345
458
|
return renderAlterColumn(op.table, op.to, dialect);
|
|
346
459
|
case "recreate_table":
|
|
347
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);
|
|
348
465
|
case "execute":
|
|
349
466
|
return [op.up];
|
|
350
467
|
}
|
|
351
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
|
+
}
|
|
352
493
|
function renderSqliteRebuild(from, to) {
|
|
353
494
|
const tmp = `__new_${to.name}`;
|
|
354
495
|
const common = Object.keys(to.columns).filter((c) => c in from.columns);
|
|
@@ -358,6 +499,7 @@ function renderSqliteRebuild(from, to) {
|
|
|
358
499
|
`PRIMARY KEY (${to.primaryKey.map((c) => quoteId(c, "sqlite")).join(", ")})`
|
|
359
500
|
);
|
|
360
501
|
}
|
|
502
|
+
cols.push(...tableConstraintClauses(to, "sqlite"));
|
|
361
503
|
const commonSql = common.map((c) => quoteId(c, "sqlite")).join(", ");
|
|
362
504
|
return [
|
|
363
505
|
"PRAGMA foreign_keys=off",
|
|
@@ -421,9 +563,74 @@ function columnSignature(col) {
|
|
|
421
563
|
type: col.type,
|
|
422
564
|
notNull: col.notNull,
|
|
423
565
|
primaryKey: col.primaryKey,
|
|
424
|
-
default: col.default
|
|
566
|
+
default: col.default,
|
|
567
|
+
unique: col.unique,
|
|
568
|
+
references: col.references
|
|
569
|
+
});
|
|
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
|
|
425
581
|
});
|
|
426
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
|
+
}
|
|
427
634
|
function diffSchema(current, target) {
|
|
428
635
|
const ops = [];
|
|
429
636
|
const drops = [];
|
|
@@ -452,6 +659,7 @@ function diffSchema(current, target) {
|
|
|
452
659
|
ops.push({ kind: "drop_column", table: name, column: currentCol });
|
|
453
660
|
}
|
|
454
661
|
}
|
|
662
|
+
ops.push(...diffConstraints(currentTable, targetTable));
|
|
455
663
|
}
|
|
456
664
|
for (const [name, currentTable] of Object.entries(current.tables)) {
|
|
457
665
|
if (!target.tables[name]) {
|
|
@@ -594,6 +802,14 @@ var Op = class {
|
|
|
594
802
|
recreateTable(from, to) {
|
|
595
803
|
this.run({ kind: "recreate_table", from, to });
|
|
596
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
|
+
}
|
|
597
813
|
/** Raw SQL escape hatch (e.g. a data migration). `down` may be `null`. */
|
|
598
814
|
execute(up, down = null) {
|
|
599
815
|
this.run({ kind: "execute", up, down });
|
|
@@ -823,6 +1039,39 @@ function affinityToKind(affinity) {
|
|
|
823
1039
|
return "text";
|
|
824
1040
|
}
|
|
825
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
|
+
}
|
|
826
1075
|
function introspectSqlite(driver) {
|
|
827
1076
|
const tablesRows = driver.execute(
|
|
828
1077
|
"SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name != 'tempest_db_js_migrations'",
|
|
@@ -842,14 +1091,39 @@ function introspectSqlite(driver) {
|
|
|
842
1091
|
type: { kind: affinityToKind(affinity), meta: {} },
|
|
843
1092
|
notNull: Number(col.notnull) === 1 || isPk,
|
|
844
1093
|
primaryKey: isPk,
|
|
845
|
-
default: null
|
|
1094
|
+
default: null,
|
|
1095
|
+
unique: false,
|
|
1096
|
+
references: null
|
|
846
1097
|
};
|
|
847
1098
|
if (isPk) primaryKey.push(col.name);
|
|
848
1099
|
}
|
|
849
|
-
tables[tableName] = {
|
|
1100
|
+
tables[tableName] = {
|
|
1101
|
+
name: tableName,
|
|
1102
|
+
columns,
|
|
1103
|
+
primaryKey,
|
|
1104
|
+
uniqueConstraints: sqliteUniques(driver, tableName),
|
|
1105
|
+
foreignKeys: sqliteForeignKeys(driver, tableName)
|
|
1106
|
+
};
|
|
850
1107
|
}
|
|
851
1108
|
return { tables };
|
|
852
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
|
+
}
|
|
853
1127
|
function checkDrift(driver, models) {
|
|
854
1128
|
const actual = introspectSqlite(driver);
|
|
855
1129
|
const expected = reflectSchema(models);
|
|
@@ -887,6 +1161,34 @@ function checkDrift(driver, models) {
|
|
|
887
1161
|
);
|
|
888
1162
|
}
|
|
889
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
|
+
}
|
|
890
1192
|
}
|
|
891
1193
|
for (const tableName of Object.keys(actual.tables)) {
|
|
892
1194
|
if (!expected.tables[tableName]) {
|
|
@@ -948,14 +1250,59 @@ async function introspectPostgres(driver) {
|
|
|
948
1250
|
},
|
|
949
1251
|
notNull: col.is_nullable === "NO" || isPk,
|
|
950
1252
|
primaryKey: isPk,
|
|
951
|
-
default: null
|
|
1253
|
+
default: null,
|
|
1254
|
+
unique: false,
|
|
1255
|
+
references: null
|
|
952
1256
|
};
|
|
953
1257
|
if (isPk) primaryKey.push(name);
|
|
954
1258
|
}
|
|
955
|
-
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
|
+
};
|
|
956
1266
|
}
|
|
957
1267
|
return { tables };
|
|
958
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
|
+
}
|
|
959
1306
|
async function checkDriftPostgres(driver, models) {
|
|
960
1307
|
const actual = await introspectPostgres(driver);
|
|
961
1308
|
const expected = reflectSchema(models);
|
|
@@ -988,6 +1335,20 @@ async function checkDriftPostgres(driver, models) {
|
|
|
988
1335
|
);
|
|
989
1336
|
}
|
|
990
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
|
+
}
|
|
991
1352
|
}
|
|
992
1353
|
for (const tableName of Object.keys(actual.tables)) {
|
|
993
1354
|
if (!expected.tables[tableName]) {
|
|
@@ -1059,6 +1420,30 @@ function applyOperation(schema, op) {
|
|
|
1059
1420
|
}
|
|
1060
1421
|
break;
|
|
1061
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
|
+
}
|
|
1062
1447
|
}
|
|
1063
1448
|
return { tables };
|
|
1064
1449
|
}
|
|
@@ -1080,7 +1465,9 @@ function columnShape(col) {
|
|
|
1080
1465
|
type: col.type,
|
|
1081
1466
|
notNull: col.notNull,
|
|
1082
1467
|
primaryKey: col.primaryKey,
|
|
1083
|
-
default: col.default
|
|
1468
|
+
default: col.default,
|
|
1469
|
+
unique: col.unique,
|
|
1470
|
+
references: col.references
|
|
1084
1471
|
});
|
|
1085
1472
|
}
|
|
1086
1473
|
function tableShape(columns) {
|