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.
@@ -1,4 +1,4 @@
1
- import { columnsOf } from './chunk-Q32CBI2A.js';
1
+ import { renderPortableToken, columnNamesOf, columnsOf } from './chunk-5QQMVTS5.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");
@@ -112,6 +116,8 @@ function renderColumnType(type, dialect) {
112
116
  return "NUMERIC";
113
117
  case "blob":
114
118
  return "BLOB";
119
+ case "array":
120
+ throw new Error(unsupportedArray("sqlite"));
115
121
  default:
116
122
  return "TEXT";
117
123
  }
@@ -153,6 +159,8 @@ function renderColumnType(type, dialect) {
153
159
  return "CHAR(36)";
154
160
  case "enum":
155
161
  return `ENUM(${(meta.values ?? []).map(quoteLiteral).join(", ")})`;
162
+ case "array":
163
+ throw new Error(unsupportedArray("mysql"));
156
164
  }
157
165
  }
158
166
  switch (kind) {
@@ -191,27 +199,39 @@ function renderColumnType(type, dialect) {
191
199
  return "UUID";
192
200
  case "enum":
193
201
  return "TEXT";
202
+ case "array":
203
+ return `${renderColumnType(arrayElement(meta.element), dialect)}[]`;
194
204
  }
195
205
  }
196
- function renderDefault(def, dialect) {
206
+ function unsupportedArray(dialect) {
207
+ 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.`;
208
+ }
209
+ function arrayElement(element) {
210
+ if (!element) {
211
+ throw new Error(
212
+ "An array column has no element type \u2014 build it with column.array()."
213
+ );
214
+ }
215
+ return element;
216
+ }
217
+ function renderDefault(def, dialect, type) {
197
218
  if (def.kind === "expression") {
198
219
  const expr = def.expression;
199
- if (typeof expr === "object") return expr.raw;
200
- switch (expr) {
201
- case "now":
202
- return dialect === "postgresql" ? "now()" : "CURRENT_TIMESTAMP";
203
- case "current_date":
204
- return "CURRENT_DATE";
205
- case "current_time":
206
- return "CURRENT_TIME";
207
- case "uuidv4":
208
- if (dialect === "postgresql") return "gen_random_uuid()";
209
- if (dialect === "mysql") return "(UUID())";
210
- return "(lower(hex(randomblob(16))))";
220
+ if (typeof expr === "object") {
221
+ if ("raw" in expr) return expr.raw;
222
+ throw new Error(
223
+ "sql.expr`...` binds parameters and cannot be rendered as a DEFAULT \u2014 use sql.raw()."
224
+ );
211
225
  }
226
+ return renderPortableToken(expr, dialect);
212
227
  }
213
228
  const value = def.value;
214
229
  if (value === null) return "NULL";
230
+ if (Array.isArray(value) && type?.kind === "array") {
231
+ const elementType = renderColumnType(arrayElement(type.meta.element), dialect);
232
+ const items = value.map((v) => renderDefault({ kind: "literal", value: v }, dialect));
233
+ return `ARRAY[${items.join(", ")}]::${elementType}[]`;
234
+ }
215
235
  if (typeof value === "boolean") {
216
236
  return dialect === "postgresql" ? value ? "TRUE" : "FALSE" : value ? "1" : "0";
217
237
  }
@@ -220,10 +240,47 @@ function renderDefault(def, dialect) {
220
240
  if (typeof value === "object") return quoteLiteral(JSON.stringify(value));
221
241
  return quoteLiteral(String(value));
222
242
  }
243
+ function renderFkAction(action) {
244
+ return action.toUpperCase();
245
+ }
246
+ function renderFkActions(fk) {
247
+ let sql = "";
248
+ if (fk.onDelete) sql += ` ON DELETE ${renderFkAction(fk.onDelete)}`;
249
+ if (fk.onUpdate) sql += ` ON UPDATE ${renderFkAction(fk.onUpdate)}`;
250
+ return sql;
251
+ }
252
+ function columnConstraintSuffix(col, dialect) {
253
+ let sql = "";
254
+ if (col.unique) sql += " UNIQUE";
255
+ if (col.references) {
256
+ const ref = col.references;
257
+ sql += ` REFERENCES ${quoteId(ref.table, dialect)} (${quoteId(ref.column, dialect)})`;
258
+ sql += renderFkActions(ref);
259
+ }
260
+ return sql;
261
+ }
262
+ function renderUniqueConstraint(uc, dialect) {
263
+ const cols = uc.columns.map((c) => quoteId(c, dialect)).join(", ");
264
+ return `CONSTRAINT ${quoteId(uc.name, dialect)} UNIQUE (${cols})`;
265
+ }
266
+ function renderForeignKeyConstraint(fk, dialect) {
267
+ const cols = fk.columns.map((c) => quoteId(c, dialect)).join(", ");
268
+ const refCols = fk.refColumns.map((c) => quoteId(c, dialect)).join(", ");
269
+ return `CONSTRAINT ${quoteId(fk.name, dialect)} FOREIGN KEY (${cols}) REFERENCES ${quoteId(fk.refTable, dialect)} (${refCols})${renderFkActions(fk)}`;
270
+ }
271
+ function tableConstraintClauses(table, dialect) {
272
+ return [
273
+ ...table.uniqueConstraints.map((uc) => renderUniqueConstraint(uc, dialect)),
274
+ ...table.foreignKeys.map((fk) => renderForeignKeyConstraint(fk, dialect))
275
+ ];
276
+ }
223
277
  function renderColumnDef(col, dialect) {
224
278
  let sql = `${quoteId(col.name, dialect)} ${renderColumnType(col.type, dialect)}`;
225
279
  if (col.notNull) sql += " NOT NULL";
226
- if (col.default !== null) sql += ` DEFAULT ${renderDefault(col.default, dialect)}`;
280
+ if (col.default !== null) {
281
+ sql += ` DEFAULT ${renderDefault(col.default, dialect, col.type)}`;
282
+ }
283
+ sql += columnConstraintSuffix(col, dialect);
227
284
  return sql;
228
285
  }
229
286
  function enumTypeName(table, column) {
@@ -246,14 +303,16 @@ function renderCreateTable(table, dialect) {
246
303
  typeStmts.push(`CREATE TYPE ${quoteId(typeName, dialect)} AS ENUM (${values})`);
247
304
  let def = `${quoteId(c.name, dialect)} ${quoteId(typeName, dialect)}`;
248
305
  if (c.notNull) def += " NOT NULL";
249
- if (c.default !== null) def += ` DEFAULT ${renderDefault(c.default, dialect)}`;
250
- return def;
306
+ if (c.default !== null) {
307
+ def += ` DEFAULT ${renderDefault(c.default, dialect, c.type)}`;
308
+ }
309
+ return def + columnConstraintSuffix(c, dialect);
251
310
  }
252
311
  if (dialect === "postgresql" && isAutoIncrementPk(table, c)) {
253
- return `${quoteId(c.name, dialect)} ${postgresSerialType(c.type.kind)}`;
312
+ return `${quoteId(c.name, dialect)} ${postgresSerialType(c.type.kind)}${columnConstraintSuffix(c, dialect)}`;
254
313
  }
255
314
  if (dialect === "mysql" && isAutoIncrementPk(table, c)) {
256
- return `${quoteId(c.name, dialect)} ${renderColumnType(c.type, dialect)} NOT NULL AUTO_INCREMENT`;
315
+ return `${quoteId(c.name, dialect)} ${renderColumnType(c.type, dialect)} NOT NULL AUTO_INCREMENT${columnConstraintSuffix(c, dialect)}`;
257
316
  }
258
317
  return renderColumnDef(c, dialect);
259
318
  });
@@ -262,6 +321,7 @@ function renderCreateTable(table, dialect) {
262
321
  `PRIMARY KEY (${table.primaryKey.map((c) => quoteId(c, dialect)).join(", ")})`
263
322
  );
264
323
  }
324
+ cols.push(...tableConstraintClauses(table, dialect));
265
325
  return [
266
326
  ...typeStmts,
267
327
  `CREATE TABLE ${quoteId(table.name, dialect)} (
@@ -295,10 +355,38 @@ function renderOperation(op, dialect) {
295
355
  return renderAlterColumn(op.table, op.to, dialect);
296
356
  case "recreate_table":
297
357
  return dialect === "sqlite" ? renderSqliteRebuild(op.from, op.to) : renderTableDiff(op.from, op.to, dialect);
358
+ case "add_constraint":
359
+ return renderAddConstraint(op.table, op.constraint, dialect);
360
+ case "drop_constraint":
361
+ return renderDropConstraint(op.table, op.constraint, dialect);
298
362
  case "execute":
299
363
  return [op.up];
300
364
  }
301
365
  }
366
+ function renderAddConstraint(table, constraint, dialect) {
367
+ if (dialect === "sqlite") {
368
+ throw new Error(
369
+ `add_constraint on SQLite needs a table-rebuild (recreate_table); constraint ${constraint.constraint.name} on ${table}`
370
+ );
371
+ }
372
+ const clause = constraint.type === "unique" ? renderUniqueConstraint(constraint.constraint, dialect) : renderForeignKeyConstraint(constraint.constraint, dialect);
373
+ return [`ALTER TABLE ${quoteId(table, dialect)} ADD ${clause}`];
374
+ }
375
+ function renderDropConstraint(table, constraint, dialect) {
376
+ if (dialect === "sqlite") {
377
+ throw new Error(
378
+ `drop_constraint on SQLite needs a table-rebuild (recreate_table); constraint ${constraint.constraint.name} on ${table}`
379
+ );
380
+ }
381
+ const t = quoteId(table, dialect);
382
+ const name = quoteId(constraint.constraint.name, dialect);
383
+ if (dialect === "mysql") {
384
+ return [
385
+ constraint.type === "unique" ? `ALTER TABLE ${t} DROP INDEX ${name}` : `ALTER TABLE ${t} DROP FOREIGN KEY ${name}`
386
+ ];
387
+ }
388
+ return [`ALTER TABLE ${t} DROP CONSTRAINT ${name}`];
389
+ }
302
390
  function renderSqliteRebuild(from, to) {
303
391
  const tmp = `__new_${to.name}`;
304
392
  const common = Object.keys(to.columns).filter((c) => c in from.columns);
@@ -308,6 +396,7 @@ function renderSqliteRebuild(from, to) {
308
396
  `PRIMARY KEY (${to.primaryKey.map((c) => quoteId(c, "sqlite")).join(", ")})`
309
397
  );
310
398
  }
399
+ cols.push(...tableConstraintClauses(to, "sqlite"));
311
400
  const commonSql = common.map((c) => quoteId(c, "sqlite")).join(", ");
312
401
  return [
313
402
  "PRAGMA foreign_keys=off",
@@ -360,7 +449,7 @@ function renderAlterColumn(table, to, dialect) {
360
449
  to.notNull ? `ALTER TABLE ${t} ALTER COLUMN ${c} SET NOT NULL` : `ALTER TABLE ${t} ALTER COLUMN ${c} DROP NOT NULL`
361
450
  );
362
451
  stmts.push(
363
- to.default !== null ? `ALTER TABLE ${t} ALTER COLUMN ${c} SET DEFAULT ${renderDefault(to.default, dialect)}` : `ALTER TABLE ${t} ALTER COLUMN ${c} DROP DEFAULT`
452
+ 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`
364
453
  );
365
454
  return stmts;
366
455
  }
@@ -371,9 +460,74 @@ function columnSignature(col) {
371
460
  type: col.type,
372
461
  notNull: col.notNull,
373
462
  primaryKey: col.primaryKey,
374
- default: col.default
463
+ default: col.default,
464
+ unique: col.unique,
465
+ references: col.references
375
466
  });
376
467
  }
468
+ function uniqueSignature(uc) {
469
+ return JSON.stringify({ columns: uc.columns });
470
+ }
471
+ function foreignKeySignature(fk) {
472
+ return JSON.stringify({
473
+ columns: fk.columns,
474
+ refTable: fk.refTable,
475
+ refColumns: fk.refColumns,
476
+ onDelete: fk.onDelete ?? null,
477
+ onUpdate: fk.onUpdate ?? null
478
+ });
479
+ }
480
+ function diffConstraints(current, target) {
481
+ const ops = [];
482
+ const table = target.name;
483
+ const currentUq = new Map(current.uniqueConstraints.map((u) => [u.name, u]));
484
+ const targetUq = new Map(target.uniqueConstraints.map((u) => [u.name, u]));
485
+ for (const [name, cur] of currentUq) {
486
+ const tgt = targetUq.get(name);
487
+ if (!tgt || uniqueSignature(cur) !== uniqueSignature(tgt)) {
488
+ ops.push(dropUnique(table, cur));
489
+ }
490
+ }
491
+ for (const [name, tgt] of targetUq) {
492
+ const cur = currentUq.get(name);
493
+ if (!cur || uniqueSignature(cur) !== uniqueSignature(tgt)) {
494
+ ops.push(addUnique(table, tgt));
495
+ }
496
+ }
497
+ const currentFk = new Map(current.foreignKeys.map((f) => [f.name, f]));
498
+ const targetFk = new Map(target.foreignKeys.map((f) => [f.name, f]));
499
+ for (const [name, cur] of currentFk) {
500
+ const tgt = targetFk.get(name);
501
+ if (!tgt || foreignKeySignature(cur) !== foreignKeySignature(tgt)) {
502
+ ops.push(dropForeignKey(table, cur));
503
+ }
504
+ }
505
+ for (const [name, tgt] of targetFk) {
506
+ const cur = currentFk.get(name);
507
+ if (!cur || foreignKeySignature(cur) !== foreignKeySignature(tgt)) {
508
+ ops.push(addForeignKey(table, tgt));
509
+ }
510
+ }
511
+ return ops;
512
+ }
513
+ function uniqueNamed(uc) {
514
+ return { type: "unique", constraint: uc };
515
+ }
516
+ function foreignKeyNamed(fk) {
517
+ return { type: "foreignKey", constraint: fk };
518
+ }
519
+ function addUnique(table, uc) {
520
+ return { kind: "add_constraint", table, constraint: uniqueNamed(uc) };
521
+ }
522
+ function dropUnique(table, uc) {
523
+ return { kind: "drop_constraint", table, constraint: uniqueNamed(uc) };
524
+ }
525
+ function addForeignKey(table, fk) {
526
+ return { kind: "add_constraint", table, constraint: foreignKeyNamed(fk) };
527
+ }
528
+ function dropForeignKey(table, fk) {
529
+ return { kind: "drop_constraint", table, constraint: foreignKeyNamed(fk) };
530
+ }
377
531
  function diffSchema(current, target) {
378
532
  const ops = [];
379
533
  const drops = [];
@@ -402,6 +556,7 @@ function diffSchema(current, target) {
402
556
  ops.push({ kind: "drop_column", table: name, column: currentCol });
403
557
  }
404
558
  }
559
+ ops.push(...diffConstraints(currentTable, targetTable));
405
560
  }
406
561
  for (const [name, currentTable] of Object.entries(current.tables)) {
407
562
  if (!target.tables[name]) {
@@ -466,21 +621,49 @@ function heads(migrations) {
466
621
  }
467
622
 
468
623
  // src/migrations/ir.ts
624
+ function constraintName(prefix, table, columns) {
625
+ return `${prefix}_${table}_${columns.join("_")}`;
626
+ }
469
627
  function reflectTable(model) {
628
+ const names = columnNamesOf(model);
629
+ const toColumn = (prop) => names?.[prop] ?? prop;
470
630
  const columns = {};
471
631
  const primaryKey = [];
472
- for (const [name, col] of Object.entries(columnsOf(model))) {
632
+ for (const [prop, col] of Object.entries(columnsOf(model))) {
473
633
  const isPk = col.flags.primaryKey;
634
+ const name = toColumn(prop);
474
635
  columns[name] = {
475
636
  name,
476
637
  type: col.type,
477
638
  notNull: col.flags.notNull || isPk,
478
639
  primaryKey: isPk,
479
- default: col.defaultValue
640
+ default: col.defaultValue,
641
+ unique: col.flags.unique,
642
+ references: col.reference
480
643
  };
481
644
  if (isPk) primaryKey.push(name);
482
645
  }
483
- return { name: model.tablename, columns, primaryKey };
646
+ const uniqueConstraints = [];
647
+ const foreignKeys = [];
648
+ for (const c of model.tableArgs?.() ?? []) {
649
+ const cols = c.columns.map(toColumn);
650
+ if (c.kind === "unique") {
651
+ uniqueConstraints.push({
652
+ name: c.name ?? constraintName("uq", model.tablename, cols),
653
+ columns: cols
654
+ });
655
+ } else {
656
+ foreignKeys.push({
657
+ name: c.name ?? constraintName("fk", model.tablename, cols),
658
+ columns: cols,
659
+ refTable: c.refTable,
660
+ refColumns: c.refColumns,
661
+ onDelete: c.onDelete,
662
+ onUpdate: c.onUpdate
663
+ });
664
+ }
665
+ }
666
+ return { name: model.tablename, columns, primaryKey, uniqueConstraints, foreignKeys };
484
667
  }
485
668
  function reflectSchema(models) {
486
669
  const tables = {};
@@ -517,6 +700,39 @@ function affinityToKind(affinity) {
517
700
  return "text";
518
701
  }
519
702
  }
703
+ function sqliteForeignKeys(driver, table) {
704
+ const rows = driver.execute(`PRAGMA foreign_key_list(${JSON.stringify(table)})`, []).rows;
705
+ const byId = /* @__PURE__ */ new Map();
706
+ for (const r of rows) {
707
+ const list = byId.get(r.id) ?? [];
708
+ list.push(r);
709
+ byId.set(r.id, list);
710
+ }
711
+ const fks = [];
712
+ for (const group of byId.values()) {
713
+ const ordered = [...group].sort((a, b) => a.seq - b.seq);
714
+ const columns = ordered.map((r) => r.from);
715
+ fks.push({
716
+ name: `fk_${table}_${columns.join("_")}`,
717
+ columns,
718
+ refTable: ordered[0]?.table ?? "",
719
+ refColumns: ordered.map((r) => r.to)
720
+ });
721
+ }
722
+ return fks;
723
+ }
724
+ function sqliteUniques(driver, table) {
725
+ const indexes = driver.execute(`PRAGMA index_list(${JSON.stringify(table)})`, []).rows;
726
+ const uniques = [];
727
+ for (const idx of indexes) {
728
+ if (Number(idx.unique) !== 1 || idx.origin === "pk") continue;
729
+ const cols = driver.execute(`PRAGMA index_info(${JSON.stringify(idx.name)})`, []).rows.sort((a, b) => a.seqno - b.seqno).map((c) => c.name);
730
+ if (cols.length > 0) {
731
+ uniques.push({ name: `uq_${table}_${cols.join("_")}`, columns: cols });
732
+ }
733
+ }
734
+ return uniques;
735
+ }
520
736
  function introspectSqlite(driver) {
521
737
  const tablesRows = driver.execute(
522
738
  "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name != 'tempest_db_js_migrations'",
@@ -536,14 +752,39 @@ function introspectSqlite(driver) {
536
752
  type: { kind: affinityToKind(affinity), meta: {} },
537
753
  notNull: Number(col.notnull) === 1 || isPk,
538
754
  primaryKey: isPk,
539
- default: null
755
+ default: null,
756
+ unique: false,
757
+ references: null
540
758
  };
541
759
  if (isPk) primaryKey.push(col.name);
542
760
  }
543
- tables[tableName] = { name: tableName, columns, primaryKey };
761
+ tables[tableName] = {
762
+ name: tableName,
763
+ columns,
764
+ primaryKey,
765
+ uniqueConstraints: sqliteUniques(driver, tableName),
766
+ foreignKeys: sqliteForeignKeys(driver, tableName)
767
+ };
544
768
  }
545
769
  return { tables };
546
770
  }
771
+ function constraintKeys(table) {
772
+ const fks = /* @__PURE__ */ new Set();
773
+ const uniques = /* @__PURE__ */ new Set();
774
+ for (const col of Object.values(table.columns)) {
775
+ if (col.unique) uniques.add(col.name);
776
+ if (col.references) {
777
+ fks.add(`${col.name}=>${col.references.table}(${col.references.column})`);
778
+ }
779
+ }
780
+ for (const uc of table.uniqueConstraints) {
781
+ uniques.add([...uc.columns].sort().join(","));
782
+ }
783
+ for (const fk of table.foreignKeys) {
784
+ fks.add(`${fk.columns.join(",")}=>${fk.refTable}(${fk.refColumns.join(",")})`);
785
+ }
786
+ return { fks, uniques };
787
+ }
547
788
  function checkDrift(driver, models) {
548
789
  const actual = introspectSqlite(driver);
549
790
  const expected = reflectSchema(models);
@@ -581,6 +822,34 @@ function checkDrift(driver, models) {
581
822
  );
582
823
  }
583
824
  }
825
+ const expectedKeys = constraintKeys(expectedTable);
826
+ const actualKeys = constraintKeys(actualTable);
827
+ for (const fk of expectedKeys.fks) {
828
+ if (!actualKeys.fks.has(fk)) {
829
+ issues.push(`foreign key "${tableName}: ${fk}" is missing from the database`);
830
+ }
831
+ }
832
+ for (const fk of actualKeys.fks) {
833
+ if (!expectedKeys.fks.has(fk)) {
834
+ issues.push(
835
+ `foreign key "${tableName}: ${fk}" exists in the database but not in the model`
836
+ );
837
+ }
838
+ }
839
+ for (const uq of expectedKeys.uniques) {
840
+ if (!actualKeys.uniques.has(uq)) {
841
+ issues.push(
842
+ `unique constraint "${tableName}: (${uq})" is missing from the database`
843
+ );
844
+ }
845
+ }
846
+ for (const uq of actualKeys.uniques) {
847
+ if (!expectedKeys.uniques.has(uq)) {
848
+ issues.push(
849
+ `unique constraint "${tableName}: (${uq})" exists in the database but not in the model`
850
+ );
851
+ }
852
+ }
584
853
  }
585
854
  for (const tableName of Object.keys(actual.tables)) {
586
855
  if (!expected.tables[tableName]) {
@@ -589,6 +858,54 @@ function checkDrift(driver, models) {
589
858
  }
590
859
  return issues;
591
860
  }
861
+ function pgTypeToColumnType(dataType, udtName) {
862
+ if (dataType.toLowerCase() === "array") {
863
+ return {
864
+ kind: "array",
865
+ meta: { element: { kind: pgUdtToKind(udtName.replace(/^_/, "")), meta: {} } }
866
+ };
867
+ }
868
+ return { kind: pgTypeToKind(dataType, udtName), meta: {} };
869
+ }
870
+ function pgUdtToKind(udtName) {
871
+ switch (udtName) {
872
+ case "int2":
873
+ return "smallint";
874
+ case "int4":
875
+ return "integer";
876
+ case "int8":
877
+ return "bigint";
878
+ case "float4":
879
+ return "real";
880
+ case "float8":
881
+ return "double";
882
+ case "numeric":
883
+ return "numeric";
884
+ case "varchar":
885
+ return "varchar";
886
+ case "bpchar":
887
+ return "char";
888
+ case "bool":
889
+ return "boolean";
890
+ case "date":
891
+ return "date";
892
+ case "time":
893
+ case "timetz":
894
+ return "time";
895
+ case "timestamp":
896
+ case "timestamptz":
897
+ return "timestamp";
898
+ case "bytea":
899
+ return "blob";
900
+ case "json":
901
+ case "jsonb":
902
+ return "json";
903
+ case "uuid":
904
+ return "uuid";
905
+ default:
906
+ return "text";
907
+ }
908
+ }
592
909
  function pgTypeToKind(dataType, udtName) {
593
910
  const t = dataType.toLowerCase();
594
911
  if (t === "user-defined") return "enum";
@@ -636,20 +953,66 @@ async function introspectPostgres(driver) {
636
953
  const isPk = pkSet.has(name);
637
954
  columns[name] = {
638
955
  name,
639
- type: {
640
- kind: pgTypeToKind(String(col.data_type), String(col.udt_name)),
641
- meta: {}
642
- },
956
+ type: pgTypeToColumnType(String(col.data_type), String(col.udt_name)),
643
957
  notNull: col.is_nullable === "NO" || isPk,
644
958
  primaryKey: isPk,
645
- default: null
959
+ default: null,
960
+ unique: false,
961
+ references: null
646
962
  };
647
963
  if (isPk) primaryKey.push(name);
648
964
  }
649
- tables[tableName] = { name: tableName, columns, primaryKey };
965
+ tables[tableName] = {
966
+ name: tableName,
967
+ columns,
968
+ primaryKey,
969
+ uniqueConstraints: await postgresUniques(driver, tableName),
970
+ foreignKeys: await postgresForeignKeys(driver, tableName)
971
+ };
650
972
  }
651
973
  return { tables };
652
974
  }
975
+ async function postgresForeignKeys(driver, table) {
976
+ const result = await driver.execute(
977
+ `SELECT c.conname AS name,
978
+ (SELECT array_agg(a.attname ORDER BY k.ord)
979
+ FROM unnest(c.conkey) WITH ORDINALITY AS k(attnum, ord)
980
+ JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum) AS cols,
981
+ cf.relname AS ref_table,
982
+ (SELECT array_agg(a.attname ORDER BY k.ord)
983
+ FROM unnest(c.confkey) WITH ORDINALITY AS k(attnum, ord)
984
+ JOIN pg_attribute a ON a.attrelid = c.confrelid AND a.attnum = k.attnum) AS ref_cols
985
+ FROM pg_constraint c
986
+ JOIN pg_class cf ON cf.oid = c.confrelid
987
+ WHERE c.contype = 'f' AND c.conrelid = $1::regclass`,
988
+ [table]
989
+ );
990
+ return result.rows.map((r) => ({
991
+ name: String(r.name),
992
+ columns: r.cols ?? [],
993
+ refTable: String(r.ref_table),
994
+ refColumns: r.ref_cols ?? []
995
+ }));
996
+ }
997
+ async function postgresUniques(driver, table) {
998
+ const result = await driver.execute(
999
+ `SELECT c.conname AS name,
1000
+ (SELECT array_agg(a.attname ORDER BY k.ord)
1001
+ FROM unnest(c.conkey) WITH ORDINALITY AS k(attnum, ord)
1002
+ JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum) AS cols
1003
+ FROM pg_constraint c
1004
+ WHERE c.contype = 'u' AND c.conrelid = $1::regclass`,
1005
+ [table]
1006
+ );
1007
+ return result.rows.map((r) => ({
1008
+ name: String(r.name),
1009
+ columns: r.cols ?? []
1010
+ }));
1011
+ }
1012
+ function describeKind(type) {
1013
+ if (type.kind !== "array") return type.kind;
1014
+ return `${type.meta.element ? describeKind(type.meta.element) : "unknown"}[]`;
1015
+ }
653
1016
  async function checkDriftPostgres(driver, models) {
654
1017
  const actual = await introspectPostgres(driver);
655
1018
  const expected = reflectSchema(models);
@@ -666,9 +1029,9 @@ async function checkDriftPostgres(driver, models) {
666
1029
  issues.push(`column "${tableName}.${colName}" is missing from the database`);
667
1030
  continue;
668
1031
  }
669
- if (expectedCol.type.kind !== actualCol.type.kind) {
1032
+ if (describeKind(expectedCol.type) !== describeKind(actualCol.type)) {
670
1033
  issues.push(
671
- `column "${tableName}.${colName}" type differs: model ${expectedCol.type.kind}, db ${actualCol.type.kind}`
1034
+ `column "${tableName}.${colName}" type differs: model ${describeKind(expectedCol.type)}, db ${describeKind(actualCol.type)}`
672
1035
  );
673
1036
  }
674
1037
  if (expectedCol.notNull !== actualCol.notNull) {
@@ -682,6 +1045,20 @@ async function checkDriftPostgres(driver, models) {
682
1045
  );
683
1046
  }
684
1047
  }
1048
+ const expectedKeys = constraintKeys(expectedTable);
1049
+ const actualKeys = constraintKeys(actualTable);
1050
+ for (const fk of expectedKeys.fks) {
1051
+ if (!actualKeys.fks.has(fk)) {
1052
+ issues.push(`foreign key "${tableName}: ${fk}" is missing from the database`);
1053
+ }
1054
+ }
1055
+ for (const uq of expectedKeys.uniques) {
1056
+ if (!actualKeys.uniques.has(uq)) {
1057
+ issues.push(
1058
+ `unique constraint "${tableName}: (${uq})" is missing from the database`
1059
+ );
1060
+ }
1061
+ }
685
1062
  }
686
1063
  for (const tableName of Object.keys(actual.tables)) {
687
1064
  if (!expected.tables[tableName]) {
@@ -697,7 +1074,9 @@ function columnShape(col) {
697
1074
  type: col.type,
698
1075
  notNull: col.notNull,
699
1076
  primaryKey: col.primaryKey,
700
- default: col.default
1077
+ default: col.default,
1078
+ unique: col.unique,
1079
+ references: col.references
701
1080
  });
702
1081
  }
703
1082
  function tableShape(columns) {
@@ -816,6 +1195,14 @@ var Op = class {
816
1195
  recreateTable(from, to) {
817
1196
  this.run({ kind: "recreate_table", from, to });
818
1197
  }
1198
+ /** Add a table-level unique / foreign-key constraint. */
1199
+ addConstraint(table, constraint) {
1200
+ this.run({ kind: "add_constraint", table, constraint });
1201
+ }
1202
+ /** Drop a table-level unique / foreign-key constraint. */
1203
+ dropConstraint(table, constraint) {
1204
+ this.run({ kind: "drop_constraint", table, constraint });
1205
+ }
819
1206
  /** Raw SQL escape hatch (e.g. a data migration). `down` may be `null`. */
820
1207
  execute(up, down = null) {
821
1208
  this.run({ kind: "execute", up, down });
@@ -1084,6 +1471,30 @@ function applyOperation(schema, op) {
1084
1471
  }
1085
1472
  break;
1086
1473
  }
1474
+ case "add_constraint": {
1475
+ const t = tables[op.table];
1476
+ if (t) {
1477
+ tables[op.table] = op.constraint.type === "unique" ? {
1478
+ ...t,
1479
+ uniqueConstraints: [...t.uniqueConstraints, op.constraint.constraint]
1480
+ } : { ...t, foreignKeys: [...t.foreignKeys, op.constraint.constraint] };
1481
+ }
1482
+ break;
1483
+ }
1484
+ case "drop_constraint": {
1485
+ const t = tables[op.table];
1486
+ if (t) {
1487
+ const dropName = op.constraint.constraint.name;
1488
+ tables[op.table] = op.constraint.type === "unique" ? {
1489
+ ...t,
1490
+ uniqueConstraints: t.uniqueConstraints.filter((u) => u.name !== dropName)
1491
+ } : {
1492
+ ...t,
1493
+ foreignKeys: t.foreignKeys.filter((f) => f.name !== dropName)
1494
+ };
1495
+ }
1496
+ break;
1497
+ }
1087
1498
  }
1088
1499
  return { tables };
1089
1500
  }
@@ -1221,5 +1632,5 @@ function runMigrationCli(argv, config) {
1221
1632
  }
1222
1633
 
1223
1634
  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 };
1224
- //# sourceMappingURL=chunk-OP7FRDI5.js.map
1225
- //# sourceMappingURL=chunk-OP7FRDI5.js.map
1635
+ //# sourceMappingURL=chunk-EPMLFNFK.js.map
1636
+ //# sourceMappingURL=chunk-EPMLFNFK.js.map