tempest-db-js 0.3.0 → 0.5.0

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