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
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
📖 **Documentation:** [Português (BR)](https://mauriciobenjamin700.github.io/tempest-db-js/) · [English (US)](https://mauriciobenjamin700.github.io/tempest-db-js/en/)
|
|
7
7
|
|
|
8
|
-
> ✅ **Status: alpha (v0.
|
|
8
|
+
> ✅ **Status: alpha (v0.4.0), published on [npm](https://www.npmjs.com/package/tempest-db-js).** The full path works end-to-end — declarative models with **foreign keys, UNIQUE and table constraints**, typed query builder (aggregations, `DISTINCT`, upsert), **real SQLite + PostgreSQL execution**, a **MySQL** dialect, joins, relations, Alembic-style migrations (sync + **async** runner) with a `tempest-db` CLI, a typed `BaseRepository`, and an opt-in active-record layer. The public API may still shift before v1.0.
|
|
9
9
|
|
|
10
10
|
## Why tempest-db-js
|
|
11
11
|
|
|
@@ -71,6 +71,7 @@ Sessions and engines are **disposable** — `using session = engine.session()` (
|
|
|
71
71
|
|
|
72
72
|
Typed extras, each with a [docs recipe](https://mauriciobenjamin700.github.io/tempest-db-js/):
|
|
73
73
|
|
|
74
|
+
- **Schema constraints** — column `.unique()` / `.references("users.id", { onDelete })` and composite/named table constraints via `static tableArgs = () => [unique(...), foreignKey(...)]` (SQLAlchemy `ForeignKey`/`__table_args__` style). Rendered across all dialects; reversible in migrations.
|
|
74
75
|
- **Aggregations** — `select(Order).aggregate(["status"], { n: count(), total: sum("amount") })` → rows typed as `{ status; n; total }`. Plus `.distinct()`.
|
|
75
76
|
- **Upsert** — `insert(Row).values(...).onConflictDoUpdate(["key"], { ... })` / `.onConflictDoNothing(["key"])` (portable SQLite ↔ PostgreSQL).
|
|
76
77
|
- **Active-record (opt-in)** — `activeRecord(User, session)` → `save`/`update`/`delete`/`reload` over `.data`; the plain-object default is unchanged.
|
|
@@ -105,7 +106,7 @@ HTTP integration recipes (Hono, Express, Fastify) live in the [docs](https://mau
|
|
|
105
106
|
|
|
106
107
|
## Roadmap
|
|
107
108
|
|
|
108
|
-
See [ROADMAP.md](./ROADMAP.md). Shipped (v0.
|
|
109
|
+
See [ROADMAP.md](./ROADMAP.md). Shipped (v0.4.0): declarative schema with foreign keys / UNIQUE / table constraints, SQLite + PostgreSQL execution (both tested in CI, Postgres against a live database), a MySQL dialect, joins, relations, sync + async migration runners with a `tempest-db` CLI, repository, aggregations/upsert, opt-in active-record. Next: MySQL execution in CI + `RETURNING` round-trip, async CLI wiring, then `tempest-ts-sdk`.
|
|
109
110
|
|
|
110
111
|
## Development
|
|
111
112
|
|
package/dist/bin.cjs
CHANGED
|
@@ -38,6 +38,10 @@ function invert(op) {
|
|
|
38
38
|
return { kind: "rename_column", table: op.table, from: op.to, to: op.from };
|
|
39
39
|
case "recreate_table":
|
|
40
40
|
return { kind: "recreate_table", from: op.to, to: op.from };
|
|
41
|
+
case "add_constraint":
|
|
42
|
+
return { kind: "drop_constraint", table: op.table, constraint: op.constraint };
|
|
43
|
+
case "drop_constraint":
|
|
44
|
+
return { kind: "add_constraint", table: op.table, constraint: op.constraint };
|
|
41
45
|
case "execute":
|
|
42
46
|
if (op.down === null) {
|
|
43
47
|
throw new IrreversibleMigration("execute() operation has no down SQL");
|
|
@@ -227,10 +231,45 @@ function renderDefault(def, dialect) {
|
|
|
227
231
|
if (typeof value === "object") return quoteLiteral(JSON.stringify(value));
|
|
228
232
|
return quoteLiteral(String(value));
|
|
229
233
|
}
|
|
234
|
+
function renderFkAction(action) {
|
|
235
|
+
return action.toUpperCase();
|
|
236
|
+
}
|
|
237
|
+
function renderFkActions(fk) {
|
|
238
|
+
let sql = "";
|
|
239
|
+
if (fk.onDelete) sql += ` ON DELETE ${renderFkAction(fk.onDelete)}`;
|
|
240
|
+
if (fk.onUpdate) sql += ` ON UPDATE ${renderFkAction(fk.onUpdate)}`;
|
|
241
|
+
return sql;
|
|
242
|
+
}
|
|
243
|
+
function columnConstraintSuffix(col, dialect) {
|
|
244
|
+
let sql = "";
|
|
245
|
+
if (col.unique) sql += " UNIQUE";
|
|
246
|
+
if (col.references) {
|
|
247
|
+
const ref = col.references;
|
|
248
|
+
sql += ` REFERENCES ${quoteId(ref.table, dialect)} (${quoteId(ref.column, dialect)})`;
|
|
249
|
+
sql += renderFkActions(ref);
|
|
250
|
+
}
|
|
251
|
+
return sql;
|
|
252
|
+
}
|
|
253
|
+
function renderUniqueConstraint(uc, dialect) {
|
|
254
|
+
const cols = uc.columns.map((c) => quoteId(c, dialect)).join(", ");
|
|
255
|
+
return `CONSTRAINT ${quoteId(uc.name, dialect)} UNIQUE (${cols})`;
|
|
256
|
+
}
|
|
257
|
+
function renderForeignKeyConstraint(fk, dialect) {
|
|
258
|
+
const cols = fk.columns.map((c) => quoteId(c, dialect)).join(", ");
|
|
259
|
+
const refCols = fk.refColumns.map((c) => quoteId(c, dialect)).join(", ");
|
|
260
|
+
return `CONSTRAINT ${quoteId(fk.name, dialect)} FOREIGN KEY (${cols}) REFERENCES ${quoteId(fk.refTable, dialect)} (${refCols})${renderFkActions(fk)}`;
|
|
261
|
+
}
|
|
262
|
+
function tableConstraintClauses(table, dialect) {
|
|
263
|
+
return [
|
|
264
|
+
...table.uniqueConstraints.map((uc) => renderUniqueConstraint(uc, dialect)),
|
|
265
|
+
...table.foreignKeys.map((fk) => renderForeignKeyConstraint(fk, dialect))
|
|
266
|
+
];
|
|
267
|
+
}
|
|
230
268
|
function renderColumnDef(col, dialect) {
|
|
231
269
|
let sql = `${quoteId(col.name, dialect)} ${renderColumnType(col.type, dialect)}`;
|
|
232
270
|
if (col.notNull) sql += " NOT NULL";
|
|
233
271
|
if (col.default !== null) sql += ` DEFAULT ${renderDefault(col.default, dialect)}`;
|
|
272
|
+
sql += columnConstraintSuffix(col, dialect);
|
|
234
273
|
return sql;
|
|
235
274
|
}
|
|
236
275
|
function enumTypeName(table, column) {
|
|
@@ -254,13 +293,13 @@ function renderCreateTable(table, dialect) {
|
|
|
254
293
|
let def = `${quoteId(c.name, dialect)} ${quoteId(typeName, dialect)}`;
|
|
255
294
|
if (c.notNull) def += " NOT NULL";
|
|
256
295
|
if (c.default !== null) def += ` DEFAULT ${renderDefault(c.default, dialect)}`;
|
|
257
|
-
return def;
|
|
296
|
+
return def + columnConstraintSuffix(c, dialect);
|
|
258
297
|
}
|
|
259
298
|
if (dialect === "postgresql" && isAutoIncrementPk(table, c)) {
|
|
260
|
-
return `${quoteId(c.name, dialect)} ${postgresSerialType(c.type.kind)}`;
|
|
299
|
+
return `${quoteId(c.name, dialect)} ${postgresSerialType(c.type.kind)}${columnConstraintSuffix(c, dialect)}`;
|
|
261
300
|
}
|
|
262
301
|
if (dialect === "mysql" && isAutoIncrementPk(table, c)) {
|
|
263
|
-
return `${quoteId(c.name, dialect)} ${renderColumnType(c.type, dialect)} NOT NULL AUTO_INCREMENT`;
|
|
302
|
+
return `${quoteId(c.name, dialect)} ${renderColumnType(c.type, dialect)} NOT NULL AUTO_INCREMENT${columnConstraintSuffix(c, dialect)}`;
|
|
264
303
|
}
|
|
265
304
|
return renderColumnDef(c, dialect);
|
|
266
305
|
});
|
|
@@ -269,6 +308,7 @@ function renderCreateTable(table, dialect) {
|
|
|
269
308
|
`PRIMARY KEY (${table.primaryKey.map((c) => quoteId(c, dialect)).join(", ")})`
|
|
270
309
|
);
|
|
271
310
|
}
|
|
311
|
+
cols.push(...tableConstraintClauses(table, dialect));
|
|
272
312
|
return [
|
|
273
313
|
...typeStmts,
|
|
274
314
|
`CREATE TABLE ${quoteId(table.name, dialect)} (
|
|
@@ -302,10 +342,38 @@ function renderOperation(op, dialect) {
|
|
|
302
342
|
return renderAlterColumn(op.table, op.to, dialect);
|
|
303
343
|
case "recreate_table":
|
|
304
344
|
return dialect === "sqlite" ? renderSqliteRebuild(op.from, op.to) : renderTableDiff(op.from, op.to, dialect);
|
|
345
|
+
case "add_constraint":
|
|
346
|
+
return renderAddConstraint(op.table, op.constraint, dialect);
|
|
347
|
+
case "drop_constraint":
|
|
348
|
+
return renderDropConstraint(op.table, op.constraint, dialect);
|
|
305
349
|
case "execute":
|
|
306
350
|
return [op.up];
|
|
307
351
|
}
|
|
308
352
|
}
|
|
353
|
+
function renderAddConstraint(table, constraint, dialect) {
|
|
354
|
+
if (dialect === "sqlite") {
|
|
355
|
+
throw new Error(
|
|
356
|
+
`add_constraint on SQLite needs a table-rebuild (recreate_table); constraint ${constraint.constraint.name} on ${table}`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
const clause = constraint.type === "unique" ? renderUniqueConstraint(constraint.constraint, dialect) : renderForeignKeyConstraint(constraint.constraint, dialect);
|
|
360
|
+
return [`ALTER TABLE ${quoteId(table, dialect)} ADD ${clause}`];
|
|
361
|
+
}
|
|
362
|
+
function renderDropConstraint(table, constraint, dialect) {
|
|
363
|
+
if (dialect === "sqlite") {
|
|
364
|
+
throw new Error(
|
|
365
|
+
`drop_constraint on SQLite needs a table-rebuild (recreate_table); constraint ${constraint.constraint.name} on ${table}`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
const t = quoteId(table, dialect);
|
|
369
|
+
const name = quoteId(constraint.constraint.name, dialect);
|
|
370
|
+
if (dialect === "mysql") {
|
|
371
|
+
return [
|
|
372
|
+
constraint.type === "unique" ? `ALTER TABLE ${t} DROP INDEX ${name}` : `ALTER TABLE ${t} DROP FOREIGN KEY ${name}`
|
|
373
|
+
];
|
|
374
|
+
}
|
|
375
|
+
return [`ALTER TABLE ${t} DROP CONSTRAINT ${name}`];
|
|
376
|
+
}
|
|
309
377
|
function renderSqliteRebuild(from, to) {
|
|
310
378
|
const tmp = `__new_${to.name}`;
|
|
311
379
|
const common = Object.keys(to.columns).filter((c) => c in from.columns);
|
|
@@ -315,6 +383,7 @@ function renderSqliteRebuild(from, to) {
|
|
|
315
383
|
`PRIMARY KEY (${to.primaryKey.map((c) => quoteId(c, "sqlite")).join(", ")})`
|
|
316
384
|
);
|
|
317
385
|
}
|
|
386
|
+
cols.push(...tableConstraintClauses(to, "sqlite"));
|
|
318
387
|
const commonSql = common.map((c) => quoteId(c, "sqlite")).join(", ");
|
|
319
388
|
return [
|
|
320
389
|
"PRAGMA foreign_keys=off",
|
|
@@ -378,9 +447,74 @@ function columnSignature(col) {
|
|
|
378
447
|
type: col.type,
|
|
379
448
|
notNull: col.notNull,
|
|
380
449
|
primaryKey: col.primaryKey,
|
|
381
|
-
default: col.default
|
|
450
|
+
default: col.default,
|
|
451
|
+
unique: col.unique,
|
|
452
|
+
references: col.references
|
|
382
453
|
});
|
|
383
454
|
}
|
|
455
|
+
function uniqueSignature(uc) {
|
|
456
|
+
return JSON.stringify({ columns: uc.columns });
|
|
457
|
+
}
|
|
458
|
+
function foreignKeySignature(fk) {
|
|
459
|
+
return JSON.stringify({
|
|
460
|
+
columns: fk.columns,
|
|
461
|
+
refTable: fk.refTable,
|
|
462
|
+
refColumns: fk.refColumns,
|
|
463
|
+
onDelete: fk.onDelete ?? null,
|
|
464
|
+
onUpdate: fk.onUpdate ?? null
|
|
465
|
+
});
|
|
466
|
+
}
|
|
467
|
+
function diffConstraints(current, target) {
|
|
468
|
+
const ops = [];
|
|
469
|
+
const table = target.name;
|
|
470
|
+
const currentUq = new Map(current.uniqueConstraints.map((u) => [u.name, u]));
|
|
471
|
+
const targetUq = new Map(target.uniqueConstraints.map((u) => [u.name, u]));
|
|
472
|
+
for (const [name, cur] of currentUq) {
|
|
473
|
+
const tgt = targetUq.get(name);
|
|
474
|
+
if (!tgt || uniqueSignature(cur) !== uniqueSignature(tgt)) {
|
|
475
|
+
ops.push(dropUnique(table, cur));
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
for (const [name, tgt] of targetUq) {
|
|
479
|
+
const cur = currentUq.get(name);
|
|
480
|
+
if (!cur || uniqueSignature(cur) !== uniqueSignature(tgt)) {
|
|
481
|
+
ops.push(addUnique(table, tgt));
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
const currentFk = new Map(current.foreignKeys.map((f) => [f.name, f]));
|
|
485
|
+
const targetFk = new Map(target.foreignKeys.map((f) => [f.name, f]));
|
|
486
|
+
for (const [name, cur] of currentFk) {
|
|
487
|
+
const tgt = targetFk.get(name);
|
|
488
|
+
if (!tgt || foreignKeySignature(cur) !== foreignKeySignature(tgt)) {
|
|
489
|
+
ops.push(dropForeignKey(table, cur));
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
for (const [name, tgt] of targetFk) {
|
|
493
|
+
const cur = currentFk.get(name);
|
|
494
|
+
if (!cur || foreignKeySignature(cur) !== foreignKeySignature(tgt)) {
|
|
495
|
+
ops.push(addForeignKey(table, tgt));
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return ops;
|
|
499
|
+
}
|
|
500
|
+
function uniqueNamed(uc) {
|
|
501
|
+
return { type: "unique", constraint: uc };
|
|
502
|
+
}
|
|
503
|
+
function foreignKeyNamed(fk) {
|
|
504
|
+
return { type: "foreignKey", constraint: fk };
|
|
505
|
+
}
|
|
506
|
+
function addUnique(table, uc) {
|
|
507
|
+
return { kind: "add_constraint", table, constraint: uniqueNamed(uc) };
|
|
508
|
+
}
|
|
509
|
+
function dropUnique(table, uc) {
|
|
510
|
+
return { kind: "drop_constraint", table, constraint: uniqueNamed(uc) };
|
|
511
|
+
}
|
|
512
|
+
function addForeignKey(table, fk) {
|
|
513
|
+
return { kind: "add_constraint", table, constraint: foreignKeyNamed(fk) };
|
|
514
|
+
}
|
|
515
|
+
function dropForeignKey(table, fk) {
|
|
516
|
+
return { kind: "drop_constraint", table, constraint: foreignKeyNamed(fk) };
|
|
517
|
+
}
|
|
384
518
|
function diffSchema(current, target) {
|
|
385
519
|
const ops = [];
|
|
386
520
|
const drops = [];
|
|
@@ -409,6 +543,7 @@ function diffSchema(current, target) {
|
|
|
409
543
|
ops.push({ kind: "drop_column", table: name, column: currentCol });
|
|
410
544
|
}
|
|
411
545
|
}
|
|
546
|
+
ops.push(...diffConstraints(currentTable, targetTable));
|
|
412
547
|
}
|
|
413
548
|
for (const [name, currentTable] of Object.entries(current.tables)) {
|
|
414
549
|
if (!target.tables[name]) {
|
|
@@ -476,23 +611,38 @@ function heads(migrations) {
|
|
|
476
611
|
function isDefaultValue(value) {
|
|
477
612
|
return typeof value === "object" && value !== null && "kind" in value && (value.kind === "literal" || value.kind === "expression");
|
|
478
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
|
+
}
|
|
479
626
|
var Column = class _Column {
|
|
480
|
-
constructor(type, flags, defaultValue = null, onUpdateValue = null) {
|
|
627
|
+
constructor(type, flags, defaultValue = null, onUpdateValue = null, reference = null) {
|
|
481
628
|
this.type = type;
|
|
482
629
|
this.flags = flags;
|
|
483
630
|
this.defaultValue = defaultValue;
|
|
484
631
|
this.onUpdateValue = onUpdateValue;
|
|
632
|
+
this.reference = reference;
|
|
485
633
|
}
|
|
486
634
|
type;
|
|
487
635
|
flags;
|
|
488
636
|
defaultValue;
|
|
489
637
|
onUpdateValue;
|
|
638
|
+
reference;
|
|
490
639
|
primaryKey() {
|
|
491
640
|
return new _Column(
|
|
492
641
|
this.type,
|
|
493
642
|
{ ...this.flags, primaryKey: true, hasDefault: true },
|
|
494
643
|
this.defaultValue,
|
|
495
|
-
this.onUpdateValue
|
|
644
|
+
this.onUpdateValue,
|
|
645
|
+
this.reference
|
|
496
646
|
);
|
|
497
647
|
}
|
|
498
648
|
notNull() {
|
|
@@ -500,7 +650,40 @@ var Column = class _Column {
|
|
|
500
650
|
this.type,
|
|
501
651
|
{ ...this.flags, notNull: true },
|
|
502
652
|
this.defaultValue,
|
|
503
|
-
this.onUpdateValue
|
|
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)
|
|
504
687
|
);
|
|
505
688
|
}
|
|
506
689
|
/**
|
|
@@ -513,7 +696,8 @@ var Column = class _Column {
|
|
|
513
696
|
this.type,
|
|
514
697
|
{ ...this.flags, hasDefault: true },
|
|
515
698
|
resolved,
|
|
516
|
-
this.onUpdateValue
|
|
699
|
+
this.onUpdateValue,
|
|
700
|
+
this.reference
|
|
517
701
|
);
|
|
518
702
|
}
|
|
519
703
|
/**
|
|
@@ -522,7 +706,7 @@ var Column = class _Column {
|
|
|
522
706
|
*/
|
|
523
707
|
onUpdate(value) {
|
|
524
708
|
const resolved = isDefaultValue(value) ? value : { kind: "literal", value };
|
|
525
|
-
return new _Column(this.type, this.flags, this.defaultValue, resolved);
|
|
709
|
+
return new _Column(this.type, this.flags, this.defaultValue, resolved, this.reference);
|
|
526
710
|
}
|
|
527
711
|
};
|
|
528
712
|
var columnsCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -541,6 +725,9 @@ function columnsOf(model) {
|
|
|
541
725
|
}
|
|
542
726
|
|
|
543
727
|
// src/migrations/ir.ts
|
|
728
|
+
function constraintName(prefix, table, columns) {
|
|
729
|
+
return `${prefix}_${table}_${columns.join("_")}`;
|
|
730
|
+
}
|
|
544
731
|
function reflectTable(model) {
|
|
545
732
|
const columns = {};
|
|
546
733
|
const primaryKey = [];
|
|
@@ -551,11 +738,32 @@ function reflectTable(model) {
|
|
|
551
738
|
type: col.type,
|
|
552
739
|
notNull: col.flags.notNull || isPk,
|
|
553
740
|
primaryKey: isPk,
|
|
554
|
-
default: col.defaultValue
|
|
741
|
+
default: col.defaultValue,
|
|
742
|
+
unique: col.flags.unique,
|
|
743
|
+
references: col.reference
|
|
555
744
|
};
|
|
556
745
|
if (isPk) primaryKey.push(name);
|
|
557
746
|
}
|
|
558
|
-
|
|
747
|
+
const uniqueConstraints = [];
|
|
748
|
+
const foreignKeys = [];
|
|
749
|
+
for (const c of model.tableArgs?.() ?? []) {
|
|
750
|
+
if (c.kind === "unique") {
|
|
751
|
+
uniqueConstraints.push({
|
|
752
|
+
name: c.name ?? constraintName("uq", model.tablename, c.columns),
|
|
753
|
+
columns: c.columns
|
|
754
|
+
});
|
|
755
|
+
} else {
|
|
756
|
+
foreignKeys.push({
|
|
757
|
+
name: c.name ?? constraintName("fk", model.tablename, c.columns),
|
|
758
|
+
columns: c.columns,
|
|
759
|
+
refTable: c.refTable,
|
|
760
|
+
refColumns: c.refColumns,
|
|
761
|
+
onDelete: c.onDelete,
|
|
762
|
+
onUpdate: c.onUpdate
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
return { name: model.tablename, columns, primaryKey, uniqueConstraints, foreignKeys };
|
|
559
767
|
}
|
|
560
768
|
function reflectSchema(models) {
|
|
561
769
|
const tables = {};
|
|
@@ -592,6 +800,39 @@ function affinityToKind(affinity) {
|
|
|
592
800
|
return "text";
|
|
593
801
|
}
|
|
594
802
|
}
|
|
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
|
+
}
|
|
595
836
|
function introspectSqlite(driver) {
|
|
596
837
|
const tablesRows = driver.execute(
|
|
597
838
|
"SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name != 'tempest_db_js_migrations'",
|
|
@@ -611,14 +852,39 @@ function introspectSqlite(driver) {
|
|
|
611
852
|
type: { kind: affinityToKind(affinity), meta: {} },
|
|
612
853
|
notNull: Number(col.notnull) === 1 || isPk,
|
|
613
854
|
primaryKey: isPk,
|
|
614
|
-
default: null
|
|
855
|
+
default: null,
|
|
856
|
+
unique: false,
|
|
857
|
+
references: null
|
|
615
858
|
};
|
|
616
859
|
if (isPk) primaryKey.push(col.name);
|
|
617
860
|
}
|
|
618
|
-
tables[tableName] = {
|
|
861
|
+
tables[tableName] = {
|
|
862
|
+
name: tableName,
|
|
863
|
+
columns,
|
|
864
|
+
primaryKey,
|
|
865
|
+
uniqueConstraints: sqliteUniques(driver, tableName),
|
|
866
|
+
foreignKeys: sqliteForeignKeys(driver, tableName)
|
|
867
|
+
};
|
|
619
868
|
}
|
|
620
869
|
return { tables };
|
|
621
870
|
}
|
|
871
|
+
function constraintKeys(table) {
|
|
872
|
+
const fks = /* @__PURE__ */ new Set();
|
|
873
|
+
const uniques = /* @__PURE__ */ new Set();
|
|
874
|
+
for (const col of Object.values(table.columns)) {
|
|
875
|
+
if (col.unique) uniques.add(col.name);
|
|
876
|
+
if (col.references) {
|
|
877
|
+
fks.add(`${col.name}=>${col.references.table}(${col.references.column})`);
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
for (const uc of table.uniqueConstraints) {
|
|
881
|
+
uniques.add([...uc.columns].sort().join(","));
|
|
882
|
+
}
|
|
883
|
+
for (const fk of table.foreignKeys) {
|
|
884
|
+
fks.add(`${fk.columns.join(",")}=>${fk.refTable}(${fk.refColumns.join(",")})`);
|
|
885
|
+
}
|
|
886
|
+
return { fks, uniques };
|
|
887
|
+
}
|
|
622
888
|
function checkDrift(driver, models) {
|
|
623
889
|
const actual = introspectSqlite(driver);
|
|
624
890
|
const expected = reflectSchema(models);
|
|
@@ -656,6 +922,34 @@ function checkDrift(driver, models) {
|
|
|
656
922
|
);
|
|
657
923
|
}
|
|
658
924
|
}
|
|
925
|
+
const expectedKeys = constraintKeys(expectedTable);
|
|
926
|
+
const actualKeys = constraintKeys(actualTable);
|
|
927
|
+
for (const fk of expectedKeys.fks) {
|
|
928
|
+
if (!actualKeys.fks.has(fk)) {
|
|
929
|
+
issues.push(`foreign key "${tableName}: ${fk}" is missing from the database`);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
for (const fk of actualKeys.fks) {
|
|
933
|
+
if (!expectedKeys.fks.has(fk)) {
|
|
934
|
+
issues.push(
|
|
935
|
+
`foreign key "${tableName}: ${fk}" exists in the database but not in the model`
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
for (const uq of expectedKeys.uniques) {
|
|
940
|
+
if (!actualKeys.uniques.has(uq)) {
|
|
941
|
+
issues.push(
|
|
942
|
+
`unique constraint "${tableName}: (${uq})" is missing from the database`
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
for (const uq of actualKeys.uniques) {
|
|
947
|
+
if (!expectedKeys.uniques.has(uq)) {
|
|
948
|
+
issues.push(
|
|
949
|
+
`unique constraint "${tableName}: (${uq})" exists in the database but not in the model`
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
659
953
|
}
|
|
660
954
|
for (const tableName of Object.keys(actual.tables)) {
|
|
661
955
|
if (!expected.tables[tableName]) {
|
|
@@ -671,7 +965,9 @@ function columnShape(col) {
|
|
|
671
965
|
type: col.type,
|
|
672
966
|
notNull: col.notNull,
|
|
673
967
|
primaryKey: col.primaryKey,
|
|
674
|
-
default: col.default
|
|
968
|
+
default: col.default,
|
|
969
|
+
unique: col.unique,
|
|
970
|
+
references: col.references
|
|
675
971
|
});
|
|
676
972
|
}
|
|
677
973
|
function tableShape(columns) {
|
|
@@ -790,6 +1086,14 @@ var Op = class {
|
|
|
790
1086
|
recreateTable(from, to) {
|
|
791
1087
|
this.run({ kind: "recreate_table", from, to });
|
|
792
1088
|
}
|
|
1089
|
+
/** Add a table-level unique / foreign-key constraint. */
|
|
1090
|
+
addConstraint(table, constraint) {
|
|
1091
|
+
this.run({ kind: "add_constraint", table, constraint });
|
|
1092
|
+
}
|
|
1093
|
+
/** Drop a table-level unique / foreign-key constraint. */
|
|
1094
|
+
dropConstraint(table, constraint) {
|
|
1095
|
+
this.run({ kind: "drop_constraint", table, constraint });
|
|
1096
|
+
}
|
|
793
1097
|
/** Raw SQL escape hatch (e.g. a data migration). `down` may be `null`. */
|
|
794
1098
|
execute(up, down = null) {
|
|
795
1099
|
this.run({ kind: "execute", up, down });
|
|
@@ -952,6 +1256,30 @@ function applyOperation(schema, op) {
|
|
|
952
1256
|
}
|
|
953
1257
|
break;
|
|
954
1258
|
}
|
|
1259
|
+
case "add_constraint": {
|
|
1260
|
+
const t = tables[op.table];
|
|
1261
|
+
if (t) {
|
|
1262
|
+
tables[op.table] = op.constraint.type === "unique" ? {
|
|
1263
|
+
...t,
|
|
1264
|
+
uniqueConstraints: [...t.uniqueConstraints, op.constraint.constraint]
|
|
1265
|
+
} : { ...t, foreignKeys: [...t.foreignKeys, op.constraint.constraint] };
|
|
1266
|
+
}
|
|
1267
|
+
break;
|
|
1268
|
+
}
|
|
1269
|
+
case "drop_constraint": {
|
|
1270
|
+
const t = tables[op.table];
|
|
1271
|
+
if (t) {
|
|
1272
|
+
const dropName = op.constraint.constraint.name;
|
|
1273
|
+
tables[op.table] = op.constraint.type === "unique" ? {
|
|
1274
|
+
...t,
|
|
1275
|
+
uniqueConstraints: t.uniqueConstraints.filter((u) => u.name !== dropName)
|
|
1276
|
+
} : {
|
|
1277
|
+
...t,
|
|
1278
|
+
foreignKeys: t.foreignKeys.filter((f) => f.name !== dropName)
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
break;
|
|
1282
|
+
}
|
|
955
1283
|
}
|
|
956
1284
|
return { tables };
|
|
957
1285
|
}
|