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/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 === "sqlite" ? "CURRENT_TIMESTAMP" : "now()";
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
- return dialect === "sqlite" ? "(lower(hex(randomblob(16))))" : "gen_random_uuid()";
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 === "sqlite" ? value ? "1" : "0" : value ? "TRUE" : "FALSE";
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(`PRIMARY KEY (${table.primaryKey.map(quoteId).join(", ")})`);
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 [`ALTER TABLE ${quoteId(op.from)} RENAME TO ${quoteId(op.to)}`];
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 [`ALTER TABLE ${quoteId(op.table)} DROP COLUMN ${quoteId(op.column.name)}`];
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) : renderPostgresTableDiff(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(`PRIMARY KEY (${to.primaryKey.map(quoteId).join(", ")})`);
382
+ cols.push(
383
+ `PRIMARY KEY (${to.primaryKey.map((c) => quoteId(c, "sqlite")).join(", ")})`
384
+ );
265
385
  }
266
- const commonSql = common.map(quoteId).join(", ");
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 renderPostgresTableDiff(from, to) {
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, "postgresql")}`
404
+ `ALTER TABLE ${quoteId(to.name, dialect)} ADD COLUMN ${renderColumnDef(col, dialect)}`
284
405
  );
285
406
  } else {
286
- stmts.push(...renderAlterColumn(to.name, col, "postgresql"));
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(`ALTER TABLE ${quoteId(to.name)} DROP COLUMN ${quoteId(name)}`);
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 batch/table-rebuild (Phase 6e); column ${table}.${to.name}`
422
+ `alter_column on SQLite needs a table-rebuild (recreate_table); column ${table}.${to.name}`
300
423
  );
301
424
  }
302
- const t = quoteId(table);
303
- const c = quoteId(to.name);
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
- return { name: model.tablename, columns, primaryKey };
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] = { name: tableName, columns, primaryKey };
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
  }