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.
@@ -1,4 +1,4 @@
1
- import { columnsOf } from './chunk-AGDD7K3F.js';
1
+ import { columnsOf } from './chunk-JR4MLFQN.js';
2
2
 
3
3
  // src/migrations/operations.ts
4
4
  var IrreversibleMigration = class extends Error {
@@ -31,6 +31,10 @@ function invert(op) {
31
31
  return { kind: "rename_column", table: op.table, from: op.to, to: op.from };
32
32
  case "recreate_table":
33
33
  return { kind: "recreate_table", from: op.to, to: op.from };
34
+ case "add_constraint":
35
+ return { kind: "drop_constraint", table: op.table, constraint: op.constraint };
36
+ case "drop_constraint":
37
+ return { kind: "add_constraint", table: op.table, constraint: op.constraint };
34
38
  case "execute":
35
39
  if (op.down === null) {
36
40
  throw new IrreversibleMigration("execute() operation has no down SQL");
@@ -90,8 +94,8 @@ function makeRevisionId(label, parents) {
90
94
  }
91
95
 
92
96
  // src/migrations/ddl.ts
93
- function quoteId(name) {
94
- return `"${name.replace(/"/g, '""')}"`;
97
+ function quoteId(name, dialect) {
98
+ return dialect === "mysql" ? `\`${name.replace(/`/g, "``")}\`` : `"${name.replace(/"/g, '""')}"`;
95
99
  }
96
100
  function quoteLiteral(value) {
97
101
  return `'${value.replace(/'/g, "''")}'`;
@@ -116,6 +120,45 @@ function renderColumnType(type, dialect) {
116
120
  return "TEXT";
117
121
  }
118
122
  }
123
+ if (dialect === "mysql") {
124
+ switch (kind) {
125
+ case "smallint":
126
+ return "SMALLINT";
127
+ case "integer":
128
+ return "INT";
129
+ case "bigint":
130
+ return "BIGINT";
131
+ case "numeric":
132
+ return meta.precision !== void 0 ? `DECIMAL(${meta.precision}${meta.scale !== void 0 ? `, ${meta.scale}` : ""})` : "DECIMAL";
133
+ case "real":
134
+ return "FLOAT";
135
+ case "double":
136
+ return "DOUBLE";
137
+ case "varchar":
138
+ return `VARCHAR(${meta.length ?? 255})`;
139
+ case "char":
140
+ return `CHAR(${meta.length ?? 255})`;
141
+ case "text":
142
+ return "TEXT";
143
+ case "boolean":
144
+ return "TINYINT(1)";
145
+ case "date":
146
+ return "DATE";
147
+ case "time":
148
+ return "TIME";
149
+ case "datetime":
150
+ case "timestamp":
151
+ return "DATETIME";
152
+ case "blob":
153
+ return "BLOB";
154
+ case "json":
155
+ return "JSON";
156
+ case "uuid":
157
+ return "CHAR(36)";
158
+ case "enum":
159
+ return `ENUM(${(meta.values ?? []).map(quoteLiteral).join(", ")})`;
160
+ }
161
+ }
119
162
  switch (kind) {
120
163
  case "smallint":
121
164
  return "SMALLINT";
@@ -160,29 +203,66 @@ function renderDefault(def, dialect) {
160
203
  if (typeof expr === "object") return expr.raw;
161
204
  switch (expr) {
162
205
  case "now":
163
- return dialect === "sqlite" ? "CURRENT_TIMESTAMP" : "now()";
206
+ return dialect === "postgresql" ? "now()" : "CURRENT_TIMESTAMP";
164
207
  case "current_date":
165
208
  return "CURRENT_DATE";
166
209
  case "current_time":
167
210
  return "CURRENT_TIME";
168
211
  case "uuidv4":
169
- return dialect === "sqlite" ? "(lower(hex(randomblob(16))))" : "gen_random_uuid()";
212
+ if (dialect === "postgresql") return "gen_random_uuid()";
213
+ if (dialect === "mysql") return "(UUID())";
214
+ return "(lower(hex(randomblob(16))))";
170
215
  }
171
216
  }
172
217
  const value = def.value;
173
218
  if (value === null) return "NULL";
174
219
  if (typeof value === "boolean") {
175
- return dialect === "sqlite" ? value ? "1" : "0" : value ? "TRUE" : "FALSE";
220
+ return dialect === "postgresql" ? value ? "TRUE" : "FALSE" : value ? "1" : "0";
176
221
  }
177
222
  if (typeof value === "number" || typeof value === "bigint") return String(value);
178
223
  if (value instanceof Date) return quoteLiteral(value.toISOString());
179
224
  if (typeof value === "object") return quoteLiteral(JSON.stringify(value));
180
225
  return quoteLiteral(String(value));
181
226
  }
227
+ function renderFkAction(action) {
228
+ return action.toUpperCase();
229
+ }
230
+ function renderFkActions(fk) {
231
+ let sql = "";
232
+ if (fk.onDelete) sql += ` ON DELETE ${renderFkAction(fk.onDelete)}`;
233
+ if (fk.onUpdate) sql += ` ON UPDATE ${renderFkAction(fk.onUpdate)}`;
234
+ return sql;
235
+ }
236
+ function columnConstraintSuffix(col, dialect) {
237
+ let sql = "";
238
+ if (col.unique) sql += " UNIQUE";
239
+ if (col.references) {
240
+ const ref = col.references;
241
+ sql += ` REFERENCES ${quoteId(ref.table, dialect)} (${quoteId(ref.column, dialect)})`;
242
+ sql += renderFkActions(ref);
243
+ }
244
+ return sql;
245
+ }
246
+ function renderUniqueConstraint(uc, dialect) {
247
+ const cols = uc.columns.map((c) => quoteId(c, dialect)).join(", ");
248
+ return `CONSTRAINT ${quoteId(uc.name, dialect)} UNIQUE (${cols})`;
249
+ }
250
+ function renderForeignKeyConstraint(fk, dialect) {
251
+ const cols = fk.columns.map((c) => quoteId(c, dialect)).join(", ");
252
+ const refCols = fk.refColumns.map((c) => quoteId(c, dialect)).join(", ");
253
+ return `CONSTRAINT ${quoteId(fk.name, dialect)} FOREIGN KEY (${cols}) REFERENCES ${quoteId(fk.refTable, dialect)} (${refCols})${renderFkActions(fk)}`;
254
+ }
255
+ function tableConstraintClauses(table, dialect) {
256
+ return [
257
+ ...table.uniqueConstraints.map((uc) => renderUniqueConstraint(uc, dialect)),
258
+ ...table.foreignKeys.map((fk) => renderForeignKeyConstraint(fk, dialect))
259
+ ];
260
+ }
182
261
  function renderColumnDef(col, dialect) {
183
- let sql = `${quoteId(col.name)} ${renderColumnType(col.type, dialect)}`;
262
+ let sql = `${quoteId(col.name, dialect)} ${renderColumnType(col.type, dialect)}`;
184
263
  if (col.notNull) sql += " NOT NULL";
185
264
  if (col.default !== null) sql += ` DEFAULT ${renderDefault(col.default, dialect)}`;
265
+ sql += columnConstraintSuffix(col, dialect);
186
266
  return sql;
187
267
  }
188
268
  function enumTypeName(table, column) {
@@ -202,23 +282,29 @@ function renderCreateTable(table, dialect) {
202
282
  if (dialect === "postgresql" && c.type.kind === "enum") {
203
283
  const typeName = enumTypeName(table.name, c.name);
204
284
  const values = (c.type.meta.values ?? []).map(quoteLiteral).join(", ");
205
- typeStmts.push(`CREATE TYPE ${quoteId(typeName)} AS ENUM (${values})`);
206
- let def = `${quoteId(c.name)} ${quoteId(typeName)}`;
285
+ typeStmts.push(`CREATE TYPE ${quoteId(typeName, dialect)} AS ENUM (${values})`);
286
+ let def = `${quoteId(c.name, dialect)} ${quoteId(typeName, dialect)}`;
207
287
  if (c.notNull) def += " NOT NULL";
208
288
  if (c.default !== null) def += ` DEFAULT ${renderDefault(c.default, dialect)}`;
209
- return def;
289
+ return def + columnConstraintSuffix(c, dialect);
210
290
  }
211
291
  if (dialect === "postgresql" && isAutoIncrementPk(table, c)) {
212
- return `${quoteId(c.name)} ${postgresSerialType(c.type.kind)}`;
292
+ return `${quoteId(c.name, dialect)} ${postgresSerialType(c.type.kind)}${columnConstraintSuffix(c, dialect)}`;
293
+ }
294
+ if (dialect === "mysql" && isAutoIncrementPk(table, c)) {
295
+ return `${quoteId(c.name, dialect)} ${renderColumnType(c.type, dialect)} NOT NULL AUTO_INCREMENT${columnConstraintSuffix(c, dialect)}`;
213
296
  }
214
297
  return renderColumnDef(c, dialect);
215
298
  });
216
299
  if (table.primaryKey.length > 0) {
217
- cols.push(`PRIMARY KEY (${table.primaryKey.map(quoteId).join(", ")})`);
300
+ cols.push(
301
+ `PRIMARY KEY (${table.primaryKey.map((c) => quoteId(c, dialect)).join(", ")})`
302
+ );
218
303
  }
304
+ cols.push(...tableConstraintClauses(table, dialect));
219
305
  return [
220
306
  ...typeStmts,
221
- `CREATE TABLE ${quoteId(table.name)} (
307
+ `CREATE TABLE ${quoteId(table.name, dialect)} (
222
308
  ${cols.join(",\n ")}
223
309
  )`
224
310
  ];
@@ -228,60 +314,97 @@ function renderOperation(op, dialect) {
228
314
  case "create_table":
229
315
  return renderCreateTable(op.table, dialect);
230
316
  case "drop_table":
231
- return [`DROP TABLE ${quoteId(op.table.name)}`];
317
+ return [`DROP TABLE ${quoteId(op.table.name, dialect)}`];
232
318
  case "rename_table":
233
- return [`ALTER TABLE ${quoteId(op.from)} RENAME TO ${quoteId(op.to)}`];
319
+ return dialect === "mysql" ? [`RENAME TABLE ${quoteId(op.from, dialect)} TO ${quoteId(op.to, dialect)}`] : [
320
+ `ALTER TABLE ${quoteId(op.from, dialect)} RENAME TO ${quoteId(op.to, dialect)}`
321
+ ];
234
322
  case "add_column":
235
323
  return [
236
- `ALTER TABLE ${quoteId(op.table)} ADD COLUMN ${renderColumnDef(op.column, dialect)}`
324
+ `ALTER TABLE ${quoteId(op.table, dialect)} ADD COLUMN ${renderColumnDef(op.column, dialect)}`
237
325
  ];
238
326
  case "drop_column":
239
- return [`ALTER TABLE ${quoteId(op.table)} DROP COLUMN ${quoteId(op.column.name)}`];
327
+ return [
328
+ `ALTER TABLE ${quoteId(op.table, dialect)} DROP COLUMN ${quoteId(op.column.name, dialect)}`
329
+ ];
240
330
  case "rename_column":
241
331
  return [
242
- `ALTER TABLE ${quoteId(op.table)} RENAME COLUMN ${quoteId(op.from)} TO ${quoteId(op.to)}`
332
+ `ALTER TABLE ${quoteId(op.table, dialect)} RENAME COLUMN ${quoteId(op.from, dialect)} TO ${quoteId(op.to, dialect)}`
243
333
  ];
244
334
  case "alter_column":
245
335
  return renderAlterColumn(op.table, op.to, dialect);
246
336
  case "recreate_table":
247
- return dialect === "sqlite" ? renderSqliteRebuild(op.from, op.to) : renderPostgresTableDiff(op.from, op.to);
337
+ return dialect === "sqlite" ? renderSqliteRebuild(op.from, op.to) : renderTableDiff(op.from, op.to, dialect);
338
+ case "add_constraint":
339
+ return renderAddConstraint(op.table, op.constraint, dialect);
340
+ case "drop_constraint":
341
+ return renderDropConstraint(op.table, op.constraint, dialect);
248
342
  case "execute":
249
343
  return [op.up];
250
344
  }
251
345
  }
346
+ function renderAddConstraint(table, constraint, dialect) {
347
+ if (dialect === "sqlite") {
348
+ throw new Error(
349
+ `add_constraint on SQLite needs a table-rebuild (recreate_table); constraint ${constraint.constraint.name} on ${table}`
350
+ );
351
+ }
352
+ const clause = constraint.type === "unique" ? renderUniqueConstraint(constraint.constraint, dialect) : renderForeignKeyConstraint(constraint.constraint, dialect);
353
+ return [`ALTER TABLE ${quoteId(table, dialect)} ADD ${clause}`];
354
+ }
355
+ function renderDropConstraint(table, constraint, dialect) {
356
+ if (dialect === "sqlite") {
357
+ throw new Error(
358
+ `drop_constraint on SQLite needs a table-rebuild (recreate_table); constraint ${constraint.constraint.name} on ${table}`
359
+ );
360
+ }
361
+ const t = quoteId(table, dialect);
362
+ const name = quoteId(constraint.constraint.name, dialect);
363
+ if (dialect === "mysql") {
364
+ return [
365
+ constraint.type === "unique" ? `ALTER TABLE ${t} DROP INDEX ${name}` : `ALTER TABLE ${t} DROP FOREIGN KEY ${name}`
366
+ ];
367
+ }
368
+ return [`ALTER TABLE ${t} DROP CONSTRAINT ${name}`];
369
+ }
252
370
  function renderSqliteRebuild(from, to) {
253
371
  const tmp = `__new_${to.name}`;
254
372
  const common = Object.keys(to.columns).filter((c) => c in from.columns);
255
373
  const cols = Object.values(to.columns).map((c) => renderColumnDef(c, "sqlite"));
256
374
  if (to.primaryKey.length > 0) {
257
- cols.push(`PRIMARY KEY (${to.primaryKey.map(quoteId).join(", ")})`);
375
+ cols.push(
376
+ `PRIMARY KEY (${to.primaryKey.map((c) => quoteId(c, "sqlite")).join(", ")})`
377
+ );
258
378
  }
259
- const commonSql = common.map(quoteId).join(", ");
379
+ cols.push(...tableConstraintClauses(to, "sqlite"));
380
+ const commonSql = common.map((c) => quoteId(c, "sqlite")).join(", ");
260
381
  return [
261
382
  "PRAGMA foreign_keys=off",
262
- `CREATE TABLE ${quoteId(tmp)} (
383
+ `CREATE TABLE ${quoteId(tmp, "sqlite")} (
263
384
  ${cols.join(",\n ")}
264
385
  )`,
265
- common.length > 0 ? `INSERT INTO ${quoteId(tmp)} (${commonSql}) SELECT ${commonSql} FROM ${quoteId(from.name)}` : `-- no common columns to copy from ${from.name}`,
266
- `DROP TABLE ${quoteId(from.name)}`,
267
- `ALTER TABLE ${quoteId(tmp)} RENAME TO ${quoteId(to.name)}`,
386
+ common.length > 0 ? `INSERT INTO ${quoteId(tmp, "sqlite")} (${commonSql}) SELECT ${commonSql} FROM ${quoteId(from.name, "sqlite")}` : `-- no common columns to copy from ${from.name}`,
387
+ `DROP TABLE ${quoteId(from.name, "sqlite")}`,
388
+ `ALTER TABLE ${quoteId(tmp, "sqlite")} RENAME TO ${quoteId(to.name, "sqlite")}`,
268
389
  "PRAGMA foreign_keys=on"
269
390
  ];
270
391
  }
271
- function renderPostgresTableDiff(from, to) {
392
+ function renderTableDiff(from, to, dialect) {
272
393
  const stmts = [];
273
394
  for (const [name, col] of Object.entries(to.columns)) {
274
395
  if (!(name in from.columns)) {
275
396
  stmts.push(
276
- `ALTER TABLE ${quoteId(to.name)} ADD COLUMN ${renderColumnDef(col, "postgresql")}`
397
+ `ALTER TABLE ${quoteId(to.name, dialect)} ADD COLUMN ${renderColumnDef(col, dialect)}`
277
398
  );
278
399
  } else {
279
- stmts.push(...renderAlterColumn(to.name, col, "postgresql"));
400
+ stmts.push(...renderAlterColumn(to.name, col, dialect));
280
401
  }
281
402
  }
282
403
  for (const name of Object.keys(from.columns)) {
283
404
  if (!(name in to.columns)) {
284
- stmts.push(`ALTER TABLE ${quoteId(to.name)} DROP COLUMN ${quoteId(name)}`);
405
+ stmts.push(
406
+ `ALTER TABLE ${quoteId(to.name, dialect)} DROP COLUMN ${quoteId(name, dialect)}`
407
+ );
285
408
  }
286
409
  }
287
410
  return stmts;
@@ -289,11 +412,16 @@ function renderPostgresTableDiff(from, to) {
289
412
  function renderAlterColumn(table, to, dialect) {
290
413
  if (dialect === "sqlite") {
291
414
  throw new Error(
292
- `alter_column on SQLite needs batch/table-rebuild (Phase 6e); column ${table}.${to.name}`
415
+ `alter_column on SQLite needs a table-rebuild (recreate_table); column ${table}.${to.name}`
293
416
  );
294
417
  }
295
- const t = quoteId(table);
296
- const c = quoteId(to.name);
418
+ if (dialect === "mysql") {
419
+ return [
420
+ `ALTER TABLE ${quoteId(table, dialect)} MODIFY COLUMN ${renderColumnDef(to, dialect)}`
421
+ ];
422
+ }
423
+ const t = quoteId(table, dialect);
424
+ const c = quoteId(to.name, dialect);
297
425
  const stmts = [
298
426
  `ALTER TABLE ${t} ALTER COLUMN ${c} TYPE ${renderColumnType(to.type, dialect)}`
299
427
  ];
@@ -312,9 +440,74 @@ function columnSignature(col) {
312
440
  type: col.type,
313
441
  notNull: col.notNull,
314
442
  primaryKey: col.primaryKey,
315
- default: col.default
443
+ default: col.default,
444
+ unique: col.unique,
445
+ references: col.references
446
+ });
447
+ }
448
+ function uniqueSignature(uc) {
449
+ return JSON.stringify({ columns: uc.columns });
450
+ }
451
+ function foreignKeySignature(fk) {
452
+ return JSON.stringify({
453
+ columns: fk.columns,
454
+ refTable: fk.refTable,
455
+ refColumns: fk.refColumns,
456
+ onDelete: fk.onDelete ?? null,
457
+ onUpdate: fk.onUpdate ?? null
316
458
  });
317
459
  }
460
+ function diffConstraints(current, target) {
461
+ const ops = [];
462
+ const table = target.name;
463
+ const currentUq = new Map(current.uniqueConstraints.map((u) => [u.name, u]));
464
+ const targetUq = new Map(target.uniqueConstraints.map((u) => [u.name, u]));
465
+ for (const [name, cur] of currentUq) {
466
+ const tgt = targetUq.get(name);
467
+ if (!tgt || uniqueSignature(cur) !== uniqueSignature(tgt)) {
468
+ ops.push(dropUnique(table, cur));
469
+ }
470
+ }
471
+ for (const [name, tgt] of targetUq) {
472
+ const cur = currentUq.get(name);
473
+ if (!cur || uniqueSignature(cur) !== uniqueSignature(tgt)) {
474
+ ops.push(addUnique(table, tgt));
475
+ }
476
+ }
477
+ const currentFk = new Map(current.foreignKeys.map((f) => [f.name, f]));
478
+ const targetFk = new Map(target.foreignKeys.map((f) => [f.name, f]));
479
+ for (const [name, cur] of currentFk) {
480
+ const tgt = targetFk.get(name);
481
+ if (!tgt || foreignKeySignature(cur) !== foreignKeySignature(tgt)) {
482
+ ops.push(dropForeignKey(table, cur));
483
+ }
484
+ }
485
+ for (const [name, tgt] of targetFk) {
486
+ const cur = currentFk.get(name);
487
+ if (!cur || foreignKeySignature(cur) !== foreignKeySignature(tgt)) {
488
+ ops.push(addForeignKey(table, tgt));
489
+ }
490
+ }
491
+ return ops;
492
+ }
493
+ function uniqueNamed(uc) {
494
+ return { type: "unique", constraint: uc };
495
+ }
496
+ function foreignKeyNamed(fk) {
497
+ return { type: "foreignKey", constraint: fk };
498
+ }
499
+ function addUnique(table, uc) {
500
+ return { kind: "add_constraint", table, constraint: uniqueNamed(uc) };
501
+ }
502
+ function dropUnique(table, uc) {
503
+ return { kind: "drop_constraint", table, constraint: uniqueNamed(uc) };
504
+ }
505
+ function addForeignKey(table, fk) {
506
+ return { kind: "add_constraint", table, constraint: foreignKeyNamed(fk) };
507
+ }
508
+ function dropForeignKey(table, fk) {
509
+ return { kind: "drop_constraint", table, constraint: foreignKeyNamed(fk) };
510
+ }
318
511
  function diffSchema(current, target) {
319
512
  const ops = [];
320
513
  const drops = [];
@@ -343,6 +536,7 @@ function diffSchema(current, target) {
343
536
  ops.push({ kind: "drop_column", table: name, column: currentCol });
344
537
  }
345
538
  }
539
+ ops.push(...diffConstraints(currentTable, targetTable));
346
540
  }
347
541
  for (const [name, currentTable] of Object.entries(current.tables)) {
348
542
  if (!target.tables[name]) {
@@ -407,6 +601,9 @@ function heads(migrations) {
407
601
  }
408
602
 
409
603
  // src/migrations/ir.ts
604
+ function constraintName(prefix, table, columns) {
605
+ return `${prefix}_${table}_${columns.join("_")}`;
606
+ }
410
607
  function reflectTable(model) {
411
608
  const columns = {};
412
609
  const primaryKey = [];
@@ -417,11 +614,32 @@ function reflectTable(model) {
417
614
  type: col.type,
418
615
  notNull: col.flags.notNull || isPk,
419
616
  primaryKey: isPk,
420
- default: col.defaultValue
617
+ default: col.defaultValue,
618
+ unique: col.flags.unique,
619
+ references: col.reference
421
620
  };
422
621
  if (isPk) primaryKey.push(name);
423
622
  }
424
- return { name: model.tablename, columns, primaryKey };
623
+ const uniqueConstraints = [];
624
+ const foreignKeys = [];
625
+ for (const c of model.tableArgs?.() ?? []) {
626
+ if (c.kind === "unique") {
627
+ uniqueConstraints.push({
628
+ name: c.name ?? constraintName("uq", model.tablename, c.columns),
629
+ columns: c.columns
630
+ });
631
+ } else {
632
+ foreignKeys.push({
633
+ name: c.name ?? constraintName("fk", model.tablename, c.columns),
634
+ columns: c.columns,
635
+ refTable: c.refTable,
636
+ refColumns: c.refColumns,
637
+ onDelete: c.onDelete,
638
+ onUpdate: c.onUpdate
639
+ });
640
+ }
641
+ }
642
+ return { name: model.tablename, columns, primaryKey, uniqueConstraints, foreignKeys };
425
643
  }
426
644
  function reflectSchema(models) {
427
645
  const tables = {};
@@ -458,6 +676,39 @@ function affinityToKind(affinity) {
458
676
  return "text";
459
677
  }
460
678
  }
679
+ function sqliteForeignKeys(driver, table) {
680
+ const rows = driver.execute(`PRAGMA foreign_key_list(${JSON.stringify(table)})`, []).rows;
681
+ const byId = /* @__PURE__ */ new Map();
682
+ for (const r of rows) {
683
+ const list = byId.get(r.id) ?? [];
684
+ list.push(r);
685
+ byId.set(r.id, list);
686
+ }
687
+ const fks = [];
688
+ for (const group of byId.values()) {
689
+ const ordered = [...group].sort((a, b) => a.seq - b.seq);
690
+ const columns = ordered.map((r) => r.from);
691
+ fks.push({
692
+ name: `fk_${table}_${columns.join("_")}`,
693
+ columns,
694
+ refTable: ordered[0]?.table ?? "",
695
+ refColumns: ordered.map((r) => r.to)
696
+ });
697
+ }
698
+ return fks;
699
+ }
700
+ function sqliteUniques(driver, table) {
701
+ const indexes = driver.execute(`PRAGMA index_list(${JSON.stringify(table)})`, []).rows;
702
+ const uniques = [];
703
+ for (const idx of indexes) {
704
+ if (Number(idx.unique) !== 1 || idx.origin === "pk") continue;
705
+ const cols = driver.execute(`PRAGMA index_info(${JSON.stringify(idx.name)})`, []).rows.sort((a, b) => a.seqno - b.seqno).map((c) => c.name);
706
+ if (cols.length > 0) {
707
+ uniques.push({ name: `uq_${table}_${cols.join("_")}`, columns: cols });
708
+ }
709
+ }
710
+ return uniques;
711
+ }
461
712
  function introspectSqlite(driver) {
462
713
  const tablesRows = driver.execute(
463
714
  "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name != 'tempest_db_js_migrations'",
@@ -477,14 +728,39 @@ function introspectSqlite(driver) {
477
728
  type: { kind: affinityToKind(affinity), meta: {} },
478
729
  notNull: Number(col.notnull) === 1 || isPk,
479
730
  primaryKey: isPk,
480
- default: null
731
+ default: null,
732
+ unique: false,
733
+ references: null
481
734
  };
482
735
  if (isPk) primaryKey.push(col.name);
483
736
  }
484
- tables[tableName] = { name: tableName, columns, primaryKey };
737
+ tables[tableName] = {
738
+ name: tableName,
739
+ columns,
740
+ primaryKey,
741
+ uniqueConstraints: sqliteUniques(driver, tableName),
742
+ foreignKeys: sqliteForeignKeys(driver, tableName)
743
+ };
485
744
  }
486
745
  return { tables };
487
746
  }
747
+ function constraintKeys(table) {
748
+ const fks = /* @__PURE__ */ new Set();
749
+ const uniques = /* @__PURE__ */ new Set();
750
+ for (const col of Object.values(table.columns)) {
751
+ if (col.unique) uniques.add(col.name);
752
+ if (col.references) {
753
+ fks.add(`${col.name}=>${col.references.table}(${col.references.column})`);
754
+ }
755
+ }
756
+ for (const uc of table.uniqueConstraints) {
757
+ uniques.add([...uc.columns].sort().join(","));
758
+ }
759
+ for (const fk of table.foreignKeys) {
760
+ fks.add(`${fk.columns.join(",")}=>${fk.refTable}(${fk.refColumns.join(",")})`);
761
+ }
762
+ return { fks, uniques };
763
+ }
488
764
  function checkDrift(driver, models) {
489
765
  const actual = introspectSqlite(driver);
490
766
  const expected = reflectSchema(models);
@@ -522,6 +798,34 @@ function checkDrift(driver, models) {
522
798
  );
523
799
  }
524
800
  }
801
+ const expectedKeys = constraintKeys(expectedTable);
802
+ const actualKeys = constraintKeys(actualTable);
803
+ for (const fk of expectedKeys.fks) {
804
+ if (!actualKeys.fks.has(fk)) {
805
+ issues.push(`foreign key "${tableName}: ${fk}" is missing from the database`);
806
+ }
807
+ }
808
+ for (const fk of actualKeys.fks) {
809
+ if (!expectedKeys.fks.has(fk)) {
810
+ issues.push(
811
+ `foreign key "${tableName}: ${fk}" exists in the database but not in the model`
812
+ );
813
+ }
814
+ }
815
+ for (const uq of expectedKeys.uniques) {
816
+ if (!actualKeys.uniques.has(uq)) {
817
+ issues.push(
818
+ `unique constraint "${tableName}: (${uq})" is missing from the database`
819
+ );
820
+ }
821
+ }
822
+ for (const uq of actualKeys.uniques) {
823
+ if (!expectedKeys.uniques.has(uq)) {
824
+ issues.push(
825
+ `unique constraint "${tableName}: (${uq})" exists in the database but not in the model`
826
+ );
827
+ }
828
+ }
525
829
  }
526
830
  for (const tableName of Object.keys(actual.tables)) {
527
831
  if (!expected.tables[tableName]) {
@@ -583,14 +887,59 @@ async function introspectPostgres(driver) {
583
887
  },
584
888
  notNull: col.is_nullable === "NO" || isPk,
585
889
  primaryKey: isPk,
586
- default: null
890
+ default: null,
891
+ unique: false,
892
+ references: null
587
893
  };
588
894
  if (isPk) primaryKey.push(name);
589
895
  }
590
- tables[tableName] = { name: tableName, columns, primaryKey };
896
+ tables[tableName] = {
897
+ name: tableName,
898
+ columns,
899
+ primaryKey,
900
+ uniqueConstraints: await postgresUniques(driver, tableName),
901
+ foreignKeys: await postgresForeignKeys(driver, tableName)
902
+ };
591
903
  }
592
904
  return { tables };
593
905
  }
906
+ async function postgresForeignKeys(driver, table) {
907
+ const result = await driver.execute(
908
+ `SELECT c.conname AS name,
909
+ (SELECT array_agg(a.attname ORDER BY k.ord)
910
+ FROM unnest(c.conkey) WITH ORDINALITY AS k(attnum, ord)
911
+ JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum) AS cols,
912
+ cf.relname AS ref_table,
913
+ (SELECT array_agg(a.attname ORDER BY k.ord)
914
+ FROM unnest(c.confkey) WITH ORDINALITY AS k(attnum, ord)
915
+ JOIN pg_attribute a ON a.attrelid = c.confrelid AND a.attnum = k.attnum) AS ref_cols
916
+ FROM pg_constraint c
917
+ JOIN pg_class cf ON cf.oid = c.confrelid
918
+ WHERE c.contype = 'f' AND c.conrelid = $1::regclass`,
919
+ [table]
920
+ );
921
+ return result.rows.map((r) => ({
922
+ name: String(r.name),
923
+ columns: r.cols ?? [],
924
+ refTable: String(r.ref_table),
925
+ refColumns: r.ref_cols ?? []
926
+ }));
927
+ }
928
+ async function postgresUniques(driver, table) {
929
+ const result = await driver.execute(
930
+ `SELECT c.conname AS name,
931
+ (SELECT array_agg(a.attname ORDER BY k.ord)
932
+ FROM unnest(c.conkey) WITH ORDINALITY AS k(attnum, ord)
933
+ JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum) AS cols
934
+ FROM pg_constraint c
935
+ WHERE c.contype = 'u' AND c.conrelid = $1::regclass`,
936
+ [table]
937
+ );
938
+ return result.rows.map((r) => ({
939
+ name: String(r.name),
940
+ columns: r.cols ?? []
941
+ }));
942
+ }
594
943
  async function checkDriftPostgres(driver, models) {
595
944
  const actual = await introspectPostgres(driver);
596
945
  const expected = reflectSchema(models);
@@ -623,6 +972,20 @@ async function checkDriftPostgres(driver, models) {
623
972
  );
624
973
  }
625
974
  }
975
+ const expectedKeys = constraintKeys(expectedTable);
976
+ const actualKeys = constraintKeys(actualTable);
977
+ for (const fk of expectedKeys.fks) {
978
+ if (!actualKeys.fks.has(fk)) {
979
+ issues.push(`foreign key "${tableName}: ${fk}" is missing from the database`);
980
+ }
981
+ }
982
+ for (const uq of expectedKeys.uniques) {
983
+ if (!actualKeys.uniques.has(uq)) {
984
+ issues.push(
985
+ `unique constraint "${tableName}: (${uq})" is missing from the database`
986
+ );
987
+ }
988
+ }
626
989
  }
627
990
  for (const tableName of Object.keys(actual.tables)) {
628
991
  if (!expected.tables[tableName]) {
@@ -638,7 +1001,9 @@ function columnShape(col) {
638
1001
  type: col.type,
639
1002
  notNull: col.notNull,
640
1003
  primaryKey: col.primaryKey,
641
- default: col.default
1004
+ default: col.default,
1005
+ unique: col.unique,
1006
+ references: col.references
642
1007
  });
643
1008
  }
644
1009
  function tableShape(columns) {
@@ -757,6 +1122,14 @@ var Op = class {
757
1122
  recreateTable(from, to) {
758
1123
  this.run({ kind: "recreate_table", from, to });
759
1124
  }
1125
+ /** Add a table-level unique / foreign-key constraint. */
1126
+ addConstraint(table, constraint) {
1127
+ this.run({ kind: "add_constraint", table, constraint });
1128
+ }
1129
+ /** Drop a table-level unique / foreign-key constraint. */
1130
+ dropConstraint(table, constraint) {
1131
+ this.run({ kind: "drop_constraint", table, constraint });
1132
+ }
760
1133
  /** Raw SQL escape hatch (e.g. a data migration). `down` may be `null`. */
761
1134
  execute(up, down = null) {
762
1135
  this.run({ kind: "execute", up, down });
@@ -856,6 +1229,112 @@ var MigrationRunner = class {
856
1229
  return reverted;
857
1230
  }
858
1231
  };
1232
+ function quoteIdent(name, dialect) {
1233
+ return dialect === "mysql" ? `\`${name.replace(/`/g, "``")}\`` : `"${name.replace(/"/g, '""')}"`;
1234
+ }
1235
+ function placeholder(index, dialect) {
1236
+ return dialect === "postgresql" ? `$${index}` : "?";
1237
+ }
1238
+ var AsyncMigrationRunner = class {
1239
+ constructor(driver, dialect) {
1240
+ this.driver = driver;
1241
+ this.dialect = dialect;
1242
+ this.vt = quoteIdent(VERSION_TABLE, dialect);
1243
+ }
1244
+ driver;
1245
+ dialect;
1246
+ vt;
1247
+ /** Create the version-tracking table if it does not exist. */
1248
+ async ensureVersionTable() {
1249
+ await this.driver.execute(
1250
+ `CREATE TABLE IF NOT EXISTS ${this.vt} (revision ${this.textType()} PRIMARY KEY, applied_at ${this.textType()} NOT NULL, down_revision ${this.textType()} NOT NULL)`,
1251
+ []
1252
+ );
1253
+ }
1254
+ /** A portable "text" column type for the version table. */
1255
+ textType() {
1256
+ return this.dialect === "mysql" ? "VARCHAR(255)" : "TEXT";
1257
+ }
1258
+ /** The set of applied revision ids. */
1259
+ async applied() {
1260
+ await this.ensureVersionTable();
1261
+ const { rows } = await this.driver.execute(`SELECT revision FROM ${this.vt}`, []);
1262
+ return new Set(rows.map((r) => String(r.revision)));
1263
+ }
1264
+ async runOps(ops) {
1265
+ for (const op of ops) {
1266
+ for (const stmt of renderOperation(op, this.dialect)) {
1267
+ const trimmed = stmt.trim();
1268
+ if (trimmed.length === 0 || trimmed.startsWith("--")) continue;
1269
+ await this.driver.execute(stmt, []);
1270
+ }
1271
+ }
1272
+ }
1273
+ async record(migration, appliedAt) {
1274
+ const p = (i) => placeholder(i, this.dialect);
1275
+ await this.driver.execute(
1276
+ `INSERT INTO ${this.vt} (revision, applied_at, down_revision) VALUES (${p(1)}, ${p(2)}, ${p(3)})`,
1277
+ [migration.revision, appliedAt, migration.downRevision.join(",")]
1278
+ );
1279
+ }
1280
+ async forget(revision) {
1281
+ await this.driver.execute(
1282
+ `DELETE FROM ${this.vt} WHERE revision = ${placeholder(1, this.dialect)}`,
1283
+ [revision]
1284
+ );
1285
+ }
1286
+ /**
1287
+ * Apply all pending migrations up to the head(s), in DAG order.
1288
+ *
1289
+ * @param migrations All known migrations.
1290
+ * @param appliedAt Timestamp string to stamp on each applied revision.
1291
+ * @returns The revision ids that were applied this run.
1292
+ */
1293
+ async upgrade(migrations, appliedAt) {
1294
+ const done = await this.applied();
1295
+ const ordered = topoOrder(migrations);
1296
+ const ran = [];
1297
+ for (const migration of ordered) {
1298
+ if (done.has(migration.revision)) continue;
1299
+ const op = new Op();
1300
+ migration.up(op);
1301
+ await this.runOps(op.operations);
1302
+ await this.record(migration, appliedAt);
1303
+ ran.push(migration.revision);
1304
+ }
1305
+ return ran;
1306
+ }
1307
+ /**
1308
+ * Revert the last `steps` applied migrations (default 1), newest first.
1309
+ *
1310
+ * @param migrations All known migrations.
1311
+ * @param steps How many applied revisions to roll back.
1312
+ * @returns The revision ids that were reverted.
1313
+ */
1314
+ async downgrade(migrations, steps = 1) {
1315
+ const done = await this.applied();
1316
+ const ordered = topoOrder(migrations).filter((m) => done.has(m.revision));
1317
+ const toRevert = ordered.slice(-steps).reverse();
1318
+ const reverted = [];
1319
+ for (const migration of toRevert) {
1320
+ const op = new Op();
1321
+ try {
1322
+ migration.down(op);
1323
+ } catch {
1324
+ op.operations.length = 0;
1325
+ }
1326
+ if (op.operations.length === 0) {
1327
+ const upOp = new Op();
1328
+ migration.up(upOp);
1329
+ op.operations.push(...invertAll(upOp.operations));
1330
+ }
1331
+ await this.runOps(op.operations);
1332
+ await this.forget(migration.revision);
1333
+ reverted.push(migration.revision);
1334
+ }
1335
+ return reverted;
1336
+ }
1337
+ };
859
1338
 
860
1339
  // src/migrations/replay.ts
861
1340
  function applyOperation(schema, op) {
@@ -919,6 +1398,30 @@ function applyOperation(schema, op) {
919
1398
  }
920
1399
  break;
921
1400
  }
1401
+ case "add_constraint": {
1402
+ const t = tables[op.table];
1403
+ if (t) {
1404
+ tables[op.table] = op.constraint.type === "unique" ? {
1405
+ ...t,
1406
+ uniqueConstraints: [...t.uniqueConstraints, op.constraint.constraint]
1407
+ } : { ...t, foreignKeys: [...t.foreignKeys, op.constraint.constraint] };
1408
+ }
1409
+ break;
1410
+ }
1411
+ case "drop_constraint": {
1412
+ const t = tables[op.table];
1413
+ if (t) {
1414
+ const dropName = op.constraint.constraint.name;
1415
+ tables[op.table] = op.constraint.type === "unique" ? {
1416
+ ...t,
1417
+ uniqueConstraints: t.uniqueConstraints.filter((u) => u.name !== dropName)
1418
+ } : {
1419
+ ...t,
1420
+ foreignKeys: t.foreignKeys.filter((f) => f.name !== dropName)
1421
+ };
1422
+ }
1423
+ break;
1424
+ }
922
1425
  }
923
1426
  return { tables };
924
1427
  }
@@ -1055,6 +1558,6 @@ function runMigrationCli(argv, config) {
1055
1558
  }
1056
1559
  }
1057
1560
 
1058
- export { CyclicMigrationGraph, IrreversibleMigration, MigrationRunner, Op, UnknownRevision, applyOperation, applyRenames, checkDrift, checkDriftPostgres, defineMigrationConfig, detectRenames, diffSchema, emptySchema, generateMigration, heads, introspectPostgres, introspectSqlite, invert, invertAll, makeRevisionId, reflectSchema, reflectTable, renderColumnDef, renderColumnType, renderDefault, renderOperation, replaySchema, runMigrationCli, sqliteAffinity, topoOrder };
1059
- //# sourceMappingURL=chunk-QMW4NKMH.js.map
1060
- //# sourceMappingURL=chunk-QMW4NKMH.js.map
1561
+ export { AsyncMigrationRunner, CyclicMigrationGraph, IrreversibleMigration, MigrationRunner, Op, UnknownRevision, applyOperation, applyRenames, checkDrift, checkDriftPostgres, defineMigrationConfig, detectRenames, diffSchema, emptySchema, generateMigration, heads, introspectPostgres, introspectSqlite, invert, invertAll, makeRevisionId, reflectSchema, reflectTable, renderColumnDef, renderColumnType, renderDefault, renderOperation, replaySchema, runMigrationCli, sqliteAffinity, topoOrder };
1562
+ //# sourceMappingURL=chunk-43XL66JG.js.map
1563
+ //# sourceMappingURL=chunk-43XL66JG.js.map