@b9g/zen 0.1.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/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +1044 -0
- package/chunk-2IEEEMRN.js +38 -0
- package/chunk-56M5Z3A6.js +1346 -0
- package/chunk-QXGEP5PB.js +310 -0
- package/ddl-NAJM37GQ.js +9 -0
- package/package.json +102 -0
- package/src/bun.d.ts +50 -0
- package/src/bun.js +906 -0
- package/src/mysql.d.ts +62 -0
- package/src/mysql.js +573 -0
- package/src/postgres.d.ts +62 -0
- package/src/postgres.js +555 -0
- package/src/sqlite.d.ts +43 -0
- package/src/sqlite.js +447 -0
- package/src/zen.d.ts +14 -0
- package/src/zen.js +2143 -0
package/src/bun.js
ADDED
|
@@ -0,0 +1,906 @@
|
|
|
1
|
+
/// <reference types="./bun.d.ts" />
|
|
2
|
+
import {
|
|
3
|
+
placeholder,
|
|
4
|
+
quoteIdent,
|
|
5
|
+
renderDDL
|
|
6
|
+
} from "../chunk-2IEEEMRN.js";
|
|
7
|
+
import {
|
|
8
|
+
generateDDL
|
|
9
|
+
} from "../chunk-QXGEP5PB.js";
|
|
10
|
+
import {
|
|
11
|
+
resolveSQLBuiltin
|
|
12
|
+
} from "../chunk-56M5Z3A6.js";
|
|
13
|
+
|
|
14
|
+
// src/bun.ts
|
|
15
|
+
import { SQL } from "bun";
|
|
16
|
+
import {
|
|
17
|
+
ConstraintViolationError,
|
|
18
|
+
EnsureError,
|
|
19
|
+
SchemaDriftError,
|
|
20
|
+
ConstraintPreflightError,
|
|
21
|
+
isSQLBuiltin,
|
|
22
|
+
isSQLIdentifier
|
|
23
|
+
} from "./zen.js";
|
|
24
|
+
function detectDialect(url) {
|
|
25
|
+
if (url.startsWith("postgres://") || url.startsWith("postgresql://")) {
|
|
26
|
+
return "postgresql";
|
|
27
|
+
}
|
|
28
|
+
if (url.startsWith("mysql://") || url.startsWith("mysql2://") || url.startsWith("mariadb://")) {
|
|
29
|
+
return "mysql";
|
|
30
|
+
}
|
|
31
|
+
return "sqlite";
|
|
32
|
+
}
|
|
33
|
+
function buildSQL(strings, values, dialect) {
|
|
34
|
+
let sql = strings[0];
|
|
35
|
+
const params = [];
|
|
36
|
+
for (let i = 0; i < values.length; i++) {
|
|
37
|
+
const value = values[i];
|
|
38
|
+
if (isSQLBuiltin(value)) {
|
|
39
|
+
sql += resolveSQLBuiltin(value) + strings[i + 1];
|
|
40
|
+
} else if (isSQLIdentifier(value)) {
|
|
41
|
+
sql += quoteIdent(value.name, dialect) + strings[i + 1];
|
|
42
|
+
} else {
|
|
43
|
+
sql += placeholder(params.length + 1, dialect) + strings[i + 1];
|
|
44
|
+
params.push(value);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { sql, params };
|
|
48
|
+
}
|
|
49
|
+
var BunDriver = class {
|
|
50
|
+
supportsReturning;
|
|
51
|
+
#dialect;
|
|
52
|
+
#sql;
|
|
53
|
+
#sqliteInitialized = false;
|
|
54
|
+
constructor(url, options) {
|
|
55
|
+
this.#dialect = detectDialect(url);
|
|
56
|
+
this.#sql = new SQL(url, options);
|
|
57
|
+
this.supportsReturning = this.#dialect !== "mysql";
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Ensure SQLite connection has foreign keys enabled.
|
|
61
|
+
* Called lazily on first query to avoid async constructor.
|
|
62
|
+
*/
|
|
63
|
+
async #ensureSqliteInit() {
|
|
64
|
+
if (this.#dialect !== "sqlite" || this.#sqliteInitialized) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
this.#sqliteInitialized = true;
|
|
68
|
+
await this.#sql.unsafe("PRAGMA foreign_keys = ON", []);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Convert database errors to Zealot errors.
|
|
72
|
+
*/
|
|
73
|
+
#handleError(error) {
|
|
74
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
75
|
+
const code = error.code;
|
|
76
|
+
const errno = error.errno;
|
|
77
|
+
const message = error.message || String(error);
|
|
78
|
+
if (this.#dialect === "sqlite") {
|
|
79
|
+
if (code === "SQLITE_CONSTRAINT" || code === "SQLITE_CONSTRAINT_UNIQUE" || code === "SQLITE_CONSTRAINT_FOREIGNKEY") {
|
|
80
|
+
const match = message.match(/constraint failed: (\w+)\.(\w+)/i);
|
|
81
|
+
const table = match ? match[1] : void 0;
|
|
82
|
+
const column = match ? match[2] : void 0;
|
|
83
|
+
const constraint = match ? `${table}.${column}` : void 0;
|
|
84
|
+
let kind = "unknown";
|
|
85
|
+
if (code === "SQLITE_CONSTRAINT_UNIQUE")
|
|
86
|
+
kind = "unique";
|
|
87
|
+
else if (code === "SQLITE_CONSTRAINT_FOREIGNKEY")
|
|
88
|
+
kind = "foreign_key";
|
|
89
|
+
else if (message.includes("UNIQUE"))
|
|
90
|
+
kind = "unique";
|
|
91
|
+
else if (message.includes("FOREIGN KEY"))
|
|
92
|
+
kind = "foreign_key";
|
|
93
|
+
else if (message.includes("NOT NULL"))
|
|
94
|
+
kind = "not_null";
|
|
95
|
+
else if (message.includes("CHECK"))
|
|
96
|
+
kind = "check";
|
|
97
|
+
throw new ConstraintViolationError(
|
|
98
|
+
message,
|
|
99
|
+
{
|
|
100
|
+
kind,
|
|
101
|
+
constraint,
|
|
102
|
+
table,
|
|
103
|
+
column
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
cause: error
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
} else if (this.#dialect === "postgresql") {
|
|
111
|
+
const pgCode = errno || code;
|
|
112
|
+
if (pgCode === "23505" || pgCode === "23503" || pgCode === "23514" || pgCode === "23502") {
|
|
113
|
+
const constraint = error.constraint_name || error.constraint;
|
|
114
|
+
const table = error.table_name || error.table;
|
|
115
|
+
const column = error.column_name || error.column;
|
|
116
|
+
let kind = "unknown";
|
|
117
|
+
if (pgCode === "23505")
|
|
118
|
+
kind = "unique";
|
|
119
|
+
else if (pgCode === "23503")
|
|
120
|
+
kind = "foreign_key";
|
|
121
|
+
else if (pgCode === "23514")
|
|
122
|
+
kind = "check";
|
|
123
|
+
else if (pgCode === "23502")
|
|
124
|
+
kind = "not_null";
|
|
125
|
+
throw new ConstraintViolationError(
|
|
126
|
+
message,
|
|
127
|
+
{
|
|
128
|
+
kind,
|
|
129
|
+
constraint,
|
|
130
|
+
table,
|
|
131
|
+
column
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
cause: error
|
|
135
|
+
}
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
} else if (this.#dialect === "mysql") {
|
|
139
|
+
if (code === "ER_DUP_ENTRY" || code === "ER_NO_REFERENCED_ROW_2" || code === "ER_ROW_IS_REFERENCED_2" || errno === 1062 || errno === 1452 || errno === 1451) {
|
|
140
|
+
let kind = "unknown";
|
|
141
|
+
let constraint;
|
|
142
|
+
let table;
|
|
143
|
+
let column;
|
|
144
|
+
if (code === "ER_DUP_ENTRY" || errno === 1062) {
|
|
145
|
+
kind = "unique";
|
|
146
|
+
const keyMatch = message.match(/for key '([^']+)'/i);
|
|
147
|
+
constraint = keyMatch ? keyMatch[1] : void 0;
|
|
148
|
+
if (constraint) {
|
|
149
|
+
const parts = constraint.split(".");
|
|
150
|
+
if (parts.length > 1) {
|
|
151
|
+
table = parts[0];
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
} else if (code === "ER_NO_REFERENCED_ROW_2" || code === "ER_ROW_IS_REFERENCED_2" || errno === 1452 || errno === 1451) {
|
|
155
|
+
kind = "foreign_key";
|
|
156
|
+
const constraintMatch = message.match(/CONSTRAINT `([^`]+)`/i);
|
|
157
|
+
constraint = constraintMatch ? constraintMatch[1] : void 0;
|
|
158
|
+
const tableMatch = message.match(/`([^`]+)`\.`([^`]+)`/);
|
|
159
|
+
if (tableMatch) {
|
|
160
|
+
table = tableMatch[2];
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
throw new ConstraintViolationError(
|
|
164
|
+
message,
|
|
165
|
+
{
|
|
166
|
+
kind,
|
|
167
|
+
constraint,
|
|
168
|
+
table,
|
|
169
|
+
column
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
cause: error
|
|
173
|
+
}
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
async all(strings, values) {
|
|
181
|
+
await this.#ensureSqliteInit();
|
|
182
|
+
try {
|
|
183
|
+
const { sql, params } = buildSQL(strings, values, this.#dialect);
|
|
184
|
+
const result = await this.#sql.unsafe(sql, params);
|
|
185
|
+
return result;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
this.#handleError(error);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
async get(strings, values) {
|
|
191
|
+
await this.#ensureSqliteInit();
|
|
192
|
+
try {
|
|
193
|
+
const { sql, params } = buildSQL(strings, values, this.#dialect);
|
|
194
|
+
const result = await this.#sql.unsafe(sql, params);
|
|
195
|
+
return result[0] ?? null;
|
|
196
|
+
} catch (error) {
|
|
197
|
+
this.#handleError(error);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async run(strings, values) {
|
|
201
|
+
await this.#ensureSqliteInit();
|
|
202
|
+
try {
|
|
203
|
+
const { sql, params } = buildSQL(strings, values, this.#dialect);
|
|
204
|
+
const result = await this.#sql.unsafe(sql, params);
|
|
205
|
+
if (this.#dialect === "mysql") {
|
|
206
|
+
return result.affectedRows ?? result.length;
|
|
207
|
+
}
|
|
208
|
+
return result.count ?? result.length;
|
|
209
|
+
} catch (error) {
|
|
210
|
+
this.#handleError(error);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
async val(strings, values) {
|
|
214
|
+
await this.#ensureSqliteInit();
|
|
215
|
+
try {
|
|
216
|
+
const { sql, params } = buildSQL(strings, values, this.#dialect);
|
|
217
|
+
const result = await this.#sql.unsafe(sql, params);
|
|
218
|
+
if (result.length === 0)
|
|
219
|
+
return null;
|
|
220
|
+
const row = result[0];
|
|
221
|
+
const firstKey = Object.keys(row)[0];
|
|
222
|
+
return row[firstKey];
|
|
223
|
+
} catch (error) {
|
|
224
|
+
this.#handleError(error);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
async close() {
|
|
228
|
+
await this.#sql.close();
|
|
229
|
+
}
|
|
230
|
+
async transaction(fn) {
|
|
231
|
+
const dialect = this.#dialect;
|
|
232
|
+
const handleError = this.#handleError.bind(this);
|
|
233
|
+
const supportsReturning = this.supportsReturning;
|
|
234
|
+
return await this.#sql.transaction(async (txSql) => {
|
|
235
|
+
const txDriver = {
|
|
236
|
+
supportsReturning,
|
|
237
|
+
all: async (strings, values) => {
|
|
238
|
+
try {
|
|
239
|
+
const { sql, params } = buildSQL(strings, values, dialect);
|
|
240
|
+
const result = await txSql.unsafe(sql, params);
|
|
241
|
+
return result;
|
|
242
|
+
} catch (error) {
|
|
243
|
+
return handleError(error);
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
get: async (strings, values) => {
|
|
247
|
+
try {
|
|
248
|
+
const { sql, params } = buildSQL(strings, values, dialect);
|
|
249
|
+
const result = await txSql.unsafe(sql, params);
|
|
250
|
+
return result[0] ?? null;
|
|
251
|
+
} catch (error) {
|
|
252
|
+
return handleError(error);
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
run: async (strings, values) => {
|
|
256
|
+
try {
|
|
257
|
+
const { sql, params } = buildSQL(strings, values, dialect);
|
|
258
|
+
const result = await txSql.unsafe(sql, params);
|
|
259
|
+
return result.count ?? result.affectedRows ?? result.length;
|
|
260
|
+
} catch (error) {
|
|
261
|
+
return handleError(error);
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
val: async (strings, values) => {
|
|
265
|
+
try {
|
|
266
|
+
const { sql, params } = buildSQL(strings, values, dialect);
|
|
267
|
+
const result = await txSql.unsafe(sql, params);
|
|
268
|
+
if (result.length === 0)
|
|
269
|
+
return null;
|
|
270
|
+
const row = result[0];
|
|
271
|
+
const firstKey = Object.keys(row)[0];
|
|
272
|
+
return row[firstKey];
|
|
273
|
+
} catch (error) {
|
|
274
|
+
return handleError(error);
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
close: async () => {
|
|
278
|
+
},
|
|
279
|
+
transaction: async () => {
|
|
280
|
+
throw new Error("Nested transactions are not supported");
|
|
281
|
+
}
|
|
282
|
+
};
|
|
283
|
+
return await fn(txDriver);
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
async withMigrationLock(fn) {
|
|
287
|
+
await this.#ensureSqliteInit();
|
|
288
|
+
if (this.#dialect === "postgresql") {
|
|
289
|
+
const MIGRATION_LOCK_ID = 1952393421;
|
|
290
|
+
await this.#sql.unsafe(`SELECT pg_advisory_lock($1)`, [
|
|
291
|
+
MIGRATION_LOCK_ID
|
|
292
|
+
]);
|
|
293
|
+
try {
|
|
294
|
+
return await fn();
|
|
295
|
+
} finally {
|
|
296
|
+
await this.#sql.unsafe(`SELECT pg_advisory_unlock($1)`, [
|
|
297
|
+
MIGRATION_LOCK_ID
|
|
298
|
+
]);
|
|
299
|
+
}
|
|
300
|
+
} else if (this.#dialect === "mysql") {
|
|
301
|
+
const LOCK_NAME = "zen_migration";
|
|
302
|
+
const LOCK_TIMEOUT = 10;
|
|
303
|
+
const result = await this.#sql.unsafe(`SELECT GET_LOCK(?, ?)`, [
|
|
304
|
+
LOCK_NAME,
|
|
305
|
+
LOCK_TIMEOUT
|
|
306
|
+
]);
|
|
307
|
+
const acquired = result[0]?.["GET_LOCK(?, ?)"] === 1;
|
|
308
|
+
if (!acquired) {
|
|
309
|
+
throw new Error(
|
|
310
|
+
`Failed to acquire migration lock after ${LOCK_TIMEOUT}s. Another migration may be in progress.`
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
try {
|
|
314
|
+
return await fn();
|
|
315
|
+
} finally {
|
|
316
|
+
await this.#sql.unsafe(`SELECT RELEASE_LOCK(?)`, [LOCK_NAME]);
|
|
317
|
+
}
|
|
318
|
+
} else {
|
|
319
|
+
await this.#sql.unsafe("BEGIN EXCLUSIVE", []);
|
|
320
|
+
try {
|
|
321
|
+
const result = await fn();
|
|
322
|
+
await this.#sql.unsafe("COMMIT", []);
|
|
323
|
+
return result;
|
|
324
|
+
} catch (error) {
|
|
325
|
+
await this.#sql.unsafe("ROLLBACK", []);
|
|
326
|
+
throw error;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
// ==========================================================================
|
|
331
|
+
// Schema Ensure Methods
|
|
332
|
+
// ==========================================================================
|
|
333
|
+
async ensureTable(table) {
|
|
334
|
+
await this.#ensureSqliteInit();
|
|
335
|
+
const tableName = table.name;
|
|
336
|
+
let step = 0;
|
|
337
|
+
let applied = false;
|
|
338
|
+
try {
|
|
339
|
+
const exists = await this.#tableExists(tableName);
|
|
340
|
+
if (!exists) {
|
|
341
|
+
step = 1;
|
|
342
|
+
const ddlTemplate = generateDDL(table, { dialect: this.#dialect });
|
|
343
|
+
const ddlSQL = renderDDL(
|
|
344
|
+
ddlTemplate[0],
|
|
345
|
+
ddlTemplate.slice(1),
|
|
346
|
+
this.#dialect
|
|
347
|
+
);
|
|
348
|
+
for (const stmt of ddlSQL.split(";").filter((s) => s.trim())) {
|
|
349
|
+
await this.#sql.unsafe(stmt.trim(), []);
|
|
350
|
+
}
|
|
351
|
+
applied = true;
|
|
352
|
+
} else {
|
|
353
|
+
step = 2;
|
|
354
|
+
const columnsApplied = await this.#ensureMissingColumns(table);
|
|
355
|
+
applied = applied || columnsApplied;
|
|
356
|
+
step = 3;
|
|
357
|
+
const indexesApplied = await this.#ensureMissingIndexes(table);
|
|
358
|
+
applied = applied || indexesApplied;
|
|
359
|
+
step = 4;
|
|
360
|
+
await this.#checkMissingConstraints(table);
|
|
361
|
+
}
|
|
362
|
+
return { applied };
|
|
363
|
+
} catch (error) {
|
|
364
|
+
if (error instanceof SchemaDriftError || error instanceof EnsureError) {
|
|
365
|
+
throw error;
|
|
366
|
+
}
|
|
367
|
+
throw new EnsureError(
|
|
368
|
+
`ensureTable failed at step ${step}: ${error instanceof Error ? error.message : String(error)}`,
|
|
369
|
+
{ operation: "ensureTable", table: tableName, step },
|
|
370
|
+
{ cause: error }
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
async ensureConstraints(table) {
|
|
375
|
+
await this.#ensureSqliteInit();
|
|
376
|
+
const tableName = table.name;
|
|
377
|
+
let step = 0;
|
|
378
|
+
let applied = false;
|
|
379
|
+
try {
|
|
380
|
+
const exists = await this.#tableExists(tableName);
|
|
381
|
+
if (!exists) {
|
|
382
|
+
throw new Error(
|
|
383
|
+
`Table "${tableName}" does not exist. Run ensureTable() first.`
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
step = 1;
|
|
387
|
+
const existingConstraints = await this.#getConstraints(tableName);
|
|
388
|
+
step = 2;
|
|
389
|
+
const uniquesApplied = await this.#ensureUniqueConstraints(
|
|
390
|
+
table,
|
|
391
|
+
existingConstraints
|
|
392
|
+
);
|
|
393
|
+
applied = applied || uniquesApplied;
|
|
394
|
+
step = 3;
|
|
395
|
+
const fksApplied = await this.#ensureForeignKeys(
|
|
396
|
+
table,
|
|
397
|
+
existingConstraints
|
|
398
|
+
);
|
|
399
|
+
applied = applied || fksApplied;
|
|
400
|
+
return { applied };
|
|
401
|
+
} catch (error) {
|
|
402
|
+
if (error instanceof ConstraintPreflightError || error instanceof EnsureError) {
|
|
403
|
+
throw error;
|
|
404
|
+
}
|
|
405
|
+
throw new EnsureError(
|
|
406
|
+
`ensureConstraints failed at step ${step}: ${error instanceof Error ? error.message : String(error)}`,
|
|
407
|
+
{ operation: "ensureConstraints", table: tableName, step },
|
|
408
|
+
{ cause: error }
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Optional introspection: list columns for a table.
|
|
414
|
+
* Exposed for Database-level helpers that need column existence checks.
|
|
415
|
+
*/
|
|
416
|
+
async getColumns(tableName) {
|
|
417
|
+
return await this.#getColumns(tableName);
|
|
418
|
+
}
|
|
419
|
+
// ==========================================================================
|
|
420
|
+
// Introspection Helpers (private)
|
|
421
|
+
// ==========================================================================
|
|
422
|
+
async #tableExists(tableName) {
|
|
423
|
+
await this.#ensureSqliteInit();
|
|
424
|
+
if (this.#dialect === "sqlite") {
|
|
425
|
+
const result = await this.#sql.unsafe(
|
|
426
|
+
`SELECT 1 FROM sqlite_master WHERE type='table' AND name=?`,
|
|
427
|
+
[tableName]
|
|
428
|
+
);
|
|
429
|
+
return result.length > 0;
|
|
430
|
+
} else if (this.#dialect === "postgresql") {
|
|
431
|
+
const result = await this.#sql.unsafe(
|
|
432
|
+
`SELECT 1 FROM information_schema.tables WHERE table_name = $1 AND table_schema = 'public'`,
|
|
433
|
+
[tableName]
|
|
434
|
+
);
|
|
435
|
+
return result.length > 0;
|
|
436
|
+
} else {
|
|
437
|
+
const result = await this.#sql.unsafe(
|
|
438
|
+
`SELECT 1 FROM information_schema.tables WHERE table_name = ? AND table_schema = DATABASE()`,
|
|
439
|
+
[tableName]
|
|
440
|
+
);
|
|
441
|
+
return result.length > 0;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
async #getColumns(tableName) {
|
|
445
|
+
await this.#ensureSqliteInit();
|
|
446
|
+
if (this.#dialect === "sqlite") {
|
|
447
|
+
const result = await this.#sql.unsafe(
|
|
448
|
+
`PRAGMA table_info(${quoteIdent(tableName, "sqlite")})`,
|
|
449
|
+
[]
|
|
450
|
+
);
|
|
451
|
+
return result.map((row) => ({
|
|
452
|
+
name: row.name,
|
|
453
|
+
type: row.type,
|
|
454
|
+
notnull: row.notnull === 1
|
|
455
|
+
}));
|
|
456
|
+
} else if (this.#dialect === "postgresql") {
|
|
457
|
+
const result = await this.#sql.unsafe(
|
|
458
|
+
`SELECT column_name as name, data_type as type, is_nullable = 'NO' as notnull
|
|
459
|
+
FROM information_schema.columns WHERE table_name = $1 AND table_schema = 'public'`,
|
|
460
|
+
[tableName]
|
|
461
|
+
);
|
|
462
|
+
return result;
|
|
463
|
+
} else {
|
|
464
|
+
const result = await this.#sql.unsafe(
|
|
465
|
+
`SELECT column_name as name, data_type as type, is_nullable = 'NO' as notnull
|
|
466
|
+
FROM information_schema.columns WHERE table_name = ? AND table_schema = DATABASE()`,
|
|
467
|
+
[tableName]
|
|
468
|
+
);
|
|
469
|
+
return result;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
async #getIndexes(tableName) {
|
|
473
|
+
await this.#ensureSqliteInit();
|
|
474
|
+
if (this.#dialect === "sqlite") {
|
|
475
|
+
const indexList = await this.#sql.unsafe(
|
|
476
|
+
`PRAGMA index_list(${quoteIdent(tableName, "sqlite")})`,
|
|
477
|
+
[]
|
|
478
|
+
);
|
|
479
|
+
const indexes = [];
|
|
480
|
+
for (const idx of indexList) {
|
|
481
|
+
if (idx.origin === "pk")
|
|
482
|
+
continue;
|
|
483
|
+
const indexInfo = await this.#sql.unsafe(
|
|
484
|
+
`PRAGMA index_info(${quoteIdent(idx.name, "sqlite")})`,
|
|
485
|
+
[]
|
|
486
|
+
);
|
|
487
|
+
indexes.push({
|
|
488
|
+
name: idx.name,
|
|
489
|
+
columns: indexInfo.map((col) => col.name),
|
|
490
|
+
unique: idx.unique === 1
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
return indexes;
|
|
494
|
+
} else if (this.#dialect === "postgresql") {
|
|
495
|
+
const result = await this.#sql.unsafe(
|
|
496
|
+
`SELECT i.relname as name,
|
|
497
|
+
array_agg(a.attname ORDER BY array_position(ix.indkey, a.attnum)) as columns,
|
|
498
|
+
ix.indisunique as unique
|
|
499
|
+
FROM pg_index ix
|
|
500
|
+
JOIN pg_class i ON i.oid = ix.indexrelid
|
|
501
|
+
JOIN pg_class t ON t.oid = ix.indrelid
|
|
502
|
+
JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = ANY(ix.indkey)
|
|
503
|
+
WHERE t.relname = $1 AND NOT ix.indisprimary
|
|
504
|
+
GROUP BY i.relname, ix.indisunique`,
|
|
505
|
+
[tableName]
|
|
506
|
+
);
|
|
507
|
+
return result;
|
|
508
|
+
} else {
|
|
509
|
+
const result = await this.#sql.unsafe(
|
|
510
|
+
`SELECT index_name as name,
|
|
511
|
+
GROUP_CONCAT(column_name ORDER BY seq_in_index) as columns,
|
|
512
|
+
NOT non_unique as \`unique\`
|
|
513
|
+
FROM information_schema.statistics
|
|
514
|
+
WHERE table_name = ? AND table_schema = DATABASE() AND index_name != 'PRIMARY'
|
|
515
|
+
GROUP BY index_name, non_unique`,
|
|
516
|
+
[tableName]
|
|
517
|
+
);
|
|
518
|
+
return result.map((row) => ({
|
|
519
|
+
...row,
|
|
520
|
+
columns: row.columns.split(",")
|
|
521
|
+
}));
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
async #getConstraints(tableName) {
|
|
525
|
+
await this.#ensureSqliteInit();
|
|
526
|
+
if (this.#dialect === "sqlite") {
|
|
527
|
+
const constraints = [];
|
|
528
|
+
const indexes = await this.#getIndexes(tableName);
|
|
529
|
+
for (const idx of indexes) {
|
|
530
|
+
if (idx.unique) {
|
|
531
|
+
constraints.push({
|
|
532
|
+
name: idx.name,
|
|
533
|
+
type: "unique",
|
|
534
|
+
columns: idx.columns
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
const fks = await this.#sql.unsafe(
|
|
539
|
+
`PRAGMA foreign_key_list(${quoteIdent(tableName, "sqlite")})`,
|
|
540
|
+
[]
|
|
541
|
+
);
|
|
542
|
+
const fkMap = /* @__PURE__ */ new Map();
|
|
543
|
+
for (const fk of fks) {
|
|
544
|
+
if (!fkMap.has(fk.id)) {
|
|
545
|
+
fkMap.set(fk.id, { table: fk.table, from: [], to: [] });
|
|
546
|
+
}
|
|
547
|
+
const entry = fkMap.get(fk.id);
|
|
548
|
+
entry.from.push(fk.from);
|
|
549
|
+
entry.to.push(fk.to);
|
|
550
|
+
}
|
|
551
|
+
for (const [id, fk] of fkMap) {
|
|
552
|
+
constraints.push({
|
|
553
|
+
name: `fk_${tableName}_${id}`,
|
|
554
|
+
type: "foreign_key",
|
|
555
|
+
columns: fk.from,
|
|
556
|
+
referencedTable: fk.table,
|
|
557
|
+
referencedColumns: fk.to
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
return constraints;
|
|
561
|
+
} else if (this.#dialect === "postgresql") {
|
|
562
|
+
const result = await this.#sql.unsafe(
|
|
563
|
+
`SELECT
|
|
564
|
+
tc.constraint_name as name,
|
|
565
|
+
tc.constraint_type as type,
|
|
566
|
+
array_agg(kcu.column_name ORDER BY kcu.ordinal_position) as columns,
|
|
567
|
+
ccu.table_name as referenced_table,
|
|
568
|
+
array_agg(ccu.column_name ORDER BY kcu.ordinal_position) as referenced_columns
|
|
569
|
+
FROM information_schema.table_constraints tc
|
|
570
|
+
JOIN information_schema.key_column_usage kcu
|
|
571
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
572
|
+
AND tc.table_schema = kcu.table_schema
|
|
573
|
+
AND tc.table_name = kcu.table_name
|
|
574
|
+
LEFT JOIN information_schema.constraint_column_usage ccu
|
|
575
|
+
ON tc.constraint_name = ccu.constraint_name AND tc.table_schema = ccu.table_schema
|
|
576
|
+
WHERE tc.table_name = $1 AND tc.table_schema = 'public'
|
|
577
|
+
AND tc.constraint_type IN ('UNIQUE', 'FOREIGN KEY')
|
|
578
|
+
GROUP BY tc.constraint_name, tc.constraint_type, ccu.table_name`,
|
|
579
|
+
[tableName]
|
|
580
|
+
);
|
|
581
|
+
const parseArray = (s) => {
|
|
582
|
+
if (!s)
|
|
583
|
+
return [];
|
|
584
|
+
return s.replace(/^\{|\}$/g, "").split(",").filter(Boolean);
|
|
585
|
+
};
|
|
586
|
+
return result.map((row) => ({
|
|
587
|
+
name: row.name,
|
|
588
|
+
type: row.type === "UNIQUE" ? "unique" : "foreign_key",
|
|
589
|
+
columns: parseArray(row.columns),
|
|
590
|
+
referencedTable: row.referenced_table,
|
|
591
|
+
referencedColumns: parseArray(row.referenced_columns)
|
|
592
|
+
}));
|
|
593
|
+
} else {
|
|
594
|
+
const result = await this.#sql.unsafe(
|
|
595
|
+
`SELECT
|
|
596
|
+
tc.constraint_name as name,
|
|
597
|
+
tc.constraint_type as type,
|
|
598
|
+
GROUP_CONCAT(DISTINCT kcu.column_name ORDER BY kcu.ordinal_position) as columns,
|
|
599
|
+
kcu.referenced_table_name as referenced_table,
|
|
600
|
+
GROUP_CONCAT(DISTINCT kcu.referenced_column_name ORDER BY kcu.ordinal_position) as referenced_columns
|
|
601
|
+
FROM information_schema.table_constraints tc
|
|
602
|
+
JOIN information_schema.key_column_usage kcu
|
|
603
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
604
|
+
AND tc.table_schema = kcu.table_schema
|
|
605
|
+
AND tc.table_name = kcu.table_name
|
|
606
|
+
WHERE tc.table_name = ? AND tc.table_schema = DATABASE()
|
|
607
|
+
AND tc.constraint_type IN ('UNIQUE', 'FOREIGN KEY')
|
|
608
|
+
GROUP BY tc.constraint_name, tc.constraint_type, kcu.referenced_table_name`,
|
|
609
|
+
[tableName]
|
|
610
|
+
);
|
|
611
|
+
return result.map((row) => ({
|
|
612
|
+
name: row.name,
|
|
613
|
+
type: row.type === "UNIQUE" ? "unique" : "foreign_key",
|
|
614
|
+
columns: row.columns.split(","),
|
|
615
|
+
referencedTable: row.referenced_table,
|
|
616
|
+
referencedColumns: row.referenced_columns?.split(",")
|
|
617
|
+
}));
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
// ==========================================================================
|
|
621
|
+
// Schema Ensure Helpers (private)
|
|
622
|
+
// ==========================================================================
|
|
623
|
+
async #ensureMissingColumns(table) {
|
|
624
|
+
const existingCols = await this.#getColumns(table.name);
|
|
625
|
+
const existingColNames = new Set(existingCols.map((c) => c.name));
|
|
626
|
+
const schemaFields = Object.keys(table.schema.shape);
|
|
627
|
+
let applied = false;
|
|
628
|
+
for (const fieldName of schemaFields) {
|
|
629
|
+
if (!existingColNames.has(fieldName)) {
|
|
630
|
+
await this.#addColumn(table, fieldName);
|
|
631
|
+
applied = true;
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
return applied;
|
|
635
|
+
}
|
|
636
|
+
async #addColumn(table, fieldName) {
|
|
637
|
+
const { generateColumnDDL } = await import("../ddl-NAJM37GQ.js");
|
|
638
|
+
const zodType = table.schema.shape[fieldName];
|
|
639
|
+
const fieldMeta = table.meta.fields[fieldName] || {};
|
|
640
|
+
const colTemplate = generateColumnDDL(
|
|
641
|
+
fieldName,
|
|
642
|
+
zodType,
|
|
643
|
+
fieldMeta,
|
|
644
|
+
this.#dialect
|
|
645
|
+
);
|
|
646
|
+
const colSQL = renderDDL(
|
|
647
|
+
colTemplate[0],
|
|
648
|
+
colTemplate.slice(1),
|
|
649
|
+
this.#dialect
|
|
650
|
+
);
|
|
651
|
+
const ifNotExists = this.#dialect === "postgresql" ? "IF NOT EXISTS " : "";
|
|
652
|
+
const sql = `ALTER TABLE ${quoteIdent(table.name, this.#dialect)} ADD COLUMN ${ifNotExists}${colSQL}`;
|
|
653
|
+
await this.#sql.unsafe(sql, []);
|
|
654
|
+
}
|
|
655
|
+
async #ensureMissingIndexes(table) {
|
|
656
|
+
const existingIndexes = await this.#getIndexes(table.name);
|
|
657
|
+
const existingIndexNames = new Set(existingIndexes.map((i) => i.name));
|
|
658
|
+
const meta = table.meta;
|
|
659
|
+
let applied = false;
|
|
660
|
+
for (const fieldName of meta.indexed) {
|
|
661
|
+
const indexName = `idx_${table.name}_${fieldName}`;
|
|
662
|
+
if (!existingIndexNames.has(indexName)) {
|
|
663
|
+
await this.#createIndex(table.name, indexName, [fieldName], false);
|
|
664
|
+
applied = true;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
for (const indexCols of table.indexes) {
|
|
668
|
+
const indexName = `idx_${table.name}_${indexCols.join("_")}`;
|
|
669
|
+
if (!existingIndexNames.has(indexName)) {
|
|
670
|
+
await this.#createIndex(table.name, indexName, indexCols, false);
|
|
671
|
+
applied = true;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return applied;
|
|
675
|
+
}
|
|
676
|
+
async #createIndex(tableName, indexName, columns, unique) {
|
|
677
|
+
const uniqueKw = unique ? "UNIQUE " : "";
|
|
678
|
+
const colList = columns.map((c) => quoteIdent(c, this.#dialect)).join(", ");
|
|
679
|
+
const ifNotExists = this.#dialect === "mysql" ? "" : "IF NOT EXISTS ";
|
|
680
|
+
const sql = `CREATE ${uniqueKw}INDEX ${ifNotExists}${quoteIdent(indexName, this.#dialect)} ON ${quoteIdent(tableName, this.#dialect)} (${colList})`;
|
|
681
|
+
await this.#sql.unsafe(sql, []);
|
|
682
|
+
}
|
|
683
|
+
async #checkMissingConstraints(table) {
|
|
684
|
+
const existingConstraints = await this.#getConstraints(table.name);
|
|
685
|
+
const meta = table.meta;
|
|
686
|
+
const fields = Object.keys(meta.fields);
|
|
687
|
+
for (const fieldName of fields) {
|
|
688
|
+
const fieldMeta = meta.fields[fieldName];
|
|
689
|
+
if (fieldMeta.unique) {
|
|
690
|
+
const hasUnique = existingConstraints.some(
|
|
691
|
+
(c) => c.type === "unique" && c.columns.length === 1 && c.columns[0] === fieldName
|
|
692
|
+
);
|
|
693
|
+
if (!hasUnique) {
|
|
694
|
+
throw new SchemaDriftError(
|
|
695
|
+
`Table "${table.name}" is missing UNIQUE constraint on column "${fieldName}"`,
|
|
696
|
+
{
|
|
697
|
+
table: table.name,
|
|
698
|
+
drift: `missing unique:${fieldName}`,
|
|
699
|
+
suggestion: `Run db.ensureConstraints(${table.name}) to apply constraints`
|
|
700
|
+
}
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
for (const ref of meta.references) {
|
|
706
|
+
const hasFk = existingConstraints.some(
|
|
707
|
+
(c) => c.type === "foreign_key" && c.columns.length === 1 && c.columns[0] === ref.fieldName && c.referencedTable === ref.table.name && c.referencedColumns?.[0] === ref.referencedField
|
|
708
|
+
);
|
|
709
|
+
if (!hasFk) {
|
|
710
|
+
throw new SchemaDriftError(
|
|
711
|
+
`Table "${table.name}" is missing FOREIGN KEY on column "${ref.fieldName}" referencing "${ref.table.name}"`,
|
|
712
|
+
{
|
|
713
|
+
table: table.name,
|
|
714
|
+
drift: `missing fk:${ref.fieldName}->${ref.table.name}`,
|
|
715
|
+
suggestion: `Run db.ensureConstraints(${table.name}) to apply constraints`
|
|
716
|
+
}
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
for (const ref of table.compoundReferences) {
|
|
721
|
+
const refFields = ref.referencedFields ?? ref.fields;
|
|
722
|
+
const hasFk = existingConstraints.some((c) => {
|
|
723
|
+
if (c.type !== "foreign_key")
|
|
724
|
+
return false;
|
|
725
|
+
if (c.columns.length !== ref.fields.length)
|
|
726
|
+
return false;
|
|
727
|
+
if (c.referencedTable !== ref.table.name)
|
|
728
|
+
return false;
|
|
729
|
+
if (!ref.fields.every((field, i) => c.columns[i] === field))
|
|
730
|
+
return false;
|
|
731
|
+
return refFields.every(
|
|
732
|
+
(field, i) => c.referencedColumns?.[i] === field
|
|
733
|
+
);
|
|
734
|
+
});
|
|
735
|
+
if (!hasFk) {
|
|
736
|
+
throw new SchemaDriftError(
|
|
737
|
+
`Table "${table.name}" is missing compound FOREIGN KEY on columns (${ref.fields.join(", ")}) referencing "${ref.table.name}"`,
|
|
738
|
+
{
|
|
739
|
+
table: table.name,
|
|
740
|
+
drift: `missing fk:(${ref.fields.join(",")}) ->${ref.table.name}`,
|
|
741
|
+
suggestion: `Run db.ensureConstraints(${table.name}) to apply constraints`
|
|
742
|
+
}
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
async #ensureUniqueConstraints(table, existingConstraints) {
|
|
748
|
+
const meta = table.meta;
|
|
749
|
+
const fields = Object.keys(meta.fields);
|
|
750
|
+
let applied = false;
|
|
751
|
+
for (const fieldName of fields) {
|
|
752
|
+
const fieldMeta = meta.fields[fieldName];
|
|
753
|
+
if (fieldMeta.unique) {
|
|
754
|
+
const hasUnique = existingConstraints.some(
|
|
755
|
+
(c) => c.type === "unique" && c.columns.length === 1 && c.columns[0] === fieldName
|
|
756
|
+
);
|
|
757
|
+
if (!hasUnique) {
|
|
758
|
+
await this.#preflightUnique(table.name, [fieldName]);
|
|
759
|
+
const indexName = `uniq_${table.name}_${fieldName}`;
|
|
760
|
+
await this.#createIndex(table.name, indexName, [fieldName], true);
|
|
761
|
+
applied = true;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
return applied;
|
|
766
|
+
}
|
|
767
|
+
async #ensureForeignKeys(table, existingConstraints) {
|
|
768
|
+
const meta = table.meta;
|
|
769
|
+
let applied = false;
|
|
770
|
+
for (const ref of meta.references) {
|
|
771
|
+
const hasFk = existingConstraints.some(
|
|
772
|
+
(c) => c.type === "foreign_key" && c.columns.length === 1 && c.columns[0] === ref.fieldName && c.referencedTable === ref.table.name && c.referencedColumns?.[0] === ref.referencedField
|
|
773
|
+
);
|
|
774
|
+
if (!hasFk) {
|
|
775
|
+
await this.#preflightForeignKey(
|
|
776
|
+
table.name,
|
|
777
|
+
ref.fieldName,
|
|
778
|
+
ref.table.name,
|
|
779
|
+
ref.referencedField
|
|
780
|
+
);
|
|
781
|
+
if (this.#dialect === "sqlite") {
|
|
782
|
+
throw new Error(
|
|
783
|
+
`Adding foreign key constraints to existing SQLite tables requires table rebuild. Table "${table.name}" column "${ref.fieldName}" -> "${ref.table.name}"."${ref.referencedField}". Please use a manual migration.`
|
|
784
|
+
);
|
|
785
|
+
}
|
|
786
|
+
const constraintName = `${table.name}_${ref.fieldName}_fkey`;
|
|
787
|
+
const onDelete = ref.onDelete ? ` ON DELETE ${ref.onDelete.toUpperCase().replace(" ", " ")}` : "";
|
|
788
|
+
const sql = `ALTER TABLE ${quoteIdent(table.name, this.#dialect)} ADD CONSTRAINT ${quoteIdent(constraintName, this.#dialect)} FOREIGN KEY (${quoteIdent(ref.fieldName, this.#dialect)}) REFERENCES ${quoteIdent(ref.table.name, this.#dialect)}(${quoteIdent(ref.referencedField, this.#dialect)})${onDelete}`;
|
|
789
|
+
await this.#sql.unsafe(sql, []);
|
|
790
|
+
applied = true;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
for (const ref of table.compoundReferences) {
|
|
794
|
+
const refFields = ref.referencedFields ?? ref.fields;
|
|
795
|
+
const hasFk = existingConstraints.some((c) => {
|
|
796
|
+
if (c.type !== "foreign_key")
|
|
797
|
+
return false;
|
|
798
|
+
if (c.columns.length !== ref.fields.length)
|
|
799
|
+
return false;
|
|
800
|
+
if (c.referencedTable !== ref.table.name)
|
|
801
|
+
return false;
|
|
802
|
+
if (!ref.fields.every((field, i) => c.columns[i] === field))
|
|
803
|
+
return false;
|
|
804
|
+
return refFields.every(
|
|
805
|
+
(field, i) => c.referencedColumns?.[i] === field
|
|
806
|
+
);
|
|
807
|
+
});
|
|
808
|
+
if (!hasFk) {
|
|
809
|
+
await this.#preflightCompoundForeignKey(
|
|
810
|
+
table.name,
|
|
811
|
+
ref.fields,
|
|
812
|
+
ref.table.name,
|
|
813
|
+
refFields
|
|
814
|
+
);
|
|
815
|
+
if (this.#dialect === "sqlite") {
|
|
816
|
+
throw new Error(
|
|
817
|
+
`Adding foreign key constraints to existing SQLite tables requires table rebuild. Table "${table.name}" columns (${ref.fields.join(", ")}) -> "${ref.table.name}".(${refFields.join(", ")}). Please use a manual migration.`
|
|
818
|
+
);
|
|
819
|
+
}
|
|
820
|
+
const constraintName = `${table.name}_${ref.fields.join("_")}_fkey`;
|
|
821
|
+
const onDelete = ref.onDelete ? ` ON DELETE ${ref.onDelete.toUpperCase().replace(" ", " ")}` : "";
|
|
822
|
+
const localCols = ref.fields.map((f) => quoteIdent(f, this.#dialect)).join(", ");
|
|
823
|
+
const refCols = refFields.map((f) => quoteIdent(f, this.#dialect)).join(", ");
|
|
824
|
+
const sql = `ALTER TABLE ${quoteIdent(table.name, this.#dialect)} ADD CONSTRAINT ${quoteIdent(constraintName, this.#dialect)} FOREIGN KEY (${localCols}) REFERENCES ${quoteIdent(ref.table.name, this.#dialect)}(${refCols})${onDelete}`;
|
|
825
|
+
await this.#sql.unsafe(sql, []);
|
|
826
|
+
applied = true;
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return applied;
|
|
830
|
+
}
|
|
831
|
+
async #preflightUnique(tableName, columns) {
|
|
832
|
+
const colList = columns.map((c) => quoteIdent(c, this.#dialect)).join(", ");
|
|
833
|
+
const sql = `SELECT ${colList}, COUNT(*) as cnt FROM ${quoteIdent(tableName, this.#dialect)} GROUP BY ${colList} HAVING COUNT(*) > 1 LIMIT 1`;
|
|
834
|
+
const result = await this.#sql.unsafe(sql, []);
|
|
835
|
+
if (result.length > 0) {
|
|
836
|
+
const diagQuery = `SELECT ${columns.join(", ")}, COUNT(*) as cnt FROM ${tableName} GROUP BY ${columns.join(", ")} HAVING COUNT(*) > 1`;
|
|
837
|
+
const countSql = `SELECT COUNT(*) as total FROM (${sql.replace(" LIMIT 1", "")}) t`;
|
|
838
|
+
const countResult = await this.#sql.unsafe(countSql, []);
|
|
839
|
+
const violationCount = countResult[0]?.total ?? 1;
|
|
840
|
+
throw new ConstraintPreflightError(
|
|
841
|
+
`Cannot add UNIQUE constraint on "${tableName}"(${columns.join(", ")}): duplicate values exist`,
|
|
842
|
+
{
|
|
843
|
+
table: tableName,
|
|
844
|
+
constraint: `unique:${columns.join(",")}`,
|
|
845
|
+
violationCount,
|
|
846
|
+
query: diagQuery
|
|
847
|
+
}
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
async #preflightForeignKey(tableName, column, refTable, refColumn) {
|
|
852
|
+
const sql = `SELECT 1 FROM ${quoteIdent(tableName, this.#dialect)} t
|
|
853
|
+
WHERE t.${quoteIdent(column, this.#dialect)} IS NOT NULL
|
|
854
|
+
AND NOT EXISTS (
|
|
855
|
+
SELECT 1 FROM ${quoteIdent(refTable, this.#dialect)} r
|
|
856
|
+
WHERE r.${quoteIdent(refColumn, this.#dialect)} = t.${quoteIdent(column, this.#dialect)}
|
|
857
|
+
) LIMIT 1`;
|
|
858
|
+
const result = await this.#sql.unsafe(sql, []);
|
|
859
|
+
if (result.length > 0) {
|
|
860
|
+
const diagQuery = `SELECT t.* FROM ${quoteIdent(tableName, this.#dialect)} t WHERE t.${quoteIdent(column, this.#dialect)} IS NOT NULL AND NOT EXISTS (SELECT 1 FROM ${quoteIdent(refTable, this.#dialect)} r WHERE r.${quoteIdent(refColumn, this.#dialect)} = t.${quoteIdent(column, this.#dialect)})`;
|
|
861
|
+
const countSql = sql.replace("SELECT 1", "SELECT COUNT(*)").replace(" LIMIT 1", "");
|
|
862
|
+
const countResult = await this.#sql.unsafe(countSql, []);
|
|
863
|
+
const violationCount = countResult[0]?.["COUNT(*)"] ?? countResult[0]?.count ?? 1;
|
|
864
|
+
throw new ConstraintPreflightError(
|
|
865
|
+
`Cannot add FOREIGN KEY on "${tableName}"."${column}" -> "${refTable}"."${refColumn}": orphan records exist`,
|
|
866
|
+
{
|
|
867
|
+
table: tableName,
|
|
868
|
+
constraint: `fk:${column}->${refTable}.${refColumn}`,
|
|
869
|
+
violationCount,
|
|
870
|
+
query: diagQuery
|
|
871
|
+
}
|
|
872
|
+
);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
async #preflightCompoundForeignKey(tableName, columns, refTable, refColumns) {
|
|
876
|
+
const joinConditions = columns.map(
|
|
877
|
+
(col, i) => `r.${quoteIdent(refColumns[i], this.#dialect)} = t.${quoteIdent(col, this.#dialect)}`
|
|
878
|
+
).join(" AND ");
|
|
879
|
+
const nullChecks = columns.map((col) => `t.${quoteIdent(col, this.#dialect)} IS NOT NULL`).join(" AND ");
|
|
880
|
+
const sql = `SELECT 1 FROM ${quoteIdent(tableName, this.#dialect)} t
|
|
881
|
+
WHERE ${nullChecks}
|
|
882
|
+
AND NOT EXISTS (
|
|
883
|
+
SELECT 1 FROM ${quoteIdent(refTable, this.#dialect)} r
|
|
884
|
+
WHERE ${joinConditions}
|
|
885
|
+
) LIMIT 1`;
|
|
886
|
+
const result = await this.#sql.unsafe(sql, []);
|
|
887
|
+
if (result.length > 0) {
|
|
888
|
+
const diagQuery = `SELECT t.* FROM ${quoteIdent(tableName, this.#dialect)} t WHERE ${nullChecks} AND NOT EXISTS (SELECT 1 FROM ${quoteIdent(refTable, this.#dialect)} r WHERE ${joinConditions})`;
|
|
889
|
+
const countSql = sql.replace("SELECT 1", "SELECT COUNT(*)").replace(" LIMIT 1", "");
|
|
890
|
+
const countResult = await this.#sql.unsafe(countSql, []);
|
|
891
|
+
const violationCount = countResult[0]?.["COUNT(*)"] ?? countResult[0]?.count ?? 1;
|
|
892
|
+
throw new ConstraintPreflightError(
|
|
893
|
+
`Cannot add compound FOREIGN KEY on "${tableName}".(${columns.join(", ")}) -> "${refTable}".(${refColumns.join(", ")}): orphan records exist`,
|
|
894
|
+
{
|
|
895
|
+
table: tableName,
|
|
896
|
+
constraint: `fk:(${columns.join(",")}) ->${refTable}.(${refColumns.join(",")})`,
|
|
897
|
+
violationCount,
|
|
898
|
+
query: diagQuery
|
|
899
|
+
}
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
export {
|
|
905
|
+
BunDriver as default
|
|
906
|
+
};
|