@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/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
+ };