tempest-db-js 0.3.0 → 0.5.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 +9 -2
- package/dist/bin.cjs +475 -44
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +2 -2
- package/dist/{chunk-Q32CBI2A.js → chunk-5QQMVTS5.js} +988 -286
- package/dist/chunk-5QQMVTS5.js.map +1 -0
- package/dist/{chunk-OP7FRDI5.js → chunk-EPMLFNFK.js} +448 -37
- package/dist/chunk-EPMLFNFK.js.map +1 -0
- package/dist/index.cjs +993 -283
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +632 -30
- package/dist/index.d.ts +632 -30
- package/dist/index.js +1 -1
- package/dist/migrations/index.cjs +591 -52
- package/dist/migrations/index.cjs.map +1 -1
- package/dist/migrations/index.d.cts +63 -14
- package/dist/migrations/index.d.ts +63 -14
- 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/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");
|
|
@@ -96,6 +100,22 @@ function makeRevisionId(label, parents) {
|
|
|
96
100
|
return hash.toString(16).padStart(8, "0");
|
|
97
101
|
}
|
|
98
102
|
|
|
103
|
+
// src/expressions.ts
|
|
104
|
+
function renderPortableToken(token, dialect) {
|
|
105
|
+
switch (token) {
|
|
106
|
+
case "now":
|
|
107
|
+
return dialect === "postgresql" ? "now()" : "CURRENT_TIMESTAMP";
|
|
108
|
+
case "current_date":
|
|
109
|
+
return "CURRENT_DATE";
|
|
110
|
+
case "current_time":
|
|
111
|
+
return "CURRENT_TIME";
|
|
112
|
+
case "uuidv4":
|
|
113
|
+
if (dialect === "postgresql") return "gen_random_uuid()";
|
|
114
|
+
if (dialect === "mysql") return "(UUID())";
|
|
115
|
+
return "(lower(hex(randomblob(16))))";
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
99
119
|
// src/migrations/ddl.ts
|
|
100
120
|
function quoteId(name, dialect) {
|
|
101
121
|
return dialect === "mysql" ? `\`${name.replace(/`/g, "``")}\`` : `"${name.replace(/"/g, '""')}"`;
|
|
@@ -119,6 +139,8 @@ function renderColumnType(type, dialect) {
|
|
|
119
139
|
return "NUMERIC";
|
|
120
140
|
case "blob":
|
|
121
141
|
return "BLOB";
|
|
142
|
+
case "array":
|
|
143
|
+
throw new Error(unsupportedArray("sqlite"));
|
|
122
144
|
default:
|
|
123
145
|
return "TEXT";
|
|
124
146
|
}
|
|
@@ -160,6 +182,8 @@ function renderColumnType(type, dialect) {
|
|
|
160
182
|
return "CHAR(36)";
|
|
161
183
|
case "enum":
|
|
162
184
|
return `ENUM(${(meta.values ?? []).map(quoteLiteral).join(", ")})`;
|
|
185
|
+
case "array":
|
|
186
|
+
throw new Error(unsupportedArray("mysql"));
|
|
163
187
|
}
|
|
164
188
|
}
|
|
165
189
|
switch (kind) {
|
|
@@ -198,27 +222,39 @@ function renderColumnType(type, dialect) {
|
|
|
198
222
|
return "UUID";
|
|
199
223
|
case "enum":
|
|
200
224
|
return "TEXT";
|
|
225
|
+
case "array":
|
|
226
|
+
return `${renderColumnType(arrayElement(meta.element), dialect)}[]`;
|
|
201
227
|
}
|
|
202
228
|
}
|
|
203
|
-
function
|
|
229
|
+
function unsupportedArray(dialect) {
|
|
230
|
+
return `column.array() is PostgreSQL-only \u2014 ${dialect} has no native array type. Model the column as JSON there, accepting that array operators will not work.`;
|
|
231
|
+
}
|
|
232
|
+
function arrayElement(element) {
|
|
233
|
+
if (!element) {
|
|
234
|
+
throw new Error(
|
|
235
|
+
"An array column has no element type \u2014 build it with column.array()."
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
return element;
|
|
239
|
+
}
|
|
240
|
+
function renderDefault(def, dialect, type) {
|
|
204
241
|
if (def.kind === "expression") {
|
|
205
242
|
const expr = def.expression;
|
|
206
|
-
if (typeof expr === "object")
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
return "CURRENT_DATE";
|
|
212
|
-
case "current_time":
|
|
213
|
-
return "CURRENT_TIME";
|
|
214
|
-
case "uuidv4":
|
|
215
|
-
if (dialect === "postgresql") return "gen_random_uuid()";
|
|
216
|
-
if (dialect === "mysql") return "(UUID())";
|
|
217
|
-
return "(lower(hex(randomblob(16))))";
|
|
243
|
+
if (typeof expr === "object") {
|
|
244
|
+
if ("raw" in expr) return expr.raw;
|
|
245
|
+
throw new Error(
|
|
246
|
+
"sql.expr`...` binds parameters and cannot be rendered as a DEFAULT \u2014 use sql.raw()."
|
|
247
|
+
);
|
|
218
248
|
}
|
|
249
|
+
return renderPortableToken(expr, dialect);
|
|
219
250
|
}
|
|
220
251
|
const value = def.value;
|
|
221
252
|
if (value === null) return "NULL";
|
|
253
|
+
if (Array.isArray(value) && type?.kind === "array") {
|
|
254
|
+
const elementType = renderColumnType(arrayElement(type.meta.element), dialect);
|
|
255
|
+
const items = value.map((v) => renderDefault({ kind: "literal", value: v }, dialect));
|
|
256
|
+
return `ARRAY[${items.join(", ")}]::${elementType}[]`;
|
|
257
|
+
}
|
|
222
258
|
if (typeof value === "boolean") {
|
|
223
259
|
return dialect === "postgresql" ? value ? "TRUE" : "FALSE" : value ? "1" : "0";
|
|
224
260
|
}
|
|
@@ -227,10 +263,47 @@ function renderDefault(def, dialect) {
|
|
|
227
263
|
if (typeof value === "object") return quoteLiteral(JSON.stringify(value));
|
|
228
264
|
return quoteLiteral(String(value));
|
|
229
265
|
}
|
|
266
|
+
function renderFkAction(action) {
|
|
267
|
+
return action.toUpperCase();
|
|
268
|
+
}
|
|
269
|
+
function renderFkActions(fk) {
|
|
270
|
+
let sql = "";
|
|
271
|
+
if (fk.onDelete) sql += ` ON DELETE ${renderFkAction(fk.onDelete)}`;
|
|
272
|
+
if (fk.onUpdate) sql += ` ON UPDATE ${renderFkAction(fk.onUpdate)}`;
|
|
273
|
+
return sql;
|
|
274
|
+
}
|
|
275
|
+
function columnConstraintSuffix(col, dialect) {
|
|
276
|
+
let sql = "";
|
|
277
|
+
if (col.unique) sql += " UNIQUE";
|
|
278
|
+
if (col.references) {
|
|
279
|
+
const ref = col.references;
|
|
280
|
+
sql += ` REFERENCES ${quoteId(ref.table, dialect)} (${quoteId(ref.column, dialect)})`;
|
|
281
|
+
sql += renderFkActions(ref);
|
|
282
|
+
}
|
|
283
|
+
return sql;
|
|
284
|
+
}
|
|
285
|
+
function renderUniqueConstraint(uc, dialect) {
|
|
286
|
+
const cols = uc.columns.map((c) => quoteId(c, dialect)).join(", ");
|
|
287
|
+
return `CONSTRAINT ${quoteId(uc.name, dialect)} UNIQUE (${cols})`;
|
|
288
|
+
}
|
|
289
|
+
function renderForeignKeyConstraint(fk, dialect) {
|
|
290
|
+
const cols = fk.columns.map((c) => quoteId(c, dialect)).join(", ");
|
|
291
|
+
const refCols = fk.refColumns.map((c) => quoteId(c, dialect)).join(", ");
|
|
292
|
+
return `CONSTRAINT ${quoteId(fk.name, dialect)} FOREIGN KEY (${cols}) REFERENCES ${quoteId(fk.refTable, dialect)} (${refCols})${renderFkActions(fk)}`;
|
|
293
|
+
}
|
|
294
|
+
function tableConstraintClauses(table, dialect) {
|
|
295
|
+
return [
|
|
296
|
+
...table.uniqueConstraints.map((uc) => renderUniqueConstraint(uc, dialect)),
|
|
297
|
+
...table.foreignKeys.map((fk) => renderForeignKeyConstraint(fk, dialect))
|
|
298
|
+
];
|
|
299
|
+
}
|
|
230
300
|
function renderColumnDef(col, dialect) {
|
|
231
301
|
let sql = `${quoteId(col.name, dialect)} ${renderColumnType(col.type, dialect)}`;
|
|
232
302
|
if (col.notNull) sql += " NOT NULL";
|
|
233
|
-
if (col.default !== null)
|
|
303
|
+
if (col.default !== null) {
|
|
304
|
+
sql += ` DEFAULT ${renderDefault(col.default, dialect, col.type)}`;
|
|
305
|
+
}
|
|
306
|
+
sql += columnConstraintSuffix(col, dialect);
|
|
234
307
|
return sql;
|
|
235
308
|
}
|
|
236
309
|
function enumTypeName(table, column) {
|
|
@@ -253,14 +326,16 @@ function renderCreateTable(table, dialect) {
|
|
|
253
326
|
typeStmts.push(`CREATE TYPE ${quoteId(typeName, dialect)} AS ENUM (${values})`);
|
|
254
327
|
let def = `${quoteId(c.name, dialect)} ${quoteId(typeName, dialect)}`;
|
|
255
328
|
if (c.notNull) def += " NOT NULL";
|
|
256
|
-
if (c.default !== null)
|
|
257
|
-
|
|
329
|
+
if (c.default !== null) {
|
|
330
|
+
def += ` DEFAULT ${renderDefault(c.default, dialect, c.type)}`;
|
|
331
|
+
}
|
|
332
|
+
return def + columnConstraintSuffix(c, dialect);
|
|
258
333
|
}
|
|
259
334
|
if (dialect === "postgresql" && isAutoIncrementPk(table, c)) {
|
|
260
|
-
return `${quoteId(c.name, dialect)} ${postgresSerialType(c.type.kind)}`;
|
|
335
|
+
return `${quoteId(c.name, dialect)} ${postgresSerialType(c.type.kind)}${columnConstraintSuffix(c, dialect)}`;
|
|
261
336
|
}
|
|
262
337
|
if (dialect === "mysql" && isAutoIncrementPk(table, c)) {
|
|
263
|
-
return `${quoteId(c.name, dialect)} ${renderColumnType(c.type, dialect)} NOT NULL AUTO_INCREMENT`;
|
|
338
|
+
return `${quoteId(c.name, dialect)} ${renderColumnType(c.type, dialect)} NOT NULL AUTO_INCREMENT${columnConstraintSuffix(c, dialect)}`;
|
|
264
339
|
}
|
|
265
340
|
return renderColumnDef(c, dialect);
|
|
266
341
|
});
|
|
@@ -269,6 +344,7 @@ function renderCreateTable(table, dialect) {
|
|
|
269
344
|
`PRIMARY KEY (${table.primaryKey.map((c) => quoteId(c, dialect)).join(", ")})`
|
|
270
345
|
);
|
|
271
346
|
}
|
|
347
|
+
cols.push(...tableConstraintClauses(table, dialect));
|
|
272
348
|
return [
|
|
273
349
|
...typeStmts,
|
|
274
350
|
`CREATE TABLE ${quoteId(table.name, dialect)} (
|
|
@@ -302,10 +378,38 @@ function renderOperation(op, dialect) {
|
|
|
302
378
|
return renderAlterColumn(op.table, op.to, dialect);
|
|
303
379
|
case "recreate_table":
|
|
304
380
|
return dialect === "sqlite" ? renderSqliteRebuild(op.from, op.to) : renderTableDiff(op.from, op.to, dialect);
|
|
381
|
+
case "add_constraint":
|
|
382
|
+
return renderAddConstraint(op.table, op.constraint, dialect);
|
|
383
|
+
case "drop_constraint":
|
|
384
|
+
return renderDropConstraint(op.table, op.constraint, dialect);
|
|
305
385
|
case "execute":
|
|
306
386
|
return [op.up];
|
|
307
387
|
}
|
|
308
388
|
}
|
|
389
|
+
function renderAddConstraint(table, constraint, dialect) {
|
|
390
|
+
if (dialect === "sqlite") {
|
|
391
|
+
throw new Error(
|
|
392
|
+
`add_constraint on SQLite needs a table-rebuild (recreate_table); constraint ${constraint.constraint.name} on ${table}`
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
const clause = constraint.type === "unique" ? renderUniqueConstraint(constraint.constraint, dialect) : renderForeignKeyConstraint(constraint.constraint, dialect);
|
|
396
|
+
return [`ALTER TABLE ${quoteId(table, dialect)} ADD ${clause}`];
|
|
397
|
+
}
|
|
398
|
+
function renderDropConstraint(table, constraint, dialect) {
|
|
399
|
+
if (dialect === "sqlite") {
|
|
400
|
+
throw new Error(
|
|
401
|
+
`drop_constraint on SQLite needs a table-rebuild (recreate_table); constraint ${constraint.constraint.name} on ${table}`
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
const t = quoteId(table, dialect);
|
|
405
|
+
const name = quoteId(constraint.constraint.name, dialect);
|
|
406
|
+
if (dialect === "mysql") {
|
|
407
|
+
return [
|
|
408
|
+
constraint.type === "unique" ? `ALTER TABLE ${t} DROP INDEX ${name}` : `ALTER TABLE ${t} DROP FOREIGN KEY ${name}`
|
|
409
|
+
];
|
|
410
|
+
}
|
|
411
|
+
return [`ALTER TABLE ${t} DROP CONSTRAINT ${name}`];
|
|
412
|
+
}
|
|
309
413
|
function renderSqliteRebuild(from, to) {
|
|
310
414
|
const tmp = `__new_${to.name}`;
|
|
311
415
|
const common = Object.keys(to.columns).filter((c) => c in from.columns);
|
|
@@ -315,6 +419,7 @@ function renderSqliteRebuild(from, to) {
|
|
|
315
419
|
`PRIMARY KEY (${to.primaryKey.map((c) => quoteId(c, "sqlite")).join(", ")})`
|
|
316
420
|
);
|
|
317
421
|
}
|
|
422
|
+
cols.push(...tableConstraintClauses(to, "sqlite"));
|
|
318
423
|
const commonSql = common.map((c) => quoteId(c, "sqlite")).join(", ");
|
|
319
424
|
return [
|
|
320
425
|
"PRAGMA foreign_keys=off",
|
|
@@ -367,7 +472,7 @@ function renderAlterColumn(table, to, dialect) {
|
|
|
367
472
|
to.notNull ? `ALTER TABLE ${t} ALTER COLUMN ${c} SET NOT NULL` : `ALTER TABLE ${t} ALTER COLUMN ${c} DROP NOT NULL`
|
|
368
473
|
);
|
|
369
474
|
stmts.push(
|
|
370
|
-
to.default !== null ? `ALTER TABLE ${t} ALTER COLUMN ${c} SET DEFAULT ${renderDefault(to.default, dialect)}` : `ALTER TABLE ${t} ALTER COLUMN ${c} DROP DEFAULT`
|
|
475
|
+
to.default !== null ? `ALTER TABLE ${t} ALTER COLUMN ${c} SET DEFAULT ${renderDefault(to.default, dialect, to.type)}` : `ALTER TABLE ${t} ALTER COLUMN ${c} DROP DEFAULT`
|
|
371
476
|
);
|
|
372
477
|
return stmts;
|
|
373
478
|
}
|
|
@@ -378,9 +483,74 @@ function columnSignature(col) {
|
|
|
378
483
|
type: col.type,
|
|
379
484
|
notNull: col.notNull,
|
|
380
485
|
primaryKey: col.primaryKey,
|
|
381
|
-
default: col.default
|
|
486
|
+
default: col.default,
|
|
487
|
+
unique: col.unique,
|
|
488
|
+
references: col.references
|
|
382
489
|
});
|
|
383
490
|
}
|
|
491
|
+
function uniqueSignature(uc) {
|
|
492
|
+
return JSON.stringify({ columns: uc.columns });
|
|
493
|
+
}
|
|
494
|
+
function foreignKeySignature(fk) {
|
|
495
|
+
return JSON.stringify({
|
|
496
|
+
columns: fk.columns,
|
|
497
|
+
refTable: fk.refTable,
|
|
498
|
+
refColumns: fk.refColumns,
|
|
499
|
+
onDelete: fk.onDelete ?? null,
|
|
500
|
+
onUpdate: fk.onUpdate ?? null
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
function diffConstraints(current, target) {
|
|
504
|
+
const ops = [];
|
|
505
|
+
const table = target.name;
|
|
506
|
+
const currentUq = new Map(current.uniqueConstraints.map((u) => [u.name, u]));
|
|
507
|
+
const targetUq = new Map(target.uniqueConstraints.map((u) => [u.name, u]));
|
|
508
|
+
for (const [name, cur] of currentUq) {
|
|
509
|
+
const tgt = targetUq.get(name);
|
|
510
|
+
if (!tgt || uniqueSignature(cur) !== uniqueSignature(tgt)) {
|
|
511
|
+
ops.push(dropUnique(table, cur));
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
for (const [name, tgt] of targetUq) {
|
|
515
|
+
const cur = currentUq.get(name);
|
|
516
|
+
if (!cur || uniqueSignature(cur) !== uniqueSignature(tgt)) {
|
|
517
|
+
ops.push(addUnique(table, tgt));
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
const currentFk = new Map(current.foreignKeys.map((f) => [f.name, f]));
|
|
521
|
+
const targetFk = new Map(target.foreignKeys.map((f) => [f.name, f]));
|
|
522
|
+
for (const [name, cur] of currentFk) {
|
|
523
|
+
const tgt = targetFk.get(name);
|
|
524
|
+
if (!tgt || foreignKeySignature(cur) !== foreignKeySignature(tgt)) {
|
|
525
|
+
ops.push(dropForeignKey(table, cur));
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
for (const [name, tgt] of targetFk) {
|
|
529
|
+
const cur = currentFk.get(name);
|
|
530
|
+
if (!cur || foreignKeySignature(cur) !== foreignKeySignature(tgt)) {
|
|
531
|
+
ops.push(addForeignKey(table, tgt));
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
return ops;
|
|
535
|
+
}
|
|
536
|
+
function uniqueNamed(uc) {
|
|
537
|
+
return { type: "unique", constraint: uc };
|
|
538
|
+
}
|
|
539
|
+
function foreignKeyNamed(fk) {
|
|
540
|
+
return { type: "foreignKey", constraint: fk };
|
|
541
|
+
}
|
|
542
|
+
function addUnique(table, uc) {
|
|
543
|
+
return { kind: "add_constraint", table, constraint: uniqueNamed(uc) };
|
|
544
|
+
}
|
|
545
|
+
function dropUnique(table, uc) {
|
|
546
|
+
return { kind: "drop_constraint", table, constraint: uniqueNamed(uc) };
|
|
547
|
+
}
|
|
548
|
+
function addForeignKey(table, fk) {
|
|
549
|
+
return { kind: "add_constraint", table, constraint: foreignKeyNamed(fk) };
|
|
550
|
+
}
|
|
551
|
+
function dropForeignKey(table, fk) {
|
|
552
|
+
return { kind: "drop_constraint", table, constraint: foreignKeyNamed(fk) };
|
|
553
|
+
}
|
|
384
554
|
function diffSchema(current, target) {
|
|
385
555
|
const ops = [];
|
|
386
556
|
const drops = [];
|
|
@@ -409,6 +579,7 @@ function diffSchema(current, target) {
|
|
|
409
579
|
ops.push({ kind: "drop_column", table: name, column: currentCol });
|
|
410
580
|
}
|
|
411
581
|
}
|
|
582
|
+
ops.push(...diffConstraints(currentTable, targetTable));
|
|
412
583
|
}
|
|
413
584
|
for (const [name, currentTable] of Object.entries(current.tables)) {
|
|
414
585
|
if (!target.tables[name]) {
|
|
@@ -476,55 +647,143 @@ function heads(migrations) {
|
|
|
476
647
|
function isDefaultValue(value) {
|
|
477
648
|
return typeof value === "object" && value !== null && "kind" in value && (value.kind === "literal" || value.kind === "expression");
|
|
478
649
|
}
|
|
650
|
+
function bindsParameters(value) {
|
|
651
|
+
return value.kind === "expression" && typeof value.expression === "object" && "parts" in value.expression;
|
|
652
|
+
}
|
|
653
|
+
function parseReference(ref, options) {
|
|
654
|
+
const dot = ref.lastIndexOf(".");
|
|
655
|
+
if (dot <= 0 || dot === ref.length - 1) {
|
|
656
|
+
throw new Error(`Invalid foreign key reference "${ref}"; expected "table.column".`);
|
|
657
|
+
}
|
|
658
|
+
return {
|
|
659
|
+
table: ref.slice(0, dot),
|
|
660
|
+
column: ref.slice(dot + 1),
|
|
661
|
+
onDelete: options?.onDelete,
|
|
662
|
+
onUpdate: options?.onUpdate
|
|
663
|
+
};
|
|
664
|
+
}
|
|
479
665
|
var Column = class _Column {
|
|
480
|
-
constructor(type, flags, defaultValue = null, onUpdateValue = null) {
|
|
666
|
+
constructor(type, flags, defaultValue = null, onUpdateValue = null, reference = null, dbName = null) {
|
|
481
667
|
this.type = type;
|
|
482
668
|
this.flags = flags;
|
|
483
669
|
this.defaultValue = defaultValue;
|
|
484
670
|
this.onUpdateValue = onUpdateValue;
|
|
671
|
+
this.reference = reference;
|
|
672
|
+
this.dbName = dbName;
|
|
485
673
|
}
|
|
486
674
|
type;
|
|
487
675
|
flags;
|
|
488
676
|
defaultValue;
|
|
489
677
|
onUpdateValue;
|
|
490
|
-
|
|
678
|
+
reference;
|
|
679
|
+
dbName;
|
|
680
|
+
/** Clone this column with one facet replaced, carrying every other over. */
|
|
681
|
+
derive(patch) {
|
|
491
682
|
return new _Column(
|
|
492
683
|
this.type,
|
|
493
|
-
|
|
494
|
-
this.defaultValue,
|
|
495
|
-
this.onUpdateValue
|
|
684
|
+
patch.flags ?? this.flags,
|
|
685
|
+
patch.defaultValue !== void 0 ? patch.defaultValue : this.defaultValue,
|
|
686
|
+
patch.onUpdateValue !== void 0 ? patch.onUpdateValue : this.onUpdateValue,
|
|
687
|
+
patch.reference !== void 0 ? patch.reference : this.reference,
|
|
688
|
+
patch.dbName !== void 0 ? patch.dbName : this.dbName
|
|
496
689
|
);
|
|
497
690
|
}
|
|
691
|
+
primaryKey() {
|
|
692
|
+
return this.derive({ flags: { ...this.flags, primaryKey: true, hasDefault: true } });
|
|
693
|
+
}
|
|
498
694
|
notNull() {
|
|
499
|
-
return
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
695
|
+
return this.derive({ flags: { ...this.flags, notNull: true } });
|
|
696
|
+
}
|
|
697
|
+
/**
|
|
698
|
+
* Add a `UNIQUE` constraint to the column (mirrors SQLAlchemy's
|
|
699
|
+
* `mapped_column(unique=True)`). DDL-only — does not change the inferred type.
|
|
700
|
+
*/
|
|
701
|
+
unique() {
|
|
702
|
+
return this.derive({ flags: { ...this.flags, unique: true } });
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Map this property to a differently-named database column, à la SQLAlchemy's
|
|
706
|
+
* `mapped_column("consumer_name")` (Django's `db_column`, Prisma's `@map`).
|
|
707
|
+
*
|
|
708
|
+
* The override applies everywhere the name reaches SQL — select, insert,
|
|
709
|
+
* update, delete, where, order by, group by, returning, conflict targets, the
|
|
710
|
+
* migration IR and the drift check — while the TypeScript row keeps the
|
|
711
|
+
* property name. Use it to keep a `snake_case` schema behind a `camelCase`
|
|
712
|
+
* model; {@link Model.naming} does the same for a whole table at once.
|
|
713
|
+
*
|
|
714
|
+
* @param dbName The real column name in the database.
|
|
715
|
+
* @returns A new column bound to that name.
|
|
716
|
+
* @throws Error When `dbName` is empty.
|
|
717
|
+
*
|
|
718
|
+
* @example
|
|
719
|
+
* ```ts
|
|
720
|
+
* class ApiKey extends Model {
|
|
721
|
+
* static tablename = "api_keys";
|
|
722
|
+
* consumerName = column.text().name("consumer_name").notNull();
|
|
723
|
+
* }
|
|
724
|
+
* ```
|
|
725
|
+
*/
|
|
726
|
+
name(dbName) {
|
|
727
|
+
if (dbName.length === 0) {
|
|
728
|
+
throw new Error("column.name() requires a non-empty database column name.");
|
|
729
|
+
}
|
|
730
|
+
return this.derive({ dbName });
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* Declare a foreign-key reference to another table's column, à la SQLAlchemy's
|
|
734
|
+
* `mapped_column(ForeignKey("table.column", ondelete=...))`. DDL-only — does
|
|
735
|
+
* not change the inferred type.
|
|
736
|
+
*
|
|
737
|
+
* @param ref The target as `"table.column"` (e.g. `"users.id"`).
|
|
738
|
+
* @param options Optional `onDelete` / `onUpdate` referential actions.
|
|
739
|
+
* @returns A new column carrying the reference.
|
|
740
|
+
* @throws Error When `ref` is not a valid `"table.column"` string.
|
|
741
|
+
*/
|
|
742
|
+
references(ref, options) {
|
|
743
|
+
return this.derive({ reference: parseReference(ref, options) });
|
|
505
744
|
}
|
|
506
745
|
/**
|
|
507
746
|
* Set the insert-time default: a constant value of type `T`, or a portable
|
|
508
747
|
* server-side expression from {@link sql} (e.g. `sql.now()`, `sql.uuidv4()`).
|
|
748
|
+
*
|
|
749
|
+
* @param value The literal default, or a {@link sql} expression.
|
|
750
|
+
* @returns A new column carrying the default.
|
|
751
|
+
* @throws Error When given a `sql.expr` fragment — a `DEFAULT` clause has
|
|
752
|
+
* nowhere to bind parameters; use `sql.raw()` for a verbatim expression.
|
|
509
753
|
*/
|
|
510
754
|
default(value) {
|
|
511
755
|
const resolved = isDefaultValue(value) ? value : { kind: "literal", value };
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
756
|
+
if (bindsParameters(resolved)) {
|
|
757
|
+
throw new Error(
|
|
758
|
+
"sql.expr`...` binds parameters and cannot be a column default \u2014 use sql.raw() for a verbatim DEFAULT expression."
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
return this.derive({
|
|
762
|
+
flags: { ...this.flags, hasDefault: true },
|
|
763
|
+
defaultValue: resolved
|
|
764
|
+
});
|
|
518
765
|
}
|
|
519
766
|
/**
|
|
520
767
|
* Re-apply a value whenever the row is updated (e.g. an `updated_at` column
|
|
521
768
|
* with `sql.now()`). Mirrors SQLAlchemy's `onupdate`.
|
|
769
|
+
*
|
|
770
|
+
* @param value The literal value, or a {@link sql} expression.
|
|
771
|
+
* @returns A new column carrying the on-update value.
|
|
772
|
+
* @throws Error When given a `sql.expr` fragment (see {@link Column.default}).
|
|
522
773
|
*/
|
|
523
774
|
onUpdate(value) {
|
|
524
775
|
const resolved = isDefaultValue(value) ? value : { kind: "literal", value };
|
|
525
|
-
|
|
776
|
+
if (bindsParameters(resolved)) {
|
|
777
|
+
throw new Error(
|
|
778
|
+
"sql.expr`...` binds parameters and cannot be an onUpdate default \u2014 use sql.raw() for a verbatim expression."
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
return this.derive({ onUpdateValue: resolved });
|
|
526
782
|
}
|
|
527
783
|
};
|
|
784
|
+
function toSnakeCase(name) {
|
|
785
|
+
return name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
786
|
+
}
|
|
528
787
|
var columnsCache = /* @__PURE__ */ new WeakMap();
|
|
529
788
|
function columnsOf(model) {
|
|
530
789
|
const cached = columnsCache.get(model);
|
|
@@ -539,23 +798,75 @@ function columnsOf(model) {
|
|
|
539
798
|
columnsCache.set(model, out);
|
|
540
799
|
return out;
|
|
541
800
|
}
|
|
801
|
+
var nameMapCache = /* @__PURE__ */ new WeakMap();
|
|
802
|
+
function columnNamesOf(model) {
|
|
803
|
+
const cached = nameMapCache.get(model);
|
|
804
|
+
if (cached !== void 0) return cached;
|
|
805
|
+
const strategy = model.naming ?? "preserve";
|
|
806
|
+
const map = {};
|
|
807
|
+
const seen = /* @__PURE__ */ new Map();
|
|
808
|
+
let renamed = false;
|
|
809
|
+
for (const [prop, col] of Object.entries(columnsOf(model))) {
|
|
810
|
+
const dbName = col.dbName ?? (strategy === "snake_case" ? toSnakeCase(prop) : prop);
|
|
811
|
+
const collision = seen.get(dbName);
|
|
812
|
+
if (collision !== void 0) {
|
|
813
|
+
throw new Error(
|
|
814
|
+
`${model.tablename}: properties "${collision}" and "${prop}" both map to column "${dbName}".`
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
seen.set(dbName, prop);
|
|
818
|
+
map[prop] = dbName;
|
|
819
|
+
if (dbName !== prop) renamed = true;
|
|
820
|
+
}
|
|
821
|
+
const result = renamed ? map : null;
|
|
822
|
+
nameMapCache.set(model, result);
|
|
823
|
+
return result;
|
|
824
|
+
}
|
|
542
825
|
|
|
543
826
|
// src/migrations/ir.ts
|
|
827
|
+
function constraintName(prefix, table, columns) {
|
|
828
|
+
return `${prefix}_${table}_${columns.join("_")}`;
|
|
829
|
+
}
|
|
544
830
|
function reflectTable(model) {
|
|
831
|
+
const names = columnNamesOf(model);
|
|
832
|
+
const toColumn = (prop) => names?.[prop] ?? prop;
|
|
545
833
|
const columns = {};
|
|
546
834
|
const primaryKey = [];
|
|
547
|
-
for (const [
|
|
835
|
+
for (const [prop, col] of Object.entries(columnsOf(model))) {
|
|
548
836
|
const isPk = col.flags.primaryKey;
|
|
837
|
+
const name = toColumn(prop);
|
|
549
838
|
columns[name] = {
|
|
550
839
|
name,
|
|
551
840
|
type: col.type,
|
|
552
841
|
notNull: col.flags.notNull || isPk,
|
|
553
842
|
primaryKey: isPk,
|
|
554
|
-
default: col.defaultValue
|
|
843
|
+
default: col.defaultValue,
|
|
844
|
+
unique: col.flags.unique,
|
|
845
|
+
references: col.reference
|
|
555
846
|
};
|
|
556
847
|
if (isPk) primaryKey.push(name);
|
|
557
848
|
}
|
|
558
|
-
|
|
849
|
+
const uniqueConstraints = [];
|
|
850
|
+
const foreignKeys = [];
|
|
851
|
+
for (const c of model.tableArgs?.() ?? []) {
|
|
852
|
+
const cols = c.columns.map(toColumn);
|
|
853
|
+
if (c.kind === "unique") {
|
|
854
|
+
uniqueConstraints.push({
|
|
855
|
+
name: c.name ?? constraintName("uq", model.tablename, cols),
|
|
856
|
+
columns: cols
|
|
857
|
+
});
|
|
858
|
+
} else {
|
|
859
|
+
foreignKeys.push({
|
|
860
|
+
name: c.name ?? constraintName("fk", model.tablename, cols),
|
|
861
|
+
columns: cols,
|
|
862
|
+
refTable: c.refTable,
|
|
863
|
+
refColumns: c.refColumns,
|
|
864
|
+
onDelete: c.onDelete,
|
|
865
|
+
onUpdate: c.onUpdate
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
return { name: model.tablename, columns, primaryKey, uniqueConstraints, foreignKeys };
|
|
559
870
|
}
|
|
560
871
|
function reflectSchema(models) {
|
|
561
872
|
const tables = {};
|
|
@@ -592,6 +903,39 @@ function affinityToKind(affinity) {
|
|
|
592
903
|
return "text";
|
|
593
904
|
}
|
|
594
905
|
}
|
|
906
|
+
function sqliteForeignKeys(driver, table) {
|
|
907
|
+
const rows = driver.execute(`PRAGMA foreign_key_list(${JSON.stringify(table)})`, []).rows;
|
|
908
|
+
const byId = /* @__PURE__ */ new Map();
|
|
909
|
+
for (const r of rows) {
|
|
910
|
+
const list = byId.get(r.id) ?? [];
|
|
911
|
+
list.push(r);
|
|
912
|
+
byId.set(r.id, list);
|
|
913
|
+
}
|
|
914
|
+
const fks = [];
|
|
915
|
+
for (const group of byId.values()) {
|
|
916
|
+
const ordered = [...group].sort((a, b) => a.seq - b.seq);
|
|
917
|
+
const columns = ordered.map((r) => r.from);
|
|
918
|
+
fks.push({
|
|
919
|
+
name: `fk_${table}_${columns.join("_")}`,
|
|
920
|
+
columns,
|
|
921
|
+
refTable: ordered[0]?.table ?? "",
|
|
922
|
+
refColumns: ordered.map((r) => r.to)
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
return fks;
|
|
926
|
+
}
|
|
927
|
+
function sqliteUniques(driver, table) {
|
|
928
|
+
const indexes = driver.execute(`PRAGMA index_list(${JSON.stringify(table)})`, []).rows;
|
|
929
|
+
const uniques = [];
|
|
930
|
+
for (const idx of indexes) {
|
|
931
|
+
if (Number(idx.unique) !== 1 || idx.origin === "pk") continue;
|
|
932
|
+
const cols = driver.execute(`PRAGMA index_info(${JSON.stringify(idx.name)})`, []).rows.sort((a, b) => a.seqno - b.seqno).map((c) => c.name);
|
|
933
|
+
if (cols.length > 0) {
|
|
934
|
+
uniques.push({ name: `uq_${table}_${cols.join("_")}`, columns: cols });
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
return uniques;
|
|
938
|
+
}
|
|
595
939
|
function introspectSqlite(driver) {
|
|
596
940
|
const tablesRows = driver.execute(
|
|
597
941
|
"SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name != 'tempest_db_js_migrations'",
|
|
@@ -611,14 +955,39 @@ function introspectSqlite(driver) {
|
|
|
611
955
|
type: { kind: affinityToKind(affinity), meta: {} },
|
|
612
956
|
notNull: Number(col.notnull) === 1 || isPk,
|
|
613
957
|
primaryKey: isPk,
|
|
614
|
-
default: null
|
|
958
|
+
default: null,
|
|
959
|
+
unique: false,
|
|
960
|
+
references: null
|
|
615
961
|
};
|
|
616
962
|
if (isPk) primaryKey.push(col.name);
|
|
617
963
|
}
|
|
618
|
-
tables[tableName] = {
|
|
964
|
+
tables[tableName] = {
|
|
965
|
+
name: tableName,
|
|
966
|
+
columns,
|
|
967
|
+
primaryKey,
|
|
968
|
+
uniqueConstraints: sqliteUniques(driver, tableName),
|
|
969
|
+
foreignKeys: sqliteForeignKeys(driver, tableName)
|
|
970
|
+
};
|
|
619
971
|
}
|
|
620
972
|
return { tables };
|
|
621
973
|
}
|
|
974
|
+
function constraintKeys(table) {
|
|
975
|
+
const fks = /* @__PURE__ */ new Set();
|
|
976
|
+
const uniques = /* @__PURE__ */ new Set();
|
|
977
|
+
for (const col of Object.values(table.columns)) {
|
|
978
|
+
if (col.unique) uniques.add(col.name);
|
|
979
|
+
if (col.references) {
|
|
980
|
+
fks.add(`${col.name}=>${col.references.table}(${col.references.column})`);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
for (const uc of table.uniqueConstraints) {
|
|
984
|
+
uniques.add([...uc.columns].sort().join(","));
|
|
985
|
+
}
|
|
986
|
+
for (const fk of table.foreignKeys) {
|
|
987
|
+
fks.add(`${fk.columns.join(",")}=>${fk.refTable}(${fk.refColumns.join(",")})`);
|
|
988
|
+
}
|
|
989
|
+
return { fks, uniques };
|
|
990
|
+
}
|
|
622
991
|
function checkDrift(driver, models) {
|
|
623
992
|
const actual = introspectSqlite(driver);
|
|
624
993
|
const expected = reflectSchema(models);
|
|
@@ -656,6 +1025,34 @@ function checkDrift(driver, models) {
|
|
|
656
1025
|
);
|
|
657
1026
|
}
|
|
658
1027
|
}
|
|
1028
|
+
const expectedKeys = constraintKeys(expectedTable);
|
|
1029
|
+
const actualKeys = constraintKeys(actualTable);
|
|
1030
|
+
for (const fk of expectedKeys.fks) {
|
|
1031
|
+
if (!actualKeys.fks.has(fk)) {
|
|
1032
|
+
issues.push(`foreign key "${tableName}: ${fk}" is missing from the database`);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
for (const fk of actualKeys.fks) {
|
|
1036
|
+
if (!expectedKeys.fks.has(fk)) {
|
|
1037
|
+
issues.push(
|
|
1038
|
+
`foreign key "${tableName}: ${fk}" exists in the database but not in the model`
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
for (const uq of expectedKeys.uniques) {
|
|
1043
|
+
if (!actualKeys.uniques.has(uq)) {
|
|
1044
|
+
issues.push(
|
|
1045
|
+
`unique constraint "${tableName}: (${uq})" is missing from the database`
|
|
1046
|
+
);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
for (const uq of actualKeys.uniques) {
|
|
1050
|
+
if (!expectedKeys.uniques.has(uq)) {
|
|
1051
|
+
issues.push(
|
|
1052
|
+
`unique constraint "${tableName}: (${uq})" exists in the database but not in the model`
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
659
1056
|
}
|
|
660
1057
|
for (const tableName of Object.keys(actual.tables)) {
|
|
661
1058
|
if (!expected.tables[tableName]) {
|
|
@@ -671,7 +1068,9 @@ function columnShape(col) {
|
|
|
671
1068
|
type: col.type,
|
|
672
1069
|
notNull: col.notNull,
|
|
673
1070
|
primaryKey: col.primaryKey,
|
|
674
|
-
default: col.default
|
|
1071
|
+
default: col.default,
|
|
1072
|
+
unique: col.unique,
|
|
1073
|
+
references: col.references
|
|
675
1074
|
});
|
|
676
1075
|
}
|
|
677
1076
|
function tableShape(columns) {
|
|
@@ -790,6 +1189,14 @@ var Op = class {
|
|
|
790
1189
|
recreateTable(from, to) {
|
|
791
1190
|
this.run({ kind: "recreate_table", from, to });
|
|
792
1191
|
}
|
|
1192
|
+
/** Add a table-level unique / foreign-key constraint. */
|
|
1193
|
+
addConstraint(table, constraint) {
|
|
1194
|
+
this.run({ kind: "add_constraint", table, constraint });
|
|
1195
|
+
}
|
|
1196
|
+
/** Drop a table-level unique / foreign-key constraint. */
|
|
1197
|
+
dropConstraint(table, constraint) {
|
|
1198
|
+
this.run({ kind: "drop_constraint", table, constraint });
|
|
1199
|
+
}
|
|
793
1200
|
/** Raw SQL escape hatch (e.g. a data migration). `down` may be `null`. */
|
|
794
1201
|
execute(up, down = null) {
|
|
795
1202
|
this.run({ kind: "execute", up, down });
|
|
@@ -952,6 +1359,30 @@ function applyOperation(schema, op) {
|
|
|
952
1359
|
}
|
|
953
1360
|
break;
|
|
954
1361
|
}
|
|
1362
|
+
case "add_constraint": {
|
|
1363
|
+
const t = tables[op.table];
|
|
1364
|
+
if (t) {
|
|
1365
|
+
tables[op.table] = op.constraint.type === "unique" ? {
|
|
1366
|
+
...t,
|
|
1367
|
+
uniqueConstraints: [...t.uniqueConstraints, op.constraint.constraint]
|
|
1368
|
+
} : { ...t, foreignKeys: [...t.foreignKeys, op.constraint.constraint] };
|
|
1369
|
+
}
|
|
1370
|
+
break;
|
|
1371
|
+
}
|
|
1372
|
+
case "drop_constraint": {
|
|
1373
|
+
const t = tables[op.table];
|
|
1374
|
+
if (t) {
|
|
1375
|
+
const dropName = op.constraint.constraint.name;
|
|
1376
|
+
tables[op.table] = op.constraint.type === "unique" ? {
|
|
1377
|
+
...t,
|
|
1378
|
+
uniqueConstraints: t.uniqueConstraints.filter((u) => u.name !== dropName)
|
|
1379
|
+
} : {
|
|
1380
|
+
...t,
|
|
1381
|
+
foreignKeys: t.foreignKeys.filter((f) => f.name !== dropName)
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
break;
|
|
1385
|
+
}
|
|
955
1386
|
}
|
|
956
1387
|
return { tables };
|
|
957
1388
|
}
|