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/README.md +9 -2
- package/dist/bin.cjs +475 -44
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +2 -2
- package/dist/{chunk-Q32CBI2A.js → chunk-5QQMVTS5.js} +988 -286
- package/dist/chunk-5QQMVTS5.js.map +1 -0
- package/dist/{chunk-OP7FRDI5.js → chunk-EPMLFNFK.js} +448 -37
- package/dist/chunk-EPMLFNFK.js.map +1 -0
- package/dist/index.cjs +993 -283
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +632 -30
- package/dist/index.d.ts +632 -30
- package/dist/index.js +1 -1
- package/dist/migrations/index.cjs +591 -52
- package/dist/migrations/index.cjs.map +1 -1
- package/dist/migrations/index.d.cts +63 -14
- package/dist/migrations/index.d.ts +63 -14
- package/dist/migrations/index.js +2 -2
- package/package.json +1 -1
- package/dist/chunk-OP7FRDI5.js.map +0 -1
- package/dist/chunk-Q32CBI2A.js.map +0 -1
|
@@ -1,58 +1,162 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// src/expressions.ts
|
|
4
|
+
function renderPortableToken(token, dialect) {
|
|
5
|
+
switch (token) {
|
|
6
|
+
case "now":
|
|
7
|
+
return dialect === "postgresql" ? "now()" : "CURRENT_TIMESTAMP";
|
|
8
|
+
case "current_date":
|
|
9
|
+
return "CURRENT_DATE";
|
|
10
|
+
case "current_time":
|
|
11
|
+
return "CURRENT_TIME";
|
|
12
|
+
case "uuidv4":
|
|
13
|
+
if (dialect === "postgresql") return "gen_random_uuid()";
|
|
14
|
+
if (dialect === "mysql") return "(UUID())";
|
|
15
|
+
return "(lower(hex(randomblob(16))))";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
3
19
|
// src/index.ts
|
|
4
20
|
function isDefaultValue(value) {
|
|
5
21
|
return typeof value === "object" && value !== null && "kind" in value && (value.kind === "literal" || value.kind === "expression");
|
|
6
22
|
}
|
|
23
|
+
function bindsParameters(value) {
|
|
24
|
+
return value.kind === "expression" && typeof value.expression === "object" && "parts" in value.expression;
|
|
25
|
+
}
|
|
26
|
+
function parseReference(ref, options) {
|
|
27
|
+
const dot = ref.lastIndexOf(".");
|
|
28
|
+
if (dot <= 0 || dot === ref.length - 1) {
|
|
29
|
+
throw new Error(`Invalid foreign key reference "${ref}"; expected "table.column".`);
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
table: ref.slice(0, dot),
|
|
33
|
+
column: ref.slice(dot + 1),
|
|
34
|
+
onDelete: options?.onDelete,
|
|
35
|
+
onUpdate: options?.onUpdate
|
|
36
|
+
};
|
|
37
|
+
}
|
|
7
38
|
var Column = class _Column {
|
|
8
|
-
constructor(type, flags, defaultValue = null, onUpdateValue = null) {
|
|
39
|
+
constructor(type, flags, defaultValue = null, onUpdateValue = null, reference = null, dbName = null) {
|
|
9
40
|
this.type = type;
|
|
10
41
|
this.flags = flags;
|
|
11
42
|
this.defaultValue = defaultValue;
|
|
12
43
|
this.onUpdateValue = onUpdateValue;
|
|
44
|
+
this.reference = reference;
|
|
45
|
+
this.dbName = dbName;
|
|
13
46
|
}
|
|
14
47
|
type;
|
|
15
48
|
flags;
|
|
16
49
|
defaultValue;
|
|
17
50
|
onUpdateValue;
|
|
18
|
-
|
|
51
|
+
reference;
|
|
52
|
+
dbName;
|
|
53
|
+
/** Clone this column with one facet replaced, carrying every other over. */
|
|
54
|
+
derive(patch) {
|
|
19
55
|
return new _Column(
|
|
20
56
|
this.type,
|
|
21
|
-
|
|
22
|
-
this.defaultValue,
|
|
23
|
-
this.onUpdateValue
|
|
57
|
+
patch.flags ?? this.flags,
|
|
58
|
+
patch.defaultValue !== void 0 ? patch.defaultValue : this.defaultValue,
|
|
59
|
+
patch.onUpdateValue !== void 0 ? patch.onUpdateValue : this.onUpdateValue,
|
|
60
|
+
patch.reference !== void 0 ? patch.reference : this.reference,
|
|
61
|
+
patch.dbName !== void 0 ? patch.dbName : this.dbName
|
|
24
62
|
);
|
|
25
63
|
}
|
|
64
|
+
primaryKey() {
|
|
65
|
+
return this.derive({ flags: { ...this.flags, primaryKey: true, hasDefault: true } });
|
|
66
|
+
}
|
|
26
67
|
notNull() {
|
|
27
|
-
return
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
68
|
+
return this.derive({ flags: { ...this.flags, notNull: true } });
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Add a `UNIQUE` constraint to the column (mirrors SQLAlchemy's
|
|
72
|
+
* `mapped_column(unique=True)`). DDL-only — does not change the inferred type.
|
|
73
|
+
*/
|
|
74
|
+
unique() {
|
|
75
|
+
return this.derive({ flags: { ...this.flags, unique: true } });
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Map this property to a differently-named database column, à la SQLAlchemy's
|
|
79
|
+
* `mapped_column("consumer_name")` (Django's `db_column`, Prisma's `@map`).
|
|
80
|
+
*
|
|
81
|
+
* The override applies everywhere the name reaches SQL — select, insert,
|
|
82
|
+
* update, delete, where, order by, group by, returning, conflict targets, the
|
|
83
|
+
* migration IR and the drift check — while the TypeScript row keeps the
|
|
84
|
+
* property name. Use it to keep a `snake_case` schema behind a `camelCase`
|
|
85
|
+
* model; {@link Model.naming} does the same for a whole table at once.
|
|
86
|
+
*
|
|
87
|
+
* @param dbName The real column name in the database.
|
|
88
|
+
* @returns A new column bound to that name.
|
|
89
|
+
* @throws Error When `dbName` is empty.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```ts
|
|
93
|
+
* class ApiKey extends Model {
|
|
94
|
+
* static tablename = "api_keys";
|
|
95
|
+
* consumerName = column.text().name("consumer_name").notNull();
|
|
96
|
+
* }
|
|
97
|
+
* ```
|
|
98
|
+
*/
|
|
99
|
+
name(dbName) {
|
|
100
|
+
if (dbName.length === 0) {
|
|
101
|
+
throw new Error("column.name() requires a non-empty database column name.");
|
|
102
|
+
}
|
|
103
|
+
return this.derive({ dbName });
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Declare a foreign-key reference to another table's column, à la SQLAlchemy's
|
|
107
|
+
* `mapped_column(ForeignKey("table.column", ondelete=...))`. DDL-only — does
|
|
108
|
+
* not change the inferred type.
|
|
109
|
+
*
|
|
110
|
+
* @param ref The target as `"table.column"` (e.g. `"users.id"`).
|
|
111
|
+
* @param options Optional `onDelete` / `onUpdate` referential actions.
|
|
112
|
+
* @returns A new column carrying the reference.
|
|
113
|
+
* @throws Error When `ref` is not a valid `"table.column"` string.
|
|
114
|
+
*/
|
|
115
|
+
references(ref, options) {
|
|
116
|
+
return this.derive({ reference: parseReference(ref, options) });
|
|
33
117
|
}
|
|
34
118
|
/**
|
|
35
119
|
* Set the insert-time default: a constant value of type `T`, or a portable
|
|
36
120
|
* server-side expression from {@link sql} (e.g. `sql.now()`, `sql.uuidv4()`).
|
|
121
|
+
*
|
|
122
|
+
* @param value The literal default, or a {@link sql} expression.
|
|
123
|
+
* @returns A new column carrying the default.
|
|
124
|
+
* @throws Error When given a `sql.expr` fragment — a `DEFAULT` clause has
|
|
125
|
+
* nowhere to bind parameters; use `sql.raw()` for a verbatim expression.
|
|
37
126
|
*/
|
|
38
127
|
default(value) {
|
|
39
128
|
const resolved = isDefaultValue(value) ? value : { kind: "literal", value };
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
129
|
+
if (bindsParameters(resolved)) {
|
|
130
|
+
throw new Error(
|
|
131
|
+
"sql.expr`...` binds parameters and cannot be a column default \u2014 use sql.raw() for a verbatim DEFAULT expression."
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
return this.derive({
|
|
135
|
+
flags: { ...this.flags, hasDefault: true },
|
|
136
|
+
defaultValue: resolved
|
|
137
|
+
});
|
|
46
138
|
}
|
|
47
139
|
/**
|
|
48
140
|
* Re-apply a value whenever the row is updated (e.g. an `updated_at` column
|
|
49
141
|
* with `sql.now()`). Mirrors SQLAlchemy's `onupdate`.
|
|
142
|
+
*
|
|
143
|
+
* @param value The literal value, or a {@link sql} expression.
|
|
144
|
+
* @returns A new column carrying the on-update value.
|
|
145
|
+
* @throws Error When given a `sql.expr` fragment (see {@link Column.default}).
|
|
50
146
|
*/
|
|
51
147
|
onUpdate(value) {
|
|
52
148
|
const resolved = isDefaultValue(value) ? value : { kind: "literal", value };
|
|
53
|
-
|
|
149
|
+
if (bindsParameters(resolved)) {
|
|
150
|
+
throw new Error(
|
|
151
|
+
"sql.expr`...` binds parameters and cannot be an onUpdate default \u2014 use sql.raw() for a verbatim expression."
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return this.derive({ onUpdateValue: resolved });
|
|
54
155
|
}
|
|
55
156
|
};
|
|
157
|
+
function toSnakeCase(name) {
|
|
158
|
+
return name.replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
159
|
+
}
|
|
56
160
|
var columnsCache = /* @__PURE__ */ new WeakMap();
|
|
57
161
|
function columnsOf(model) {
|
|
58
162
|
const cached = columnsCache.get(model);
|
|
@@ -67,23 +171,75 @@ function columnsOf(model) {
|
|
|
67
171
|
columnsCache.set(model, out);
|
|
68
172
|
return out;
|
|
69
173
|
}
|
|
174
|
+
var nameMapCache = /* @__PURE__ */ new WeakMap();
|
|
175
|
+
function columnNamesOf(model) {
|
|
176
|
+
const cached = nameMapCache.get(model);
|
|
177
|
+
if (cached !== void 0) return cached;
|
|
178
|
+
const strategy = model.naming ?? "preserve";
|
|
179
|
+
const map = {};
|
|
180
|
+
const seen = /* @__PURE__ */ new Map();
|
|
181
|
+
let renamed = false;
|
|
182
|
+
for (const [prop, col] of Object.entries(columnsOf(model))) {
|
|
183
|
+
const dbName = col.dbName ?? (strategy === "snake_case" ? toSnakeCase(prop) : prop);
|
|
184
|
+
const collision = seen.get(dbName);
|
|
185
|
+
if (collision !== void 0) {
|
|
186
|
+
throw new Error(
|
|
187
|
+
`${model.tablename}: properties "${collision}" and "${prop}" both map to column "${dbName}".`
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
seen.set(dbName, prop);
|
|
191
|
+
map[prop] = dbName;
|
|
192
|
+
if (dbName !== prop) renamed = true;
|
|
193
|
+
}
|
|
194
|
+
const result = renamed ? map : null;
|
|
195
|
+
nameMapCache.set(model, result);
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
70
198
|
|
|
71
199
|
// src/migrations/ir.ts
|
|
200
|
+
function constraintName(prefix, table, columns) {
|
|
201
|
+
return `${prefix}_${table}_${columns.join("_")}`;
|
|
202
|
+
}
|
|
72
203
|
function reflectTable(model) {
|
|
204
|
+
const names = columnNamesOf(model);
|
|
205
|
+
const toColumn = (prop) => names?.[prop] ?? prop;
|
|
73
206
|
const columns = {};
|
|
74
207
|
const primaryKey = [];
|
|
75
|
-
for (const [
|
|
208
|
+
for (const [prop, col] of Object.entries(columnsOf(model))) {
|
|
76
209
|
const isPk = col.flags.primaryKey;
|
|
210
|
+
const name = toColumn(prop);
|
|
77
211
|
columns[name] = {
|
|
78
212
|
name,
|
|
79
213
|
type: col.type,
|
|
80
214
|
notNull: col.flags.notNull || isPk,
|
|
81
215
|
primaryKey: isPk,
|
|
82
|
-
default: col.defaultValue
|
|
216
|
+
default: col.defaultValue,
|
|
217
|
+
unique: col.flags.unique,
|
|
218
|
+
references: col.reference
|
|
83
219
|
};
|
|
84
220
|
if (isPk) primaryKey.push(name);
|
|
85
221
|
}
|
|
86
|
-
|
|
222
|
+
const uniqueConstraints = [];
|
|
223
|
+
const foreignKeys = [];
|
|
224
|
+
for (const c of model.tableArgs?.() ?? []) {
|
|
225
|
+
const cols = c.columns.map(toColumn);
|
|
226
|
+
if (c.kind === "unique") {
|
|
227
|
+
uniqueConstraints.push({
|
|
228
|
+
name: c.name ?? constraintName("uq", model.tablename, cols),
|
|
229
|
+
columns: cols
|
|
230
|
+
});
|
|
231
|
+
} else {
|
|
232
|
+
foreignKeys.push({
|
|
233
|
+
name: c.name ?? constraintName("fk", model.tablename, cols),
|
|
234
|
+
columns: cols,
|
|
235
|
+
refTable: c.refTable,
|
|
236
|
+
refColumns: c.refColumns,
|
|
237
|
+
onDelete: c.onDelete,
|
|
238
|
+
onUpdate: c.onUpdate
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return { name: model.tablename, columns, primaryKey, uniqueConstraints, foreignKeys };
|
|
87
243
|
}
|
|
88
244
|
function reflectSchema(models) {
|
|
89
245
|
const tables = {};
|
|
@@ -128,6 +284,10 @@ function invert(op) {
|
|
|
128
284
|
return { kind: "rename_column", table: op.table, from: op.to, to: op.from };
|
|
129
285
|
case "recreate_table":
|
|
130
286
|
return { kind: "recreate_table", from: op.to, to: op.from };
|
|
287
|
+
case "add_constraint":
|
|
288
|
+
return { kind: "drop_constraint", table: op.table, constraint: op.constraint };
|
|
289
|
+
case "drop_constraint":
|
|
290
|
+
return { kind: "add_constraint", table: op.table, constraint: op.constraint };
|
|
131
291
|
case "execute":
|
|
132
292
|
if (op.down === null) {
|
|
133
293
|
throw new IrreversibleMigration("execute() operation has no down SQL");
|
|
@@ -162,6 +322,8 @@ function renderColumnType(type, dialect) {
|
|
|
162
322
|
return "NUMERIC";
|
|
163
323
|
case "blob":
|
|
164
324
|
return "BLOB";
|
|
325
|
+
case "array":
|
|
326
|
+
throw new Error(unsupportedArray("sqlite"));
|
|
165
327
|
default:
|
|
166
328
|
return "TEXT";
|
|
167
329
|
}
|
|
@@ -203,6 +365,8 @@ function renderColumnType(type, dialect) {
|
|
|
203
365
|
return "CHAR(36)";
|
|
204
366
|
case "enum":
|
|
205
367
|
return `ENUM(${(meta.values ?? []).map(quoteLiteral).join(", ")})`;
|
|
368
|
+
case "array":
|
|
369
|
+
throw new Error(unsupportedArray("mysql"));
|
|
206
370
|
}
|
|
207
371
|
}
|
|
208
372
|
switch (kind) {
|
|
@@ -241,27 +405,39 @@ function renderColumnType(type, dialect) {
|
|
|
241
405
|
return "UUID";
|
|
242
406
|
case "enum":
|
|
243
407
|
return "TEXT";
|
|
408
|
+
case "array":
|
|
409
|
+
return `${renderColumnType(arrayElement(meta.element), dialect)}[]`;
|
|
244
410
|
}
|
|
245
411
|
}
|
|
246
|
-
function
|
|
412
|
+
function unsupportedArray(dialect) {
|
|
413
|
+
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.`;
|
|
414
|
+
}
|
|
415
|
+
function arrayElement(element) {
|
|
416
|
+
if (!element) {
|
|
417
|
+
throw new Error(
|
|
418
|
+
"An array column has no element type \u2014 build it with column.array()."
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
return element;
|
|
422
|
+
}
|
|
423
|
+
function renderDefault(def, dialect, type) {
|
|
247
424
|
if (def.kind === "expression") {
|
|
248
425
|
const expr = def.expression;
|
|
249
|
-
if (typeof expr === "object")
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
return "CURRENT_DATE";
|
|
255
|
-
case "current_time":
|
|
256
|
-
return "CURRENT_TIME";
|
|
257
|
-
case "uuidv4":
|
|
258
|
-
if (dialect === "postgresql") return "gen_random_uuid()";
|
|
259
|
-
if (dialect === "mysql") return "(UUID())";
|
|
260
|
-
return "(lower(hex(randomblob(16))))";
|
|
426
|
+
if (typeof expr === "object") {
|
|
427
|
+
if ("raw" in expr) return expr.raw;
|
|
428
|
+
throw new Error(
|
|
429
|
+
"sql.expr`...` binds parameters and cannot be rendered as a DEFAULT \u2014 use sql.raw()."
|
|
430
|
+
);
|
|
261
431
|
}
|
|
432
|
+
return renderPortableToken(expr, dialect);
|
|
262
433
|
}
|
|
263
434
|
const value = def.value;
|
|
264
435
|
if (value === null) return "NULL";
|
|
436
|
+
if (Array.isArray(value) && type?.kind === "array") {
|
|
437
|
+
const elementType = renderColumnType(arrayElement(type.meta.element), dialect);
|
|
438
|
+
const items = value.map((v) => renderDefault({ kind: "literal", value: v }, dialect));
|
|
439
|
+
return `ARRAY[${items.join(", ")}]::${elementType}[]`;
|
|
440
|
+
}
|
|
265
441
|
if (typeof value === "boolean") {
|
|
266
442
|
return dialect === "postgresql" ? value ? "TRUE" : "FALSE" : value ? "1" : "0";
|
|
267
443
|
}
|
|
@@ -270,10 +446,47 @@ function renderDefault(def, dialect) {
|
|
|
270
446
|
if (typeof value === "object") return quoteLiteral(JSON.stringify(value));
|
|
271
447
|
return quoteLiteral(String(value));
|
|
272
448
|
}
|
|
449
|
+
function renderFkAction(action) {
|
|
450
|
+
return action.toUpperCase();
|
|
451
|
+
}
|
|
452
|
+
function renderFkActions(fk) {
|
|
453
|
+
let sql = "";
|
|
454
|
+
if (fk.onDelete) sql += ` ON DELETE ${renderFkAction(fk.onDelete)}`;
|
|
455
|
+
if (fk.onUpdate) sql += ` ON UPDATE ${renderFkAction(fk.onUpdate)}`;
|
|
456
|
+
return sql;
|
|
457
|
+
}
|
|
458
|
+
function columnConstraintSuffix(col, dialect) {
|
|
459
|
+
let sql = "";
|
|
460
|
+
if (col.unique) sql += " UNIQUE";
|
|
461
|
+
if (col.references) {
|
|
462
|
+
const ref = col.references;
|
|
463
|
+
sql += ` REFERENCES ${quoteId(ref.table, dialect)} (${quoteId(ref.column, dialect)})`;
|
|
464
|
+
sql += renderFkActions(ref);
|
|
465
|
+
}
|
|
466
|
+
return sql;
|
|
467
|
+
}
|
|
468
|
+
function renderUniqueConstraint(uc, dialect) {
|
|
469
|
+
const cols = uc.columns.map((c) => quoteId(c, dialect)).join(", ");
|
|
470
|
+
return `CONSTRAINT ${quoteId(uc.name, dialect)} UNIQUE (${cols})`;
|
|
471
|
+
}
|
|
472
|
+
function renderForeignKeyConstraint(fk, dialect) {
|
|
473
|
+
const cols = fk.columns.map((c) => quoteId(c, dialect)).join(", ");
|
|
474
|
+
const refCols = fk.refColumns.map((c) => quoteId(c, dialect)).join(", ");
|
|
475
|
+
return `CONSTRAINT ${quoteId(fk.name, dialect)} FOREIGN KEY (${cols}) REFERENCES ${quoteId(fk.refTable, dialect)} (${refCols})${renderFkActions(fk)}`;
|
|
476
|
+
}
|
|
477
|
+
function tableConstraintClauses(table, dialect) {
|
|
478
|
+
return [
|
|
479
|
+
...table.uniqueConstraints.map((uc) => renderUniqueConstraint(uc, dialect)),
|
|
480
|
+
...table.foreignKeys.map((fk) => renderForeignKeyConstraint(fk, dialect))
|
|
481
|
+
];
|
|
482
|
+
}
|
|
273
483
|
function renderColumnDef(col, dialect) {
|
|
274
484
|
let sql = `${quoteId(col.name, dialect)} ${renderColumnType(col.type, dialect)}`;
|
|
275
485
|
if (col.notNull) sql += " NOT NULL";
|
|
276
|
-
if (col.default !== null)
|
|
486
|
+
if (col.default !== null) {
|
|
487
|
+
sql += ` DEFAULT ${renderDefault(col.default, dialect, col.type)}`;
|
|
488
|
+
}
|
|
489
|
+
sql += columnConstraintSuffix(col, dialect);
|
|
277
490
|
return sql;
|
|
278
491
|
}
|
|
279
492
|
function enumTypeName(table, column) {
|
|
@@ -296,14 +509,16 @@ function renderCreateTable(table, dialect) {
|
|
|
296
509
|
typeStmts.push(`CREATE TYPE ${quoteId(typeName, dialect)} AS ENUM (${values})`);
|
|
297
510
|
let def = `${quoteId(c.name, dialect)} ${quoteId(typeName, dialect)}`;
|
|
298
511
|
if (c.notNull) def += " NOT NULL";
|
|
299
|
-
if (c.default !== null)
|
|
300
|
-
|
|
512
|
+
if (c.default !== null) {
|
|
513
|
+
def += ` DEFAULT ${renderDefault(c.default, dialect, c.type)}`;
|
|
514
|
+
}
|
|
515
|
+
return def + columnConstraintSuffix(c, dialect);
|
|
301
516
|
}
|
|
302
517
|
if (dialect === "postgresql" && isAutoIncrementPk(table, c)) {
|
|
303
|
-
return `${quoteId(c.name, dialect)} ${postgresSerialType(c.type.kind)}`;
|
|
518
|
+
return `${quoteId(c.name, dialect)} ${postgresSerialType(c.type.kind)}${columnConstraintSuffix(c, dialect)}`;
|
|
304
519
|
}
|
|
305
520
|
if (dialect === "mysql" && isAutoIncrementPk(table, c)) {
|
|
306
|
-
return `${quoteId(c.name, dialect)} ${renderColumnType(c.type, dialect)} NOT NULL AUTO_INCREMENT`;
|
|
521
|
+
return `${quoteId(c.name, dialect)} ${renderColumnType(c.type, dialect)} NOT NULL AUTO_INCREMENT${columnConstraintSuffix(c, dialect)}`;
|
|
307
522
|
}
|
|
308
523
|
return renderColumnDef(c, dialect);
|
|
309
524
|
});
|
|
@@ -312,6 +527,7 @@ function renderCreateTable(table, dialect) {
|
|
|
312
527
|
`PRIMARY KEY (${table.primaryKey.map((c) => quoteId(c, dialect)).join(", ")})`
|
|
313
528
|
);
|
|
314
529
|
}
|
|
530
|
+
cols.push(...tableConstraintClauses(table, dialect));
|
|
315
531
|
return [
|
|
316
532
|
...typeStmts,
|
|
317
533
|
`CREATE TABLE ${quoteId(table.name, dialect)} (
|
|
@@ -345,10 +561,38 @@ function renderOperation(op, dialect) {
|
|
|
345
561
|
return renderAlterColumn(op.table, op.to, dialect);
|
|
346
562
|
case "recreate_table":
|
|
347
563
|
return dialect === "sqlite" ? renderSqliteRebuild(op.from, op.to) : renderTableDiff(op.from, op.to, dialect);
|
|
564
|
+
case "add_constraint":
|
|
565
|
+
return renderAddConstraint(op.table, op.constraint, dialect);
|
|
566
|
+
case "drop_constraint":
|
|
567
|
+
return renderDropConstraint(op.table, op.constraint, dialect);
|
|
348
568
|
case "execute":
|
|
349
569
|
return [op.up];
|
|
350
570
|
}
|
|
351
571
|
}
|
|
572
|
+
function renderAddConstraint(table, constraint, dialect) {
|
|
573
|
+
if (dialect === "sqlite") {
|
|
574
|
+
throw new Error(
|
|
575
|
+
`add_constraint on SQLite needs a table-rebuild (recreate_table); constraint ${constraint.constraint.name} on ${table}`
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
const clause = constraint.type === "unique" ? renderUniqueConstraint(constraint.constraint, dialect) : renderForeignKeyConstraint(constraint.constraint, dialect);
|
|
579
|
+
return [`ALTER TABLE ${quoteId(table, dialect)} ADD ${clause}`];
|
|
580
|
+
}
|
|
581
|
+
function renderDropConstraint(table, constraint, dialect) {
|
|
582
|
+
if (dialect === "sqlite") {
|
|
583
|
+
throw new Error(
|
|
584
|
+
`drop_constraint on SQLite needs a table-rebuild (recreate_table); constraint ${constraint.constraint.name} on ${table}`
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
const t = quoteId(table, dialect);
|
|
588
|
+
const name = quoteId(constraint.constraint.name, dialect);
|
|
589
|
+
if (dialect === "mysql") {
|
|
590
|
+
return [
|
|
591
|
+
constraint.type === "unique" ? `ALTER TABLE ${t} DROP INDEX ${name}` : `ALTER TABLE ${t} DROP FOREIGN KEY ${name}`
|
|
592
|
+
];
|
|
593
|
+
}
|
|
594
|
+
return [`ALTER TABLE ${t} DROP CONSTRAINT ${name}`];
|
|
595
|
+
}
|
|
352
596
|
function renderSqliteRebuild(from, to) {
|
|
353
597
|
const tmp = `__new_${to.name}`;
|
|
354
598
|
const common = Object.keys(to.columns).filter((c) => c in from.columns);
|
|
@@ -358,6 +602,7 @@ function renderSqliteRebuild(from, to) {
|
|
|
358
602
|
`PRIMARY KEY (${to.primaryKey.map((c) => quoteId(c, "sqlite")).join(", ")})`
|
|
359
603
|
);
|
|
360
604
|
}
|
|
605
|
+
cols.push(...tableConstraintClauses(to, "sqlite"));
|
|
361
606
|
const commonSql = common.map((c) => quoteId(c, "sqlite")).join(", ");
|
|
362
607
|
return [
|
|
363
608
|
"PRAGMA foreign_keys=off",
|
|
@@ -410,7 +655,7 @@ function renderAlterColumn(table, to, dialect) {
|
|
|
410
655
|
to.notNull ? `ALTER TABLE ${t} ALTER COLUMN ${c} SET NOT NULL` : `ALTER TABLE ${t} ALTER COLUMN ${c} DROP NOT NULL`
|
|
411
656
|
);
|
|
412
657
|
stmts.push(
|
|
413
|
-
to.default !== null ? `ALTER TABLE ${t} ALTER COLUMN ${c} SET DEFAULT ${renderDefault(to.default, dialect)}` : `ALTER TABLE ${t} ALTER COLUMN ${c} DROP DEFAULT`
|
|
658
|
+
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`
|
|
414
659
|
);
|
|
415
660
|
return stmts;
|
|
416
661
|
}
|
|
@@ -421,9 +666,74 @@ function columnSignature(col) {
|
|
|
421
666
|
type: col.type,
|
|
422
667
|
notNull: col.notNull,
|
|
423
668
|
primaryKey: col.primaryKey,
|
|
424
|
-
default: col.default
|
|
669
|
+
default: col.default,
|
|
670
|
+
unique: col.unique,
|
|
671
|
+
references: col.references
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
function uniqueSignature(uc) {
|
|
675
|
+
return JSON.stringify({ columns: uc.columns });
|
|
676
|
+
}
|
|
677
|
+
function foreignKeySignature(fk) {
|
|
678
|
+
return JSON.stringify({
|
|
679
|
+
columns: fk.columns,
|
|
680
|
+
refTable: fk.refTable,
|
|
681
|
+
refColumns: fk.refColumns,
|
|
682
|
+
onDelete: fk.onDelete ?? null,
|
|
683
|
+
onUpdate: fk.onUpdate ?? null
|
|
425
684
|
});
|
|
426
685
|
}
|
|
686
|
+
function diffConstraints(current, target) {
|
|
687
|
+
const ops = [];
|
|
688
|
+
const table = target.name;
|
|
689
|
+
const currentUq = new Map(current.uniqueConstraints.map((u) => [u.name, u]));
|
|
690
|
+
const targetUq = new Map(target.uniqueConstraints.map((u) => [u.name, u]));
|
|
691
|
+
for (const [name, cur] of currentUq) {
|
|
692
|
+
const tgt = targetUq.get(name);
|
|
693
|
+
if (!tgt || uniqueSignature(cur) !== uniqueSignature(tgt)) {
|
|
694
|
+
ops.push(dropUnique(table, cur));
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
for (const [name, tgt] of targetUq) {
|
|
698
|
+
const cur = currentUq.get(name);
|
|
699
|
+
if (!cur || uniqueSignature(cur) !== uniqueSignature(tgt)) {
|
|
700
|
+
ops.push(addUnique(table, tgt));
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
const currentFk = new Map(current.foreignKeys.map((f) => [f.name, f]));
|
|
704
|
+
const targetFk = new Map(target.foreignKeys.map((f) => [f.name, f]));
|
|
705
|
+
for (const [name, cur] of currentFk) {
|
|
706
|
+
const tgt = targetFk.get(name);
|
|
707
|
+
if (!tgt || foreignKeySignature(cur) !== foreignKeySignature(tgt)) {
|
|
708
|
+
ops.push(dropForeignKey(table, cur));
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
for (const [name, tgt] of targetFk) {
|
|
712
|
+
const cur = currentFk.get(name);
|
|
713
|
+
if (!cur || foreignKeySignature(cur) !== foreignKeySignature(tgt)) {
|
|
714
|
+
ops.push(addForeignKey(table, tgt));
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return ops;
|
|
718
|
+
}
|
|
719
|
+
function uniqueNamed(uc) {
|
|
720
|
+
return { type: "unique", constraint: uc };
|
|
721
|
+
}
|
|
722
|
+
function foreignKeyNamed(fk) {
|
|
723
|
+
return { type: "foreignKey", constraint: fk };
|
|
724
|
+
}
|
|
725
|
+
function addUnique(table, uc) {
|
|
726
|
+
return { kind: "add_constraint", table, constraint: uniqueNamed(uc) };
|
|
727
|
+
}
|
|
728
|
+
function dropUnique(table, uc) {
|
|
729
|
+
return { kind: "drop_constraint", table, constraint: uniqueNamed(uc) };
|
|
730
|
+
}
|
|
731
|
+
function addForeignKey(table, fk) {
|
|
732
|
+
return { kind: "add_constraint", table, constraint: foreignKeyNamed(fk) };
|
|
733
|
+
}
|
|
734
|
+
function dropForeignKey(table, fk) {
|
|
735
|
+
return { kind: "drop_constraint", table, constraint: foreignKeyNamed(fk) };
|
|
736
|
+
}
|
|
427
737
|
function diffSchema(current, target) {
|
|
428
738
|
const ops = [];
|
|
429
739
|
const drops = [];
|
|
@@ -452,6 +762,7 @@ function diffSchema(current, target) {
|
|
|
452
762
|
ops.push({ kind: "drop_column", table: name, column: currentCol });
|
|
453
763
|
}
|
|
454
764
|
}
|
|
765
|
+
ops.push(...diffConstraints(currentTable, targetTable));
|
|
455
766
|
}
|
|
456
767
|
for (const [name, currentTable] of Object.entries(current.tables)) {
|
|
457
768
|
if (!target.tables[name]) {
|
|
@@ -594,6 +905,14 @@ var Op = class {
|
|
|
594
905
|
recreateTable(from, to) {
|
|
595
906
|
this.run({ kind: "recreate_table", from, to });
|
|
596
907
|
}
|
|
908
|
+
/** Add a table-level unique / foreign-key constraint. */
|
|
909
|
+
addConstraint(table, constraint) {
|
|
910
|
+
this.run({ kind: "add_constraint", table, constraint });
|
|
911
|
+
}
|
|
912
|
+
/** Drop a table-level unique / foreign-key constraint. */
|
|
913
|
+
dropConstraint(table, constraint) {
|
|
914
|
+
this.run({ kind: "drop_constraint", table, constraint });
|
|
915
|
+
}
|
|
597
916
|
/** Raw SQL escape hatch (e.g. a data migration). `down` may be `null`. */
|
|
598
917
|
execute(up, down = null) {
|
|
599
918
|
this.run({ kind: "execute", up, down });
|
|
@@ -823,6 +1142,39 @@ function affinityToKind(affinity) {
|
|
|
823
1142
|
return "text";
|
|
824
1143
|
}
|
|
825
1144
|
}
|
|
1145
|
+
function sqliteForeignKeys(driver, table) {
|
|
1146
|
+
const rows = driver.execute(`PRAGMA foreign_key_list(${JSON.stringify(table)})`, []).rows;
|
|
1147
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1148
|
+
for (const r of rows) {
|
|
1149
|
+
const list = byId.get(r.id) ?? [];
|
|
1150
|
+
list.push(r);
|
|
1151
|
+
byId.set(r.id, list);
|
|
1152
|
+
}
|
|
1153
|
+
const fks = [];
|
|
1154
|
+
for (const group of byId.values()) {
|
|
1155
|
+
const ordered = [...group].sort((a, b) => a.seq - b.seq);
|
|
1156
|
+
const columns = ordered.map((r) => r.from);
|
|
1157
|
+
fks.push({
|
|
1158
|
+
name: `fk_${table}_${columns.join("_")}`,
|
|
1159
|
+
columns,
|
|
1160
|
+
refTable: ordered[0]?.table ?? "",
|
|
1161
|
+
refColumns: ordered.map((r) => r.to)
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
return fks;
|
|
1165
|
+
}
|
|
1166
|
+
function sqliteUniques(driver, table) {
|
|
1167
|
+
const indexes = driver.execute(`PRAGMA index_list(${JSON.stringify(table)})`, []).rows;
|
|
1168
|
+
const uniques = [];
|
|
1169
|
+
for (const idx of indexes) {
|
|
1170
|
+
if (Number(idx.unique) !== 1 || idx.origin === "pk") continue;
|
|
1171
|
+
const cols = driver.execute(`PRAGMA index_info(${JSON.stringify(idx.name)})`, []).rows.sort((a, b) => a.seqno - b.seqno).map((c) => c.name);
|
|
1172
|
+
if (cols.length > 0) {
|
|
1173
|
+
uniques.push({ name: `uq_${table}_${cols.join("_")}`, columns: cols });
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
return uniques;
|
|
1177
|
+
}
|
|
826
1178
|
function introspectSqlite(driver) {
|
|
827
1179
|
const tablesRows = driver.execute(
|
|
828
1180
|
"SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' AND name != 'tempest_db_js_migrations'",
|
|
@@ -842,14 +1194,39 @@ function introspectSqlite(driver) {
|
|
|
842
1194
|
type: { kind: affinityToKind(affinity), meta: {} },
|
|
843
1195
|
notNull: Number(col.notnull) === 1 || isPk,
|
|
844
1196
|
primaryKey: isPk,
|
|
845
|
-
default: null
|
|
1197
|
+
default: null,
|
|
1198
|
+
unique: false,
|
|
1199
|
+
references: null
|
|
846
1200
|
};
|
|
847
1201
|
if (isPk) primaryKey.push(col.name);
|
|
848
1202
|
}
|
|
849
|
-
tables[tableName] = {
|
|
1203
|
+
tables[tableName] = {
|
|
1204
|
+
name: tableName,
|
|
1205
|
+
columns,
|
|
1206
|
+
primaryKey,
|
|
1207
|
+
uniqueConstraints: sqliteUniques(driver, tableName),
|
|
1208
|
+
foreignKeys: sqliteForeignKeys(driver, tableName)
|
|
1209
|
+
};
|
|
850
1210
|
}
|
|
851
1211
|
return { tables };
|
|
852
1212
|
}
|
|
1213
|
+
function constraintKeys(table) {
|
|
1214
|
+
const fks = /* @__PURE__ */ new Set();
|
|
1215
|
+
const uniques = /* @__PURE__ */ new Set();
|
|
1216
|
+
for (const col of Object.values(table.columns)) {
|
|
1217
|
+
if (col.unique) uniques.add(col.name);
|
|
1218
|
+
if (col.references) {
|
|
1219
|
+
fks.add(`${col.name}=>${col.references.table}(${col.references.column})`);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
for (const uc of table.uniqueConstraints) {
|
|
1223
|
+
uniques.add([...uc.columns].sort().join(","));
|
|
1224
|
+
}
|
|
1225
|
+
for (const fk of table.foreignKeys) {
|
|
1226
|
+
fks.add(`${fk.columns.join(",")}=>${fk.refTable}(${fk.refColumns.join(",")})`);
|
|
1227
|
+
}
|
|
1228
|
+
return { fks, uniques };
|
|
1229
|
+
}
|
|
853
1230
|
function checkDrift(driver, models) {
|
|
854
1231
|
const actual = introspectSqlite(driver);
|
|
855
1232
|
const expected = reflectSchema(models);
|
|
@@ -887,6 +1264,34 @@ function checkDrift(driver, models) {
|
|
|
887
1264
|
);
|
|
888
1265
|
}
|
|
889
1266
|
}
|
|
1267
|
+
const expectedKeys = constraintKeys(expectedTable);
|
|
1268
|
+
const actualKeys = constraintKeys(actualTable);
|
|
1269
|
+
for (const fk of expectedKeys.fks) {
|
|
1270
|
+
if (!actualKeys.fks.has(fk)) {
|
|
1271
|
+
issues.push(`foreign key "${tableName}: ${fk}" is missing from the database`);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
for (const fk of actualKeys.fks) {
|
|
1275
|
+
if (!expectedKeys.fks.has(fk)) {
|
|
1276
|
+
issues.push(
|
|
1277
|
+
`foreign key "${tableName}: ${fk}" exists in the database but not in the model`
|
|
1278
|
+
);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
for (const uq of expectedKeys.uniques) {
|
|
1282
|
+
if (!actualKeys.uniques.has(uq)) {
|
|
1283
|
+
issues.push(
|
|
1284
|
+
`unique constraint "${tableName}: (${uq})" is missing from the database`
|
|
1285
|
+
);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
for (const uq of actualKeys.uniques) {
|
|
1289
|
+
if (!expectedKeys.uniques.has(uq)) {
|
|
1290
|
+
issues.push(
|
|
1291
|
+
`unique constraint "${tableName}: (${uq})" exists in the database but not in the model`
|
|
1292
|
+
);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
890
1295
|
}
|
|
891
1296
|
for (const tableName of Object.keys(actual.tables)) {
|
|
892
1297
|
if (!expected.tables[tableName]) {
|
|
@@ -895,6 +1300,54 @@ function checkDrift(driver, models) {
|
|
|
895
1300
|
}
|
|
896
1301
|
return issues;
|
|
897
1302
|
}
|
|
1303
|
+
function pgTypeToColumnType(dataType, udtName) {
|
|
1304
|
+
if (dataType.toLowerCase() === "array") {
|
|
1305
|
+
return {
|
|
1306
|
+
kind: "array",
|
|
1307
|
+
meta: { element: { kind: pgUdtToKind(udtName.replace(/^_/, "")), meta: {} } }
|
|
1308
|
+
};
|
|
1309
|
+
}
|
|
1310
|
+
return { kind: pgTypeToKind(dataType, udtName), meta: {} };
|
|
1311
|
+
}
|
|
1312
|
+
function pgUdtToKind(udtName) {
|
|
1313
|
+
switch (udtName) {
|
|
1314
|
+
case "int2":
|
|
1315
|
+
return "smallint";
|
|
1316
|
+
case "int4":
|
|
1317
|
+
return "integer";
|
|
1318
|
+
case "int8":
|
|
1319
|
+
return "bigint";
|
|
1320
|
+
case "float4":
|
|
1321
|
+
return "real";
|
|
1322
|
+
case "float8":
|
|
1323
|
+
return "double";
|
|
1324
|
+
case "numeric":
|
|
1325
|
+
return "numeric";
|
|
1326
|
+
case "varchar":
|
|
1327
|
+
return "varchar";
|
|
1328
|
+
case "bpchar":
|
|
1329
|
+
return "char";
|
|
1330
|
+
case "bool":
|
|
1331
|
+
return "boolean";
|
|
1332
|
+
case "date":
|
|
1333
|
+
return "date";
|
|
1334
|
+
case "time":
|
|
1335
|
+
case "timetz":
|
|
1336
|
+
return "time";
|
|
1337
|
+
case "timestamp":
|
|
1338
|
+
case "timestamptz":
|
|
1339
|
+
return "timestamp";
|
|
1340
|
+
case "bytea":
|
|
1341
|
+
return "blob";
|
|
1342
|
+
case "json":
|
|
1343
|
+
case "jsonb":
|
|
1344
|
+
return "json";
|
|
1345
|
+
case "uuid":
|
|
1346
|
+
return "uuid";
|
|
1347
|
+
default:
|
|
1348
|
+
return "text";
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
898
1351
|
function pgTypeToKind(dataType, udtName) {
|
|
899
1352
|
const t = dataType.toLowerCase();
|
|
900
1353
|
if (t === "user-defined") return "enum";
|
|
@@ -942,20 +1395,66 @@ async function introspectPostgres(driver) {
|
|
|
942
1395
|
const isPk = pkSet.has(name);
|
|
943
1396
|
columns[name] = {
|
|
944
1397
|
name,
|
|
945
|
-
type:
|
|
946
|
-
kind: pgTypeToKind(String(col.data_type), String(col.udt_name)),
|
|
947
|
-
meta: {}
|
|
948
|
-
},
|
|
1398
|
+
type: pgTypeToColumnType(String(col.data_type), String(col.udt_name)),
|
|
949
1399
|
notNull: col.is_nullable === "NO" || isPk,
|
|
950
1400
|
primaryKey: isPk,
|
|
951
|
-
default: null
|
|
1401
|
+
default: null,
|
|
1402
|
+
unique: false,
|
|
1403
|
+
references: null
|
|
952
1404
|
};
|
|
953
1405
|
if (isPk) primaryKey.push(name);
|
|
954
1406
|
}
|
|
955
|
-
tables[tableName] = {
|
|
1407
|
+
tables[tableName] = {
|
|
1408
|
+
name: tableName,
|
|
1409
|
+
columns,
|
|
1410
|
+
primaryKey,
|
|
1411
|
+
uniqueConstraints: await postgresUniques(driver, tableName),
|
|
1412
|
+
foreignKeys: await postgresForeignKeys(driver, tableName)
|
|
1413
|
+
};
|
|
956
1414
|
}
|
|
957
1415
|
return { tables };
|
|
958
1416
|
}
|
|
1417
|
+
async function postgresForeignKeys(driver, table) {
|
|
1418
|
+
const result = await driver.execute(
|
|
1419
|
+
`SELECT c.conname AS name,
|
|
1420
|
+
(SELECT array_agg(a.attname ORDER BY k.ord)
|
|
1421
|
+
FROM unnest(c.conkey) WITH ORDINALITY AS k(attnum, ord)
|
|
1422
|
+
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum) AS cols,
|
|
1423
|
+
cf.relname AS ref_table,
|
|
1424
|
+
(SELECT array_agg(a.attname ORDER BY k.ord)
|
|
1425
|
+
FROM unnest(c.confkey) WITH ORDINALITY AS k(attnum, ord)
|
|
1426
|
+
JOIN pg_attribute a ON a.attrelid = c.confrelid AND a.attnum = k.attnum) AS ref_cols
|
|
1427
|
+
FROM pg_constraint c
|
|
1428
|
+
JOIN pg_class cf ON cf.oid = c.confrelid
|
|
1429
|
+
WHERE c.contype = 'f' AND c.conrelid = $1::regclass`,
|
|
1430
|
+
[table]
|
|
1431
|
+
);
|
|
1432
|
+
return result.rows.map((r) => ({
|
|
1433
|
+
name: String(r.name),
|
|
1434
|
+
columns: r.cols ?? [],
|
|
1435
|
+
refTable: String(r.ref_table),
|
|
1436
|
+
refColumns: r.ref_cols ?? []
|
|
1437
|
+
}));
|
|
1438
|
+
}
|
|
1439
|
+
async function postgresUniques(driver, table) {
|
|
1440
|
+
const result = await driver.execute(
|
|
1441
|
+
`SELECT c.conname AS name,
|
|
1442
|
+
(SELECT array_agg(a.attname ORDER BY k.ord)
|
|
1443
|
+
FROM unnest(c.conkey) WITH ORDINALITY AS k(attnum, ord)
|
|
1444
|
+
JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.attnum) AS cols
|
|
1445
|
+
FROM pg_constraint c
|
|
1446
|
+
WHERE c.contype = 'u' AND c.conrelid = $1::regclass`,
|
|
1447
|
+
[table]
|
|
1448
|
+
);
|
|
1449
|
+
return result.rows.map((r) => ({
|
|
1450
|
+
name: String(r.name),
|
|
1451
|
+
columns: r.cols ?? []
|
|
1452
|
+
}));
|
|
1453
|
+
}
|
|
1454
|
+
function describeKind(type) {
|
|
1455
|
+
if (type.kind !== "array") return type.kind;
|
|
1456
|
+
return `${type.meta.element ? describeKind(type.meta.element) : "unknown"}[]`;
|
|
1457
|
+
}
|
|
959
1458
|
async function checkDriftPostgres(driver, models) {
|
|
960
1459
|
const actual = await introspectPostgres(driver);
|
|
961
1460
|
const expected = reflectSchema(models);
|
|
@@ -972,9 +1471,9 @@ async function checkDriftPostgres(driver, models) {
|
|
|
972
1471
|
issues.push(`column "${tableName}.${colName}" is missing from the database`);
|
|
973
1472
|
continue;
|
|
974
1473
|
}
|
|
975
|
-
if (expectedCol.type
|
|
1474
|
+
if (describeKind(expectedCol.type) !== describeKind(actualCol.type)) {
|
|
976
1475
|
issues.push(
|
|
977
|
-
`column "${tableName}.${colName}" type differs: model ${expectedCol.type
|
|
1476
|
+
`column "${tableName}.${colName}" type differs: model ${describeKind(expectedCol.type)}, db ${describeKind(actualCol.type)}`
|
|
978
1477
|
);
|
|
979
1478
|
}
|
|
980
1479
|
if (expectedCol.notNull !== actualCol.notNull) {
|
|
@@ -988,6 +1487,20 @@ async function checkDriftPostgres(driver, models) {
|
|
|
988
1487
|
);
|
|
989
1488
|
}
|
|
990
1489
|
}
|
|
1490
|
+
const expectedKeys = constraintKeys(expectedTable);
|
|
1491
|
+
const actualKeys = constraintKeys(actualTable);
|
|
1492
|
+
for (const fk of expectedKeys.fks) {
|
|
1493
|
+
if (!actualKeys.fks.has(fk)) {
|
|
1494
|
+
issues.push(`foreign key "${tableName}: ${fk}" is missing from the database`);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
for (const uq of expectedKeys.uniques) {
|
|
1498
|
+
if (!actualKeys.uniques.has(uq)) {
|
|
1499
|
+
issues.push(
|
|
1500
|
+
`unique constraint "${tableName}: (${uq})" is missing from the database`
|
|
1501
|
+
);
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
991
1504
|
}
|
|
992
1505
|
for (const tableName of Object.keys(actual.tables)) {
|
|
993
1506
|
if (!expected.tables[tableName]) {
|
|
@@ -1059,6 +1572,30 @@ function applyOperation(schema, op) {
|
|
|
1059
1572
|
}
|
|
1060
1573
|
break;
|
|
1061
1574
|
}
|
|
1575
|
+
case "add_constraint": {
|
|
1576
|
+
const t = tables[op.table];
|
|
1577
|
+
if (t) {
|
|
1578
|
+
tables[op.table] = op.constraint.type === "unique" ? {
|
|
1579
|
+
...t,
|
|
1580
|
+
uniqueConstraints: [...t.uniqueConstraints, op.constraint.constraint]
|
|
1581
|
+
} : { ...t, foreignKeys: [...t.foreignKeys, op.constraint.constraint] };
|
|
1582
|
+
}
|
|
1583
|
+
break;
|
|
1584
|
+
}
|
|
1585
|
+
case "drop_constraint": {
|
|
1586
|
+
const t = tables[op.table];
|
|
1587
|
+
if (t) {
|
|
1588
|
+
const dropName = op.constraint.constraint.name;
|
|
1589
|
+
tables[op.table] = op.constraint.type === "unique" ? {
|
|
1590
|
+
...t,
|
|
1591
|
+
uniqueConstraints: t.uniqueConstraints.filter((u) => u.name !== dropName)
|
|
1592
|
+
} : {
|
|
1593
|
+
...t,
|
|
1594
|
+
foreignKeys: t.foreignKeys.filter((f) => f.name !== dropName)
|
|
1595
|
+
};
|
|
1596
|
+
}
|
|
1597
|
+
break;
|
|
1598
|
+
}
|
|
1062
1599
|
}
|
|
1063
1600
|
return { tables };
|
|
1064
1601
|
}
|
|
@@ -1080,7 +1617,9 @@ function columnShape(col) {
|
|
|
1080
1617
|
type: col.type,
|
|
1081
1618
|
notNull: col.notNull,
|
|
1082
1619
|
primaryKey: col.primaryKey,
|
|
1083
|
-
default: col.default
|
|
1620
|
+
default: col.default,
|
|
1621
|
+
unique: col.unique,
|
|
1622
|
+
references: col.references
|
|
1084
1623
|
});
|
|
1085
1624
|
}
|
|
1086
1625
|
function tableShape(columns) {
|