@vertz/db 0.2.0 → 0.2.3

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/dist/index.js CHANGED
@@ -1,10 +1,14 @@
1
1
  import {
2
+ PostgresDialect,
3
+ SqliteDialect,
2
4
  buildDelete,
3
5
  buildInsert,
4
6
  buildSelect,
5
7
  buildUpdate,
6
- buildWhere
7
- } from "./shared/chunk-3f2grpak.js";
8
+ buildWhere,
9
+ defaultPostgresDialect,
10
+ defaultSqliteDialect
11
+ } from "./shared/chunk-0e1vy9qd.js";
8
12
  import {
9
13
  CheckConstraintError,
10
14
  ConnectionError,
@@ -15,21 +19,87 @@ import {
15
19
  NotNullError,
16
20
  UniqueConstraintError,
17
21
  executeQuery,
22
+ getAutoUpdateColumns,
18
23
  getPrimaryKeyColumns,
24
+ getReadOnlyColumns,
19
25
  getTimestampColumns,
20
26
  mapRow,
21
27
  mapRows,
22
28
  parsePgError,
23
29
  resolveSelectColumns
24
- } from "./shared/chunk-xp022dyp.js";
30
+ } from "./shared/chunk-agyds4jw.js";
31
+ import"./shared/chunk-kb4tnn2k.js";
25
32
  import {
26
33
  camelToSnake
27
- } from "./shared/chunk-hrfdj0rr.js";
34
+ } from "./shared/chunk-v2qm94qp.js";
35
+ import {
36
+ sha256Hex
37
+ } from "./shared/chunk-ssga2xea.js";
28
38
  import {
29
39
  diagnoseError,
30
40
  explainError,
31
41
  formatDiagnostic
32
- } from "./shared/chunk-wj026daz.js";
42
+ } from "./shared/chunk-k04v1jjx.js";
43
+ import {
44
+ createD1Adapter,
45
+ createD1Driver
46
+ } from "./shared/chunk-pnk6yzjv.js";
47
+ import {
48
+ generateId
49
+ } from "./shared/chunk-sfmyxz6r.js";
50
+ // src/adapters/database-bridge-adapter.ts
51
+ function createDatabaseBridgeAdapter(db, tableName) {
52
+ const delegate = db[tableName];
53
+ return {
54
+ async get(id) {
55
+ const result = await delegate.get({ where: { id } });
56
+ if (!result.ok) {
57
+ return null;
58
+ }
59
+ return result.data;
60
+ },
61
+ async list(options) {
62
+ const dbOptions = {};
63
+ if (options?.where) {
64
+ dbOptions.where = options.where;
65
+ }
66
+ if (options?.orderBy) {
67
+ dbOptions.orderBy = options.orderBy;
68
+ }
69
+ if (options?.limit !== undefined) {
70
+ dbOptions.limit = options.limit;
71
+ }
72
+ const result = await delegate.listAndCount(dbOptions);
73
+ if (!result.ok) {
74
+ throw result.error;
75
+ }
76
+ return result.data;
77
+ },
78
+ async create(data) {
79
+ const result = await delegate.create({ data });
80
+ if (!result.ok) {
81
+ throw result.error;
82
+ }
83
+ return result.data;
84
+ },
85
+ async update(id, data) {
86
+ const result = await delegate.update({ where: { id }, data });
87
+ if (!result.ok) {
88
+ throw result.error;
89
+ }
90
+ return result.data;
91
+ },
92
+ async delete(id) {
93
+ const result = await delegate.delete({ where: { id } });
94
+ if (!result.ok) {
95
+ return null;
96
+ }
97
+ return result.data;
98
+ }
99
+ };
100
+ }
101
+ // src/migration/auto-migrate.ts
102
+ import { isErr } from "@vertz/errors";
33
103
 
34
104
  // src/migration/differ.ts
35
105
  function columnSimilarity(a, b) {
@@ -189,10 +259,26 @@ function computeDiff(before, after) {
189
259
  }
190
260
  return { changes };
191
261
  }
262
+
192
263
  // src/migration/runner.ts
193
- import { createHash } from "node:crypto";
264
+ import {
265
+ createMigrationQueryError,
266
+ err,
267
+ ok
268
+ } from "@vertz/errors";
194
269
  var HISTORY_TABLE = "_vertz_migrations";
195
- var CREATE_HISTORY_SQL = `
270
+ function buildCreateHistorySql(dialect) {
271
+ if (dialect.name === "sqlite") {
272
+ return `
273
+ CREATE TABLE IF NOT EXISTS "${HISTORY_TABLE}" (
274
+ "id" INTEGER PRIMARY KEY AUTOINCREMENT,
275
+ "name" TEXT NOT NULL UNIQUE,
276
+ "checksum" TEXT NOT NULL,
277
+ "applied_at" TEXT NOT NULL DEFAULT (datetime('now'))
278
+ );
279
+ `;
280
+ }
281
+ return `
196
282
  CREATE TABLE IF NOT EXISTS "${HISTORY_TABLE}" (
197
283
  "id" serial PRIMARY KEY,
198
284
  "name" text NOT NULL UNIQUE,
@@ -200,8 +286,9 @@ CREATE TABLE IF NOT EXISTS "${HISTORY_TABLE}" (
200
286
  "applied_at" timestamp with time zone NOT NULL DEFAULT now()
201
287
  );
202
288
  `;
203
- function computeChecksum(sql) {
204
- return createHash("sha256").update(sql).digest("hex");
289
+ }
290
+ async function computeChecksum(sql) {
291
+ return sha256Hex(sql);
205
292
  }
206
293
  function parseMigrationName(filename) {
207
294
  const match = filename.match(/^(\d+)_(.+)\.sql$/);
@@ -212,52 +299,75 @@ function parseMigrationName(filename) {
212
299
  name: filename
213
300
  };
214
301
  }
215
- function createMigrationRunner() {
302
+ function createMigrationRunner(options) {
303
+ const dialect = options?.dialect ?? defaultPostgresDialect;
304
+ const createHistorySql = buildCreateHistorySql(dialect);
216
305
  return {
217
306
  async createHistoryTable(queryFn) {
218
- await queryFn(CREATE_HISTORY_SQL, []);
307
+ try {
308
+ await queryFn(createHistorySql, []);
309
+ return ok(undefined);
310
+ } catch (cause) {
311
+ return err(createMigrationQueryError("Failed to create migration history table", {
312
+ sql: createHistorySql,
313
+ cause
314
+ }));
315
+ }
219
316
  },
220
- async apply(queryFn, sql, name, options) {
221
- const checksum = computeChecksum(sql);
222
- const recordSql = `INSERT INTO "${HISTORY_TABLE}" ("name", "checksum") VALUES ($1, $2)`;
317
+ async apply(queryFn, sql, name, options2) {
318
+ const checksum = await computeChecksum(sql);
319
+ const recordSql = `INSERT INTO "${HISTORY_TABLE}" ("name", "checksum") VALUES (${dialect.param(1)}, ${dialect.param(2)})`;
223
320
  const statements = [sql, recordSql];
224
- if (options?.dryRun) {
225
- return {
321
+ if (options2?.dryRun) {
322
+ return ok({
226
323
  name,
227
324
  sql,
228
325
  checksum,
229
326
  dryRun: true,
230
327
  statements
231
- };
328
+ });
329
+ }
330
+ try {
331
+ await queryFn(sql, []);
332
+ await queryFn(recordSql, [name, checksum]);
333
+ return ok({
334
+ name,
335
+ sql,
336
+ checksum,
337
+ dryRun: false,
338
+ statements
339
+ });
340
+ } catch (cause) {
341
+ return err(createMigrationQueryError(`Failed to apply migration: ${name}`, {
342
+ sql,
343
+ cause
344
+ }));
232
345
  }
233
- await queryFn(sql, []);
234
- await queryFn(recordSql, [name, checksum]);
235
- return {
236
- name,
237
- sql,
238
- checksum,
239
- dryRun: false,
240
- statements
241
- };
242
346
  },
243
347
  async getApplied(queryFn) {
244
- const result = await queryFn(`SELECT "name", "checksum", "applied_at" FROM "${HISTORY_TABLE}" ORDER BY "id" ASC`, []);
245
- return result.rows.map((row) => ({
246
- name: row.name,
247
- checksum: row.checksum,
248
- appliedAt: new Date(row.applied_at)
249
- }));
348
+ try {
349
+ const result = await queryFn(`SELECT "name", "checksum", "applied_at" FROM "${HISTORY_TABLE}" ORDER BY "id" ASC`, []);
350
+ return ok(result.rows.map((row) => ({
351
+ name: row.name,
352
+ checksum: row.checksum,
353
+ appliedAt: new Date(row.applied_at)
354
+ })));
355
+ } catch (cause) {
356
+ return err(createMigrationQueryError("Failed to retrieve applied migrations", {
357
+ cause
358
+ }));
359
+ }
250
360
  },
251
361
  getPending(files, applied) {
252
362
  const appliedNames = new Set(applied.map((a) => a.name));
253
363
  return files.filter((f) => !appliedNames.has(f.name)).sort((a, b) => a.timestamp - b.timestamp);
254
364
  },
255
- detectDrift(files, applied) {
365
+ async detectDrift(files, applied) {
256
366
  const drifted = [];
257
367
  const appliedMap = new Map(applied.map((a) => [a.name, a.checksum]));
258
368
  for (const file of files) {
259
369
  const appliedChecksum = appliedMap.get(file.name);
260
- if (appliedChecksum && appliedChecksum !== computeChecksum(file.sql)) {
370
+ if (appliedChecksum && appliedChecksum !== await computeChecksum(file.sql)) {
261
371
  drifted.push(file.name);
262
372
  }
263
373
  }
@@ -278,27 +388,55 @@ function createMigrationRunner() {
278
388
  };
279
389
  }
280
390
 
281
- // src/migration/files.ts
282
- function formatMigrationFilename(num, description) {
283
- return `${String(num).padStart(4, "0")}_${description}.sql`;
391
+ // src/migration/sql-generator.ts
392
+ function escapeSqlString(value) {
393
+ return value.replace(/'/g, "''");
284
394
  }
285
- function nextMigrationNumber(existingFiles) {
286
- let max = 0;
287
- for (const file of existingFiles) {
288
- const parsed = parseMigrationName(file);
289
- if (parsed && parsed.timestamp > max) {
290
- max = parsed.timestamp;
395
+ function isEnumType(col, enums) {
396
+ const typeLower = col.type.toLowerCase();
397
+ if (enums) {
398
+ for (const enumName of Object.keys(enums)) {
399
+ if (typeLower === enumName.toLowerCase() || typeLower === enumName) {
400
+ return true;
401
+ }
291
402
  }
292
403
  }
293
- return max + 1;
404
+ return false;
294
405
  }
295
- // src/migration/sql-generator.ts
296
- function escapeSqlString(value) {
297
- return value.replace(/'/g, "''");
406
+ function getEnumValues(col, enums) {
407
+ if (!enums)
408
+ return;
409
+ for (const [enumName, values] of Object.entries(enums)) {
410
+ if (col.type.toLowerCase() === enumName.toLowerCase() || col.type === enumName) {
411
+ return values;
412
+ }
413
+ }
414
+ return;
298
415
  }
299
- function columnDef(name, col) {
416
+ function columnDef(name, col, dialect, enums) {
300
417
  const snakeName = camelToSnake(name);
301
- const parts = [`"${snakeName}" ${col.type}`];
418
+ const isEnum = isEnumType(col, enums);
419
+ let sqlType;
420
+ let checkConstraint;
421
+ if (isEnum && dialect.name === "sqlite") {
422
+ sqlType = dialect.mapColumnType("text");
423
+ const enumValues = getEnumValues(col, enums);
424
+ if (enumValues && enumValues.length > 0) {
425
+ const escapedValues = enumValues.map((v) => `'${escapeSqlString(v)}'`).join(", ");
426
+ checkConstraint = `CHECK("${snakeName}" IN (${escapedValues}))`;
427
+ }
428
+ } else {
429
+ if (dialect.name === "postgres" && col.type === col.type.toLowerCase()) {
430
+ sqlType = col.type;
431
+ } else {
432
+ const normalizedType = normalizeColumnType(col.type);
433
+ sqlType = dialect.mapColumnType(normalizedType);
434
+ }
435
+ }
436
+ const parts = [`"${snakeName}" ${sqlType}`];
437
+ if (checkConstraint) {
438
+ parts.push(checkConstraint);
439
+ }
302
440
  if (!col.nullable) {
303
441
  parts.push("NOT NULL");
304
442
  }
@@ -310,12 +448,44 @@ function columnDef(name, col) {
310
448
  }
311
449
  return parts.join(" ");
312
450
  }
313
- function generateMigrationSql(changes, ctx) {
451
+ function normalizeColumnType(type) {
452
+ const typeUpper = type.toUpperCase();
453
+ const typeMap = {
454
+ UUID: "uuid",
455
+ TEXT: "text",
456
+ INTEGER: "integer",
457
+ SERIAL: "serial",
458
+ BOOLEAN: "boolean",
459
+ TIMESTAMPTZ: "timestamp",
460
+ TIMESTAMP: "timestamp",
461
+ "DOUBLE PRECISION": "float",
462
+ JSONB: "json",
463
+ JSON: "json",
464
+ NUMERIC: "decimal",
465
+ REAL: "float",
466
+ VARCHAR: "varchar"
467
+ };
468
+ return typeMap[typeUpper] || type.toLowerCase();
469
+ }
470
+ function generateMigrationSql(changes, ctx, dialect = defaultPostgresDialect) {
314
471
  const statements = [];
315
472
  const tables = ctx?.tables;
316
473
  const enums = ctx?.enums;
317
474
  for (const change of changes) {
318
475
  switch (change.type) {
476
+ case "enum_added": {
477
+ if (!change.enumName)
478
+ break;
479
+ if (dialect.name === "postgres") {
480
+ const values = enums?.[change.enumName];
481
+ if (!values || values.length === 0)
482
+ break;
483
+ const enumSnakeName = camelToSnake(change.enumName);
484
+ const valuesStr = values.map((v) => `'${escapeSqlString(v)}'`).join(", ");
485
+ statements.push(`CREATE TYPE "${enumSnakeName}" AS ENUM (${valuesStr});`);
486
+ }
487
+ break;
488
+ }
319
489
  case "table_added": {
320
490
  if (!change.table)
321
491
  break;
@@ -325,8 +495,23 @@ function generateMigrationSql(changes, ctx) {
325
495
  const tableName = camelToSnake(change.table);
326
496
  const cols = [];
327
497
  const primaryKeys = [];
498
+ if (dialect.name === "postgres" && enums) {
499
+ for (const [, col] of Object.entries(table.columns)) {
500
+ if (isEnumType(col, enums)) {
501
+ const enumValues = getEnumValues(col, enums);
502
+ if (enumValues && enumValues.length > 0) {
503
+ const enumSnakeName = camelToSnake(col.type);
504
+ const alreadyEmitted = statements.some((s) => s.includes(`CREATE TYPE "${enumSnakeName}"`));
505
+ if (!alreadyEmitted) {
506
+ const valuesStr = enumValues.map((v) => `'${escapeSqlString(v)}'`).join(", ");
507
+ statements.push(`CREATE TYPE "${enumSnakeName}" AS ENUM (${valuesStr});`);
508
+ }
509
+ }
510
+ }
511
+ }
512
+ }
328
513
  for (const [colName, col] of Object.entries(table.columns)) {
329
- cols.push(` ${columnDef(colName, col)}`);
514
+ cols.push(` ${columnDef(colName, col, dialect, enums)}`);
330
515
  if (col.primary) {
331
516
  primaryKeys.push(`"${camelToSnake(colName)}"`);
332
517
  }
@@ -346,7 +531,7 @@ ${cols.join(`,
346
531
  );`);
347
532
  for (const idx of table.indexes) {
348
533
  const idxCols = idx.columns.map((c) => `"${camelToSnake(c)}"`).join(", ");
349
- const idxName = `idx_${tableName}_${idx.columns.map(camelToSnake).join("_")}`;
534
+ const idxName = `idx_${tableName}_${idx.columns.map((c) => camelToSnake(c)).join("_")}`;
350
535
  statements.push(`CREATE INDEX "${idxName}" ON "${tableName}" (${idxCols});`);
351
536
  }
352
537
  break;
@@ -363,7 +548,7 @@ ${cols.join(`,
363
548
  const col = tables?.[change.table]?.columns[change.column];
364
549
  if (!col)
365
550
  break;
366
- statements.push(`ALTER TABLE "${camelToSnake(change.table)}" ADD COLUMN ${columnDef(change.column, col)};`);
551
+ statements.push(`ALTER TABLE "${camelToSnake(change.table)}" ADD COLUMN ${columnDef(change.column, col, dialect, enums)};`);
367
552
  break;
368
553
  }
369
554
  case "column_removed": {
@@ -407,7 +592,7 @@ ${cols.join(`,
407
592
  break;
408
593
  const snakeTable = camelToSnake(change.table);
409
594
  const idxCols = change.columns.map((c) => `"${camelToSnake(c)}"`).join(", ");
410
- const idxName = `idx_${snakeTable}_${change.columns.map(camelToSnake).join("_")}`;
595
+ const idxName = `idx_${snakeTable}_${change.columns.map((c) => camelToSnake(c)).join("_")}`;
411
596
  statements.push(`CREATE INDEX "${idxName}" ON "${snakeTable}" (${idxCols});`);
412
597
  break;
413
598
  }
@@ -415,33 +600,26 @@ ${cols.join(`,
415
600
  if (!change.table || !change.columns)
416
601
  break;
417
602
  const snakeTable = camelToSnake(change.table);
418
- const idxName = `idx_${snakeTable}_${change.columns.map(camelToSnake).join("_")}`;
603
+ const idxName = `idx_${snakeTable}_${change.columns.map((c) => camelToSnake(c)).join("_")}`;
419
604
  statements.push(`DROP INDEX "${idxName}";`);
420
605
  break;
421
606
  }
422
- case "enum_added": {
423
- if (!change.enumName)
424
- break;
425
- const values = enums?.[change.enumName];
426
- if (!values || values.length === 0)
427
- break;
428
- const enumSnakeName = camelToSnake(change.enumName);
429
- const valuesStr = values.map((v) => `'${escapeSqlString(v)}'`).join(", ");
430
- statements.push(`CREATE TYPE "${enumSnakeName}" AS ENUM (${valuesStr});`);
431
- break;
432
- }
433
607
  case "enum_removed": {
434
608
  if (!change.enumName)
435
609
  break;
436
- statements.push(`DROP TYPE "${camelToSnake(change.enumName)}";`);
610
+ if (dialect.name === "postgres") {
611
+ statements.push(`DROP TYPE "${camelToSnake(change.enumName)}";`);
612
+ }
437
613
  break;
438
614
  }
439
615
  case "enum_altered": {
440
616
  if (!change.enumName || !change.addedValues)
441
617
  break;
442
- const enumSnakeName = camelToSnake(change.enumName);
443
- for (const val of change.addedValues) {
444
- statements.push(`ALTER TYPE "${enumSnakeName}" ADD VALUE '${escapeSqlString(val)}';`);
618
+ if (dialect.name === "postgres") {
619
+ const enumSnakeName = camelToSnake(change.enumName);
620
+ for (const val of change.addedValues) {
621
+ statements.push(`ALTER TYPE "${enumSnakeName}" ADD VALUE '${escapeSqlString(val)}';`);
622
+ }
445
623
  }
446
624
  break;
447
625
  }
@@ -451,22 +629,397 @@ ${cols.join(`,
451
629
 
452
630
  `);
453
631
  }
632
+ // src/migration/files.ts
633
+ function formatMigrationFilename(num, description) {
634
+ return `${String(num).padStart(4, "0")}_${description}.sql`;
635
+ }
636
+ function nextMigrationNumber(existingFiles) {
637
+ let max = 0;
638
+ for (const file of existingFiles) {
639
+ const parsed = parseMigrationName(file);
640
+ if (parsed && parsed.timestamp > max) {
641
+ max = parsed.timestamp;
642
+ }
643
+ }
644
+ return max + 1;
645
+ }
646
+ // src/migration/introspect.ts
647
+ var SQLITE_EXCLUDED_TABLES = new Set(["sqlite_sequence", "_vertz_migrations"]);
648
+ function validateIdentifier(name) {
649
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) {
650
+ throw new Error(`Invalid SQL identifier: "${name}"`);
651
+ }
652
+ return name;
653
+ }
654
+ function mapSqliteType(rawType) {
655
+ const upper = rawType.toUpperCase();
656
+ if (upper === "TEXT")
657
+ return "text";
658
+ if (upper === "INTEGER" || upper === "INT")
659
+ return "integer";
660
+ if (upper === "REAL")
661
+ return "float";
662
+ if (upper === "BLOB")
663
+ return "blob";
664
+ return rawType.toLowerCase();
665
+ }
666
+ async function introspectSqlite(queryFn) {
667
+ const snapshot = {
668
+ version: 1,
669
+ tables: {},
670
+ enums: {}
671
+ };
672
+ const { rows: tableRows } = await queryFn("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'", []);
673
+ for (const row of tableRows) {
674
+ const tableName = row.name;
675
+ if (SQLITE_EXCLUDED_TABLES.has(tableName))
676
+ continue;
677
+ const columns = {};
678
+ const { rows: colRows } = await queryFn(`PRAGMA table_info("${validateIdentifier(tableName)}")`, []);
679
+ for (const col of colRows) {
680
+ const colName = col.name;
681
+ const colSnap = {
682
+ type: mapSqliteType(col.type),
683
+ nullable: col.notnull === 0 && col.pk === 0,
684
+ primary: col.pk > 0,
685
+ unique: false
686
+ };
687
+ if (col.dflt_value != null) {
688
+ colSnap.default = String(col.dflt_value);
689
+ }
690
+ columns[colName] = colSnap;
691
+ }
692
+ const indexes = [];
693
+ const { rows: indexRows } = await queryFn(`PRAGMA index_list("${validateIdentifier(tableName)}")`, []);
694
+ for (const idx of indexRows) {
695
+ const idxName = idx.name;
696
+ const isUnique = idx.unique === 1;
697
+ const origin = idx.origin;
698
+ const { rows: idxInfoRows } = await queryFn(`PRAGMA index_info("${validateIdentifier(idxName)}")`, []);
699
+ const idxColumns = idxInfoRows.map((r) => r.name);
700
+ if (isUnique && idxColumns.length === 1 && origin === "u") {
701
+ const colName = idxColumns[0];
702
+ if (colName && columns[colName]) {
703
+ columns[colName].unique = true;
704
+ }
705
+ }
706
+ if (origin === "c") {
707
+ indexes.push({
708
+ columns: idxColumns,
709
+ name: idxName,
710
+ unique: isUnique
711
+ });
712
+ }
713
+ }
714
+ const foreignKeys = [];
715
+ const { rows: fkRows } = await queryFn(`PRAGMA foreign_key_list("${validateIdentifier(tableName)}")`, []);
716
+ for (const fk of fkRows) {
717
+ foreignKeys.push({
718
+ column: fk.from,
719
+ targetTable: fk.table,
720
+ targetColumn: fk.to
721
+ });
722
+ }
723
+ snapshot.tables[tableName] = {
724
+ columns,
725
+ indexes,
726
+ foreignKeys,
727
+ _metadata: {}
728
+ };
729
+ }
730
+ return snapshot;
731
+ }
732
+ var PG_EXCLUDED_TABLES = new Set(["_vertz_migrations"]);
733
+ async function introspectPostgres(queryFn) {
734
+ const snapshot = {
735
+ version: 1,
736
+ tables: {},
737
+ enums: {}
738
+ };
739
+ const { rows: tableRows } = await queryFn("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE'", []);
740
+ const { rows: pkRows } = await queryFn(`SELECT kcu.table_name, kcu.column_name
741
+ FROM information_schema.table_constraints tc
742
+ JOIN information_schema.key_column_usage kcu
743
+ ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
744
+ WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = 'public'`, []);
745
+ const pkMap = new Map;
746
+ for (const pk of pkRows) {
747
+ const tbl = pk.table_name;
748
+ if (!pkMap.has(tbl))
749
+ pkMap.set(tbl, new Set);
750
+ pkMap.get(tbl)?.add(pk.column_name);
751
+ }
752
+ const { rows: uniqueRows } = await queryFn(`SELECT tc.table_name, kcu.column_name, tc.constraint_name
753
+ FROM information_schema.table_constraints tc
754
+ JOIN information_schema.key_column_usage kcu
755
+ ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
756
+ WHERE tc.constraint_type = 'UNIQUE' AND tc.table_schema = 'public'`, []);
757
+ const uniqueConstraintCols = new Map;
758
+ for (const u of uniqueRows) {
759
+ const cName = u.constraint_name;
760
+ if (!uniqueConstraintCols.has(cName)) {
761
+ uniqueConstraintCols.set(cName, { table: u.table_name, columns: [] });
762
+ }
763
+ uniqueConstraintCols.get(cName)?.columns.push(u.column_name);
764
+ }
765
+ const uniqueColMap = new Map;
766
+ for (const [, val] of uniqueConstraintCols) {
767
+ if (val.columns.length === 1) {
768
+ if (!uniqueColMap.has(val.table))
769
+ uniqueColMap.set(val.table, new Set);
770
+ uniqueColMap.get(val.table)?.add(val.columns[0]);
771
+ }
772
+ }
773
+ for (const row of tableRows) {
774
+ const tableName = row.table_name;
775
+ if (PG_EXCLUDED_TABLES.has(tableName))
776
+ continue;
777
+ const columns = {};
778
+ const pkCols = pkMap.get(tableName) ?? new Set;
779
+ const uniqueCols = uniqueColMap.get(tableName) ?? new Set;
780
+ const { rows: colRows } = await queryFn(`SELECT column_name, data_type, is_nullable, column_default
781
+ FROM information_schema.columns
782
+ WHERE table_name = $1 AND table_schema = 'public'
783
+ ORDER BY ordinal_position`, [tableName]);
784
+ for (const col of colRows) {
785
+ const colName = col.column_name;
786
+ const isPrimary = pkCols.has(colName);
787
+ const isUnique = uniqueCols.has(colName);
788
+ const colSnap = {
789
+ type: col.data_type,
790
+ nullable: col.is_nullable === "YES",
791
+ primary: isPrimary,
792
+ unique: isUnique
793
+ };
794
+ if (col.column_default != null) {
795
+ colSnap.default = String(col.column_default);
796
+ }
797
+ columns[colName] = colSnap;
798
+ }
799
+ const foreignKeys = [];
800
+ const { rows: fkRows } = await queryFn(`SELECT
801
+ kcu.column_name,
802
+ ccu.table_name AS target_table,
803
+ ccu.column_name AS target_column
804
+ FROM information_schema.table_constraints tc
805
+ JOIN information_schema.key_column_usage kcu
806
+ ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
807
+ JOIN information_schema.constraint_column_usage ccu
808
+ ON tc.constraint_name = ccu.constraint_name AND tc.table_schema = ccu.table_schema
809
+ WHERE tc.constraint_type = 'FOREIGN KEY'
810
+ AND tc.table_name = $1
811
+ AND tc.table_schema = 'public'`, [tableName]);
812
+ for (const fk of fkRows) {
813
+ foreignKeys.push({
814
+ column: fk.column_name,
815
+ targetTable: fk.target_table,
816
+ targetColumn: fk.target_column
817
+ });
818
+ }
819
+ const indexes = [];
820
+ const { rows: idxRows } = await queryFn(`SELECT i.relname AS index_name,
821
+ array_agg(a.attname ORDER BY k.n) AS columns,
822
+ ix.indisunique AS is_unique
823
+ FROM pg_index ix
824
+ JOIN pg_class i ON i.oid = ix.indexrelid
825
+ JOIN pg_class t ON t.oid = ix.indrelid
826
+ JOIN pg_namespace ns ON ns.oid = t.relnamespace
827
+ JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS k(attnum, n) ON true
828
+ JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
829
+ WHERE t.relname = $1
830
+ AND ns.nspname = 'public'
831
+ AND NOT ix.indisprimary
832
+ AND NOT ix.indisunique
833
+ GROUP BY i.relname, ix.indisunique`, [tableName]);
834
+ for (const idx of idxRows) {
835
+ indexes.push({
836
+ columns: idx.columns,
837
+ name: idx.index_name,
838
+ unique: idx.is_unique
839
+ });
840
+ }
841
+ snapshot.tables[tableName] = {
842
+ columns,
843
+ indexes,
844
+ foreignKeys,
845
+ _metadata: {}
846
+ };
847
+ }
848
+ const { rows: enumRows } = await queryFn(`SELECT t.typname AS enum_name, e.enumlabel AS enum_value
849
+ FROM pg_type t
850
+ JOIN pg_enum e ON t.oid = e.enumtypid
851
+ JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
852
+ WHERE n.nspname = 'public'
853
+ ORDER BY t.typname, e.enumsortorder`, []);
854
+ for (const row of enumRows) {
855
+ const enumName = row.enum_name;
856
+ const enumValue = row.enum_value;
857
+ if (!snapshot.enums[enumName]) {
858
+ snapshot.enums[enumName] = [];
859
+ }
860
+ snapshot.enums[enumName].push(enumValue);
861
+ }
862
+ return snapshot;
863
+ }
864
+ // src/migration/journal.ts
865
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
866
+ import { dirname } from "node:path";
867
+ function createJournal() {
868
+ return {
869
+ version: 1,
870
+ migrations: []
871
+ };
872
+ }
873
+ function addJournalEntry(journal, entry) {
874
+ return {
875
+ ...journal,
876
+ migrations: [...journal.migrations, entry]
877
+ };
878
+ }
879
+ function detectCollisions(journal, existingFiles) {
880
+ const journalSeqNumbers = new Map;
881
+ for (const entry of journal.migrations) {
882
+ const parsed = parseMigrationName(entry.name);
883
+ if (parsed) {
884
+ journalSeqNumbers.set(parsed.timestamp, entry.name);
885
+ }
886
+ }
887
+ const allUsedSeqNumbers = new Set(journalSeqNumbers.keys());
888
+ for (const file of existingFiles) {
889
+ const parsed = parseMigrationName(file);
890
+ if (parsed) {
891
+ allUsedSeqNumbers.add(parsed.timestamp);
892
+ }
893
+ }
894
+ const collisions = [];
895
+ for (const file of existingFiles) {
896
+ const parsed = parseMigrationName(file);
897
+ if (!parsed)
898
+ continue;
899
+ const journalEntry = journalSeqNumbers.get(parsed.timestamp);
900
+ if (journalEntry && journalEntry !== file) {
901
+ let nextSeq = Math.max(...allUsedSeqNumbers) + 1;
902
+ for (const c of collisions) {
903
+ const suggestedParsed = parseMigrationName(c.suggestedName);
904
+ if (suggestedParsed && suggestedParsed.timestamp >= nextSeq) {
905
+ nextSeq = suggestedParsed.timestamp + 1;
906
+ }
907
+ }
908
+ const description = file.replace(/^\d+_/, "").replace(/\.sql$/, "");
909
+ collisions.push({
910
+ existingName: journalEntry,
911
+ conflictingName: file,
912
+ sequenceNumber: parsed.timestamp,
913
+ suggestedName: formatMigrationFilename(nextSeq, description)
914
+ });
915
+ }
916
+ }
917
+ return collisions;
918
+ }
919
+ // src/migration/snapshot.ts
920
+ function createSnapshot(tables) {
921
+ const snapshot = {
922
+ version: 1,
923
+ tables: {},
924
+ enums: {}
925
+ };
926
+ for (const table of tables) {
927
+ const columns = {};
928
+ const foreignKeys = [];
929
+ const indexes = [];
930
+ for (const [colName, col] of Object.entries(table._columns)) {
931
+ const meta = col._meta;
932
+ const colSnap = {
933
+ type: meta.sqlType,
934
+ nullable: meta.nullable,
935
+ primary: meta.primary,
936
+ unique: meta.unique
937
+ };
938
+ if (meta.hasDefault && meta.defaultValue !== undefined) {
939
+ const rawDefault = String(meta.defaultValue);
940
+ colSnap.default = rawDefault === "now" ? "now()" : rawDefault;
941
+ }
942
+ const annotationNames = meta._annotations ? Object.keys(meta._annotations) : [];
943
+ if (annotationNames.length > 0) {
944
+ colSnap.annotations = annotationNames;
945
+ }
946
+ columns[colName] = colSnap;
947
+ if (meta.references) {
948
+ foreignKeys.push({
949
+ column: colName,
950
+ targetTable: meta.references.table,
951
+ targetColumn: meta.references.column
952
+ });
953
+ }
954
+ if (meta.enumName && meta.enumValues) {
955
+ snapshot.enums[meta.enumName] = [...meta.enumValues];
956
+ }
957
+ }
958
+ for (const idx of table._indexes) {
959
+ indexes.push({ columns: [...idx.columns] });
960
+ }
961
+ snapshot.tables[table._name] = {
962
+ columns,
963
+ indexes,
964
+ foreignKeys,
965
+ _metadata: {}
966
+ };
967
+ }
968
+ return snapshot;
969
+ }
970
+ // src/cli/baseline.ts
971
+ async function baseline(options) {
972
+ const runner = createMigrationRunner({ dialect: options.dialect });
973
+ const createResult = await runner.createHistoryTable(options.queryFn);
974
+ if (!createResult.ok) {
975
+ return createResult;
976
+ }
977
+ const appliedResult = await runner.getApplied(options.queryFn);
978
+ if (!appliedResult.ok) {
979
+ return appliedResult;
980
+ }
981
+ const appliedNames = new Set(appliedResult.data.map((a) => a.name));
982
+ const recorded = [];
983
+ for (const file of options.migrationFiles) {
984
+ if (appliedNames.has(file.name)) {
985
+ continue;
986
+ }
987
+ const checksum = await computeChecksum(file.sql);
988
+ const dialect = options.dialect;
989
+ const param1 = dialect ? dialect.param(1) : "$1";
990
+ const param2 = dialect ? dialect.param(2) : "$2";
991
+ await options.queryFn(`INSERT INTO "_vertz_migrations" ("name", "checksum") VALUES (${param1}, ${param2})`, [file.name, checksum]);
992
+ recorded.push(file.name);
993
+ }
994
+ return {
995
+ ok: true,
996
+ data: { recorded }
997
+ };
998
+ }
454
999
  // src/cli/migrate-deploy.ts
455
1000
  async function migrateDeploy(options) {
456
1001
  const runner = createMigrationRunner();
457
1002
  const isDryRun = options.dryRun ?? false;
458
1003
  if (!isDryRun) {
459
- await runner.createHistoryTable(options.queryFn);
1004
+ const createResult = await runner.createHistoryTable(options.queryFn);
1005
+ if (!createResult.ok) {
1006
+ return createResult;
1007
+ }
460
1008
  }
461
1009
  let applied;
462
1010
  if (isDryRun) {
463
- try {
464
- applied = await runner.getApplied(options.queryFn);
465
- } catch {
1011
+ const appliedResult = await runner.getApplied(options.queryFn);
1012
+ if (!appliedResult.ok) {
466
1013
  applied = [];
1014
+ } else {
1015
+ applied = appliedResult.data;
467
1016
  }
468
1017
  } else {
469
- applied = await runner.getApplied(options.queryFn);
1018
+ const appliedResult = await runner.getApplied(options.queryFn);
1019
+ if (!appliedResult.ok) {
1020
+ return appliedResult;
1021
+ }
1022
+ applied = appliedResult.data;
470
1023
  }
471
1024
  const pending = runner.getPending(options.migrationFiles, applied);
472
1025
  const appliedNames = [];
@@ -475,17 +1028,72 @@ async function migrateDeploy(options) {
475
1028
  const result = await runner.apply(options.queryFn, migration.sql, migration.name, {
476
1029
  dryRun: isDryRun
477
1030
  });
1031
+ if (!result.ok) {
1032
+ return result;
1033
+ }
478
1034
  appliedNames.push(migration.name);
479
- migrationResults.push(result);
1035
+ migrationResults.push(result.data);
480
1036
  }
481
1037
  return {
482
- applied: appliedNames,
483
- alreadyApplied: applied.map((a) => a.name),
484
- dryRun: isDryRun,
485
- migrations: migrationResults.length > 0 ? migrationResults : undefined
1038
+ ok: true,
1039
+ data: {
1040
+ applied: appliedNames,
1041
+ alreadyApplied: applied.map((a) => a.name),
1042
+ dryRun: isDryRun,
1043
+ migrations: migrationResults.length > 0 ? migrationResults : undefined
1044
+ }
486
1045
  };
487
1046
  }
488
1047
  // src/cli/migrate-dev.ts
1048
+ function toKebab(str) {
1049
+ return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
1050
+ }
1051
+ function generateMigrationName(changes) {
1052
+ if (changes.length === 0)
1053
+ return "empty-migration";
1054
+ if (changes.length === 1) {
1055
+ const change = changes[0];
1056
+ switch (change.type) {
1057
+ case "table_added":
1058
+ return `add-${toKebab(change.table)}-table`;
1059
+ case "table_removed":
1060
+ return `drop-${toKebab(change.table)}-table`;
1061
+ case "column_added":
1062
+ return `add-${toKebab(change.column)}-to-${toKebab(change.table)}`;
1063
+ case "column_removed":
1064
+ return `drop-${toKebab(change.column)}-from-${toKebab(change.table)}`;
1065
+ case "column_altered":
1066
+ return `alter-${toKebab(change.column)}-in-${toKebab(change.table)}`;
1067
+ case "column_renamed":
1068
+ return `rename-${toKebab(change.oldColumn)}-to-${toKebab(change.newColumn)}-in-${toKebab(change.table)}`;
1069
+ case "index_added":
1070
+ return `add-index-to-${toKebab(change.table)}`;
1071
+ case "index_removed":
1072
+ return `drop-index-from-${toKebab(change.table)}`;
1073
+ case "enum_added":
1074
+ return `add-${toKebab(change.enumName)}-enum`;
1075
+ case "enum_removed":
1076
+ return `drop-${toKebab(change.enumName)}-enum`;
1077
+ case "enum_altered":
1078
+ return `alter-${toKebab(change.enumName)}-enum`;
1079
+ }
1080
+ }
1081
+ const tables = new Set(changes.map((c) => c.table).filter(Boolean));
1082
+ const types = new Set(changes.map((c) => c.type));
1083
+ if (tables.size === 1 && types.size === 1) {
1084
+ const table = [...tables][0];
1085
+ const type = [...types][0];
1086
+ switch (type) {
1087
+ case "column_added":
1088
+ return `add-columns-to-${toKebab(table)}`;
1089
+ case "column_removed":
1090
+ return `drop-columns-from-${toKebab(table)}`;
1091
+ default:
1092
+ return `update-${toKebab(table)}`;
1093
+ }
1094
+ }
1095
+ return "update-schema";
1096
+ }
489
1097
  async function migrateDev(options) {
490
1098
  const diff = computeDiff(options.previousSnapshot, options.currentSnapshot);
491
1099
  const sql = generateMigrationSql(diff.changes, {
@@ -498,19 +1106,40 @@ async function migrateDev(options) {
498
1106
  newColumn: c.newColumn,
499
1107
  confidence: c.confidence
500
1108
  }));
1109
+ const migrationName = options.migrationName ?? generateMigrationName(diff.changes);
501
1110
  const num = nextMigrationNumber(options.existingFiles);
502
- const filename = formatMigrationFilename(num, options.migrationName);
1111
+ const filename = formatMigrationFilename(num, migrationName);
503
1112
  const filePath = `${options.migrationsDir}/${filename}`;
1113
+ const journalPath = `${options.migrationsDir}/_journal.json`;
1114
+ let journal;
1115
+ try {
1116
+ const content = options.readFile ? await options.readFile(journalPath) : '{"version":1,"migrations":[]}';
1117
+ journal = JSON.parse(content);
1118
+ } catch {
1119
+ journal = createJournal();
1120
+ }
1121
+ const collisions = detectCollisions(journal, options.existingFiles);
504
1122
  if (options.dryRun) {
505
1123
  return {
506
1124
  migrationFile: filename,
507
1125
  sql,
508
1126
  dryRun: true,
509
1127
  renames: renames.length > 0 ? renames : undefined,
1128
+ collisions: collisions.length > 0 ? collisions : undefined,
510
1129
  snapshot: options.currentSnapshot
511
1130
  };
512
1131
  }
513
1132
  await options.writeFile(filePath, sql);
1133
+ const checksum = await computeChecksum(sql);
1134
+ journal = addJournalEntry(journal, {
1135
+ name: filename,
1136
+ description: migrationName,
1137
+ createdAt: new Date().toISOString(),
1138
+ checksum
1139
+ });
1140
+ await options.writeFile(journalPath, JSON.stringify(journal, null, 2));
1141
+ const snapshotPath = `${options.migrationsDir}/_snapshot.json`;
1142
+ await options.writeFile(snapshotPath, JSON.stringify(options.currentSnapshot, null, 2));
514
1143
  const runner = createMigrationRunner();
515
1144
  await runner.createHistoryTable(options.queryFn);
516
1145
  await runner.apply(options.queryFn, sql, filename, { dryRun: false });
@@ -520,6 +1149,7 @@ async function migrateDev(options) {
520
1149
  appliedAt: new Date,
521
1150
  dryRun: false,
522
1151
  renames: renames.length > 0 ? renames : undefined,
1152
+ collisions: collisions.length > 0 ? collisions : undefined,
523
1153
  snapshot: options.currentSnapshot
524
1154
  };
525
1155
  }
@@ -538,21 +1168,419 @@ async function push(options) {
538
1168
  ];
539
1169
  return { sql, tablesAffected };
540
1170
  }
1171
+ // src/cli/reset.ts
1172
+ import { createMigrationQueryError as createMigrationQueryError2, err as err2 } from "@vertz/errors";
1173
+ var HISTORY_TABLE2 = "_vertz_migrations";
1174
+ function getUserTablesQuery(dialect) {
1175
+ if (dialect.name === "sqlite") {
1176
+ return `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`;
1177
+ }
1178
+ return `SELECT tablename AS name FROM pg_tables WHERE schemaname = 'public'`;
1179
+ }
1180
+ function buildDropTableSql(tableName, dialect) {
1181
+ if (dialect.name === "postgres") {
1182
+ return `DROP TABLE IF EXISTS "${tableName}" CASCADE`;
1183
+ }
1184
+ return `DROP TABLE IF EXISTS "${tableName}"`;
1185
+ }
1186
+ async function reset(options) {
1187
+ const dialect = options.dialect ?? defaultPostgresDialect;
1188
+ const runner = createMigrationRunner({ dialect });
1189
+ let tableNames;
1190
+ try {
1191
+ const tablesResult = await options.queryFn(getUserTablesQuery(dialect), []);
1192
+ tableNames = tablesResult.rows.map((row) => row.name);
1193
+ } catch (cause) {
1194
+ return err2(createMigrationQueryError2("Failed to list user tables", { cause }));
1195
+ }
1196
+ const tablesDropped = [];
1197
+ for (const tableName of tableNames) {
1198
+ try {
1199
+ await options.queryFn(buildDropTableSql(tableName, dialect), []);
1200
+ if (tableName !== HISTORY_TABLE2) {
1201
+ tablesDropped.push(tableName);
1202
+ }
1203
+ } catch (cause) {
1204
+ return err2(createMigrationQueryError2(`Failed to drop table: ${tableName}`, { cause }));
1205
+ }
1206
+ }
1207
+ try {
1208
+ await options.queryFn(buildDropTableSql(HISTORY_TABLE2, dialect), []);
1209
+ } catch (cause) {
1210
+ return err2(createMigrationQueryError2("Failed to drop history table", { cause }));
1211
+ }
1212
+ const createResult = await runner.createHistoryTable(options.queryFn);
1213
+ if (!createResult.ok) {
1214
+ return createResult;
1215
+ }
1216
+ const sorted = [...options.migrationFiles].sort((a, b) => a.timestamp - b.timestamp);
1217
+ const migrationsApplied = [];
1218
+ for (const file of sorted) {
1219
+ const applyResult = await runner.apply(options.queryFn, file.sql, file.name);
1220
+ if (!applyResult.ok) {
1221
+ return applyResult;
1222
+ }
1223
+ migrationsApplied.push(file.name);
1224
+ }
1225
+ return {
1226
+ ok: true,
1227
+ data: { tablesDropped, migrationsApplied }
1228
+ };
1229
+ }
541
1230
  // src/cli/status.ts
1231
+ function diffChangeToCodeChange(change) {
1232
+ switch (change.type) {
1233
+ case "table_added":
1234
+ return {
1235
+ description: `Added table '${change.table}'`,
1236
+ type: change.type,
1237
+ table: change.table
1238
+ };
1239
+ case "table_removed":
1240
+ return {
1241
+ description: `Removed table '${change.table}'`,
1242
+ type: change.type,
1243
+ table: change.table
1244
+ };
1245
+ case "column_added":
1246
+ return {
1247
+ description: `Added column '${change.column}' to table '${change.table}'`,
1248
+ type: change.type,
1249
+ table: change.table,
1250
+ column: change.column
1251
+ };
1252
+ case "column_removed":
1253
+ return {
1254
+ description: `Removed column '${change.column}' from table '${change.table}'`,
1255
+ type: change.type,
1256
+ table: change.table,
1257
+ column: change.column
1258
+ };
1259
+ case "column_altered":
1260
+ return {
1261
+ description: `Altered column '${change.column}' in table '${change.table}'`,
1262
+ type: change.type,
1263
+ table: change.table,
1264
+ column: change.column
1265
+ };
1266
+ case "column_renamed":
1267
+ return {
1268
+ description: `Renamed column in table '${change.table}'`,
1269
+ type: change.type,
1270
+ table: change.table
1271
+ };
1272
+ case "index_added":
1273
+ return {
1274
+ description: `Added index on table '${change.table}'`,
1275
+ type: change.type,
1276
+ table: change.table
1277
+ };
1278
+ case "index_removed":
1279
+ return {
1280
+ description: `Removed index on table '${change.table}'`,
1281
+ type: change.type,
1282
+ table: change.table
1283
+ };
1284
+ case "enum_added":
1285
+ return {
1286
+ description: `Added enum type`,
1287
+ type: change.type
1288
+ };
1289
+ case "enum_removed":
1290
+ return {
1291
+ description: `Removed enum type`,
1292
+ type: change.type
1293
+ };
1294
+ case "enum_altered":
1295
+ return {
1296
+ description: `Altered enum type`,
1297
+ type: change.type
1298
+ };
1299
+ }
1300
+ }
1301
+ function detectSchemaDrift(expected, actual) {
1302
+ const drift = [];
1303
+ for (const tableName of Object.keys(actual.tables)) {
1304
+ if (!(tableName in expected.tables)) {
1305
+ drift.push({
1306
+ description: `Table '${tableName}' exists in database but not in schema`,
1307
+ type: "extra_table",
1308
+ table: tableName
1309
+ });
1310
+ }
1311
+ }
1312
+ for (const tableName of Object.keys(expected.tables)) {
1313
+ if (!(tableName in actual.tables)) {
1314
+ drift.push({
1315
+ description: `Table '${tableName}' exists in schema but not in database`,
1316
+ type: "missing_table",
1317
+ table: tableName
1318
+ });
1319
+ }
1320
+ }
1321
+ for (const tableName of Object.keys(expected.tables)) {
1322
+ if (!(tableName in actual.tables))
1323
+ continue;
1324
+ const expectedTable = expected.tables[tableName];
1325
+ const actualTable = actual.tables[tableName];
1326
+ if (!expectedTable || !actualTable)
1327
+ continue;
1328
+ for (const colName of Object.keys(actualTable.columns)) {
1329
+ if (!(colName in expectedTable.columns)) {
1330
+ drift.push({
1331
+ description: `Column '${colName}' exists in database table '${tableName}' but not in schema`,
1332
+ type: "extra_column",
1333
+ table: tableName,
1334
+ column: colName
1335
+ });
1336
+ }
1337
+ }
1338
+ for (const colName of Object.keys(expectedTable.columns)) {
1339
+ if (!(colName in actualTable.columns)) {
1340
+ drift.push({
1341
+ description: `Column '${colName}' exists in schema table '${tableName}' but not in database`,
1342
+ type: "missing_column",
1343
+ table: tableName,
1344
+ column: colName
1345
+ });
1346
+ }
1347
+ }
1348
+ for (const colName of Object.keys(expectedTable.columns)) {
1349
+ if (!(colName in actualTable.columns))
1350
+ continue;
1351
+ const expectedCol = expectedTable.columns[colName];
1352
+ const actualCol = actualTable.columns[colName];
1353
+ if (!expectedCol || !actualCol)
1354
+ continue;
1355
+ if (expectedCol.type !== actualCol.type) {
1356
+ drift.push({
1357
+ description: `Column '${colName}' in table '${tableName}' has type '${actualCol.type}' in database but '${expectedCol.type}' in schema`,
1358
+ type: "column_type_mismatch",
1359
+ table: tableName,
1360
+ column: colName
1361
+ });
1362
+ }
1363
+ }
1364
+ }
1365
+ return drift;
1366
+ }
542
1367
  async function migrateStatus(options) {
543
1368
  const runner = createMigrationRunner();
544
- await runner.createHistoryTable(options.queryFn);
545
- const applied = await runner.getApplied(options.queryFn);
1369
+ const createResult = await runner.createHistoryTable(options.queryFn);
1370
+ if (!createResult.ok) {
1371
+ return createResult;
1372
+ }
1373
+ const appliedResult = await runner.getApplied(options.queryFn);
1374
+ if (!appliedResult.ok) {
1375
+ return appliedResult;
1376
+ }
1377
+ const applied = appliedResult.data;
546
1378
  const pending = runner.getPending(options.migrationFiles, applied);
1379
+ let codeChanges = [];
1380
+ if (options.savedSnapshot && options.currentSnapshot) {
1381
+ const diff = computeDiff(options.savedSnapshot, options.currentSnapshot);
1382
+ codeChanges = diff.changes.map(diffChangeToCodeChange);
1383
+ }
1384
+ let drift = [];
1385
+ if (options.dialect) {
1386
+ const introspect = options.dialect.name === "sqlite" ? introspectSqlite : introspectPostgres;
1387
+ const actualSchema = await introspect(options.queryFn);
1388
+ const expectedSchema = options.savedSnapshot ?? options.currentSnapshot;
1389
+ if (expectedSchema) {
1390
+ drift = detectSchemaDrift(expectedSchema, actualSchema);
1391
+ }
1392
+ }
547
1393
  return {
548
- applied: applied.map((a) => ({
549
- name: a.name,
550
- checksum: a.checksum,
551
- appliedAt: a.appliedAt
552
- })),
553
- pending: pending.map((p) => p.name)
1394
+ ok: true,
1395
+ data: {
1396
+ applied: applied.map((a) => ({
1397
+ name: a.name,
1398
+ checksum: a.checksum,
1399
+ appliedAt: a.appliedAt
1400
+ })),
1401
+ pending: pending.map((p) => p.name),
1402
+ codeChanges,
1403
+ drift
1404
+ }
554
1405
  };
555
1406
  }
1407
+ // src/client/database.ts
1408
+ import { err as err3, ok as ok2 } from "@vertz/schema";
1409
+ // src/errors/error-codes.ts
1410
+ var DbErrorCode = {
1411
+ UNIQUE_VIOLATION: "23505",
1412
+ FOREIGN_KEY_VIOLATION: "23503",
1413
+ NOT_NULL_VIOLATION: "23502",
1414
+ CHECK_VIOLATION: "23514",
1415
+ EXCLUSION_VIOLATION: "23P01",
1416
+ SERIALIZATION_FAILURE: "40001",
1417
+ DEADLOCK_DETECTED: "40P01",
1418
+ CONNECTION_EXCEPTION: "08000",
1419
+ CONNECTION_DOES_NOT_EXIST: "08003",
1420
+ CONNECTION_FAILURE: "08006",
1421
+ NotFound: "NotFound",
1422
+ CONNECTION_ERROR: "CONNECTION_ERROR",
1423
+ POOL_EXHAUSTED: "POOL_EXHAUSTED"
1424
+ };
1425
+ var PgCodeToName = Object.fromEntries(Object.entries(DbErrorCode).map(([name, pgCode]) => [pgCode, name]));
1426
+ function resolveErrorCode(pgCode) {
1427
+ return PgCodeToName[pgCode];
1428
+ }
1429
+ // src/errors/http-adapter.ts
1430
+ function dbErrorToHttpError(error) {
1431
+ const body = error.toJSON();
1432
+ if (error instanceof UniqueConstraintError) {
1433
+ return { status: 409, body };
1434
+ }
1435
+ if (error instanceof NotFoundError) {
1436
+ return { status: 404, body };
1437
+ }
1438
+ if (error instanceof ForeignKeyError) {
1439
+ return { status: 422, body };
1440
+ }
1441
+ if (error instanceof NotNullError) {
1442
+ return { status: 422, body };
1443
+ }
1444
+ if (error instanceof CheckConstraintError) {
1445
+ return { status: 422, body };
1446
+ }
1447
+ if (error instanceof ConnectionError) {
1448
+ return { status: 503, body };
1449
+ }
1450
+ return { status: 500, body };
1451
+ }
1452
+ // src/errors.ts
1453
+ function toReadError(error, query) {
1454
+ if (typeof error === "object" && error !== null && "code" in error) {
1455
+ const errWithCode = error;
1456
+ if (errWithCode.code === "NotFound") {
1457
+ return {
1458
+ code: "NotFound",
1459
+ message: errWithCode.message,
1460
+ table: errWithCode.table ?? "unknown",
1461
+ cause: error
1462
+ };
1463
+ }
1464
+ if (errWithCode.code.startsWith("08")) {
1465
+ return {
1466
+ code: "CONNECTION_ERROR",
1467
+ message: errWithCode.message,
1468
+ cause: error
1469
+ };
1470
+ }
1471
+ return {
1472
+ code: "QUERY_ERROR",
1473
+ message: errWithCode.message,
1474
+ sql: query,
1475
+ cause: error
1476
+ };
1477
+ }
1478
+ if (error instanceof Error) {
1479
+ const message2 = error.message.toLowerCase();
1480
+ if (message2.includes("connection") || message2.includes("ECONNREFUSED") || message2.includes("timeout")) {
1481
+ return {
1482
+ code: "CONNECTION_ERROR",
1483
+ message: error.message,
1484
+ cause: error
1485
+ };
1486
+ }
1487
+ }
1488
+ const message = error instanceof Error ? error.message : String(error);
1489
+ return {
1490
+ code: "QUERY_ERROR",
1491
+ message,
1492
+ sql: query,
1493
+ cause: error
1494
+ };
1495
+ }
1496
+ function toWriteError(error, query) {
1497
+ if (error instanceof UniqueConstraintError) {
1498
+ return {
1499
+ code: "CONSTRAINT_ERROR",
1500
+ message: error.message,
1501
+ column: error.column,
1502
+ table: error.table,
1503
+ cause: error
1504
+ };
1505
+ }
1506
+ if (error instanceof ForeignKeyError) {
1507
+ return {
1508
+ code: "CONSTRAINT_ERROR",
1509
+ message: error.message,
1510
+ constraint: error.constraint,
1511
+ table: error.table,
1512
+ cause: error
1513
+ };
1514
+ }
1515
+ if (error instanceof NotNullError) {
1516
+ return {
1517
+ code: "CONSTRAINT_ERROR",
1518
+ message: error.message,
1519
+ column: error.column,
1520
+ table: error.table,
1521
+ cause: error
1522
+ };
1523
+ }
1524
+ if (error instanceof CheckConstraintError) {
1525
+ return {
1526
+ code: "CONSTRAINT_ERROR",
1527
+ message: error.message,
1528
+ constraint: error.constraint,
1529
+ table: error.table,
1530
+ cause: error
1531
+ };
1532
+ }
1533
+ if (error instanceof ConnectionError) {
1534
+ return {
1535
+ code: "CONNECTION_ERROR",
1536
+ message: error.message,
1537
+ cause: error
1538
+ };
1539
+ }
1540
+ if (typeof error === "object" && error !== null && "code" in error) {
1541
+ const pgError = error;
1542
+ if (pgError.code.startsWith("08")) {
1543
+ return {
1544
+ code: "CONNECTION_ERROR",
1545
+ message: pgError.message,
1546
+ cause: error
1547
+ };
1548
+ }
1549
+ if (pgError.code === "23505" || pgError.code === "23503" || pgError.code === "23502" || pgError.code === "23514") {
1550
+ if (pgError.code === "23505" || pgError.code === "23502") {
1551
+ return {
1552
+ code: "CONSTRAINT_ERROR",
1553
+ message: pgError.message,
1554
+ table: pgError.table,
1555
+ column: pgError.column,
1556
+ cause: error
1557
+ };
1558
+ } else {
1559
+ return {
1560
+ code: "CONSTRAINT_ERROR",
1561
+ message: pgError.message,
1562
+ table: pgError.table,
1563
+ constraint: pgError.constraint,
1564
+ cause: error
1565
+ };
1566
+ }
1567
+ }
1568
+ return {
1569
+ code: "QUERY_ERROR",
1570
+ message: pgError.message,
1571
+ sql: query,
1572
+ cause: error
1573
+ };
1574
+ }
1575
+ const message = error instanceof Error ? error.message : String(error);
1576
+ return {
1577
+ code: "QUERY_ERROR",
1578
+ message,
1579
+ sql: query,
1580
+ cause: error
1581
+ };
1582
+ }
1583
+
556
1584
  // src/query/aggregate.ts
557
1585
  async function count(queryFn, table, options) {
558
1586
  const allParams = [];
@@ -779,19 +1807,33 @@ async function groupBy(queryFn, table, options) {
779
1807
  }
780
1808
 
781
1809
  // src/query/crud.ts
1810
+ function fillGeneratedIds(table, data) {
1811
+ const filled = { ...data };
1812
+ for (const [name, col] of Object.entries(table._columns)) {
1813
+ const meta = col._meta;
1814
+ if (meta.generate && filled[name] === undefined) {
1815
+ if (meta.sqlType === "integer" || meta.sqlType === "serial" || meta.sqlType === "bigint") {
1816
+ throw new Error(`Column "${name}" has generate: '${meta.generate}' but is type '${meta.sqlType}'. ` + `ID generation is only supported on string column types (text, uuid, varchar).`);
1817
+ }
1818
+ filled[name] = generateId(meta.generate);
1819
+ }
1820
+ }
1821
+ return filled;
1822
+ }
782
1823
  function assertNonEmptyWhere(where, operation) {
783
1824
  if (Object.keys(where).length === 0) {
784
1825
  throw new Error(`${operation} requires a non-empty where clause. ` + "Passing an empty where object would affect all rows.");
785
1826
  }
786
1827
  }
787
- async function get(queryFn, table, options) {
1828
+ async function get(queryFn, table, options, dialect = defaultPostgresDialect) {
788
1829
  const columns = resolveSelectColumns(table, options?.select);
789
1830
  const result = buildSelect({
790
1831
  table: table._name,
791
1832
  columns,
792
1833
  where: options?.where,
793
1834
  orderBy: options?.orderBy,
794
- limit: 1
1835
+ limit: 1,
1836
+ dialect
795
1837
  });
796
1838
  const res = await executeQuery(queryFn, result.sql, result.params);
797
1839
  if (res.rows.length === 0) {
@@ -799,14 +1841,7 @@ async function get(queryFn, table, options) {
799
1841
  }
800
1842
  return mapRow(res.rows[0]);
801
1843
  }
802
- async function getOrThrow(queryFn, table, options) {
803
- const row = await get(queryFn, table, options);
804
- if (row === null) {
805
- throw new NotFoundError(table._name);
806
- }
807
- return row;
808
- }
809
- async function list(queryFn, table, options) {
1844
+ async function list(queryFn, table, options, dialect = defaultPostgresDialect) {
810
1845
  const columns = resolveSelectColumns(table, options?.select);
811
1846
  const result = buildSelect({
812
1847
  table: table._name,
@@ -816,12 +1851,13 @@ async function list(queryFn, table, options) {
816
1851
  limit: options?.limit,
817
1852
  offset: options?.offset,
818
1853
  cursor: options?.cursor,
819
- take: options?.take
1854
+ take: options?.take,
1855
+ dialect
820
1856
  });
821
1857
  const res = await executeQuery(queryFn, result.sql, result.params);
822
1858
  return mapRows(res.rows);
823
1859
  }
824
- async function listAndCount(queryFn, table, options) {
1860
+ async function listAndCount(queryFn, table, options, dialect = defaultPostgresDialect) {
825
1861
  const columns = resolveSelectColumns(table, options?.select);
826
1862
  const result = buildSelect({
827
1863
  table: table._name,
@@ -832,7 +1868,8 @@ async function listAndCount(queryFn, table, options) {
832
1868
  offset: options?.offset,
833
1869
  cursor: options?.cursor,
834
1870
  take: options?.take,
835
- withCount: true
1871
+ withCount: true,
1872
+ dialect
836
1873
  });
837
1874
  const res = await executeQuery(queryFn, result.sql, result.params);
838
1875
  const rows = res.rows;
@@ -847,55 +1884,97 @@ async function listAndCount(queryFn, table, options) {
847
1884
  });
848
1885
  return { data, total };
849
1886
  }
850
- async function create(queryFn, table, options) {
1887
+ async function create(queryFn, table, options, dialect = defaultPostgresDialect) {
851
1888
  const returningColumns = resolveSelectColumns(table, options.select);
852
1889
  const nowColumns = getTimestampColumns(table);
1890
+ const readOnlyCols = getReadOnlyColumns(table);
1891
+ const withIds = fillGeneratedIds(table, options.data);
1892
+ const filteredData = Object.fromEntries(Object.entries(withIds).filter(([key]) => {
1893
+ const col = table._columns[key];
1894
+ const meta = col ? col._meta : undefined;
1895
+ if (meta?.generate)
1896
+ return true;
1897
+ return !readOnlyCols.includes(key);
1898
+ }));
853
1899
  const result = buildInsert({
854
1900
  table: table._name,
855
- data: options.data,
1901
+ data: filteredData,
856
1902
  returning: returningColumns,
857
- nowColumns
1903
+ nowColumns,
1904
+ dialect
858
1905
  });
859
1906
  const res = await executeQuery(queryFn, result.sql, result.params);
860
1907
  return mapRow(res.rows[0]);
861
1908
  }
862
- async function createMany(queryFn, table, options) {
1909
+ async function createMany(queryFn, table, options, dialect = defaultPostgresDialect) {
863
1910
  if (options.data.length === 0) {
864
1911
  return { count: 0 };
865
1912
  }
866
1913
  const nowColumns = getTimestampColumns(table);
1914
+ const readOnlyCols = getReadOnlyColumns(table);
1915
+ const filteredData = options.data.map((row) => {
1916
+ const withIds = fillGeneratedIds(table, row);
1917
+ return Object.fromEntries(Object.entries(withIds).filter(([key]) => {
1918
+ const col = table._columns[key];
1919
+ const meta = col ? col._meta : undefined;
1920
+ if (meta?.generate)
1921
+ return true;
1922
+ return !readOnlyCols.includes(key);
1923
+ }));
1924
+ });
867
1925
  const result = buildInsert({
868
1926
  table: table._name,
869
- data: options.data,
870
- nowColumns
1927
+ data: filteredData,
1928
+ nowColumns,
1929
+ dialect
871
1930
  });
872
1931
  const res = await executeQuery(queryFn, result.sql, result.params);
873
1932
  return { count: res.rowCount };
874
1933
  }
875
- async function createManyAndReturn(queryFn, table, options) {
1934
+ async function createManyAndReturn(queryFn, table, options, dialect = defaultPostgresDialect) {
876
1935
  if (options.data.length === 0) {
877
1936
  return [];
878
1937
  }
879
1938
  const returningColumns = resolveSelectColumns(table, options.select);
880
1939
  const nowColumns = getTimestampColumns(table);
1940
+ const readOnlyCols = getReadOnlyColumns(table);
1941
+ const filteredData = options.data.map((row) => {
1942
+ const withIds = fillGeneratedIds(table, row);
1943
+ return Object.fromEntries(Object.entries(withIds).filter(([key]) => {
1944
+ const col = table._columns[key];
1945
+ const meta = col ? col._meta : undefined;
1946
+ if (meta?.generate)
1947
+ return true;
1948
+ return !readOnlyCols.includes(key);
1949
+ }));
1950
+ });
881
1951
  const result = buildInsert({
882
1952
  table: table._name,
883
- data: options.data,
1953
+ data: filteredData,
884
1954
  returning: returningColumns,
885
- nowColumns
1955
+ nowColumns,
1956
+ dialect
886
1957
  });
887
1958
  const res = await executeQuery(queryFn, result.sql, result.params);
888
1959
  return mapRows(res.rows);
889
1960
  }
890
- async function update(queryFn, table, options) {
1961
+ async function update(queryFn, table, options, dialect = defaultPostgresDialect) {
891
1962
  const returningColumns = resolveSelectColumns(table, options.select);
892
1963
  const nowColumns = getTimestampColumns(table);
1964
+ const readOnlyCols = getReadOnlyColumns(table);
1965
+ const autoUpdateCols = getAutoUpdateColumns(table);
1966
+ const filteredData = Object.fromEntries(Object.entries(options.data).filter(([key]) => !readOnlyCols.includes(key)));
1967
+ for (const col of autoUpdateCols) {
1968
+ filteredData[col] = "now";
1969
+ }
1970
+ const allNowColumns = [...new Set([...nowColumns, ...autoUpdateCols])];
893
1971
  const result = buildUpdate({
894
1972
  table: table._name,
895
- data: options.data,
1973
+ data: filteredData,
896
1974
  where: options.where,
897
1975
  returning: returningColumns,
898
- nowColumns
1976
+ nowColumns: allNowColumns,
1977
+ dialect
899
1978
  });
900
1979
  const res = await executeQuery(queryFn, result.sql, result.params);
901
1980
  if (res.rows.length === 0) {
@@ -903,44 +1982,69 @@ async function update(queryFn, table, options) {
903
1982
  }
904
1983
  return mapRow(res.rows[0]);
905
1984
  }
906
- async function updateMany(queryFn, table, options) {
1985
+ async function updateMany(queryFn, table, options, dialect = defaultPostgresDialect) {
907
1986
  assertNonEmptyWhere(options.where, "updateMany");
908
1987
  const nowColumns = getTimestampColumns(table);
1988
+ const readOnlyCols = getReadOnlyColumns(table);
1989
+ const autoUpdateCols = getAutoUpdateColumns(table);
1990
+ const filteredData = Object.fromEntries(Object.entries(options.data).filter(([key]) => !readOnlyCols.includes(key)));
1991
+ for (const col of autoUpdateCols) {
1992
+ filteredData[col] = "now";
1993
+ }
1994
+ const allNowColumns = [...new Set([...nowColumns, ...autoUpdateCols])];
909
1995
  const result = buildUpdate({
910
1996
  table: table._name,
911
- data: options.data,
1997
+ data: filteredData,
912
1998
  where: options.where,
913
- nowColumns
1999
+ nowColumns: allNowColumns,
2000
+ dialect
914
2001
  });
915
2002
  const res = await executeQuery(queryFn, result.sql, result.params);
916
2003
  return { count: res.rowCount };
917
2004
  }
918
- async function upsert(queryFn, table, options) {
2005
+ async function upsert(queryFn, table, options, dialect = defaultPostgresDialect) {
919
2006
  const returningColumns = resolveSelectColumns(table, options.select);
920
2007
  const nowColumns = getTimestampColumns(table);
2008
+ const readOnlyCols = getReadOnlyColumns(table);
2009
+ const autoUpdateCols = getAutoUpdateColumns(table);
921
2010
  const conflictColumns = Object.keys(options.where);
922
- const updateColumns = Object.keys(options.update);
2011
+ const createWithIds = fillGeneratedIds(table, options.create);
2012
+ const filteredCreate = Object.fromEntries(Object.entries(createWithIds).filter(([key]) => {
2013
+ const col = table._columns[key];
2014
+ const meta = col ? col._meta : undefined;
2015
+ if (meta?.generate)
2016
+ return true;
2017
+ return !readOnlyCols.includes(key);
2018
+ }));
2019
+ const filteredUpdate = Object.fromEntries(Object.entries(options.update).filter(([key]) => !readOnlyCols.includes(key)));
2020
+ for (const col of autoUpdateCols) {
2021
+ filteredUpdate[col] = "now";
2022
+ }
2023
+ const allNowColumns = [...new Set([...nowColumns, ...autoUpdateCols])];
2024
+ const updateColumns = Object.keys(filteredUpdate);
923
2025
  const result = buildInsert({
924
2026
  table: table._name,
925
- data: options.create,
2027
+ data: filteredCreate,
926
2028
  returning: returningColumns,
927
- nowColumns,
2029
+ nowColumns: allNowColumns,
928
2030
  onConflict: {
929
2031
  columns: conflictColumns,
930
2032
  action: "update",
931
2033
  updateColumns,
932
- updateValues: options.update
933
- }
2034
+ updateValues: filteredUpdate
2035
+ },
2036
+ dialect
934
2037
  });
935
2038
  const res = await executeQuery(queryFn, result.sql, result.params);
936
2039
  return mapRow(res.rows[0]);
937
2040
  }
938
- async function deleteOne(queryFn, table, options) {
2041
+ async function deleteOne(queryFn, table, options, dialect = defaultPostgresDialect) {
939
2042
  const returningColumns = resolveSelectColumns(table, options.select);
940
2043
  const result = buildDelete({
941
2044
  table: table._name,
942
2045
  where: options.where,
943
- returning: returningColumns
2046
+ returning: returningColumns,
2047
+ dialect
944
2048
  });
945
2049
  const res = await executeQuery(queryFn, result.sql, result.params);
946
2050
  if (res.rows.length === 0) {
@@ -948,11 +2052,12 @@ async function deleteOne(queryFn, table, options) {
948
2052
  }
949
2053
  return mapRow(res.rows[0]);
950
2054
  }
951
- async function deleteMany(queryFn, table, options) {
2055
+ async function deleteMany(queryFn, table, options, dialect = defaultPostgresDialect) {
952
2056
  assertNonEmptyWhere(options.where, "deleteMany");
953
2057
  const result = buildDelete({
954
2058
  table: table._name,
955
- where: options.where
2059
+ where: options.where,
2060
+ dialect
956
2061
  });
957
2062
  const res = await executeQuery(queryFn, result.sql, result.params);
958
2063
  return { count: res.rowCount };
@@ -1187,60 +2292,95 @@ async function loadManyToManyRelation(queryFn, primaryRows, def, target, relName
1187
2292
  }
1188
2293
  }
1189
2294
 
1190
- // src/client/postgres-driver.ts
1191
- import postgresLib from "postgres";
1192
- function isPostgresError(error) {
1193
- return typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" && "message" in error && typeof error.message === "string";
1194
- }
1195
- function adaptPostgresError(error) {
1196
- if (isPostgresError(error)) {
1197
- const adapted = Object.assign(new Error(error.message), {
1198
- code: error.code,
1199
- message: error.message,
1200
- table: error.table_name,
1201
- column: error.column_name,
1202
- constraint: error.constraint_name,
1203
- detail: error.detail
1204
- });
1205
- throw adapted;
2295
+ // src/client/sqlite-value-converter.ts
2296
+ function fromSqliteValue(value, columnType) {
2297
+ if (columnType === "boolean") {
2298
+ if (value === 1) {
2299
+ return true;
2300
+ }
2301
+ if (value === 0) {
2302
+ return false;
2303
+ }
2304
+ }
2305
+ if ((columnType === "timestamp" || columnType === "timestamp with time zone") && typeof value === "string") {
2306
+ return new Date(value);
1206
2307
  }
1207
- throw error;
2308
+ return value;
1208
2309
  }
1209
- function createPostgresDriver(url, pool) {
1210
- const sql = postgresLib(url, {
1211
- max: pool?.max ?? 10,
1212
- idle_timeout: pool?.idleTimeout !== undefined ? pool.idleTimeout / 1000 : 30,
1213
- connect_timeout: pool?.connectionTimeout !== undefined ? pool.connectionTimeout / 1000 : 10,
1214
- fetch_types: false
1215
- });
1216
- const queryFn = async (sqlStr, params) => {
1217
- try {
1218
- const result = await sql.unsafe(sqlStr, params);
1219
- const rows = result.map((row) => {
1220
- const mapped = {};
1221
- for (const [key, value] of Object.entries(row)) {
1222
- mapped[key] = coerceValue(value);
2310
+
2311
+ // src/client/sqlite-driver.ts
2312
+ function buildTableSchema(models) {
2313
+ const registry = new Map;
2314
+ for (const [, entry] of Object.entries(models)) {
2315
+ const tableName = entry.table._name;
2316
+ const columnTypes = {};
2317
+ for (const [colName, colBuilder] of Object.entries(entry.table._columns)) {
2318
+ const meta = colBuilder._meta;
2319
+ columnTypes[colName] = meta.sqlType;
2320
+ }
2321
+ registry.set(tableName, columnTypes);
2322
+ }
2323
+ return registry;
2324
+ }
2325
+ function extractTableName(sql) {
2326
+ const fromMatch = sql.match(/\bFROM\s+"?([a-zA-Z_][a-zA-Z0-9_]*)"?/i);
2327
+ if (fromMatch) {
2328
+ return fromMatch[1].toLowerCase();
2329
+ }
2330
+ const insertMatch = sql.match(/\bINSERT\s+INTO\s+"?([a-zA-Z_][a-zA-Z0-9_]*)"?/i);
2331
+ if (insertMatch) {
2332
+ return insertMatch[1].toLowerCase();
2333
+ }
2334
+ const updateMatch = sql.match(/\bUPDATE\s+"?([a-zA-Z_][a-zA-Z0-9_]*)"?/i);
2335
+ if (updateMatch) {
2336
+ return updateMatch[1].toLowerCase();
2337
+ }
2338
+ const deleteMatch = sql.match(/\bDELETE\s+FROM\s+"?([a-zA-Z_][a-zA-Z0-9_]*)"?/i);
2339
+ if (deleteMatch) {
2340
+ return deleteMatch[1].toLowerCase();
2341
+ }
2342
+ return null;
2343
+ }
2344
+ function createSqliteDriver(d1, tableSchema) {
2345
+ const query = async (sql, params) => {
2346
+ const prepared = d1.prepare(sql);
2347
+ const bound = params ? prepared.bind(...params) : prepared;
2348
+ const result = await bound.all();
2349
+ if (tableSchema && result.results.length > 0) {
2350
+ const tableName = extractTableName(sql);
2351
+ if (tableName) {
2352
+ const schema = tableSchema.get(tableName);
2353
+ if (schema) {
2354
+ return result.results.map((row) => {
2355
+ const convertedRow = {};
2356
+ for (const [key, value] of Object.entries(row)) {
2357
+ const columnType = schema[key];
2358
+ if (columnType) {
2359
+ convertedRow[key] = fromSqliteValue(value, columnType);
2360
+ } else {
2361
+ convertedRow[key] = value;
2362
+ }
2363
+ }
2364
+ return convertedRow;
2365
+ });
1223
2366
  }
1224
- return mapped;
1225
- });
1226
- return {
1227
- rows,
1228
- rowCount: result.count ?? rows.length
1229
- };
1230
- } catch (error) {
1231
- adaptPostgresError(error);
2367
+ }
1232
2368
  }
2369
+ return result.results;
2370
+ };
2371
+ const execute = async (sql, params) => {
2372
+ const prepared = d1.prepare(sql);
2373
+ const bound = params ? prepared.bind(...params) : prepared;
2374
+ const result = await bound.run();
2375
+ return { rowsAffected: result.meta.changes };
1233
2376
  };
1234
2377
  return {
1235
- queryFn,
1236
- async close() {
1237
- await sql.end();
1238
- },
1239
- async isHealthy() {
2378
+ query,
2379
+ execute,
2380
+ close: async () => {},
2381
+ isHealthy: async () => {
1240
2382
  try {
1241
- const healthCheckTimeout = pool?.healthCheckTimeout ?? 5000;
1242
- const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("Health check timed out")), healthCheckTimeout));
1243
- await Promise.race([sql`SELECT 1`, timeout]);
2383
+ await query("SELECT 1");
1244
2384
  return true;
1245
2385
  } catch {
1246
2386
  return false;
@@ -1248,18 +2388,6 @@ function createPostgresDriver(url, pool) {
1248
2388
  }
1249
2389
  };
1250
2390
  }
1251
- function coerceValue(value) {
1252
- if (typeof value === "string" && isTimestampString(value)) {
1253
- const date = new Date(value);
1254
- if (!Number.isNaN(date.getTime())) {
1255
- return date;
1256
- }
1257
- }
1258
- return value;
1259
- }
1260
- function isTimestampString(value) {
1261
- return /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}/.test(value);
1262
- }
1263
2391
 
1264
2392
  // src/client/tenant-graph.ts
1265
2393
  function computeTenantGraph(registry) {
@@ -1379,16 +2507,31 @@ function isReadQuery(sqlStr) {
1379
2507
  }
1380
2508
  return upper.startsWith("SELECT");
1381
2509
  }
1382
- function resolveTable(tables, name) {
1383
- const entry = tables[name];
2510
+ function resolveModel(models, name) {
2511
+ const entry = models[name];
1384
2512
  if (!entry) {
1385
2513
  throw new Error(`Table "${name}" is not registered in the database.`);
1386
2514
  }
1387
2515
  return entry;
1388
2516
  }
2517
+ var RESERVED_MODEL_NAMES = new Set(["query", "close", "isHealthy", "_internals"]);
1389
2518
  function createDb(options) {
1390
- const { tables, log } = options;
1391
- const tenantGraph = computeTenantGraph(tables);
2519
+ const { models, log, dialect } = options;
2520
+ for (const key of Object.keys(models)) {
2521
+ if (RESERVED_MODEL_NAMES.has(key)) {
2522
+ throw new Error(`Model name "${key}" is reserved. Choose a different name for this model. ` + `Reserved names: ${[...RESERVED_MODEL_NAMES].join(", ")}`);
2523
+ }
2524
+ }
2525
+ if (dialect === "sqlite") {
2526
+ if (!options.d1) {
2527
+ throw new Error("SQLite dialect requires a D1 binding");
2528
+ }
2529
+ if (options.url) {
2530
+ throw new Error("SQLite dialect uses D1, not a connection URL");
2531
+ }
2532
+ }
2533
+ const dialectObj = dialect === "sqlite" ? defaultSqliteDialect : defaultPostgresDialect;
2534
+ const tenantGraph = computeTenantGraph(models);
1392
2535
  if (log && tenantGraph.root !== null) {
1393
2536
  const allScoped = new Set([
1394
2537
  ...tenantGraph.root !== null ? [tenantGraph.root] : [],
@@ -1396,28 +2539,51 @@ function createDb(options) {
1396
2539
  ...tenantGraph.indirectlyScoped,
1397
2540
  ...tenantGraph.shared
1398
2541
  ]);
1399
- for (const [key, entry] of Object.entries(tables)) {
2542
+ for (const [key, entry] of Object.entries(models)) {
1400
2543
  if (!allScoped.has(key)) {
1401
2544
  log(`[vertz/db] Table "${entry.table._name}" has no tenant path and is not marked .shared(). ` + "It will not be automatically scoped to a tenant.");
1402
2545
  }
1403
2546
  }
1404
2547
  }
1405
- const tablesRegistry = tables;
2548
+ const modelsRegistry = models;
1406
2549
  let driver = null;
2550
+ let sqliteDriver = null;
1407
2551
  let replicaDrivers = [];
1408
2552
  let replicaIndex = 0;
1409
2553
  const queryFn = (() => {
1410
2554
  if (options._queryFn) {
1411
2555
  return options._queryFn;
1412
2556
  }
2557
+ if (dialect === "sqlite" && options.d1) {
2558
+ const tableSchema = buildTableSchema(models);
2559
+ sqliteDriver = createSqliteDriver(options.d1, tableSchema);
2560
+ return async (sqlStr, params) => {
2561
+ if (!sqliteDriver) {
2562
+ throw new Error("SQLite driver not initialized");
2563
+ }
2564
+ const rows = await sqliteDriver.query(sqlStr, params);
2565
+ return { rows, rowCount: rows.length };
2566
+ };
2567
+ }
1413
2568
  if (options.url) {
1414
- driver = createPostgresDriver(options.url, options.pool);
1415
- const replicas = options.pool?.replicas;
1416
- if (replicas && replicas.length > 0) {
1417
- replicaDrivers = replicas.map((replicaUrl) => createPostgresDriver(replicaUrl, options.pool));
1418
- }
2569
+ let initialized = false;
2570
+ const initPostgres = async () => {
2571
+ if (initialized)
2572
+ return;
2573
+ const { createPostgresDriver } = await import("./shared/chunk-2gd1fqcw.js");
2574
+ driver = createPostgresDriver(options.url, options.pool);
2575
+ const replicas = options.pool?.replicas;
2576
+ if (replicas && replicas.length > 0) {
2577
+ replicaDrivers = replicas.map((replicaUrl) => createPostgresDriver(replicaUrl, options.pool));
2578
+ }
2579
+ initialized = true;
2580
+ };
1419
2581
  return async (sqlStr, params) => {
2582
+ await initPostgres();
1420
2583
  if (replicaDrivers.length === 0) {
2584
+ if (!driver) {
2585
+ throw new Error("Database driver not initialized");
2586
+ }
1421
2587
  return driver.queryFn(sqlStr, params);
1422
2588
  }
1423
2589
  if (isReadQuery(sqlStr)) {
@@ -1425,127 +2591,260 @@ function createDb(options) {
1425
2591
  replicaIndex = (replicaIndex + 1) % replicaDrivers.length;
1426
2592
  try {
1427
2593
  return await targetReplica.queryFn(sqlStr, params);
1428
- } catch (err) {
1429
- console.warn("[vertz/db] replica query failed, falling back to primary:", err.message);
2594
+ } catch (err4) {
2595
+ console.warn("[vertz/db] replica query failed, falling back to primary:", err4.message);
1430
2596
  }
1431
2597
  }
2598
+ if (!driver) {
2599
+ throw new Error("Database driver not initialized");
2600
+ }
1432
2601
  return driver.queryFn(sqlStr, params);
1433
2602
  };
1434
2603
  }
1435
2604
  return async () => {
1436
- throw new Error("db.query() requires a connected postgres driver. " + "Provide a `url` to connect to PostgreSQL, or `_queryFn` for testing.");
2605
+ throw new Error("db.query() requires a connected database driver. Provide a `url` to connect to PostgreSQL, a `dialect` with D1 binding for SQLite, or `_queryFn` for testing.");
1437
2606
  };
1438
2607
  })();
1439
- return {
1440
- _tables: tables,
1441
- $tenantGraph: tenantGraph,
1442
- async query(fragment) {
1443
- return executeQuery(queryFn, fragment.sql, fragment.params);
1444
- },
1445
- async close() {
1446
- if (driver) {
1447
- await driver.close();
2608
+ function implGet(name, opts) {
2609
+ return (async () => {
2610
+ try {
2611
+ const entry = resolveModel(models, name);
2612
+ const result = await get(queryFn, entry.table, opts, dialectObj);
2613
+ if (result !== null && opts?.include) {
2614
+ const rows = await loadRelations(queryFn, [result], entry.relations, opts.include, 0, modelsRegistry, entry.table);
2615
+ return ok2(rows[0] ?? null);
2616
+ }
2617
+ return ok2(result);
2618
+ } catch (e) {
2619
+ return err3(toReadError(e));
1448
2620
  }
1449
- await Promise.all(replicaDrivers.map((r) => r.close()));
1450
- },
1451
- async isHealthy() {
1452
- if (driver) {
1453
- return driver.isHealthy();
2621
+ })();
2622
+ }
2623
+ function implGetRequired(name, opts) {
2624
+ return (async () => {
2625
+ try {
2626
+ const entry = resolveModel(models, name);
2627
+ const result = await get(queryFn, entry.table, opts, dialectObj);
2628
+ if (result === null) {
2629
+ return err3({
2630
+ code: "NotFound",
2631
+ message: `Record not found in table ${name}`,
2632
+ table: name
2633
+ });
2634
+ }
2635
+ if (opts?.include) {
2636
+ const rows = await loadRelations(queryFn, [result], entry.relations, opts.include, 0, modelsRegistry, entry.table);
2637
+ return ok2(rows[0]);
2638
+ }
2639
+ return ok2(result);
2640
+ } catch (e) {
2641
+ return err3(toReadError(e));
1454
2642
  }
1455
- return true;
1456
- },
1457
- async get(name, opts) {
1458
- const entry = resolveTable(tables, name);
1459
- const result = await get(queryFn, entry.table, opts);
1460
- if (result !== null && opts?.include) {
1461
- const rows = await loadRelations(queryFn, [result], entry.relations, opts.include, 0, tablesRegistry, entry.table);
1462
- return rows[0] ?? null;
2643
+ })();
2644
+ }
2645
+ function implList(name, opts) {
2646
+ return (async () => {
2647
+ try {
2648
+ const entry = resolveModel(models, name);
2649
+ const results = await list(queryFn, entry.table, opts, dialectObj);
2650
+ if (opts?.include && results.length > 0) {
2651
+ const withRelations = await loadRelations(queryFn, results, entry.relations, opts.include, 0, modelsRegistry, entry.table);
2652
+ return ok2(withRelations);
2653
+ }
2654
+ return ok2(results);
2655
+ } catch (e) {
2656
+ return err3(toReadError(e));
1463
2657
  }
1464
- return result;
1465
- },
1466
- async getOrThrow(name, opts) {
1467
- const entry = resolveTable(tables, name);
1468
- const result = await getOrThrow(queryFn, entry.table, opts);
1469
- if (opts?.include) {
1470
- const rows = await loadRelations(queryFn, [result], entry.relations, opts.include, 0, tablesRegistry, entry.table);
1471
- return rows[0];
2658
+ })();
2659
+ }
2660
+ function implListAndCount(name, opts) {
2661
+ return (async () => {
2662
+ try {
2663
+ const entry = resolveModel(models, name);
2664
+ const { data, total } = await listAndCount(queryFn, entry.table, opts, dialectObj);
2665
+ if (opts?.include && data.length > 0) {
2666
+ const withRelations = await loadRelations(queryFn, data, entry.relations, opts.include, 0, modelsRegistry, entry.table);
2667
+ return ok2({ data: withRelations, total });
2668
+ }
2669
+ return ok2({ data, total });
2670
+ } catch (e) {
2671
+ return err3(toReadError(e));
1472
2672
  }
1473
- return result;
1474
- },
1475
- async list(name, opts) {
1476
- const entry = resolveTable(tables, name);
1477
- const results = await list(queryFn, entry.table, opts);
1478
- if (opts?.include && results.length > 0) {
1479
- return loadRelations(queryFn, results, entry.relations, opts.include, 0, tablesRegistry, entry.table);
2673
+ })();
2674
+ }
2675
+ function implCreate(name, opts) {
2676
+ return (async () => {
2677
+ try {
2678
+ const entry = resolveModel(models, name);
2679
+ const result = await create(queryFn, entry.table, opts, dialectObj);
2680
+ return ok2(result);
2681
+ } catch (e) {
2682
+ return err3(toWriteError(e));
1480
2683
  }
1481
- return results;
1482
- },
1483
- async listAndCount(name, opts) {
1484
- const entry = resolveTable(tables, name);
1485
- const { data, total } = await listAndCount(queryFn, entry.table, opts);
1486
- if (opts?.include && data.length > 0) {
1487
- const withRelations = await loadRelations(queryFn, data, entry.relations, opts.include, 0, tablesRegistry, entry.table);
1488
- return { data: withRelations, total };
1489
- }
1490
- return { data, total };
1491
- },
1492
- get findOne() {
1493
- return this.get;
1494
- },
1495
- get findOneOrThrow() {
1496
- return this.getOrThrow;
1497
- },
1498
- get findMany() {
1499
- return this.list;
1500
- },
1501
- get findManyAndCount() {
1502
- return this.listAndCount;
1503
- },
1504
- async create(name, opts) {
1505
- const entry = resolveTable(tables, name);
1506
- return create(queryFn, entry.table, opts);
1507
- },
1508
- async createMany(name, opts) {
1509
- const entry = resolveTable(tables, name);
1510
- return createMany(queryFn, entry.table, opts);
1511
- },
1512
- async createManyAndReturn(name, opts) {
1513
- const entry = resolveTable(tables, name);
1514
- return createManyAndReturn(queryFn, entry.table, opts);
1515
- },
1516
- async update(name, opts) {
1517
- const entry = resolveTable(tables, name);
1518
- return update(queryFn, entry.table, opts);
1519
- },
1520
- async updateMany(name, opts) {
1521
- const entry = resolveTable(tables, name);
1522
- return updateMany(queryFn, entry.table, opts);
1523
- },
1524
- async upsert(name, opts) {
1525
- const entry = resolveTable(tables, name);
1526
- return upsert(queryFn, entry.table, opts);
1527
- },
1528
- async delete(name, opts) {
1529
- const entry = resolveTable(tables, name);
1530
- return deleteOne(queryFn, entry.table, opts);
1531
- },
1532
- async deleteMany(name, opts) {
1533
- const entry = resolveTable(tables, name);
1534
- return deleteMany(queryFn, entry.table, opts);
1535
- },
1536
- async count(name, opts) {
1537
- const entry = resolveTable(tables, name);
1538
- return count(queryFn, entry.table, opts);
1539
- },
1540
- async aggregate(name, opts) {
1541
- const entry = resolveTable(tables, name);
1542
- return aggregate(queryFn, entry.table, opts);
1543
- },
1544
- async groupBy(name, opts) {
1545
- const entry = resolveTable(tables, name);
1546
- return groupBy(queryFn, entry.table, opts);
2684
+ })();
2685
+ }
2686
+ function implCreateMany(name, opts) {
2687
+ return (async () => {
2688
+ try {
2689
+ const entry = resolveModel(models, name);
2690
+ const result = await createMany(queryFn, entry.table, opts, dialectObj);
2691
+ return ok2(result);
2692
+ } catch (e) {
2693
+ return err3(toWriteError(e));
2694
+ }
2695
+ })();
2696
+ }
2697
+ function implCreateManyAndReturn(name, opts) {
2698
+ return (async () => {
2699
+ try {
2700
+ const entry = resolveModel(models, name);
2701
+ const result = await createManyAndReturn(queryFn, entry.table, opts, dialectObj);
2702
+ return ok2(result);
2703
+ } catch (e) {
2704
+ return err3(toWriteError(e));
2705
+ }
2706
+ })();
2707
+ }
2708
+ function implUpdate(name, opts) {
2709
+ return (async () => {
2710
+ try {
2711
+ const entry = resolveModel(models, name);
2712
+ const result = await update(queryFn, entry.table, opts, dialectObj);
2713
+ return ok2(result);
2714
+ } catch (e) {
2715
+ return err3(toWriteError(e));
2716
+ }
2717
+ })();
2718
+ }
2719
+ function implUpdateMany(name, opts) {
2720
+ return (async () => {
2721
+ try {
2722
+ const entry = resolveModel(models, name);
2723
+ const result = await updateMany(queryFn, entry.table, opts, dialectObj);
2724
+ return ok2(result);
2725
+ } catch (e) {
2726
+ return err3(toWriteError(e));
2727
+ }
2728
+ })();
2729
+ }
2730
+ function implUpsert(name, opts) {
2731
+ return (async () => {
2732
+ try {
2733
+ const entry = resolveModel(models, name);
2734
+ const result = await upsert(queryFn, entry.table, opts, dialectObj);
2735
+ return ok2(result);
2736
+ } catch (e) {
2737
+ return err3(toWriteError(e));
2738
+ }
2739
+ })();
2740
+ }
2741
+ function implDelete(name, opts) {
2742
+ return (async () => {
2743
+ try {
2744
+ const entry = resolveModel(models, name);
2745
+ const result = await deleteOne(queryFn, entry.table, opts, dialectObj);
2746
+ return ok2(result);
2747
+ } catch (e) {
2748
+ return err3(toWriteError(e));
2749
+ }
2750
+ })();
2751
+ }
2752
+ function implDeleteMany(name, opts) {
2753
+ return (async () => {
2754
+ try {
2755
+ const entry = resolveModel(models, name);
2756
+ const result = await deleteMany(queryFn, entry.table, opts, dialectObj);
2757
+ return ok2(result);
2758
+ } catch (e) {
2759
+ return err3(toWriteError(e));
2760
+ }
2761
+ })();
2762
+ }
2763
+ function implCount(name, opts) {
2764
+ return (async () => {
2765
+ try {
2766
+ const entry = resolveModel(models, name);
2767
+ const result = await count(queryFn, entry.table, opts);
2768
+ return ok2(result);
2769
+ } catch (e) {
2770
+ return err3(toReadError(e));
2771
+ }
2772
+ })();
2773
+ }
2774
+ function implAggregate(name, opts) {
2775
+ return (async () => {
2776
+ try {
2777
+ const entry = resolveModel(models, name);
2778
+ const result = await aggregate(queryFn, entry.table, opts);
2779
+ return ok2(result);
2780
+ } catch (e) {
2781
+ return err3(toReadError(e));
2782
+ }
2783
+ })();
2784
+ }
2785
+ function implGroupBy(name, opts) {
2786
+ return (async () => {
2787
+ try {
2788
+ const entry = resolveModel(models, name);
2789
+ const result = await groupBy(queryFn, entry.table, opts);
2790
+ return ok2(result);
2791
+ } catch (e) {
2792
+ return err3(toReadError(e));
2793
+ }
2794
+ })();
2795
+ }
2796
+ const client = {};
2797
+ for (const name of Object.keys(models)) {
2798
+ client[name] = {
2799
+ get: (opts) => implGet(name, opts),
2800
+ getOrThrow: (opts) => implGetRequired(name, opts),
2801
+ list: (opts) => implList(name, opts),
2802
+ listAndCount: (opts) => implListAndCount(name, opts),
2803
+ create: (opts) => implCreate(name, opts),
2804
+ createMany: (opts) => implCreateMany(name, opts),
2805
+ createManyAndReturn: (opts) => implCreateManyAndReturn(name, opts),
2806
+ update: (opts) => implUpdate(name, opts),
2807
+ updateMany: (opts) => implUpdateMany(name, opts),
2808
+ upsert: (opts) => implUpsert(name, opts),
2809
+ delete: (opts) => implDelete(name, opts),
2810
+ deleteMany: (opts) => implDeleteMany(name, opts),
2811
+ count: (opts) => implCount(name, opts),
2812
+ aggregate: (opts) => implAggregate(name, opts),
2813
+ groupBy: (opts) => implGroupBy(name, opts)
2814
+ };
2815
+ }
2816
+ client.query = async (fragment) => {
2817
+ try {
2818
+ const result = await executeQuery(queryFn, fragment.sql, fragment.params);
2819
+ return ok2(result);
2820
+ } catch (e) {
2821
+ return err3(toReadError(e, fragment.sql));
2822
+ }
2823
+ };
2824
+ client.close = async () => {
2825
+ if (driver) {
2826
+ await driver.close();
2827
+ }
2828
+ if (sqliteDriver) {
2829
+ await sqliteDriver.close();
2830
+ }
2831
+ await Promise.all(replicaDrivers.map((r) => r.close()));
2832
+ };
2833
+ client.isHealthy = async () => {
2834
+ if (driver) {
2835
+ return driver.isHealthy();
1547
2836
  }
2837
+ if (sqliteDriver) {
2838
+ return sqliteDriver.isHealthy();
2839
+ }
2840
+ return true;
2841
+ };
2842
+ client._internals = {
2843
+ models,
2844
+ dialect: dialectObj,
2845
+ tenantGraph
1548
2846
  };
2847
+ return client;
1549
2848
  }
1550
2849
  // src/schema/column.ts
1551
2850
  function cloneWith(source, metaOverrides) {
@@ -1554,8 +2853,12 @@ function cloneWith(source, metaOverrides) {
1554
2853
  function createColumnWithMeta(meta) {
1555
2854
  const col = {
1556
2855
  _meta: meta,
1557
- primary() {
1558
- return cloneWith(this, { primary: true, hasDefault: true });
2856
+ primary(options) {
2857
+ const meta2 = { primary: true, hasDefault: true };
2858
+ if (options?.generate) {
2859
+ meta2.generate = options.generate;
2860
+ }
2861
+ return cloneWith(this, meta2);
1559
2862
  },
1560
2863
  unique() {
1561
2864
  return cloneWith(this, { unique: true });
@@ -1566,11 +2869,17 @@ function createColumnWithMeta(meta) {
1566
2869
  default(value) {
1567
2870
  return cloneWith(this, { hasDefault: true, defaultValue: value });
1568
2871
  },
1569
- sensitive() {
1570
- return cloneWith(this, { sensitive: true });
2872
+ is(flag) {
2873
+ return createColumnWithMeta({
2874
+ ...this._meta,
2875
+ _annotations: { ...this._meta._annotations, [flag]: true }
2876
+ });
2877
+ },
2878
+ readOnly() {
2879
+ return cloneWith(this, { isReadOnly: true });
1571
2880
  },
1572
- hidden() {
1573
- return cloneWith(this, { hidden: true });
2881
+ autoUpdate() {
2882
+ return cloneWith(this, { isAutoUpdate: true, isReadOnly: true });
1574
2883
  },
1575
2884
  check(sql) {
1576
2885
  return cloneWith(this, { check: sql });
@@ -1590,8 +2899,9 @@ function defaultMeta(sqlType) {
1590
2899
  unique: false,
1591
2900
  nullable: false,
1592
2901
  hasDefault: false,
1593
- sensitive: false,
1594
- hidden: false,
2902
+ _annotations: {},
2903
+ isReadOnly: false,
2904
+ isAutoUpdate: false,
1595
2905
  isTenant: false,
1596
2906
  references: null,
1597
2907
  check: null
@@ -1610,8 +2920,9 @@ function createSerialColumn() {
1610
2920
  unique: false,
1611
2921
  nullable: false,
1612
2922
  hasDefault: true,
1613
- sensitive: false,
1614
- hidden: false,
2923
+ _annotations: {},
2924
+ isReadOnly: false,
2925
+ isAutoUpdate: false,
1615
2926
  isTenant: false,
1616
2927
  references: null,
1617
2928
  check: null
@@ -1624,14 +2935,101 @@ function createTenantColumn(targetTableName) {
1624
2935
  unique: false,
1625
2936
  nullable: false,
1626
2937
  hasDefault: false,
1627
- sensitive: false,
1628
- hidden: false,
2938
+ _annotations: {},
2939
+ isReadOnly: false,
2940
+ isAutoUpdate: false,
1629
2941
  isTenant: true,
1630
2942
  references: { table: targetTableName, column: "id" },
1631
2943
  check: null
1632
2944
  });
1633
2945
  }
1634
2946
 
2947
+ // src/schema/model-schemas.ts
2948
+ function deriveSchemas(table) {
2949
+ const hiddenCols = getColumnNamesWithAnnotation(table, "hidden");
2950
+ const readOnlyCols = getColumnNamesWhere(table, "isReadOnly");
2951
+ const primaryCols = getColumnNamesWhere(table, "primary");
2952
+ const allCols = new Set(Object.keys(table._columns));
2953
+ const defaultCols = getColumnNamesWhere(table, "hasDefault");
2954
+ const responseCols = setDifference(allCols, hiddenCols);
2955
+ const inputCols = setDifference(setDifference(allCols, readOnlyCols), primaryCols);
2956
+ const requiredCols = getRequiredInputColumns(table, inputCols, defaultCols);
2957
+ return {
2958
+ response: {
2959
+ parse(value) {
2960
+ return { ok: true, data: pickKeys(value, responseCols) };
2961
+ }
2962
+ },
2963
+ createInput: {
2964
+ parse(value) {
2965
+ const data = value;
2966
+ const missing = requiredCols.filter((col) => !(col in data) || data[col] === undefined);
2967
+ if (missing.length > 0) {
2968
+ return {
2969
+ ok: false,
2970
+ error: new Error(`Missing required fields: ${missing.join(", ")}`)
2971
+ };
2972
+ }
2973
+ return { ok: true, data: pickKeys(value, inputCols) };
2974
+ }
2975
+ },
2976
+ updateInput: {
2977
+ parse(value) {
2978
+ return { ok: true, data: pickKeys(value, inputCols) };
2979
+ }
2980
+ }
2981
+ };
2982
+ }
2983
+ function pickKeys(value, allowed) {
2984
+ const data = value;
2985
+ const result = {};
2986
+ for (const [key, val] of Object.entries(data)) {
2987
+ if (allowed.has(key)) {
2988
+ result[key] = val;
2989
+ }
2990
+ }
2991
+ return result;
2992
+ }
2993
+ function setDifference(a, b) {
2994
+ const result = new Set;
2995
+ for (const item of a) {
2996
+ if (!b.has(item)) {
2997
+ result.add(item);
2998
+ }
2999
+ }
3000
+ return result;
3001
+ }
3002
+ function getRequiredInputColumns(_table, allowed, defaults) {
3003
+ return [...allowed].filter((key) => !defaults.has(key));
3004
+ }
3005
+ function getColumnNamesWhere(table, flag) {
3006
+ const result = new Set;
3007
+ for (const [key, col] of Object.entries(table._columns)) {
3008
+ if (col._meta[flag]) {
3009
+ result.add(key);
3010
+ }
3011
+ }
3012
+ return result;
3013
+ }
3014
+ function getColumnNamesWithAnnotation(table, annotation) {
3015
+ const result = new Set;
3016
+ for (const [key, col] of Object.entries(table._columns)) {
3017
+ if (col._meta._annotations[annotation]) {
3018
+ result.add(key);
3019
+ }
3020
+ }
3021
+ return result;
3022
+ }
3023
+
3024
+ // src/schema/model.ts
3025
+ function createModel(table, relations) {
3026
+ return {
3027
+ table,
3028
+ relations: relations ?? {},
3029
+ schemas: deriveSchemas(table)
3030
+ };
3031
+ }
3032
+
1635
3033
  // src/schema/relation.ts
1636
3034
  function createOneRelation(target, foreignKey) {
1637
3035
  return {
@@ -1663,9 +3061,10 @@ function createManyRelation(target, foreignKey) {
1663
3061
  }
1664
3062
 
1665
3063
  // src/schema/table.ts
1666
- function createIndex(columns) {
3064
+ function createIndex(columns, options) {
1667
3065
  return {
1668
- columns: Array.isArray(columns) ? columns : [columns]
3066
+ columns: Array.isArray(columns) ? columns : [columns],
3067
+ ...options
1669
3068
  };
1670
3069
  }
1671
3070
  function createTable(name, columns, options) {
@@ -1689,10 +3088,13 @@ function createTableInternal(name, columns, indexes, shared) {
1689
3088
  get $update() {
1690
3089
  return;
1691
3090
  },
1692
- get $not_sensitive() {
3091
+ get $response() {
3092
+ return;
3093
+ },
3094
+ get $create_input() {
1693
3095
  return;
1694
3096
  },
1695
- get $not_hidden() {
3097
+ get $update_input() {
1696
3098
  return;
1697
3099
  },
1698
3100
  shared() {
@@ -1718,7 +3120,13 @@ var d = {
1718
3120
  timestamp: () => createColumn("timestamp with time zone"),
1719
3121
  date: () => createColumn("date"),
1720
3122
  time: () => createColumn("time"),
1721
- jsonb: (opts) => createColumn("jsonb", opts?.validator ? { validator: opts.validator } : {}),
3123
+ jsonb: (schemaOrOpts) => {
3124
+ if (schemaOrOpts && "parse" in schemaOrOpts && !("validator" in schemaOrOpts)) {
3125
+ return createColumn("jsonb", { validator: schemaOrOpts });
3126
+ }
3127
+ const opts = schemaOrOpts;
3128
+ return createColumn("jsonb", opts?.validator ? { validator: opts.validator } : {});
3129
+ },
1722
3130
  textArray: () => createColumn("text[]"),
1723
3131
  integerArray: () => createColumn("integer[]"),
1724
3132
  enum: (name, valuesOrSchema) => {
@@ -1738,50 +3146,24 @@ var d = {
1738
3146
  entry: (table, relations = {}) => ({
1739
3147
  table,
1740
3148
  relations
1741
- })
1742
- };
1743
- // src/errors/error-codes.ts
1744
- var DbErrorCode = {
1745
- UNIQUE_VIOLATION: "23505",
1746
- FOREIGN_KEY_VIOLATION: "23503",
1747
- NOT_NULL_VIOLATION: "23502",
1748
- CHECK_VIOLATION: "23514",
1749
- EXCLUSION_VIOLATION: "23P01",
1750
- SERIALIZATION_FAILURE: "40001",
1751
- DEADLOCK_DETECTED: "40P01",
1752
- CONNECTION_EXCEPTION: "08000",
1753
- CONNECTION_DOES_NOT_EXIST: "08003",
1754
- CONNECTION_FAILURE: "08006",
1755
- NOT_FOUND: "NOT_FOUND",
1756
- CONNECTION_ERROR: "CONNECTION_ERROR",
1757
- POOL_EXHAUSTED: "POOL_EXHAUSTED"
3149
+ }),
3150
+ model: (table, relations = {}) => createModel(table, relations)
1758
3151
  };
1759
- var PgCodeToName = Object.fromEntries(Object.entries(DbErrorCode).map(([name, pgCode]) => [pgCode, name]));
1760
- function resolveErrorCode(pgCode) {
1761
- return PgCodeToName[pgCode];
1762
- }
1763
- // src/errors/http-adapter.ts
1764
- function dbErrorToHttpError(error) {
1765
- const body = error.toJSON();
1766
- if (error instanceof UniqueConstraintError) {
1767
- return { status: 409, body };
1768
- }
1769
- if (error instanceof NotFoundError) {
1770
- return { status: 404, body };
1771
- }
1772
- if (error instanceof ForeignKeyError) {
1773
- return { status: 422, body };
1774
- }
1775
- if (error instanceof NotNullError) {
1776
- return { status: 422, body };
1777
- }
1778
- if (error instanceof CheckConstraintError) {
1779
- return { status: 422, body };
3152
+ // src/schema/define-annotations.ts
3153
+ function defineAnnotations(...annotations) {
3154
+ const result = {};
3155
+ for (const annotation of annotations) {
3156
+ result[annotation] = annotation;
1780
3157
  }
1781
- if (error instanceof ConnectionError) {
1782
- return { status: 503, body };
3158
+ return Object.freeze(result);
3159
+ }
3160
+ // src/schema/enum-registry.ts
3161
+ function createEnumRegistry(definitions) {
3162
+ const registry = {};
3163
+ for (const [name, values] of Object.entries(definitions)) {
3164
+ registry[name] = { name, values };
1783
3165
  }
1784
- return { status: 500, body };
3166
+ return registry;
1785
3167
  }
1786
3168
  // src/schema/registry.ts
1787
3169
  function createRegistry(tables, relationsCallback) {
@@ -1809,195 +3191,39 @@ function createRegistry(tables, relationsCallback) {
1809
3191
  }
1810
3192
  return result;
1811
3193
  }
1812
- // src/schema/enum-registry.ts
1813
- function createEnumRegistry(definitions) {
1814
- const registry = {};
1815
- for (const [name, values] of Object.entries(definitions)) {
1816
- registry[name] = { name, values };
1817
- }
1818
- return registry;
1819
- }
1820
- // src/codegen/type-gen.ts
1821
- function fieldTypeToTs(field) {
1822
- switch (field.type) {
1823
- case "string":
1824
- case "uuid":
1825
- return "string";
1826
- case "number":
1827
- return "number";
1828
- case "boolean":
1829
- return "boolean";
1830
- case "date":
1831
- return "Date";
1832
- case "json":
1833
- return "unknown";
1834
- case "enum":
1835
- return field.enumName || "string";
1836
- default:
1837
- return "unknown";
1838
- }
1839
- }
1840
- function generateTypes(domain) {
1841
- const { name, fields, relations } = domain;
1842
- const pascalName = name.charAt(0).toUpperCase() + name.slice(1);
1843
- const lines = [];
1844
- for (const field of Object.values(fields)) {
1845
- if (field.type === "enum" && field.enumName && field.enumValues) {
1846
- lines.push(`enum ${field.enumName} {`);
1847
- for (const value of field.enumValues) {
1848
- lines.push(` ${value.toUpperCase()} = '${value}',`);
1849
- }
1850
- lines.push("}");
1851
- lines.push("");
1852
- }
1853
- }
1854
- lines.push(`interface ${pascalName} {`);
1855
- for (const [fieldName, field] of Object.entries(fields)) {
1856
- const tsType = fieldTypeToTs(field);
1857
- const optional = field.required === false ? "?" : "";
1858
- lines.push(` ${fieldName}${optional}: ${tsType};`);
1859
- }
1860
- if (relations) {
1861
- for (const [relName, rel] of Object.entries(relations)) {
1862
- if (rel.type === "belongsTo") {
1863
- const targetPascal = rel.target.charAt(0).toUpperCase() + rel.target.slice(1);
1864
- lines.push(` ${relName}: () => Promise<${targetPascal} | null>;`);
1865
- } else if (rel.type === "hasMany") {
1866
- const targetPascal = rel.target.charAt(0).toUpperCase() + rel.target.slice(1);
1867
- lines.push(` ${relName}: () => Promise<${targetPascal}[]>;`);
1868
- }
1869
- }
1870
- }
1871
- lines.push("}");
1872
- lines.push("");
1873
- lines.push(`interface Create${pascalName}Input {`);
1874
- for (const [fieldName, field] of Object.entries(fields)) {
1875
- if (field.primary)
1876
- continue;
1877
- const tsType = fieldTypeToTs(field);
1878
- if (field.required !== false) {
1879
- lines.push(` ${fieldName}: ${tsType};`);
1880
- }
1881
- }
1882
- lines.push("}");
1883
- lines.push("");
1884
- lines.push(`interface Update${pascalName}Input {`);
1885
- for (const [fieldName, field] of Object.entries(fields)) {
1886
- if (field.primary)
1887
- continue;
1888
- const tsType = fieldTypeToTs(field);
1889
- lines.push(` ${fieldName}?: ${tsType};`);
1890
- }
1891
- lines.push("}");
1892
- lines.push("");
1893
- lines.push(`interface ${pascalName}Where {`);
1894
- for (const [fieldName, field] of Object.entries(fields)) {
1895
- const tsType = fieldTypeToTs(field);
1896
- lines.push(` ${fieldName}?: ${tsType};`);
1897
- }
1898
- lines.push("}");
1899
- lines.push("");
1900
- lines.push(`interface ${pascalName}OrderBy {`);
1901
- for (const fieldName of Object.keys(fields)) {
1902
- lines.push(` ${fieldName}?: 'asc' | 'desc';`);
1903
- }
1904
- lines.push("}");
1905
- lines.push("");
1906
- lines.push(`interface List${pascalName}Params {`);
1907
- lines.push(` where?: ${pascalName}Where;`);
1908
- lines.push(` orderBy?: ${pascalName}OrderBy;`);
1909
- lines.push(` limit?: number;`);
1910
- lines.push(` offset?: number;`);
1911
- lines.push("}");
1912
- lines.push("");
1913
- lines.push(`interface ${pascalName}Client {`);
1914
- lines.push(` list(): Promise<${pascalName}[]>;`);
1915
- lines.push(` list(params?: List${pascalName}Params): Promise<${pascalName}[]>;`);
1916
- let idField = "id";
1917
- for (const [fieldName, field] of Object.entries(fields)) {
1918
- if (field.primary) {
1919
- idField = fieldName;
1920
- break;
1921
- }
1922
- }
1923
- lines.push(` get(${idField}: string): Promise<${pascalName} | null>;`);
1924
- lines.push(` create(data: Create${pascalName}Input): Promise<${pascalName}>;`);
1925
- lines.push(` update(${idField}: string, data: Update${pascalName}Input): Promise<${pascalName}>;`);
1926
- lines.push(` delete(${idField}: string): Promise<void>;`);
1927
- lines.push("}");
1928
- return lines.join(`
1929
- `);
1930
- }
1931
- // src/codegen/client-gen.ts
1932
- function generateClient(domains) {
1933
- const lines = [];
1934
- for (const domain of domains) {
1935
- const types = generateTypes(domain);
1936
- lines.push(types);
1937
- lines.push("");
1938
- }
1939
- lines.push("export const db = {");
1940
- for (const domain of domains) {
1941
- const { name, fields, relations } = domain;
1942
- const pascalName = name.charAt(0).toUpperCase() + name.slice(1);
1943
- let idField = "id";
1944
- for (const [fieldName, field] of Object.entries(fields)) {
1945
- if (field.primary) {
1946
- idField = fieldName;
1947
- break;
1948
- }
1949
- }
1950
- lines.push(` ${name}: {`);
1951
- lines.push(` list: (params?: List${pascalName}Params) => Promise<${pascalName}[]>,`);
1952
- lines.push(` get: (${idField}: string) => Promise<${pascalName} | null>,`);
1953
- lines.push(` create: (data: Create${pascalName}Input) => Promise<${pascalName}>,`);
1954
- lines.push(` update: (${idField}: string, data: Update${pascalName}Input) => Promise<${pascalName}>,`);
1955
- lines.push(` delete: (${idField}: string) => Promise<void>,`);
1956
- if (relations) {
1957
- for (const [relName, rel] of Object.entries(relations)) {
1958
- if (rel.type === "belongsTo") {
1959
- const targetPascal = rel.target.charAt(0).toUpperCase() + rel.target.slice(1);
1960
- const relParamName = `${name}Id`;
1961
- lines.push(` ${relName}: {`);
1962
- lines.push(` get(${relParamName}: string): Promise<${targetPascal} | null>,`);
1963
- lines.push(` },`);
1964
- }
1965
- }
1966
- }
1967
- lines.push(` },`);
1968
- }
1969
- lines.push("} as const;");
1970
- return lines.join(`
1971
- `);
1972
- }
1973
- // src/domain.ts
1974
- function defineDomain(name, config) {
1975
- return {
1976
- name,
1977
- fields: config.fields,
1978
- relations: config.relations
1979
- };
1980
- }
1981
3194
  export {
3195
+ toWriteError,
3196
+ toReadError,
1982
3197
  resolveErrorCode,
3198
+ reset,
1983
3199
  push,
1984
3200
  parsePgError,
3201
+ parseMigrationName,
1985
3202
  migrateStatus,
1986
3203
  migrateDev,
1987
3204
  migrateDeploy,
1988
- generateTypes,
1989
- generateClient,
3205
+ generateId,
1990
3206
  formatDiagnostic,
1991
3207
  explainError,
1992
3208
  diagnoseError,
1993
- defineDomain,
3209
+ detectSchemaDrift,
3210
+ defineAnnotations,
3211
+ defaultSqliteDialect,
3212
+ defaultPostgresDialect,
1994
3213
  dbErrorToHttpError,
1995
3214
  d,
3215
+ createSnapshot,
1996
3216
  createRegistry,
1997
3217
  createEnumRegistry,
1998
3218
  createDb,
3219
+ createDatabaseBridgeAdapter,
3220
+ createD1Driver,
3221
+ createD1Adapter,
1999
3222
  computeTenantGraph,
3223
+ baseline,
2000
3224
  UniqueConstraintError,
3225
+ SqliteDialect,
3226
+ PostgresDialect,
2001
3227
  PgCodeToName,
2002
3228
  NotNullError,
2003
3229
  NotFoundError,