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
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");
|
|
@@ -97,8 +101,8 @@ function makeRevisionId(label, parents) {
|
|
|
97
101
|
}
|
|
98
102
|
|
|
99
103
|
// src/migrations/ddl.ts
|
|
100
|
-
function quoteId(name) {
|
|
101
|
-
return `"${name.replace(/"/g, '""')}"`;
|
|
104
|
+
function quoteId(name, dialect) {
|
|
105
|
+
return dialect === "mysql" ? `\`${name.replace(/`/g, "``")}\`` : `"${name.replace(/"/g, '""')}"`;
|
|
102
106
|
}
|
|
103
107
|
function quoteLiteral(value) {
|
|
104
108
|
return `'${value.replace(/'/g, "''")}'`;
|
|
@@ -123,6 +127,45 @@ function renderColumnType(type, dialect) {
|
|
|
123
127
|
return "TEXT";
|
|
124
128
|
}
|
|
125
129
|
}
|
|
130
|
+
if (dialect === "mysql") {
|
|
131
|
+
switch (kind) {
|
|
132
|
+
case "smallint":
|
|
133
|
+
return "SMALLINT";
|
|
134
|
+
case "integer":
|
|
135
|
+
return "INT";
|
|
136
|
+
case "bigint":
|
|
137
|
+
return "BIGINT";
|
|
138
|
+
case "numeric":
|
|
139
|
+
return meta.precision !== void 0 ? `DECIMAL(${meta.precision}${meta.scale !== void 0 ? `, ${meta.scale}` : ""})` : "DECIMAL";
|
|
140
|
+
case "real":
|
|
141
|
+
return "FLOAT";
|
|
142
|
+
case "double":
|
|
143
|
+
return "DOUBLE";
|
|
144
|
+
case "varchar":
|
|
145
|
+
return `VARCHAR(${meta.length ?? 255})`;
|
|
146
|
+
case "char":
|
|
147
|
+
return `CHAR(${meta.length ?? 255})`;
|
|
148
|
+
case "text":
|
|
149
|
+
return "TEXT";
|
|
150
|
+
case "boolean":
|
|
151
|
+
return "TINYINT(1)";
|
|
152
|
+
case "date":
|
|
153
|
+
return "DATE";
|
|
154
|
+
case "time":
|
|
155
|
+
return "TIME";
|
|
156
|
+
case "datetime":
|
|
157
|
+
case "timestamp":
|
|
158
|
+
return "DATETIME";
|
|
159
|
+
case "blob":
|
|
160
|
+
return "BLOB";
|
|
161
|
+
case "json":
|
|
162
|
+
return "JSON";
|
|
163
|
+
case "uuid":
|
|
164
|
+
return "CHAR(36)";
|
|
165
|
+
case "enum":
|
|
166
|
+
return `ENUM(${(meta.values ?? []).map(quoteLiteral).join(", ")})`;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
126
169
|
switch (kind) {
|
|
127
170
|
case "smallint":
|
|
128
171
|
return "SMALLINT";
|
|
@@ -167,29 +210,66 @@ function renderDefault(def, dialect) {
|
|
|
167
210
|
if (typeof expr === "object") return expr.raw;
|
|
168
211
|
switch (expr) {
|
|
169
212
|
case "now":
|
|
170
|
-
return dialect === "
|
|
213
|
+
return dialect === "postgresql" ? "now()" : "CURRENT_TIMESTAMP";
|
|
171
214
|
case "current_date":
|
|
172
215
|
return "CURRENT_DATE";
|
|
173
216
|
case "current_time":
|
|
174
217
|
return "CURRENT_TIME";
|
|
175
218
|
case "uuidv4":
|
|
176
|
-
|
|
219
|
+
if (dialect === "postgresql") return "gen_random_uuid()";
|
|
220
|
+
if (dialect === "mysql") return "(UUID())";
|
|
221
|
+
return "(lower(hex(randomblob(16))))";
|
|
177
222
|
}
|
|
178
223
|
}
|
|
179
224
|
const value = def.value;
|
|
180
225
|
if (value === null) return "NULL";
|
|
181
226
|
if (typeof value === "boolean") {
|
|
182
|
-
return dialect === "
|
|
227
|
+
return dialect === "postgresql" ? value ? "TRUE" : "FALSE" : value ? "1" : "0";
|
|
183
228
|
}
|
|
184
229
|
if (typeof value === "number" || typeof value === "bigint") return String(value);
|
|
185
230
|
if (value instanceof Date) return quoteLiteral(value.toISOString());
|
|
186
231
|
if (typeof value === "object") return quoteLiteral(JSON.stringify(value));
|
|
187
232
|
return quoteLiteral(String(value));
|
|
188
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
|
+
}
|
|
189
268
|
function renderColumnDef(col, dialect) {
|
|
190
|
-
let sql = `${quoteId(col.name)} ${renderColumnType(col.type, dialect)}`;
|
|
269
|
+
let sql = `${quoteId(col.name, dialect)} ${renderColumnType(col.type, dialect)}`;
|
|
191
270
|
if (col.notNull) sql += " NOT NULL";
|
|
192
271
|
if (col.default !== null) sql += ` DEFAULT ${renderDefault(col.default, dialect)}`;
|
|
272
|
+
sql += columnConstraintSuffix(col, dialect);
|
|
193
273
|
return sql;
|
|
194
274
|
}
|
|
195
275
|
function enumTypeName(table, column) {
|
|
@@ -209,23 +289,29 @@ function renderCreateTable(table, dialect) {
|
|
|
209
289
|
if (dialect === "postgresql" && c.type.kind === "enum") {
|
|
210
290
|
const typeName = enumTypeName(table.name, c.name);
|
|
211
291
|
const values = (c.type.meta.values ?? []).map(quoteLiteral).join(", ");
|
|
212
|
-
typeStmts.push(`CREATE TYPE ${quoteId(typeName)} AS ENUM (${values})`);
|
|
213
|
-
let def = `${quoteId(c.name)} ${quoteId(typeName)}`;
|
|
292
|
+
typeStmts.push(`CREATE TYPE ${quoteId(typeName, dialect)} AS ENUM (${values})`);
|
|
293
|
+
let def = `${quoteId(c.name, dialect)} ${quoteId(typeName, dialect)}`;
|
|
214
294
|
if (c.notNull) def += " NOT NULL";
|
|
215
295
|
if (c.default !== null) def += ` DEFAULT ${renderDefault(c.default, dialect)}`;
|
|
216
|
-
return def;
|
|
296
|
+
return def + columnConstraintSuffix(c, dialect);
|
|
217
297
|
}
|
|
218
298
|
if (dialect === "postgresql" && isAutoIncrementPk(table, c)) {
|
|
219
|
-
return `${quoteId(c.name)} ${postgresSerialType(c.type.kind)}`;
|
|
299
|
+
return `${quoteId(c.name, dialect)} ${postgresSerialType(c.type.kind)}${columnConstraintSuffix(c, dialect)}`;
|
|
300
|
+
}
|
|
301
|
+
if (dialect === "mysql" && isAutoIncrementPk(table, c)) {
|
|
302
|
+
return `${quoteId(c.name, dialect)} ${renderColumnType(c.type, dialect)} NOT NULL AUTO_INCREMENT${columnConstraintSuffix(c, dialect)}`;
|
|
220
303
|
}
|
|
221
304
|
return renderColumnDef(c, dialect);
|
|
222
305
|
});
|
|
223
306
|
if (table.primaryKey.length > 0) {
|
|
224
|
-
cols.push(
|
|
307
|
+
cols.push(
|
|
308
|
+
`PRIMARY KEY (${table.primaryKey.map((c) => quoteId(c, dialect)).join(", ")})`
|
|
309
|
+
);
|
|
225
310
|
}
|
|
311
|
+
cols.push(...tableConstraintClauses(table, dialect));
|
|
226
312
|
return [
|
|
227
313
|
...typeStmts,
|
|
228
|
-
`CREATE TABLE ${quoteId(table.name)} (
|
|
314
|
+
`CREATE TABLE ${quoteId(table.name, dialect)} (
|
|
229
315
|
${cols.join(",\n ")}
|
|
230
316
|
)`
|
|
231
317
|
];
|
|
@@ -235,60 +321,97 @@ function renderOperation(op, dialect) {
|
|
|
235
321
|
case "create_table":
|
|
236
322
|
return renderCreateTable(op.table, dialect);
|
|
237
323
|
case "drop_table":
|
|
238
|
-
return [`DROP TABLE ${quoteId(op.table.name)}`];
|
|
324
|
+
return [`DROP TABLE ${quoteId(op.table.name, dialect)}`];
|
|
239
325
|
case "rename_table":
|
|
240
|
-
return [`
|
|
326
|
+
return dialect === "mysql" ? [`RENAME TABLE ${quoteId(op.from, dialect)} TO ${quoteId(op.to, dialect)}`] : [
|
|
327
|
+
`ALTER TABLE ${quoteId(op.from, dialect)} RENAME TO ${quoteId(op.to, dialect)}`
|
|
328
|
+
];
|
|
241
329
|
case "add_column":
|
|
242
330
|
return [
|
|
243
|
-
`ALTER TABLE ${quoteId(op.table)} ADD COLUMN ${renderColumnDef(op.column, dialect)}`
|
|
331
|
+
`ALTER TABLE ${quoteId(op.table, dialect)} ADD COLUMN ${renderColumnDef(op.column, dialect)}`
|
|
244
332
|
];
|
|
245
333
|
case "drop_column":
|
|
246
|
-
return [
|
|
334
|
+
return [
|
|
335
|
+
`ALTER TABLE ${quoteId(op.table, dialect)} DROP COLUMN ${quoteId(op.column.name, dialect)}`
|
|
336
|
+
];
|
|
247
337
|
case "rename_column":
|
|
248
338
|
return [
|
|
249
|
-
`ALTER TABLE ${quoteId(op.table)} RENAME COLUMN ${quoteId(op.from)} TO ${quoteId(op.to)}`
|
|
339
|
+
`ALTER TABLE ${quoteId(op.table, dialect)} RENAME COLUMN ${quoteId(op.from, dialect)} TO ${quoteId(op.to, dialect)}`
|
|
250
340
|
];
|
|
251
341
|
case "alter_column":
|
|
252
342
|
return renderAlterColumn(op.table, op.to, dialect);
|
|
253
343
|
case "recreate_table":
|
|
254
|
-
return dialect === "sqlite" ? renderSqliteRebuild(op.from, op.to) :
|
|
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);
|
|
255
349
|
case "execute":
|
|
256
350
|
return [op.up];
|
|
257
351
|
}
|
|
258
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
|
+
}
|
|
259
377
|
function renderSqliteRebuild(from, to) {
|
|
260
378
|
const tmp = `__new_${to.name}`;
|
|
261
379
|
const common = Object.keys(to.columns).filter((c) => c in from.columns);
|
|
262
380
|
const cols = Object.values(to.columns).map((c) => renderColumnDef(c, "sqlite"));
|
|
263
381
|
if (to.primaryKey.length > 0) {
|
|
264
|
-
cols.push(
|
|
382
|
+
cols.push(
|
|
383
|
+
`PRIMARY KEY (${to.primaryKey.map((c) => quoteId(c, "sqlite")).join(", ")})`
|
|
384
|
+
);
|
|
265
385
|
}
|
|
266
|
-
|
|
386
|
+
cols.push(...tableConstraintClauses(to, "sqlite"));
|
|
387
|
+
const commonSql = common.map((c) => quoteId(c, "sqlite")).join(", ");
|
|
267
388
|
return [
|
|
268
389
|
"PRAGMA foreign_keys=off",
|
|
269
|
-
`CREATE TABLE ${quoteId(tmp)} (
|
|
390
|
+
`CREATE TABLE ${quoteId(tmp, "sqlite")} (
|
|
270
391
|
${cols.join(",\n ")}
|
|
271
392
|
)`,
|
|
272
|
-
common.length > 0 ? `INSERT INTO ${quoteId(tmp)} (${commonSql}) SELECT ${commonSql} FROM ${quoteId(from.name)}` : `-- no common columns to copy from ${from.name}`,
|
|
273
|
-
`DROP TABLE ${quoteId(from.name)}`,
|
|
274
|
-
`ALTER TABLE ${quoteId(tmp)} RENAME TO ${quoteId(to.name)}`,
|
|
393
|
+
common.length > 0 ? `INSERT INTO ${quoteId(tmp, "sqlite")} (${commonSql}) SELECT ${commonSql} FROM ${quoteId(from.name, "sqlite")}` : `-- no common columns to copy from ${from.name}`,
|
|
394
|
+
`DROP TABLE ${quoteId(from.name, "sqlite")}`,
|
|
395
|
+
`ALTER TABLE ${quoteId(tmp, "sqlite")} RENAME TO ${quoteId(to.name, "sqlite")}`,
|
|
275
396
|
"PRAGMA foreign_keys=on"
|
|
276
397
|
];
|
|
277
398
|
}
|
|
278
|
-
function
|
|
399
|
+
function renderTableDiff(from, to, dialect) {
|
|
279
400
|
const stmts = [];
|
|
280
401
|
for (const [name, col] of Object.entries(to.columns)) {
|
|
281
402
|
if (!(name in from.columns)) {
|
|
282
403
|
stmts.push(
|
|
283
|
-
`ALTER TABLE ${quoteId(to.name)} ADD COLUMN ${renderColumnDef(col,
|
|
404
|
+
`ALTER TABLE ${quoteId(to.name, dialect)} ADD COLUMN ${renderColumnDef(col, dialect)}`
|
|
284
405
|
);
|
|
285
406
|
} else {
|
|
286
|
-
stmts.push(...renderAlterColumn(to.name, col,
|
|
407
|
+
stmts.push(...renderAlterColumn(to.name, col, dialect));
|
|
287
408
|
}
|
|
288
409
|
}
|
|
289
410
|
for (const name of Object.keys(from.columns)) {
|
|
290
411
|
if (!(name in to.columns)) {
|
|
291
|
-
stmts.push(
|
|
412
|
+
stmts.push(
|
|
413
|
+
`ALTER TABLE ${quoteId(to.name, dialect)} DROP COLUMN ${quoteId(name, dialect)}`
|
|
414
|
+
);
|
|
292
415
|
}
|
|
293
416
|
}
|
|
294
417
|
return stmts;
|
|
@@ -296,11 +419,16 @@ function renderPostgresTableDiff(from, to) {
|
|
|
296
419
|
function renderAlterColumn(table, to, dialect) {
|
|
297
420
|
if (dialect === "sqlite") {
|
|
298
421
|
throw new Error(
|
|
299
|
-
`alter_column on SQLite needs
|
|
422
|
+
`alter_column on SQLite needs a table-rebuild (recreate_table); column ${table}.${to.name}`
|
|
300
423
|
);
|
|
301
424
|
}
|
|
302
|
-
|
|
303
|
-
|
|
425
|
+
if (dialect === "mysql") {
|
|
426
|
+
return [
|
|
427
|
+
`ALTER TABLE ${quoteId(table, dialect)} MODIFY COLUMN ${renderColumnDef(to, dialect)}`
|
|
428
|
+
];
|
|
429
|
+
}
|
|
430
|
+
const t = quoteId(table, dialect);
|
|
431
|
+
const c = quoteId(to.name, dialect);
|
|
304
432
|
const stmts = [
|
|
305
433
|
`ALTER TABLE ${t} ALTER COLUMN ${c} TYPE ${renderColumnType(to.type, dialect)}`
|
|
306
434
|
];
|
|
@@ -319,9 +447,74 @@ function columnSignature(col) {
|
|
|
319
447
|
type: col.type,
|
|
320
448
|
notNull: col.notNull,
|
|
321
449
|
primaryKey: col.primaryKey,
|
|
322
|
-
default: col.default
|
|
450
|
+
default: col.default,
|
|
451
|
+
unique: col.unique,
|
|
452
|
+
references: col.references
|
|
453
|
+
});
|
|
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
|
|
323
465
|
});
|
|
324
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
|
+
}
|
|
325
518
|
function diffSchema(current, target) {
|
|
326
519
|
const ops = [];
|
|
327
520
|
const drops = [];
|
|
@@ -350,6 +543,7 @@ function diffSchema(current, target) {
|
|
|
350
543
|
ops.push({ kind: "drop_column", table: name, column: currentCol });
|
|
351
544
|
}
|
|
352
545
|
}
|
|
546
|
+
ops.push(...diffConstraints(currentTable, targetTable));
|
|
353
547
|
}
|
|
354
548
|
for (const [name, currentTable] of Object.entries(current.tables)) {
|
|
355
549
|
if (!target.tables[name]) {
|
|
@@ -417,23 +611,38 @@ function heads(migrations) {
|
|
|
417
611
|
function isDefaultValue(value) {
|
|
418
612
|
return typeof value === "object" && value !== null && "kind" in value && (value.kind === "literal" || value.kind === "expression");
|
|
419
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
|
+
}
|
|
420
626
|
var Column = class _Column {
|
|
421
|
-
constructor(type, flags, defaultValue = null, onUpdateValue = null) {
|
|
627
|
+
constructor(type, flags, defaultValue = null, onUpdateValue = null, reference = null) {
|
|
422
628
|
this.type = type;
|
|
423
629
|
this.flags = flags;
|
|
424
630
|
this.defaultValue = defaultValue;
|
|
425
631
|
this.onUpdateValue = onUpdateValue;
|
|
632
|
+
this.reference = reference;
|
|
426
633
|
}
|
|
427
634
|
type;
|
|
428
635
|
flags;
|
|
429
636
|
defaultValue;
|
|
430
637
|
onUpdateValue;
|
|
638
|
+
reference;
|
|
431
639
|
primaryKey() {
|
|
432
640
|
return new _Column(
|
|
433
641
|
this.type,
|
|
434
642
|
{ ...this.flags, primaryKey: true, hasDefault: true },
|
|
435
643
|
this.defaultValue,
|
|
436
|
-
this.onUpdateValue
|
|
644
|
+
this.onUpdateValue,
|
|
645
|
+
this.reference
|
|
437
646
|
);
|
|
438
647
|
}
|
|
439
648
|
notNull() {
|
|
@@ -441,7 +650,40 @@ var Column = class _Column {
|
|
|
441
650
|
this.type,
|
|
442
651
|
{ ...this.flags, notNull: true },
|
|
443
652
|
this.defaultValue,
|
|
444
|
-
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)
|
|
445
687
|
);
|
|
446
688
|
}
|
|
447
689
|
/**
|
|
@@ -454,7 +696,8 @@ var Column = class _Column {
|
|
|
454
696
|
this.type,
|
|
455
697
|
{ ...this.flags, hasDefault: true },
|
|
456
698
|
resolved,
|
|
457
|
-
this.onUpdateValue
|
|
699
|
+
this.onUpdateValue,
|
|
700
|
+
this.reference
|
|
458
701
|
);
|
|
459
702
|
}
|
|
460
703
|
/**
|
|
@@ -463,7 +706,7 @@ var Column = class _Column {
|
|
|
463
706
|
*/
|
|
464
707
|
onUpdate(value) {
|
|
465
708
|
const resolved = isDefaultValue(value) ? value : { kind: "literal", value };
|
|
466
|
-
return new _Column(this.type, this.flags, this.defaultValue, resolved);
|
|
709
|
+
return new _Column(this.type, this.flags, this.defaultValue, resolved, this.reference);
|
|
467
710
|
}
|
|
468
711
|
};
|
|
469
712
|
var columnsCache = /* @__PURE__ */ new WeakMap();
|
|
@@ -482,6 +725,9 @@ function columnsOf(model) {
|
|
|
482
725
|
}
|
|
483
726
|
|
|
484
727
|
// src/migrations/ir.ts
|
|
728
|
+
function constraintName(prefix, table, columns) {
|
|
729
|
+
return `${prefix}_${table}_${columns.join("_")}`;
|
|
730
|
+
}
|
|
485
731
|
function reflectTable(model) {
|
|
486
732
|
const columns = {};
|
|
487
733
|
const primaryKey = [];
|
|
@@ -492,11 +738,32 @@ function reflectTable(model) {
|
|
|
492
738
|
type: col.type,
|
|
493
739
|
notNull: col.flags.notNull || isPk,
|
|
494
740
|
primaryKey: isPk,
|
|
495
|
-
default: col.defaultValue
|
|
741
|
+
default: col.defaultValue,
|
|
742
|
+
unique: col.flags.unique,
|
|
743
|
+
references: col.reference
|
|
496
744
|
};
|
|
497
745
|
if (isPk) primaryKey.push(name);
|
|
498
746
|
}
|
|
499
|
-
|
|
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 };
|
|
500
767
|
}
|
|
501
768
|
function reflectSchema(models) {
|
|
502
769
|
const tables = {};
|
|
@@ -533,6 +800,39 @@ function affinityToKind(affinity) {
|
|
|
533
800
|
return "text";
|
|
534
801
|
}
|
|
535
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
|
+
}
|
|
536
836
|
function introspectSqlite(driver) {
|
|
537
837
|
const tablesRows = driver.execute(
|
|
538
838
|
"SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name != 'tempest_db_js_migrations'",
|
|
@@ -552,14 +852,39 @@ function introspectSqlite(driver) {
|
|
|
552
852
|
type: { kind: affinityToKind(affinity), meta: {} },
|
|
553
853
|
notNull: Number(col.notnull) === 1 || isPk,
|
|
554
854
|
primaryKey: isPk,
|
|
555
|
-
default: null
|
|
855
|
+
default: null,
|
|
856
|
+
unique: false,
|
|
857
|
+
references: null
|
|
556
858
|
};
|
|
557
859
|
if (isPk) primaryKey.push(col.name);
|
|
558
860
|
}
|
|
559
|
-
tables[tableName] = {
|
|
861
|
+
tables[tableName] = {
|
|
862
|
+
name: tableName,
|
|
863
|
+
columns,
|
|
864
|
+
primaryKey,
|
|
865
|
+
uniqueConstraints: sqliteUniques(driver, tableName),
|
|
866
|
+
foreignKeys: sqliteForeignKeys(driver, tableName)
|
|
867
|
+
};
|
|
560
868
|
}
|
|
561
869
|
return { tables };
|
|
562
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
|
+
}
|
|
563
888
|
function checkDrift(driver, models) {
|
|
564
889
|
const actual = introspectSqlite(driver);
|
|
565
890
|
const expected = reflectSchema(models);
|
|
@@ -597,6 +922,34 @@ function checkDrift(driver, models) {
|
|
|
597
922
|
);
|
|
598
923
|
}
|
|
599
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
|
+
}
|
|
600
953
|
}
|
|
601
954
|
for (const tableName of Object.keys(actual.tables)) {
|
|
602
955
|
if (!expected.tables[tableName]) {
|
|
@@ -612,7 +965,9 @@ function columnShape(col) {
|
|
|
612
965
|
type: col.type,
|
|
613
966
|
notNull: col.notNull,
|
|
614
967
|
primaryKey: col.primaryKey,
|
|
615
|
-
default: col.default
|
|
968
|
+
default: col.default,
|
|
969
|
+
unique: col.unique,
|
|
970
|
+
references: col.references
|
|
616
971
|
});
|
|
617
972
|
}
|
|
618
973
|
function tableShape(columns) {
|
|
@@ -731,6 +1086,14 @@ var Op = class {
|
|
|
731
1086
|
recreateTable(from, to) {
|
|
732
1087
|
this.run({ kind: "recreate_table", from, to });
|
|
733
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
|
+
}
|
|
734
1097
|
/** Raw SQL escape hatch (e.g. a data migration). `down` may be `null`. */
|
|
735
1098
|
execute(up, down = null) {
|
|
736
1099
|
this.run({ kind: "execute", up, down });
|
|
@@ -893,6 +1256,30 @@ function applyOperation(schema, op) {
|
|
|
893
1256
|
}
|
|
894
1257
|
break;
|
|
895
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
|
+
}
|
|
896
1283
|
}
|
|
897
1284
|
return { tables };
|
|
898
1285
|
}
|