tempest-db-js 0.3.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.
@@ -4,23 +4,38 @@
4
4
  function isDefaultValue(value) {
5
5
  return typeof value === "object" && value !== null && "kind" in value && (value.kind === "literal" || value.kind === "expression");
6
6
  }
7
+ function parseReference(ref, options) {
8
+ const dot = ref.lastIndexOf(".");
9
+ if (dot <= 0 || dot === ref.length - 1) {
10
+ throw new Error(`Invalid foreign key reference "${ref}"; expected "table.column".`);
11
+ }
12
+ return {
13
+ table: ref.slice(0, dot),
14
+ column: ref.slice(dot + 1),
15
+ onDelete: options?.onDelete,
16
+ onUpdate: options?.onUpdate
17
+ };
18
+ }
7
19
  var Column = class _Column {
8
- constructor(type, flags, defaultValue = null, onUpdateValue = null) {
20
+ constructor(type, flags, defaultValue = null, onUpdateValue = null, reference = null) {
9
21
  this.type = type;
10
22
  this.flags = flags;
11
23
  this.defaultValue = defaultValue;
12
24
  this.onUpdateValue = onUpdateValue;
25
+ this.reference = reference;
13
26
  }
14
27
  type;
15
28
  flags;
16
29
  defaultValue;
17
30
  onUpdateValue;
31
+ reference;
18
32
  primaryKey() {
19
33
  return new _Column(
20
34
  this.type,
21
35
  { ...this.flags, primaryKey: true, hasDefault: true },
22
36
  this.defaultValue,
23
- this.onUpdateValue
37
+ this.onUpdateValue,
38
+ this.reference
24
39
  );
25
40
  }
26
41
  notNull() {
@@ -28,7 +43,40 @@ var Column = class _Column {
28
43
  this.type,
29
44
  { ...this.flags, notNull: true },
30
45
  this.defaultValue,
31
- this.onUpdateValue
46
+ this.onUpdateValue,
47
+ this.reference
48
+ );
49
+ }
50
+ /**
51
+ * Add a `UNIQUE` constraint to the column (mirrors SQLAlchemy's
52
+ * `mapped_column(unique=True)`). DDL-only — does not change the inferred type.
53
+ */
54
+ unique() {
55
+ return new _Column(
56
+ this.type,
57
+ { ...this.flags, unique: true },
58
+ this.defaultValue,
59
+ this.onUpdateValue,
60
+ this.reference
61
+ );
62
+ }
63
+ /**
64
+ * Declare a foreign-key reference to another table's column, à la SQLAlchemy's
65
+ * `mapped_column(ForeignKey("table.column", ondelete=...))`. DDL-only — does
66
+ * not change the inferred type.
67
+ *
68
+ * @param ref The target as `"table.column"` (e.g. `"users.id"`).
69
+ * @param options Optional `onDelete` / `onUpdate` referential actions.
70
+ * @returns A new column carrying the reference.
71
+ * @throws Error When `ref` is not a valid `"table.column"` string.
72
+ */
73
+ references(ref, options) {
74
+ return new _Column(
75
+ this.type,
76
+ this.flags,
77
+ this.defaultValue,
78
+ this.onUpdateValue,
79
+ parseReference(ref, options)
32
80
  );
33
81
  }
34
82
  /**
@@ -41,7 +89,8 @@ var Column = class _Column {
41
89
  this.type,
42
90
  { ...this.flags, hasDefault: true },
43
91
  resolved,
44
- this.onUpdateValue
92
+ this.onUpdateValue,
93
+ this.reference
45
94
  );
46
95
  }
47
96
  /**
@@ -50,7 +99,7 @@ var Column = class _Column {
50
99
  */
51
100
  onUpdate(value) {
52
101
  const resolved = isDefaultValue(value) ? value : { kind: "literal", value };
53
- return new _Column(this.type, this.flags, this.defaultValue, resolved);
102
+ return new _Column(this.type, this.flags, this.defaultValue, resolved, this.reference);
54
103
  }
55
104
  };
56
105
  var columnsCache = /* @__PURE__ */ new WeakMap();
@@ -69,6 +118,9 @@ function columnsOf(model) {
69
118
  }
70
119
 
71
120
  // src/migrations/ir.ts
121
+ function constraintName(prefix, table, columns) {
122
+ return `${prefix}_${table}_${columns.join("_")}`;
123
+ }
72
124
  function reflectTable(model) {
73
125
  const columns = {};
74
126
  const primaryKey = [];
@@ -79,11 +131,32 @@ function reflectTable(model) {
79
131
  type: col.type,
80
132
  notNull: col.flags.notNull || isPk,
81
133
  primaryKey: isPk,
82
- default: col.defaultValue
134
+ default: col.defaultValue,
135
+ unique: col.flags.unique,
136
+ references: col.reference
83
137
  };
84
138
  if (isPk) primaryKey.push(name);
85
139
  }
86
- return { name: model.tablename, columns, primaryKey };
140
+ const uniqueConstraints = [];
141
+ const foreignKeys = [];
142
+ for (const c of model.tableArgs?.() ?? []) {
143
+ if (c.kind === "unique") {
144
+ uniqueConstraints.push({
145
+ name: c.name ?? constraintName("uq", model.tablename, c.columns),
146
+ columns: c.columns
147
+ });
148
+ } else {
149
+ foreignKeys.push({
150
+ name: c.name ?? constraintName("fk", model.tablename, c.columns),
151
+ columns: c.columns,
152
+ refTable: c.refTable,
153
+ refColumns: c.refColumns,
154
+ onDelete: c.onDelete,
155
+ onUpdate: c.onUpdate
156
+ });
157
+ }
158
+ }
159
+ return { name: model.tablename, columns, primaryKey, uniqueConstraints, foreignKeys };
87
160
  }
88
161
  function reflectSchema(models) {
89
162
  const tables = {};
@@ -128,6 +201,10 @@ function invert(op) {
128
201
  return { kind: "rename_column", table: op.table, from: op.to, to: op.from };
129
202
  case "recreate_table":
130
203
  return { kind: "recreate_table", from: op.to, to: op.from };
204
+ case "add_constraint":
205
+ return { kind: "drop_constraint", table: op.table, constraint: op.constraint };
206
+ case "drop_constraint":
207
+ return { kind: "add_constraint", table: op.table, constraint: op.constraint };
131
208
  case "execute":
132
209
  if (op.down === null) {
133
210
  throw new IrreversibleMigration("execute() operation has no down SQL");
@@ -270,10 +347,45 @@ function renderDefault(def, dialect) {
270
347
  if (typeof value === "object") return quoteLiteral(JSON.stringify(value));
271
348
  return quoteLiteral(String(value));
272
349
  }
350
+ function renderFkAction(action) {
351
+ return action.toUpperCase();
352
+ }
353
+ function renderFkActions(fk) {
354
+ let sql = "";
355
+ if (fk.onDelete) sql += ` ON DELETE ${renderFkAction(fk.onDelete)}`;
356
+ if (fk.onUpdate) sql += ` ON UPDATE ${renderFkAction(fk.onUpdate)}`;
357
+ return sql;
358
+ }
359
+ function columnConstraintSuffix(col, dialect) {
360
+ let sql = "";
361
+ if (col.unique) sql += " UNIQUE";
362
+ if (col.references) {
363
+ const ref = col.references;
364
+ sql += ` REFERENCES ${quoteId(ref.table, dialect)} (${quoteId(ref.column, dialect)})`;
365
+ sql += renderFkActions(ref);
366
+ }
367
+ return sql;
368
+ }
369
+ function renderUniqueConstraint(uc, dialect) {
370
+ const cols = uc.columns.map((c) => quoteId(c, dialect)).join(", ");
371
+ return `CONSTRAINT ${quoteId(uc.name, dialect)} UNIQUE (${cols})`;
372
+ }
373
+ function renderForeignKeyConstraint(fk, dialect) {
374
+ const cols = fk.columns.map((c) => quoteId(c, dialect)).join(", ");
375
+ const refCols = fk.refColumns.map((c) => quoteId(c, dialect)).join(", ");
376
+ return `CONSTRAINT ${quoteId(fk.name, dialect)} FOREIGN KEY (${cols}) REFERENCES ${quoteId(fk.refTable, dialect)} (${refCols})${renderFkActions(fk)}`;
377
+ }
378
+ function tableConstraintClauses(table, dialect) {
379
+ return [
380
+ ...table.uniqueConstraints.map((uc) => renderUniqueConstraint(uc, dialect)),
381
+ ...table.foreignKeys.map((fk) => renderForeignKeyConstraint(fk, dialect))
382
+ ];
383
+ }
273
384
  function renderColumnDef(col, dialect) {
274
385
  let sql = `${quoteId(col.name, dialect)} ${renderColumnType(col.type, dialect)}`;
275
386
  if (col.notNull) sql += " NOT NULL";
276
387
  if (col.default !== null) sql += ` DEFAULT ${renderDefault(col.default, dialect)}`;
388
+ sql += columnConstraintSuffix(col, dialect);
277
389
  return sql;
278
390
  }
279
391
  function enumTypeName(table, column) {
@@ -297,13 +409,13 @@ function renderCreateTable(table, dialect) {
297
409
  let def = `${quoteId(c.name, dialect)} ${quoteId(typeName, dialect)}`;
298
410
  if (c.notNull) def += " NOT NULL";
299
411
  if (c.default !== null) def += ` DEFAULT ${renderDefault(c.default, dialect)}`;
300
- return def;
412
+ return def + columnConstraintSuffix(c, dialect);
301
413
  }
302
414
  if (dialect === "postgresql" && isAutoIncrementPk(table, c)) {
303
- return `${quoteId(c.name, dialect)} ${postgresSerialType(c.type.kind)}`;
415
+ return `${quoteId(c.name, dialect)} ${postgresSerialType(c.type.kind)}${columnConstraintSuffix(c, dialect)}`;
304
416
  }
305
417
  if (dialect === "mysql" && isAutoIncrementPk(table, c)) {
306
- return `${quoteId(c.name, dialect)} ${renderColumnType(c.type, dialect)} NOT NULL AUTO_INCREMENT`;
418
+ return `${quoteId(c.name, dialect)} ${renderColumnType(c.type, dialect)} NOT NULL AUTO_INCREMENT${columnConstraintSuffix(c, dialect)}`;
307
419
  }
308
420
  return renderColumnDef(c, dialect);
309
421
  });
@@ -312,6 +424,7 @@ function renderCreateTable(table, dialect) {
312
424
  `PRIMARY KEY (${table.primaryKey.map((c) => quoteId(c, dialect)).join(", ")})`
313
425
  );
314
426
  }
427
+ cols.push(...tableConstraintClauses(table, dialect));
315
428
  return [
316
429
  ...typeStmts,
317
430
  `CREATE TABLE ${quoteId(table.name, dialect)} (
@@ -345,10 +458,38 @@ function renderOperation(op, dialect) {
345
458
  return renderAlterColumn(op.table, op.to, dialect);
346
459
  case "recreate_table":
347
460
  return dialect === "sqlite" ? renderSqliteRebuild(op.from, op.to) : renderTableDiff(op.from, op.to, dialect);
461
+ case "add_constraint":
462
+ return renderAddConstraint(op.table, op.constraint, dialect);
463
+ case "drop_constraint":
464
+ return renderDropConstraint(op.table, op.constraint, dialect);
348
465
  case "execute":
349
466
  return [op.up];
350
467
  }
351
468
  }
469
+ function renderAddConstraint(table, constraint, dialect) {
470
+ if (dialect === "sqlite") {
471
+ throw new Error(
472
+ `add_constraint on SQLite needs a table-rebuild (recreate_table); constraint ${constraint.constraint.name} on ${table}`
473
+ );
474
+ }
475
+ const clause = constraint.type === "unique" ? renderUniqueConstraint(constraint.constraint, dialect) : renderForeignKeyConstraint(constraint.constraint, dialect);
476
+ return [`ALTER TABLE ${quoteId(table, dialect)} ADD ${clause}`];
477
+ }
478
+ function renderDropConstraint(table, constraint, dialect) {
479
+ if (dialect === "sqlite") {
480
+ throw new Error(
481
+ `drop_constraint on SQLite needs a table-rebuild (recreate_table); constraint ${constraint.constraint.name} on ${table}`
482
+ );
483
+ }
484
+ const t = quoteId(table, dialect);
485
+ const name = quoteId(constraint.constraint.name, dialect);
486
+ if (dialect === "mysql") {
487
+ return [
488
+ constraint.type === "unique" ? `ALTER TABLE ${t} DROP INDEX ${name}` : `ALTER TABLE ${t} DROP FOREIGN KEY ${name}`
489
+ ];
490
+ }
491
+ return [`ALTER TABLE ${t} DROP CONSTRAINT ${name}`];
492
+ }
352
493
  function renderSqliteRebuild(from, to) {
353
494
  const tmp = `__new_${to.name}`;
354
495
  const common = Object.keys(to.columns).filter((c) => c in from.columns);
@@ -358,6 +499,7 @@ function renderSqliteRebuild(from, to) {
358
499
  `PRIMARY KEY (${to.primaryKey.map((c) => quoteId(c, "sqlite")).join(", ")})`
359
500
  );
360
501
  }
502
+ cols.push(...tableConstraintClauses(to, "sqlite"));
361
503
  const commonSql = common.map((c) => quoteId(c, "sqlite")).join(", ");
362
504
  return [
363
505
  "PRAGMA foreign_keys=off",
@@ -421,9 +563,74 @@ function columnSignature(col) {
421
563
  type: col.type,
422
564
  notNull: col.notNull,
423
565
  primaryKey: col.primaryKey,
424
- default: col.default
566
+ default: col.default,
567
+ unique: col.unique,
568
+ references: col.references
569
+ });
570
+ }
571
+ function uniqueSignature(uc) {
572
+ return JSON.stringify({ columns: uc.columns });
573
+ }
574
+ function foreignKeySignature(fk) {
575
+ return JSON.stringify({
576
+ columns: fk.columns,
577
+ refTable: fk.refTable,
578
+ refColumns: fk.refColumns,
579
+ onDelete: fk.onDelete ?? null,
580
+ onUpdate: fk.onUpdate ?? null
425
581
  });
426
582
  }
583
+ function diffConstraints(current, target) {
584
+ const ops = [];
585
+ const table = target.name;
586
+ const currentUq = new Map(current.uniqueConstraints.map((u) => [u.name, u]));
587
+ const targetUq = new Map(target.uniqueConstraints.map((u) => [u.name, u]));
588
+ for (const [name, cur] of currentUq) {
589
+ const tgt = targetUq.get(name);
590
+ if (!tgt || uniqueSignature(cur) !== uniqueSignature(tgt)) {
591
+ ops.push(dropUnique(table, cur));
592
+ }
593
+ }
594
+ for (const [name, tgt] of targetUq) {
595
+ const cur = currentUq.get(name);
596
+ if (!cur || uniqueSignature(cur) !== uniqueSignature(tgt)) {
597
+ ops.push(addUnique(table, tgt));
598
+ }
599
+ }
600
+ const currentFk = new Map(current.foreignKeys.map((f) => [f.name, f]));
601
+ const targetFk = new Map(target.foreignKeys.map((f) => [f.name, f]));
602
+ for (const [name, cur] of currentFk) {
603
+ const tgt = targetFk.get(name);
604
+ if (!tgt || foreignKeySignature(cur) !== foreignKeySignature(tgt)) {
605
+ ops.push(dropForeignKey(table, cur));
606
+ }
607
+ }
608
+ for (const [name, tgt] of targetFk) {
609
+ const cur = currentFk.get(name);
610
+ if (!cur || foreignKeySignature(cur) !== foreignKeySignature(tgt)) {
611
+ ops.push(addForeignKey(table, tgt));
612
+ }
613
+ }
614
+ return ops;
615
+ }
616
+ function uniqueNamed(uc) {
617
+ return { type: "unique", constraint: uc };
618
+ }
619
+ function foreignKeyNamed(fk) {
620
+ return { type: "foreignKey", constraint: fk };
621
+ }
622
+ function addUnique(table, uc) {
623
+ return { kind: "add_constraint", table, constraint: uniqueNamed(uc) };
624
+ }
625
+ function dropUnique(table, uc) {
626
+ return { kind: "drop_constraint", table, constraint: uniqueNamed(uc) };
627
+ }
628
+ function addForeignKey(table, fk) {
629
+ return { kind: "add_constraint", table, constraint: foreignKeyNamed(fk) };
630
+ }
631
+ function dropForeignKey(table, fk) {
632
+ return { kind: "drop_constraint", table, constraint: foreignKeyNamed(fk) };
633
+ }
427
634
  function diffSchema(current, target) {
428
635
  const ops = [];
429
636
  const drops = [];
@@ -452,6 +659,7 @@ function diffSchema(current, target) {
452
659
  ops.push({ kind: "drop_column", table: name, column: currentCol });
453
660
  }
454
661
  }
662
+ ops.push(...diffConstraints(currentTable, targetTable));
455
663
  }
456
664
  for (const [name, currentTable] of Object.entries(current.tables)) {
457
665
  if (!target.tables[name]) {
@@ -594,6 +802,14 @@ var Op = class {
594
802
  recreateTable(from, to) {
595
803
  this.run({ kind: "recreate_table", from, to });
596
804
  }
805
+ /** Add a table-level unique / foreign-key constraint. */
806
+ addConstraint(table, constraint) {
807
+ this.run({ kind: "add_constraint", table, constraint });
808
+ }
809
+ /** Drop a table-level unique / foreign-key constraint. */
810
+ dropConstraint(table, constraint) {
811
+ this.run({ kind: "drop_constraint", table, constraint });
812
+ }
597
813
  /** Raw SQL escape hatch (e.g. a data migration). `down` may be `null`. */
598
814
  execute(up, down = null) {
599
815
  this.run({ kind: "execute", up, down });
@@ -823,6 +1039,39 @@ function affinityToKind(affinity) {
823
1039
  return "text";
824
1040
  }
825
1041
  }
1042
+ function sqliteForeignKeys(driver, table) {
1043
+ const rows = driver.execute(`PRAGMA foreign_key_list(${JSON.stringify(table)})`, []).rows;
1044
+ const byId = /* @__PURE__ */ new Map();
1045
+ for (const r of rows) {
1046
+ const list = byId.get(r.id) ?? [];
1047
+ list.push(r);
1048
+ byId.set(r.id, list);
1049
+ }
1050
+ const fks = [];
1051
+ for (const group of byId.values()) {
1052
+ const ordered = [...group].sort((a, b) => a.seq - b.seq);
1053
+ const columns = ordered.map((r) => r.from);
1054
+ fks.push({
1055
+ name: `fk_${table}_${columns.join("_")}`,
1056
+ columns,
1057
+ refTable: ordered[0]?.table ?? "",
1058
+ refColumns: ordered.map((r) => r.to)
1059
+ });
1060
+ }
1061
+ return fks;
1062
+ }
1063
+ function sqliteUniques(driver, table) {
1064
+ const indexes = driver.execute(`PRAGMA index_list(${JSON.stringify(table)})`, []).rows;
1065
+ const uniques = [];
1066
+ for (const idx of indexes) {
1067
+ if (Number(idx.unique) !== 1 || idx.origin === "pk") continue;
1068
+ const cols = driver.execute(`PRAGMA index_info(${JSON.stringify(idx.name)})`, []).rows.sort((a, b) => a.seqno - b.seqno).map((c) => c.name);
1069
+ if (cols.length > 0) {
1070
+ uniques.push({ name: `uq_${table}_${cols.join("_")}`, columns: cols });
1071
+ }
1072
+ }
1073
+ return uniques;
1074
+ }
826
1075
  function introspectSqlite(driver) {
827
1076
  const tablesRows = driver.execute(
828
1077
  "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name != 'tempest_db_js_migrations'",
@@ -842,14 +1091,39 @@ function introspectSqlite(driver) {
842
1091
  type: { kind: affinityToKind(affinity), meta: {} },
843
1092
  notNull: Number(col.notnull) === 1 || isPk,
844
1093
  primaryKey: isPk,
845
- default: null
1094
+ default: null,
1095
+ unique: false,
1096
+ references: null
846
1097
  };
847
1098
  if (isPk) primaryKey.push(col.name);
848
1099
  }
849
- tables[tableName] = { name: tableName, columns, primaryKey };
1100
+ tables[tableName] = {
1101
+ name: tableName,
1102
+ columns,
1103
+ primaryKey,
1104
+ uniqueConstraints: sqliteUniques(driver, tableName),
1105
+ foreignKeys: sqliteForeignKeys(driver, tableName)
1106
+ };
850
1107
  }
851
1108
  return { tables };
852
1109
  }
1110
+ function constraintKeys(table) {
1111
+ const fks = /* @__PURE__ */ new Set();
1112
+ const uniques = /* @__PURE__ */ new Set();
1113
+ for (const col of Object.values(table.columns)) {
1114
+ if (col.unique) uniques.add(col.name);
1115
+ if (col.references) {
1116
+ fks.add(`${col.name}=>${col.references.table}(${col.references.column})`);
1117
+ }
1118
+ }
1119
+ for (const uc of table.uniqueConstraints) {
1120
+ uniques.add([...uc.columns].sort().join(","));
1121
+ }
1122
+ for (const fk of table.foreignKeys) {
1123
+ fks.add(`${fk.columns.join(",")}=>${fk.refTable}(${fk.refColumns.join(",")})`);
1124
+ }
1125
+ return { fks, uniques };
1126
+ }
853
1127
  function checkDrift(driver, models) {
854
1128
  const actual = introspectSqlite(driver);
855
1129
  const expected = reflectSchema(models);
@@ -887,6 +1161,34 @@ function checkDrift(driver, models) {
887
1161
  );
888
1162
  }
889
1163
  }
1164
+ const expectedKeys = constraintKeys(expectedTable);
1165
+ const actualKeys = constraintKeys(actualTable);
1166
+ for (const fk of expectedKeys.fks) {
1167
+ if (!actualKeys.fks.has(fk)) {
1168
+ issues.push(`foreign key "${tableName}: ${fk}" is missing from the database`);
1169
+ }
1170
+ }
1171
+ for (const fk of actualKeys.fks) {
1172
+ if (!expectedKeys.fks.has(fk)) {
1173
+ issues.push(
1174
+ `foreign key "${tableName}: ${fk}" exists in the database but not in the model`
1175
+ );
1176
+ }
1177
+ }
1178
+ for (const uq of expectedKeys.uniques) {
1179
+ if (!actualKeys.uniques.has(uq)) {
1180
+ issues.push(
1181
+ `unique constraint "${tableName}: (${uq})" is missing from the database`
1182
+ );
1183
+ }
1184
+ }
1185
+ for (const uq of actualKeys.uniques) {
1186
+ if (!expectedKeys.uniques.has(uq)) {
1187
+ issues.push(
1188
+ `unique constraint "${tableName}: (${uq})" exists in the database but not in the model`
1189
+ );
1190
+ }
1191
+ }
890
1192
  }
891
1193
  for (const tableName of Object.keys(actual.tables)) {
892
1194
  if (!expected.tables[tableName]) {
@@ -948,14 +1250,59 @@ async function introspectPostgres(driver) {
948
1250
  },
949
1251
  notNull: col.is_nullable === "NO" || isPk,
950
1252
  primaryKey: isPk,
951
- default: null
1253
+ default: null,
1254
+ unique: false,
1255
+ references: null
952
1256
  };
953
1257
  if (isPk) primaryKey.push(name);
954
1258
  }
955
- tables[tableName] = { name: tableName, columns, primaryKey };
1259
+ tables[tableName] = {
1260
+ name: tableName,
1261
+ columns,
1262
+ primaryKey,
1263
+ uniqueConstraints: await postgresUniques(driver, tableName),
1264
+ foreignKeys: await postgresForeignKeys(driver, tableName)
1265
+ };
956
1266
  }
957
1267
  return { tables };
958
1268
  }
1269
+ async function postgresForeignKeys(driver, table) {
1270
+ const result = await driver.execute(
1271
+ `SELECT c.conname AS name,
1272
+ (SELECT array_agg(a.attname ORDER BY k.ord)
1273
+ FROM unnest(c.conkey) WITH ORDINALITY AS k(attnum, ord)
1274
+ JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum) AS cols,
1275
+ cf.relname AS ref_table,
1276
+ (SELECT array_agg(a.attname ORDER BY k.ord)
1277
+ FROM unnest(c.confkey) WITH ORDINALITY AS k(attnum, ord)
1278
+ JOIN pg_attribute a ON a.attrelid = c.confrelid AND a.attnum = k.attnum) AS ref_cols
1279
+ FROM pg_constraint c
1280
+ JOIN pg_class cf ON cf.oid = c.confrelid
1281
+ WHERE c.contype = 'f' AND c.conrelid = $1::regclass`,
1282
+ [table]
1283
+ );
1284
+ return result.rows.map((r) => ({
1285
+ name: String(r.name),
1286
+ columns: r.cols ?? [],
1287
+ refTable: String(r.ref_table),
1288
+ refColumns: r.ref_cols ?? []
1289
+ }));
1290
+ }
1291
+ async function postgresUniques(driver, table) {
1292
+ const result = await driver.execute(
1293
+ `SELECT c.conname AS name,
1294
+ (SELECT array_agg(a.attname ORDER BY k.ord)
1295
+ FROM unnest(c.conkey) WITH ORDINALITY AS k(attnum, ord)
1296
+ JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum) AS cols
1297
+ FROM pg_constraint c
1298
+ WHERE c.contype = 'u' AND c.conrelid = $1::regclass`,
1299
+ [table]
1300
+ );
1301
+ return result.rows.map((r) => ({
1302
+ name: String(r.name),
1303
+ columns: r.cols ?? []
1304
+ }));
1305
+ }
959
1306
  async function checkDriftPostgres(driver, models) {
960
1307
  const actual = await introspectPostgres(driver);
961
1308
  const expected = reflectSchema(models);
@@ -988,6 +1335,20 @@ async function checkDriftPostgres(driver, models) {
988
1335
  );
989
1336
  }
990
1337
  }
1338
+ const expectedKeys = constraintKeys(expectedTable);
1339
+ const actualKeys = constraintKeys(actualTable);
1340
+ for (const fk of expectedKeys.fks) {
1341
+ if (!actualKeys.fks.has(fk)) {
1342
+ issues.push(`foreign key "${tableName}: ${fk}" is missing from the database`);
1343
+ }
1344
+ }
1345
+ for (const uq of expectedKeys.uniques) {
1346
+ if (!actualKeys.uniques.has(uq)) {
1347
+ issues.push(
1348
+ `unique constraint "${tableName}: (${uq})" is missing from the database`
1349
+ );
1350
+ }
1351
+ }
991
1352
  }
992
1353
  for (const tableName of Object.keys(actual.tables)) {
993
1354
  if (!expected.tables[tableName]) {
@@ -1059,6 +1420,30 @@ function applyOperation(schema, op) {
1059
1420
  }
1060
1421
  break;
1061
1422
  }
1423
+ case "add_constraint": {
1424
+ const t = tables[op.table];
1425
+ if (t) {
1426
+ tables[op.table] = op.constraint.type === "unique" ? {
1427
+ ...t,
1428
+ uniqueConstraints: [...t.uniqueConstraints, op.constraint.constraint]
1429
+ } : { ...t, foreignKeys: [...t.foreignKeys, op.constraint.constraint] };
1430
+ }
1431
+ break;
1432
+ }
1433
+ case "drop_constraint": {
1434
+ const t = tables[op.table];
1435
+ if (t) {
1436
+ const dropName = op.constraint.constraint.name;
1437
+ tables[op.table] = op.constraint.type === "unique" ? {
1438
+ ...t,
1439
+ uniqueConstraints: t.uniqueConstraints.filter((u) => u.name !== dropName)
1440
+ } : {
1441
+ ...t,
1442
+ foreignKeys: t.foreignKeys.filter((f) => f.name !== dropName)
1443
+ };
1444
+ }
1445
+ break;
1446
+ }
1062
1447
  }
1063
1448
  return { tables };
1064
1449
  }
@@ -1080,7 +1465,9 @@ function columnShape(col) {
1080
1465
  type: col.type,
1081
1466
  notNull: col.notNull,
1082
1467
  primaryKey: col.primaryKey,
1083
- default: col.default
1468
+ default: col.default,
1469
+ unique: col.unique,
1470
+ references: col.references
1084
1471
  });
1085
1472
  }
1086
1473
  function tableShape(columns) {