@vertz/db 0.2.0 → 0.2.1

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-ktbebkz5.js";
47
+ import {
48
+ generateId
49
+ } from "./shared/chunk-fwk49jvg.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,391 @@ ${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 mapSqliteType(rawType) {
649
+ const upper = rawType.toUpperCase();
650
+ if (upper === "TEXT")
651
+ return "text";
652
+ if (upper === "INTEGER" || upper === "INT")
653
+ return "integer";
654
+ if (upper === "REAL")
655
+ return "float";
656
+ if (upper === "BLOB")
657
+ return "blob";
658
+ return rawType.toLowerCase();
659
+ }
660
+ async function introspectSqlite(queryFn) {
661
+ const snapshot = {
662
+ version: 1,
663
+ tables: {},
664
+ enums: {}
665
+ };
666
+ const { rows: tableRows } = await queryFn("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'", []);
667
+ for (const row of tableRows) {
668
+ const tableName = row.name;
669
+ if (SQLITE_EXCLUDED_TABLES.has(tableName))
670
+ continue;
671
+ const columns = {};
672
+ const { rows: colRows } = await queryFn(`PRAGMA table_info("${tableName}")`, []);
673
+ for (const col of colRows) {
674
+ const colName = col.name;
675
+ const colSnap = {
676
+ type: mapSqliteType(col.type),
677
+ nullable: col.notnull === 0 && col.pk === 0,
678
+ primary: col.pk > 0,
679
+ unique: false
680
+ };
681
+ if (col.dflt_value != null) {
682
+ colSnap.default = String(col.dflt_value);
683
+ }
684
+ columns[colName] = colSnap;
685
+ }
686
+ const indexes = [];
687
+ const { rows: indexRows } = await queryFn(`PRAGMA index_list("${tableName}")`, []);
688
+ for (const idx of indexRows) {
689
+ const idxName = idx.name;
690
+ const isUnique = idx.unique === 1;
691
+ const origin = idx.origin;
692
+ const { rows: idxInfoRows } = await queryFn(`PRAGMA index_info("${idxName}")`, []);
693
+ const idxColumns = idxInfoRows.map((r) => r.name);
694
+ if (isUnique && idxColumns.length === 1 && origin === "u") {
695
+ const colName = idxColumns[0];
696
+ if (colName && columns[colName]) {
697
+ columns[colName].unique = true;
698
+ }
699
+ }
700
+ if (origin === "c") {
701
+ indexes.push({
702
+ columns: idxColumns,
703
+ name: idxName,
704
+ unique: isUnique
705
+ });
706
+ }
707
+ }
708
+ const foreignKeys = [];
709
+ const { rows: fkRows } = await queryFn(`PRAGMA foreign_key_list("${tableName}")`, []);
710
+ for (const fk of fkRows) {
711
+ foreignKeys.push({
712
+ column: fk.from,
713
+ targetTable: fk.table,
714
+ targetColumn: fk.to
715
+ });
716
+ }
717
+ snapshot.tables[tableName] = {
718
+ columns,
719
+ indexes,
720
+ foreignKeys,
721
+ _metadata: {}
722
+ };
723
+ }
724
+ return snapshot;
725
+ }
726
+ var PG_EXCLUDED_TABLES = new Set(["_vertz_migrations"]);
727
+ async function introspectPostgres(queryFn) {
728
+ const snapshot = {
729
+ version: 1,
730
+ tables: {},
731
+ enums: {}
732
+ };
733
+ const { rows: tableRows } = await queryFn("SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type = 'BASE TABLE'", []);
734
+ const { rows: pkRows } = await queryFn(`SELECT kcu.table_name, kcu.column_name
735
+ FROM information_schema.table_constraints tc
736
+ JOIN information_schema.key_column_usage kcu
737
+ ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
738
+ WHERE tc.constraint_type = 'PRIMARY KEY' AND tc.table_schema = 'public'`, []);
739
+ const pkMap = new Map;
740
+ for (const pk of pkRows) {
741
+ const tbl = pk.table_name;
742
+ if (!pkMap.has(tbl))
743
+ pkMap.set(tbl, new Set);
744
+ pkMap.get(tbl)?.add(pk.column_name);
745
+ }
746
+ const { rows: uniqueRows } = await queryFn(`SELECT tc.table_name, kcu.column_name, tc.constraint_name
747
+ FROM information_schema.table_constraints tc
748
+ JOIN information_schema.key_column_usage kcu
749
+ ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
750
+ WHERE tc.constraint_type = 'UNIQUE' AND tc.table_schema = 'public'`, []);
751
+ const uniqueConstraintCols = new Map;
752
+ for (const u of uniqueRows) {
753
+ const cName = u.constraint_name;
754
+ if (!uniqueConstraintCols.has(cName)) {
755
+ uniqueConstraintCols.set(cName, { table: u.table_name, columns: [] });
756
+ }
757
+ uniqueConstraintCols.get(cName)?.columns.push(u.column_name);
758
+ }
759
+ const uniqueColMap = new Map;
760
+ for (const [, val] of uniqueConstraintCols) {
761
+ if (val.columns.length === 1) {
762
+ if (!uniqueColMap.has(val.table))
763
+ uniqueColMap.set(val.table, new Set);
764
+ uniqueColMap.get(val.table)?.add(val.columns[0]);
765
+ }
766
+ }
767
+ for (const row of tableRows) {
768
+ const tableName = row.table_name;
769
+ if (PG_EXCLUDED_TABLES.has(tableName))
770
+ continue;
771
+ const columns = {};
772
+ const pkCols = pkMap.get(tableName) ?? new Set;
773
+ const uniqueCols = uniqueColMap.get(tableName) ?? new Set;
774
+ const { rows: colRows } = await queryFn(`SELECT column_name, data_type, is_nullable, column_default
775
+ FROM information_schema.columns
776
+ WHERE table_name = $1 AND table_schema = 'public'
777
+ ORDER BY ordinal_position`, [tableName]);
778
+ for (const col of colRows) {
779
+ const colName = col.column_name;
780
+ const isPrimary = pkCols.has(colName);
781
+ const isUnique = uniqueCols.has(colName);
782
+ const colSnap = {
783
+ type: col.data_type,
784
+ nullable: col.is_nullable === "YES",
785
+ primary: isPrimary,
786
+ unique: isUnique
787
+ };
788
+ if (col.column_default != null) {
789
+ colSnap.default = String(col.column_default);
790
+ }
791
+ columns[colName] = colSnap;
792
+ }
793
+ const foreignKeys = [];
794
+ const { rows: fkRows } = await queryFn(`SELECT
795
+ kcu.column_name,
796
+ ccu.table_name AS target_table,
797
+ ccu.column_name AS target_column
798
+ FROM information_schema.table_constraints tc
799
+ JOIN information_schema.key_column_usage kcu
800
+ ON tc.constraint_name = kcu.constraint_name AND tc.table_schema = kcu.table_schema
801
+ JOIN information_schema.constraint_column_usage ccu
802
+ ON tc.constraint_name = ccu.constraint_name AND tc.table_schema = ccu.table_schema
803
+ WHERE tc.constraint_type = 'FOREIGN KEY'
804
+ AND tc.table_name = $1
805
+ AND tc.table_schema = 'public'`, [tableName]);
806
+ for (const fk of fkRows) {
807
+ foreignKeys.push({
808
+ column: fk.column_name,
809
+ targetTable: fk.target_table,
810
+ targetColumn: fk.target_column
811
+ });
812
+ }
813
+ const indexes = [];
814
+ const { rows: idxRows } = await queryFn(`SELECT i.relname AS index_name,
815
+ array_agg(a.attname ORDER BY k.n) AS columns,
816
+ ix.indisunique AS is_unique
817
+ FROM pg_index ix
818
+ JOIN pg_class i ON i.oid = ix.indexrelid
819
+ JOIN pg_class t ON t.oid = ix.indrelid
820
+ JOIN pg_namespace ns ON ns.oid = t.relnamespace
821
+ JOIN LATERAL unnest(ix.indkey) WITH ORDINALITY AS k(attnum, n) ON true
822
+ JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = k.attnum
823
+ WHERE t.relname = $1
824
+ AND ns.nspname = 'public'
825
+ AND NOT ix.indisprimary
826
+ AND NOT ix.indisunique
827
+ GROUP BY i.relname, ix.indisunique`, [tableName]);
828
+ for (const idx of idxRows) {
829
+ indexes.push({
830
+ columns: idx.columns,
831
+ name: idx.index_name,
832
+ unique: idx.is_unique
833
+ });
834
+ }
835
+ snapshot.tables[tableName] = {
836
+ columns,
837
+ indexes,
838
+ foreignKeys,
839
+ _metadata: {}
840
+ };
841
+ }
842
+ const { rows: enumRows } = await queryFn(`SELECT t.typname AS enum_name, e.enumlabel AS enum_value
843
+ FROM pg_type t
844
+ JOIN pg_enum e ON t.oid = e.enumtypid
845
+ JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace
846
+ WHERE n.nspname = 'public'
847
+ ORDER BY t.typname, e.enumsortorder`, []);
848
+ for (const row of enumRows) {
849
+ const enumName = row.enum_name;
850
+ const enumValue = row.enum_value;
851
+ if (!snapshot.enums[enumName]) {
852
+ snapshot.enums[enumName] = [];
853
+ }
854
+ snapshot.enums[enumName].push(enumValue);
855
+ }
856
+ return snapshot;
857
+ }
858
+ // src/migration/journal.ts
859
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
860
+ import { dirname } from "node:path";
861
+ function createJournal() {
862
+ return {
863
+ version: 1,
864
+ migrations: []
865
+ };
866
+ }
867
+ function addJournalEntry(journal, entry) {
868
+ return {
869
+ ...journal,
870
+ migrations: [...journal.migrations, entry]
871
+ };
872
+ }
873
+ function detectCollisions(journal, existingFiles) {
874
+ const journalSeqNumbers = new Map;
875
+ for (const entry of journal.migrations) {
876
+ const parsed = parseMigrationName(entry.name);
877
+ if (parsed) {
878
+ journalSeqNumbers.set(parsed.timestamp, entry.name);
879
+ }
880
+ }
881
+ const allUsedSeqNumbers = new Set(journalSeqNumbers.keys());
882
+ for (const file of existingFiles) {
883
+ const parsed = parseMigrationName(file);
884
+ if (parsed) {
885
+ allUsedSeqNumbers.add(parsed.timestamp);
886
+ }
887
+ }
888
+ const collisions = [];
889
+ for (const file of existingFiles) {
890
+ const parsed = parseMigrationName(file);
891
+ if (!parsed)
892
+ continue;
893
+ const journalEntry = journalSeqNumbers.get(parsed.timestamp);
894
+ if (journalEntry && journalEntry !== file) {
895
+ let nextSeq = Math.max(...allUsedSeqNumbers) + 1;
896
+ for (const c of collisions) {
897
+ const suggestedParsed = parseMigrationName(c.suggestedName);
898
+ if (suggestedParsed && suggestedParsed.timestamp >= nextSeq) {
899
+ nextSeq = suggestedParsed.timestamp + 1;
900
+ }
901
+ }
902
+ const description = file.replace(/^\d+_/, "").replace(/\.sql$/, "");
903
+ collisions.push({
904
+ existingName: journalEntry,
905
+ conflictingName: file,
906
+ sequenceNumber: parsed.timestamp,
907
+ suggestedName: formatMigrationFilename(nextSeq, description)
908
+ });
909
+ }
910
+ }
911
+ return collisions;
912
+ }
913
+ // src/migration/snapshot.ts
914
+ function createSnapshot(tables) {
915
+ const snapshot = {
916
+ version: 1,
917
+ tables: {},
918
+ enums: {}
919
+ };
920
+ for (const table of tables) {
921
+ const columns = {};
922
+ const foreignKeys = [];
923
+ const indexes = [];
924
+ for (const [colName, col] of Object.entries(table._columns)) {
925
+ const meta = col._meta;
926
+ const colSnap = {
927
+ type: meta.sqlType,
928
+ nullable: meta.nullable,
929
+ primary: meta.primary,
930
+ unique: meta.unique
931
+ };
932
+ if (meta.hasDefault && meta.defaultValue !== undefined) {
933
+ const rawDefault = String(meta.defaultValue);
934
+ colSnap.default = rawDefault === "now" ? "now()" : rawDefault;
935
+ }
936
+ const annotationNames = meta._annotations ? Object.keys(meta._annotations) : [];
937
+ if (annotationNames.length > 0) {
938
+ colSnap.annotations = annotationNames;
939
+ }
940
+ columns[colName] = colSnap;
941
+ if (meta.references) {
942
+ foreignKeys.push({
943
+ column: colName,
944
+ targetTable: meta.references.table,
945
+ targetColumn: meta.references.column
946
+ });
947
+ }
948
+ if (meta.enumName && meta.enumValues) {
949
+ snapshot.enums[meta.enumName] = [...meta.enumValues];
950
+ }
951
+ }
952
+ for (const idx of table._indexes) {
953
+ indexes.push({ columns: [...idx.columns] });
954
+ }
955
+ snapshot.tables[table._name] = {
956
+ columns,
957
+ indexes,
958
+ foreignKeys,
959
+ _metadata: {}
960
+ };
961
+ }
962
+ return snapshot;
963
+ }
964
+ // src/cli/baseline.ts
965
+ async function baseline(options) {
966
+ const runner = createMigrationRunner({ dialect: options.dialect });
967
+ const createResult = await runner.createHistoryTable(options.queryFn);
968
+ if (!createResult.ok) {
969
+ return createResult;
970
+ }
971
+ const appliedResult = await runner.getApplied(options.queryFn);
972
+ if (!appliedResult.ok) {
973
+ return appliedResult;
974
+ }
975
+ const appliedNames = new Set(appliedResult.data.map((a) => a.name));
976
+ const recorded = [];
977
+ for (const file of options.migrationFiles) {
978
+ if (appliedNames.has(file.name)) {
979
+ continue;
980
+ }
981
+ const checksum = await computeChecksum(file.sql);
982
+ const dialect = options.dialect;
983
+ const param1 = dialect ? dialect.param(1) : "$1";
984
+ const param2 = dialect ? dialect.param(2) : "$2";
985
+ await options.queryFn(`INSERT INTO "_vertz_migrations" ("name", "checksum") VALUES (${param1}, ${param2})`, [file.name, checksum]);
986
+ recorded.push(file.name);
987
+ }
988
+ return {
989
+ ok: true,
990
+ data: { recorded }
991
+ };
992
+ }
454
993
  // src/cli/migrate-deploy.ts
455
994
  async function migrateDeploy(options) {
456
995
  const runner = createMigrationRunner();
457
996
  const isDryRun = options.dryRun ?? false;
458
997
  if (!isDryRun) {
459
- await runner.createHistoryTable(options.queryFn);
998
+ const createResult = await runner.createHistoryTable(options.queryFn);
999
+ if (!createResult.ok) {
1000
+ return createResult;
1001
+ }
460
1002
  }
461
1003
  let applied;
462
1004
  if (isDryRun) {
463
- try {
464
- applied = await runner.getApplied(options.queryFn);
465
- } catch {
1005
+ const appliedResult = await runner.getApplied(options.queryFn);
1006
+ if (!appliedResult.ok) {
466
1007
  applied = [];
1008
+ } else {
1009
+ applied = appliedResult.data;
467
1010
  }
468
1011
  } else {
469
- applied = await runner.getApplied(options.queryFn);
1012
+ const appliedResult = await runner.getApplied(options.queryFn);
1013
+ if (!appliedResult.ok) {
1014
+ return appliedResult;
1015
+ }
1016
+ applied = appliedResult.data;
470
1017
  }
471
1018
  const pending = runner.getPending(options.migrationFiles, applied);
472
1019
  const appliedNames = [];
@@ -475,17 +1022,72 @@ async function migrateDeploy(options) {
475
1022
  const result = await runner.apply(options.queryFn, migration.sql, migration.name, {
476
1023
  dryRun: isDryRun
477
1024
  });
1025
+ if (!result.ok) {
1026
+ return result;
1027
+ }
478
1028
  appliedNames.push(migration.name);
479
- migrationResults.push(result);
1029
+ migrationResults.push(result.data);
480
1030
  }
481
1031
  return {
482
- applied: appliedNames,
483
- alreadyApplied: applied.map((a) => a.name),
484
- dryRun: isDryRun,
485
- migrations: migrationResults.length > 0 ? migrationResults : undefined
1032
+ ok: true,
1033
+ data: {
1034
+ applied: appliedNames,
1035
+ alreadyApplied: applied.map((a) => a.name),
1036
+ dryRun: isDryRun,
1037
+ migrations: migrationResults.length > 0 ? migrationResults : undefined
1038
+ }
486
1039
  };
487
1040
  }
488
1041
  // src/cli/migrate-dev.ts
1042
+ function toKebab(str) {
1043
+ return str.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
1044
+ }
1045
+ function generateMigrationName(changes) {
1046
+ if (changes.length === 0)
1047
+ return "empty-migration";
1048
+ if (changes.length === 1) {
1049
+ const change = changes[0];
1050
+ switch (change.type) {
1051
+ case "table_added":
1052
+ return `add-${toKebab(change.table)}-table`;
1053
+ case "table_removed":
1054
+ return `drop-${toKebab(change.table)}-table`;
1055
+ case "column_added":
1056
+ return `add-${toKebab(change.column)}-to-${toKebab(change.table)}`;
1057
+ case "column_removed":
1058
+ return `drop-${toKebab(change.column)}-from-${toKebab(change.table)}`;
1059
+ case "column_altered":
1060
+ return `alter-${toKebab(change.column)}-in-${toKebab(change.table)}`;
1061
+ case "column_renamed":
1062
+ return `rename-${toKebab(change.oldColumn)}-to-${toKebab(change.newColumn)}-in-${toKebab(change.table)}`;
1063
+ case "index_added":
1064
+ return `add-index-to-${toKebab(change.table)}`;
1065
+ case "index_removed":
1066
+ return `drop-index-from-${toKebab(change.table)}`;
1067
+ case "enum_added":
1068
+ return `add-${toKebab(change.enumName)}-enum`;
1069
+ case "enum_removed":
1070
+ return `drop-${toKebab(change.enumName)}-enum`;
1071
+ case "enum_altered":
1072
+ return `alter-${toKebab(change.enumName)}-enum`;
1073
+ }
1074
+ }
1075
+ const tables = new Set(changes.map((c) => c.table).filter(Boolean));
1076
+ const types = new Set(changes.map((c) => c.type));
1077
+ if (tables.size === 1 && types.size === 1) {
1078
+ const table = [...tables][0];
1079
+ const type = [...types][0];
1080
+ switch (type) {
1081
+ case "column_added":
1082
+ return `add-columns-to-${toKebab(table)}`;
1083
+ case "column_removed":
1084
+ return `drop-columns-from-${toKebab(table)}`;
1085
+ default:
1086
+ return `update-${toKebab(table)}`;
1087
+ }
1088
+ }
1089
+ return "update-schema";
1090
+ }
489
1091
  async function migrateDev(options) {
490
1092
  const diff = computeDiff(options.previousSnapshot, options.currentSnapshot);
491
1093
  const sql = generateMigrationSql(diff.changes, {
@@ -498,19 +1100,40 @@ async function migrateDev(options) {
498
1100
  newColumn: c.newColumn,
499
1101
  confidence: c.confidence
500
1102
  }));
1103
+ const migrationName = options.migrationName ?? generateMigrationName(diff.changes);
501
1104
  const num = nextMigrationNumber(options.existingFiles);
502
- const filename = formatMigrationFilename(num, options.migrationName);
1105
+ const filename = formatMigrationFilename(num, migrationName);
503
1106
  const filePath = `${options.migrationsDir}/${filename}`;
1107
+ const journalPath = `${options.migrationsDir}/_journal.json`;
1108
+ let journal;
1109
+ try {
1110
+ const content = options.readFile ? await options.readFile(journalPath) : '{"version":1,"migrations":[]}';
1111
+ journal = JSON.parse(content);
1112
+ } catch {
1113
+ journal = createJournal();
1114
+ }
1115
+ const collisions = detectCollisions(journal, options.existingFiles);
504
1116
  if (options.dryRun) {
505
1117
  return {
506
1118
  migrationFile: filename,
507
1119
  sql,
508
1120
  dryRun: true,
509
1121
  renames: renames.length > 0 ? renames : undefined,
1122
+ collisions: collisions.length > 0 ? collisions : undefined,
510
1123
  snapshot: options.currentSnapshot
511
1124
  };
512
1125
  }
513
1126
  await options.writeFile(filePath, sql);
1127
+ const checksum = await computeChecksum(sql);
1128
+ journal = addJournalEntry(journal, {
1129
+ name: filename,
1130
+ description: migrationName,
1131
+ createdAt: new Date().toISOString(),
1132
+ checksum
1133
+ });
1134
+ await options.writeFile(journalPath, JSON.stringify(journal, null, 2));
1135
+ const snapshotPath = `${options.migrationsDir}/_snapshot.json`;
1136
+ await options.writeFile(snapshotPath, JSON.stringify(options.currentSnapshot, null, 2));
514
1137
  const runner = createMigrationRunner();
515
1138
  await runner.createHistoryTable(options.queryFn);
516
1139
  await runner.apply(options.queryFn, sql, filename, { dryRun: false });
@@ -520,6 +1143,7 @@ async function migrateDev(options) {
520
1143
  appliedAt: new Date,
521
1144
  dryRun: false,
522
1145
  renames: renames.length > 0 ? renames : undefined,
1146
+ collisions: collisions.length > 0 ? collisions : undefined,
523
1147
  snapshot: options.currentSnapshot
524
1148
  };
525
1149
  }
@@ -538,21 +1162,419 @@ async function push(options) {
538
1162
  ];
539
1163
  return { sql, tablesAffected };
540
1164
  }
1165
+ // src/cli/reset.ts
1166
+ import { createMigrationQueryError as createMigrationQueryError2, err as err2 } from "@vertz/errors";
1167
+ var HISTORY_TABLE2 = "_vertz_migrations";
1168
+ function getUserTablesQuery(dialect) {
1169
+ if (dialect.name === "sqlite") {
1170
+ return `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`;
1171
+ }
1172
+ return `SELECT tablename AS name FROM pg_tables WHERE schemaname = 'public'`;
1173
+ }
1174
+ function buildDropTableSql(tableName, dialect) {
1175
+ if (dialect.name === "postgres") {
1176
+ return `DROP TABLE IF EXISTS "${tableName}" CASCADE`;
1177
+ }
1178
+ return `DROP TABLE IF EXISTS "${tableName}"`;
1179
+ }
1180
+ async function reset(options) {
1181
+ const dialect = options.dialect ?? defaultPostgresDialect;
1182
+ const runner = createMigrationRunner({ dialect });
1183
+ let tableNames;
1184
+ try {
1185
+ const tablesResult = await options.queryFn(getUserTablesQuery(dialect), []);
1186
+ tableNames = tablesResult.rows.map((row) => row.name);
1187
+ } catch (cause) {
1188
+ return err2(createMigrationQueryError2("Failed to list user tables", { cause }));
1189
+ }
1190
+ const tablesDropped = [];
1191
+ for (const tableName of tableNames) {
1192
+ try {
1193
+ await options.queryFn(buildDropTableSql(tableName, dialect), []);
1194
+ if (tableName !== HISTORY_TABLE2) {
1195
+ tablesDropped.push(tableName);
1196
+ }
1197
+ } catch (cause) {
1198
+ return err2(createMigrationQueryError2(`Failed to drop table: ${tableName}`, { cause }));
1199
+ }
1200
+ }
1201
+ try {
1202
+ await options.queryFn(buildDropTableSql(HISTORY_TABLE2, dialect), []);
1203
+ } catch (cause) {
1204
+ return err2(createMigrationQueryError2("Failed to drop history table", { cause }));
1205
+ }
1206
+ const createResult = await runner.createHistoryTable(options.queryFn);
1207
+ if (!createResult.ok) {
1208
+ return createResult;
1209
+ }
1210
+ const sorted = [...options.migrationFiles].sort((a, b) => a.timestamp - b.timestamp);
1211
+ const migrationsApplied = [];
1212
+ for (const file of sorted) {
1213
+ const applyResult = await runner.apply(options.queryFn, file.sql, file.name);
1214
+ if (!applyResult.ok) {
1215
+ return applyResult;
1216
+ }
1217
+ migrationsApplied.push(file.name);
1218
+ }
1219
+ return {
1220
+ ok: true,
1221
+ data: { tablesDropped, migrationsApplied }
1222
+ };
1223
+ }
541
1224
  // src/cli/status.ts
1225
+ function diffChangeToCodeChange(change) {
1226
+ switch (change.type) {
1227
+ case "table_added":
1228
+ return {
1229
+ description: `Added table '${change.table}'`,
1230
+ type: change.type,
1231
+ table: change.table
1232
+ };
1233
+ case "table_removed":
1234
+ return {
1235
+ description: `Removed table '${change.table}'`,
1236
+ type: change.type,
1237
+ table: change.table
1238
+ };
1239
+ case "column_added":
1240
+ return {
1241
+ description: `Added column '${change.column}' to table '${change.table}'`,
1242
+ type: change.type,
1243
+ table: change.table,
1244
+ column: change.column
1245
+ };
1246
+ case "column_removed":
1247
+ return {
1248
+ description: `Removed column '${change.column}' from table '${change.table}'`,
1249
+ type: change.type,
1250
+ table: change.table,
1251
+ column: change.column
1252
+ };
1253
+ case "column_altered":
1254
+ return {
1255
+ description: `Altered column '${change.column}' in table '${change.table}'`,
1256
+ type: change.type,
1257
+ table: change.table,
1258
+ column: change.column
1259
+ };
1260
+ case "column_renamed":
1261
+ return {
1262
+ description: `Renamed column in table '${change.table}'`,
1263
+ type: change.type,
1264
+ table: change.table
1265
+ };
1266
+ case "index_added":
1267
+ return {
1268
+ description: `Added index on table '${change.table}'`,
1269
+ type: change.type,
1270
+ table: change.table
1271
+ };
1272
+ case "index_removed":
1273
+ return {
1274
+ description: `Removed index on table '${change.table}'`,
1275
+ type: change.type,
1276
+ table: change.table
1277
+ };
1278
+ case "enum_added":
1279
+ return {
1280
+ description: `Added enum type`,
1281
+ type: change.type
1282
+ };
1283
+ case "enum_removed":
1284
+ return {
1285
+ description: `Removed enum type`,
1286
+ type: change.type
1287
+ };
1288
+ case "enum_altered":
1289
+ return {
1290
+ description: `Altered enum type`,
1291
+ type: change.type
1292
+ };
1293
+ }
1294
+ }
1295
+ function detectSchemaDrift(expected, actual) {
1296
+ const drift = [];
1297
+ for (const tableName of Object.keys(actual.tables)) {
1298
+ if (!(tableName in expected.tables)) {
1299
+ drift.push({
1300
+ description: `Table '${tableName}' exists in database but not in schema`,
1301
+ type: "extra_table",
1302
+ table: tableName
1303
+ });
1304
+ }
1305
+ }
1306
+ for (const tableName of Object.keys(expected.tables)) {
1307
+ if (!(tableName in actual.tables)) {
1308
+ drift.push({
1309
+ description: `Table '${tableName}' exists in schema but not in database`,
1310
+ type: "missing_table",
1311
+ table: tableName
1312
+ });
1313
+ }
1314
+ }
1315
+ for (const tableName of Object.keys(expected.tables)) {
1316
+ if (!(tableName in actual.tables))
1317
+ continue;
1318
+ const expectedTable = expected.tables[tableName];
1319
+ const actualTable = actual.tables[tableName];
1320
+ if (!expectedTable || !actualTable)
1321
+ continue;
1322
+ for (const colName of Object.keys(actualTable.columns)) {
1323
+ if (!(colName in expectedTable.columns)) {
1324
+ drift.push({
1325
+ description: `Column '${colName}' exists in database table '${tableName}' but not in schema`,
1326
+ type: "extra_column",
1327
+ table: tableName,
1328
+ column: colName
1329
+ });
1330
+ }
1331
+ }
1332
+ for (const colName of Object.keys(expectedTable.columns)) {
1333
+ if (!(colName in actualTable.columns)) {
1334
+ drift.push({
1335
+ description: `Column '${colName}' exists in schema table '${tableName}' but not in database`,
1336
+ type: "missing_column",
1337
+ table: tableName,
1338
+ column: colName
1339
+ });
1340
+ }
1341
+ }
1342
+ for (const colName of Object.keys(expectedTable.columns)) {
1343
+ if (!(colName in actualTable.columns))
1344
+ continue;
1345
+ const expectedCol = expectedTable.columns[colName];
1346
+ const actualCol = actualTable.columns[colName];
1347
+ if (!expectedCol || !actualCol)
1348
+ continue;
1349
+ if (expectedCol.type !== actualCol.type) {
1350
+ drift.push({
1351
+ description: `Column '${colName}' in table '${tableName}' has type '${actualCol.type}' in database but '${expectedCol.type}' in schema`,
1352
+ type: "column_type_mismatch",
1353
+ table: tableName,
1354
+ column: colName
1355
+ });
1356
+ }
1357
+ }
1358
+ }
1359
+ return drift;
1360
+ }
542
1361
  async function migrateStatus(options) {
543
1362
  const runner = createMigrationRunner();
544
- await runner.createHistoryTable(options.queryFn);
545
- const applied = await runner.getApplied(options.queryFn);
1363
+ const createResult = await runner.createHistoryTable(options.queryFn);
1364
+ if (!createResult.ok) {
1365
+ return createResult;
1366
+ }
1367
+ const appliedResult = await runner.getApplied(options.queryFn);
1368
+ if (!appliedResult.ok) {
1369
+ return appliedResult;
1370
+ }
1371
+ const applied = appliedResult.data;
546
1372
  const pending = runner.getPending(options.migrationFiles, applied);
1373
+ let codeChanges = [];
1374
+ if (options.savedSnapshot && options.currentSnapshot) {
1375
+ const diff = computeDiff(options.savedSnapshot, options.currentSnapshot);
1376
+ codeChanges = diff.changes.map(diffChangeToCodeChange);
1377
+ }
1378
+ let drift = [];
1379
+ if (options.dialect) {
1380
+ const introspect = options.dialect.name === "sqlite" ? introspectSqlite : introspectPostgres;
1381
+ const actualSchema = await introspect(options.queryFn);
1382
+ const expectedSchema = options.savedSnapshot ?? options.currentSnapshot;
1383
+ if (expectedSchema) {
1384
+ drift = detectSchemaDrift(expectedSchema, actualSchema);
1385
+ }
1386
+ }
547
1387
  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)
1388
+ ok: true,
1389
+ data: {
1390
+ applied: applied.map((a) => ({
1391
+ name: a.name,
1392
+ checksum: a.checksum,
1393
+ appliedAt: a.appliedAt
1394
+ })),
1395
+ pending: pending.map((p) => p.name),
1396
+ codeChanges,
1397
+ drift
1398
+ }
554
1399
  };
555
1400
  }
1401
+ // src/client/database.ts
1402
+ import { err as err3, ok as ok2 } from "@vertz/schema";
1403
+ // src/errors/error-codes.ts
1404
+ var DbErrorCode = {
1405
+ UNIQUE_VIOLATION: "23505",
1406
+ FOREIGN_KEY_VIOLATION: "23503",
1407
+ NOT_NULL_VIOLATION: "23502",
1408
+ CHECK_VIOLATION: "23514",
1409
+ EXCLUSION_VIOLATION: "23P01",
1410
+ SERIALIZATION_FAILURE: "40001",
1411
+ DEADLOCK_DETECTED: "40P01",
1412
+ CONNECTION_EXCEPTION: "08000",
1413
+ CONNECTION_DOES_NOT_EXIST: "08003",
1414
+ CONNECTION_FAILURE: "08006",
1415
+ NotFound: "NotFound",
1416
+ CONNECTION_ERROR: "CONNECTION_ERROR",
1417
+ POOL_EXHAUSTED: "POOL_EXHAUSTED"
1418
+ };
1419
+ var PgCodeToName = Object.fromEntries(Object.entries(DbErrorCode).map(([name, pgCode]) => [pgCode, name]));
1420
+ function resolveErrorCode(pgCode) {
1421
+ return PgCodeToName[pgCode];
1422
+ }
1423
+ // src/errors/http-adapter.ts
1424
+ function dbErrorToHttpError(error) {
1425
+ const body = error.toJSON();
1426
+ if (error instanceof UniqueConstraintError) {
1427
+ return { status: 409, body };
1428
+ }
1429
+ if (error instanceof NotFoundError) {
1430
+ return { status: 404, body };
1431
+ }
1432
+ if (error instanceof ForeignKeyError) {
1433
+ return { status: 422, body };
1434
+ }
1435
+ if (error instanceof NotNullError) {
1436
+ return { status: 422, body };
1437
+ }
1438
+ if (error instanceof CheckConstraintError) {
1439
+ return { status: 422, body };
1440
+ }
1441
+ if (error instanceof ConnectionError) {
1442
+ return { status: 503, body };
1443
+ }
1444
+ return { status: 500, body };
1445
+ }
1446
+ // src/errors.ts
1447
+ function toReadError(error, query) {
1448
+ if (typeof error === "object" && error !== null && "code" in error) {
1449
+ const errWithCode = error;
1450
+ if (errWithCode.code === "NotFound") {
1451
+ return {
1452
+ code: "NotFound",
1453
+ message: errWithCode.message,
1454
+ table: errWithCode.table ?? "unknown",
1455
+ cause: error
1456
+ };
1457
+ }
1458
+ if (errWithCode.code.startsWith("08")) {
1459
+ return {
1460
+ code: "CONNECTION_ERROR",
1461
+ message: errWithCode.message,
1462
+ cause: error
1463
+ };
1464
+ }
1465
+ return {
1466
+ code: "QUERY_ERROR",
1467
+ message: errWithCode.message,
1468
+ sql: query,
1469
+ cause: error
1470
+ };
1471
+ }
1472
+ if (error instanceof Error) {
1473
+ const message2 = error.message.toLowerCase();
1474
+ if (message2.includes("connection") || message2.includes("ECONNREFUSED") || message2.includes("timeout")) {
1475
+ return {
1476
+ code: "CONNECTION_ERROR",
1477
+ message: error.message,
1478
+ cause: error
1479
+ };
1480
+ }
1481
+ }
1482
+ const message = error instanceof Error ? error.message : String(error);
1483
+ return {
1484
+ code: "QUERY_ERROR",
1485
+ message,
1486
+ sql: query,
1487
+ cause: error
1488
+ };
1489
+ }
1490
+ function toWriteError(error, query) {
1491
+ if (error instanceof UniqueConstraintError) {
1492
+ return {
1493
+ code: "CONSTRAINT_ERROR",
1494
+ message: error.message,
1495
+ column: error.column,
1496
+ table: error.table,
1497
+ cause: error
1498
+ };
1499
+ }
1500
+ if (error instanceof ForeignKeyError) {
1501
+ return {
1502
+ code: "CONSTRAINT_ERROR",
1503
+ message: error.message,
1504
+ constraint: error.constraint,
1505
+ table: error.table,
1506
+ cause: error
1507
+ };
1508
+ }
1509
+ if (error instanceof NotNullError) {
1510
+ return {
1511
+ code: "CONSTRAINT_ERROR",
1512
+ message: error.message,
1513
+ column: error.column,
1514
+ table: error.table,
1515
+ cause: error
1516
+ };
1517
+ }
1518
+ if (error instanceof CheckConstraintError) {
1519
+ return {
1520
+ code: "CONSTRAINT_ERROR",
1521
+ message: error.message,
1522
+ constraint: error.constraint,
1523
+ table: error.table,
1524
+ cause: error
1525
+ };
1526
+ }
1527
+ if (error instanceof ConnectionError) {
1528
+ return {
1529
+ code: "CONNECTION_ERROR",
1530
+ message: error.message,
1531
+ cause: error
1532
+ };
1533
+ }
1534
+ if (typeof error === "object" && error !== null && "code" in error) {
1535
+ const pgError = error;
1536
+ if (pgError.code.startsWith("08")) {
1537
+ return {
1538
+ code: "CONNECTION_ERROR",
1539
+ message: pgError.message,
1540
+ cause: error
1541
+ };
1542
+ }
1543
+ if (pgError.code === "23505" || pgError.code === "23503" || pgError.code === "23502" || pgError.code === "23514") {
1544
+ if (pgError.code === "23505" || pgError.code === "23502") {
1545
+ return {
1546
+ code: "CONSTRAINT_ERROR",
1547
+ message: pgError.message,
1548
+ table: pgError.table,
1549
+ column: pgError.column,
1550
+ cause: error
1551
+ };
1552
+ } else {
1553
+ return {
1554
+ code: "CONSTRAINT_ERROR",
1555
+ message: pgError.message,
1556
+ table: pgError.table,
1557
+ constraint: pgError.constraint,
1558
+ cause: error
1559
+ };
1560
+ }
1561
+ }
1562
+ return {
1563
+ code: "QUERY_ERROR",
1564
+ message: pgError.message,
1565
+ sql: query,
1566
+ cause: error
1567
+ };
1568
+ }
1569
+ const message = error instanceof Error ? error.message : String(error);
1570
+ return {
1571
+ code: "QUERY_ERROR",
1572
+ message,
1573
+ sql: query,
1574
+ cause: error
1575
+ };
1576
+ }
1577
+
556
1578
  // src/query/aggregate.ts
557
1579
  async function count(queryFn, table, options) {
558
1580
  const allParams = [];
@@ -779,19 +1801,33 @@ async function groupBy(queryFn, table, options) {
779
1801
  }
780
1802
 
781
1803
  // src/query/crud.ts
1804
+ function fillGeneratedIds(table, data) {
1805
+ const filled = { ...data };
1806
+ for (const [name, col] of Object.entries(table._columns)) {
1807
+ const meta = col._meta;
1808
+ if (meta.generate && filled[name] === undefined) {
1809
+ if (meta.sqlType === "integer" || meta.sqlType === "serial" || meta.sqlType === "bigint") {
1810
+ 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).`);
1811
+ }
1812
+ filled[name] = generateId(meta.generate);
1813
+ }
1814
+ }
1815
+ return filled;
1816
+ }
782
1817
  function assertNonEmptyWhere(where, operation) {
783
1818
  if (Object.keys(where).length === 0) {
784
1819
  throw new Error(`${operation} requires a non-empty where clause. ` + "Passing an empty where object would affect all rows.");
785
1820
  }
786
1821
  }
787
- async function get(queryFn, table, options) {
1822
+ async function get(queryFn, table, options, dialect = defaultPostgresDialect) {
788
1823
  const columns = resolveSelectColumns(table, options?.select);
789
1824
  const result = buildSelect({
790
1825
  table: table._name,
791
1826
  columns,
792
1827
  where: options?.where,
793
1828
  orderBy: options?.orderBy,
794
- limit: 1
1829
+ limit: 1,
1830
+ dialect
795
1831
  });
796
1832
  const res = await executeQuery(queryFn, result.sql, result.params);
797
1833
  if (res.rows.length === 0) {
@@ -799,14 +1835,7 @@ async function get(queryFn, table, options) {
799
1835
  }
800
1836
  return mapRow(res.rows[0]);
801
1837
  }
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) {
1838
+ async function list(queryFn, table, options, dialect = defaultPostgresDialect) {
810
1839
  const columns = resolveSelectColumns(table, options?.select);
811
1840
  const result = buildSelect({
812
1841
  table: table._name,
@@ -816,12 +1845,13 @@ async function list(queryFn, table, options) {
816
1845
  limit: options?.limit,
817
1846
  offset: options?.offset,
818
1847
  cursor: options?.cursor,
819
- take: options?.take
1848
+ take: options?.take,
1849
+ dialect
820
1850
  });
821
1851
  const res = await executeQuery(queryFn, result.sql, result.params);
822
1852
  return mapRows(res.rows);
823
1853
  }
824
- async function listAndCount(queryFn, table, options) {
1854
+ async function listAndCount(queryFn, table, options, dialect = defaultPostgresDialect) {
825
1855
  const columns = resolveSelectColumns(table, options?.select);
826
1856
  const result = buildSelect({
827
1857
  table: table._name,
@@ -832,7 +1862,8 @@ async function listAndCount(queryFn, table, options) {
832
1862
  offset: options?.offset,
833
1863
  cursor: options?.cursor,
834
1864
  take: options?.take,
835
- withCount: true
1865
+ withCount: true,
1866
+ dialect
836
1867
  });
837
1868
  const res = await executeQuery(queryFn, result.sql, result.params);
838
1869
  const rows = res.rows;
@@ -847,55 +1878,97 @@ async function listAndCount(queryFn, table, options) {
847
1878
  });
848
1879
  return { data, total };
849
1880
  }
850
- async function create(queryFn, table, options) {
1881
+ async function create(queryFn, table, options, dialect = defaultPostgresDialect) {
851
1882
  const returningColumns = resolveSelectColumns(table, options.select);
852
1883
  const nowColumns = getTimestampColumns(table);
1884
+ const readOnlyCols = getReadOnlyColumns(table);
1885
+ const withIds = fillGeneratedIds(table, options.data);
1886
+ const filteredData = Object.fromEntries(Object.entries(withIds).filter(([key]) => {
1887
+ const col = table._columns[key];
1888
+ const meta = col ? col._meta : undefined;
1889
+ if (meta?.generate)
1890
+ return true;
1891
+ return !readOnlyCols.includes(key);
1892
+ }));
853
1893
  const result = buildInsert({
854
1894
  table: table._name,
855
- data: options.data,
1895
+ data: filteredData,
856
1896
  returning: returningColumns,
857
- nowColumns
1897
+ nowColumns,
1898
+ dialect
858
1899
  });
859
1900
  const res = await executeQuery(queryFn, result.sql, result.params);
860
1901
  return mapRow(res.rows[0]);
861
1902
  }
862
- async function createMany(queryFn, table, options) {
1903
+ async function createMany(queryFn, table, options, dialect = defaultPostgresDialect) {
863
1904
  if (options.data.length === 0) {
864
1905
  return { count: 0 };
865
1906
  }
866
1907
  const nowColumns = getTimestampColumns(table);
1908
+ const readOnlyCols = getReadOnlyColumns(table);
1909
+ const filteredData = options.data.map((row) => {
1910
+ const withIds = fillGeneratedIds(table, row);
1911
+ return Object.fromEntries(Object.entries(withIds).filter(([key]) => {
1912
+ const col = table._columns[key];
1913
+ const meta = col ? col._meta : undefined;
1914
+ if (meta?.generate)
1915
+ return true;
1916
+ return !readOnlyCols.includes(key);
1917
+ }));
1918
+ });
867
1919
  const result = buildInsert({
868
1920
  table: table._name,
869
- data: options.data,
870
- nowColumns
1921
+ data: filteredData,
1922
+ nowColumns,
1923
+ dialect
871
1924
  });
872
1925
  const res = await executeQuery(queryFn, result.sql, result.params);
873
1926
  return { count: res.rowCount };
874
1927
  }
875
- async function createManyAndReturn(queryFn, table, options) {
1928
+ async function createManyAndReturn(queryFn, table, options, dialect = defaultPostgresDialect) {
876
1929
  if (options.data.length === 0) {
877
1930
  return [];
878
1931
  }
879
1932
  const returningColumns = resolveSelectColumns(table, options.select);
880
1933
  const nowColumns = getTimestampColumns(table);
1934
+ const readOnlyCols = getReadOnlyColumns(table);
1935
+ const filteredData = options.data.map((row) => {
1936
+ const withIds = fillGeneratedIds(table, row);
1937
+ return Object.fromEntries(Object.entries(withIds).filter(([key]) => {
1938
+ const col = table._columns[key];
1939
+ const meta = col ? col._meta : undefined;
1940
+ if (meta?.generate)
1941
+ return true;
1942
+ return !readOnlyCols.includes(key);
1943
+ }));
1944
+ });
881
1945
  const result = buildInsert({
882
1946
  table: table._name,
883
- data: options.data,
1947
+ data: filteredData,
884
1948
  returning: returningColumns,
885
- nowColumns
1949
+ nowColumns,
1950
+ dialect
886
1951
  });
887
1952
  const res = await executeQuery(queryFn, result.sql, result.params);
888
1953
  return mapRows(res.rows);
889
1954
  }
890
- async function update(queryFn, table, options) {
1955
+ async function update(queryFn, table, options, dialect = defaultPostgresDialect) {
891
1956
  const returningColumns = resolveSelectColumns(table, options.select);
892
1957
  const nowColumns = getTimestampColumns(table);
1958
+ const readOnlyCols = getReadOnlyColumns(table);
1959
+ const autoUpdateCols = getAutoUpdateColumns(table);
1960
+ const filteredData = Object.fromEntries(Object.entries(options.data).filter(([key]) => !readOnlyCols.includes(key)));
1961
+ for (const col of autoUpdateCols) {
1962
+ filteredData[col] = "now";
1963
+ }
1964
+ const allNowColumns = [...new Set([...nowColumns, ...autoUpdateCols])];
893
1965
  const result = buildUpdate({
894
1966
  table: table._name,
895
- data: options.data,
1967
+ data: filteredData,
896
1968
  where: options.where,
897
1969
  returning: returningColumns,
898
- nowColumns
1970
+ nowColumns: allNowColumns,
1971
+ dialect
899
1972
  });
900
1973
  const res = await executeQuery(queryFn, result.sql, result.params);
901
1974
  if (res.rows.length === 0) {
@@ -903,44 +1976,69 @@ async function update(queryFn, table, options) {
903
1976
  }
904
1977
  return mapRow(res.rows[0]);
905
1978
  }
906
- async function updateMany(queryFn, table, options) {
1979
+ async function updateMany(queryFn, table, options, dialect = defaultPostgresDialect) {
907
1980
  assertNonEmptyWhere(options.where, "updateMany");
908
1981
  const nowColumns = getTimestampColumns(table);
1982
+ const readOnlyCols = getReadOnlyColumns(table);
1983
+ const autoUpdateCols = getAutoUpdateColumns(table);
1984
+ const filteredData = Object.fromEntries(Object.entries(options.data).filter(([key]) => !readOnlyCols.includes(key)));
1985
+ for (const col of autoUpdateCols) {
1986
+ filteredData[col] = "now";
1987
+ }
1988
+ const allNowColumns = [...new Set([...nowColumns, ...autoUpdateCols])];
909
1989
  const result = buildUpdate({
910
1990
  table: table._name,
911
- data: options.data,
1991
+ data: filteredData,
912
1992
  where: options.where,
913
- nowColumns
1993
+ nowColumns: allNowColumns,
1994
+ dialect
914
1995
  });
915
1996
  const res = await executeQuery(queryFn, result.sql, result.params);
916
1997
  return { count: res.rowCount };
917
1998
  }
918
- async function upsert(queryFn, table, options) {
1999
+ async function upsert(queryFn, table, options, dialect = defaultPostgresDialect) {
919
2000
  const returningColumns = resolveSelectColumns(table, options.select);
920
2001
  const nowColumns = getTimestampColumns(table);
2002
+ const readOnlyCols = getReadOnlyColumns(table);
2003
+ const autoUpdateCols = getAutoUpdateColumns(table);
921
2004
  const conflictColumns = Object.keys(options.where);
922
- const updateColumns = Object.keys(options.update);
2005
+ const createWithIds = fillGeneratedIds(table, options.create);
2006
+ const filteredCreate = Object.fromEntries(Object.entries(createWithIds).filter(([key]) => {
2007
+ const col = table._columns[key];
2008
+ const meta = col ? col._meta : undefined;
2009
+ if (meta?.generate)
2010
+ return true;
2011
+ return !readOnlyCols.includes(key);
2012
+ }));
2013
+ const filteredUpdate = Object.fromEntries(Object.entries(options.update).filter(([key]) => !readOnlyCols.includes(key)));
2014
+ for (const col of autoUpdateCols) {
2015
+ filteredUpdate[col] = "now";
2016
+ }
2017
+ const allNowColumns = [...new Set([...nowColumns, ...autoUpdateCols])];
2018
+ const updateColumns = Object.keys(filteredUpdate);
923
2019
  const result = buildInsert({
924
2020
  table: table._name,
925
- data: options.create,
2021
+ data: filteredCreate,
926
2022
  returning: returningColumns,
927
- nowColumns,
2023
+ nowColumns: allNowColumns,
928
2024
  onConflict: {
929
2025
  columns: conflictColumns,
930
2026
  action: "update",
931
2027
  updateColumns,
932
- updateValues: options.update
933
- }
2028
+ updateValues: filteredUpdate
2029
+ },
2030
+ dialect
934
2031
  });
935
2032
  const res = await executeQuery(queryFn, result.sql, result.params);
936
2033
  return mapRow(res.rows[0]);
937
2034
  }
938
- async function deleteOne(queryFn, table, options) {
2035
+ async function deleteOne(queryFn, table, options, dialect = defaultPostgresDialect) {
939
2036
  const returningColumns = resolveSelectColumns(table, options.select);
940
2037
  const result = buildDelete({
941
2038
  table: table._name,
942
2039
  where: options.where,
943
- returning: returningColumns
2040
+ returning: returningColumns,
2041
+ dialect
944
2042
  });
945
2043
  const res = await executeQuery(queryFn, result.sql, result.params);
946
2044
  if (res.rows.length === 0) {
@@ -948,11 +2046,12 @@ async function deleteOne(queryFn, table, options) {
948
2046
  }
949
2047
  return mapRow(res.rows[0]);
950
2048
  }
951
- async function deleteMany(queryFn, table, options) {
2049
+ async function deleteMany(queryFn, table, options, dialect = defaultPostgresDialect) {
952
2050
  assertNonEmptyWhere(options.where, "deleteMany");
953
2051
  const result = buildDelete({
954
2052
  table: table._name,
955
- where: options.where
2053
+ where: options.where,
2054
+ dialect
956
2055
  });
957
2056
  const res = await executeQuery(queryFn, result.sql, result.params);
958
2057
  return { count: res.rowCount };
@@ -1187,60 +2286,95 @@ async function loadManyToManyRelation(queryFn, primaryRows, def, target, relName
1187
2286
  }
1188
2287
  }
1189
2288
 
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;
2289
+ // src/client/sqlite-value-converter.ts
2290
+ function fromSqliteValue(value, columnType) {
2291
+ if (columnType === "boolean") {
2292
+ if (value === 1) {
2293
+ return true;
2294
+ }
2295
+ if (value === 0) {
2296
+ return false;
2297
+ }
2298
+ }
2299
+ if ((columnType === "timestamp" || columnType === "timestamp with time zone") && typeof value === "string") {
2300
+ return new Date(value);
1206
2301
  }
1207
- throw error;
2302
+ return value;
1208
2303
  }
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);
2304
+
2305
+ // src/client/sqlite-driver.ts
2306
+ function buildTableSchema(models) {
2307
+ const registry = new Map;
2308
+ for (const [, entry] of Object.entries(models)) {
2309
+ const tableName = entry.table._name;
2310
+ const columnTypes = {};
2311
+ for (const [colName, colBuilder] of Object.entries(entry.table._columns)) {
2312
+ const meta = colBuilder._meta;
2313
+ columnTypes[colName] = meta.sqlType;
2314
+ }
2315
+ registry.set(tableName, columnTypes);
2316
+ }
2317
+ return registry;
2318
+ }
2319
+ function extractTableName(sql) {
2320
+ const fromMatch = sql.match(/\bFROM\s+"?([a-zA-Z_][a-zA-Z0-9_]*)"?/i);
2321
+ if (fromMatch) {
2322
+ return fromMatch[1].toLowerCase();
2323
+ }
2324
+ const insertMatch = sql.match(/\bINSERT\s+INTO\s+"?([a-zA-Z_][a-zA-Z0-9_]*)"?/i);
2325
+ if (insertMatch) {
2326
+ return insertMatch[1].toLowerCase();
2327
+ }
2328
+ const updateMatch = sql.match(/\bUPDATE\s+"?([a-zA-Z_][a-zA-Z0-9_]*)"?/i);
2329
+ if (updateMatch) {
2330
+ return updateMatch[1].toLowerCase();
2331
+ }
2332
+ const deleteMatch = sql.match(/\bDELETE\s+FROM\s+"?([a-zA-Z_][a-zA-Z0-9_]*)"?/i);
2333
+ if (deleteMatch) {
2334
+ return deleteMatch[1].toLowerCase();
2335
+ }
2336
+ return null;
2337
+ }
2338
+ function createSqliteDriver(d1, tableSchema) {
2339
+ const query = async (sql, params) => {
2340
+ const prepared = d1.prepare(sql);
2341
+ const bound = params ? prepared.bind(...params) : prepared;
2342
+ const result = await bound.all();
2343
+ if (tableSchema && result.results.length > 0) {
2344
+ const tableName = extractTableName(sql);
2345
+ if (tableName) {
2346
+ const schema = tableSchema.get(tableName);
2347
+ if (schema) {
2348
+ return result.results.map((row) => {
2349
+ const convertedRow = {};
2350
+ for (const [key, value] of Object.entries(row)) {
2351
+ const columnType = schema[key];
2352
+ if (columnType) {
2353
+ convertedRow[key] = fromSqliteValue(value, columnType);
2354
+ } else {
2355
+ convertedRow[key] = value;
2356
+ }
2357
+ }
2358
+ return convertedRow;
2359
+ });
1223
2360
  }
1224
- return mapped;
1225
- });
1226
- return {
1227
- rows,
1228
- rowCount: result.count ?? rows.length
1229
- };
1230
- } catch (error) {
1231
- adaptPostgresError(error);
2361
+ }
1232
2362
  }
2363
+ return result.results;
2364
+ };
2365
+ const execute = async (sql, params) => {
2366
+ const prepared = d1.prepare(sql);
2367
+ const bound = params ? prepared.bind(...params) : prepared;
2368
+ const result = await bound.run();
2369
+ return { rowsAffected: result.meta.changes };
1233
2370
  };
1234
2371
  return {
1235
- queryFn,
1236
- async close() {
1237
- await sql.end();
1238
- },
1239
- async isHealthy() {
2372
+ query,
2373
+ execute,
2374
+ close: async () => {},
2375
+ isHealthy: async () => {
1240
2376
  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]);
2377
+ await query("SELECT 1");
1244
2378
  return true;
1245
2379
  } catch {
1246
2380
  return false;
@@ -1248,18 +2382,6 @@ function createPostgresDriver(url, pool) {
1248
2382
  }
1249
2383
  };
1250
2384
  }
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
2385
 
1264
2386
  // src/client/tenant-graph.ts
1265
2387
  function computeTenantGraph(registry) {
@@ -1379,16 +2501,31 @@ function isReadQuery(sqlStr) {
1379
2501
  }
1380
2502
  return upper.startsWith("SELECT");
1381
2503
  }
1382
- function resolveTable(tables, name) {
1383
- const entry = tables[name];
2504
+ function resolveModel(models, name) {
2505
+ const entry = models[name];
1384
2506
  if (!entry) {
1385
2507
  throw new Error(`Table "${name}" is not registered in the database.`);
1386
2508
  }
1387
2509
  return entry;
1388
2510
  }
2511
+ var RESERVED_MODEL_NAMES = new Set(["query", "close", "isHealthy", "_internals"]);
1389
2512
  function createDb(options) {
1390
- const { tables, log } = options;
1391
- const tenantGraph = computeTenantGraph(tables);
2513
+ const { models, log, dialect } = options;
2514
+ for (const key of Object.keys(models)) {
2515
+ if (RESERVED_MODEL_NAMES.has(key)) {
2516
+ throw new Error(`Model name "${key}" is reserved. Choose a different name for this model. ` + `Reserved names: ${[...RESERVED_MODEL_NAMES].join(", ")}`);
2517
+ }
2518
+ }
2519
+ if (dialect === "sqlite") {
2520
+ if (!options.d1) {
2521
+ throw new Error("SQLite dialect requires a D1 binding");
2522
+ }
2523
+ if (options.url) {
2524
+ throw new Error("SQLite dialect uses D1, not a connection URL");
2525
+ }
2526
+ }
2527
+ const dialectObj = dialect === "sqlite" ? defaultSqliteDialect : defaultPostgresDialect;
2528
+ const tenantGraph = computeTenantGraph(models);
1392
2529
  if (log && tenantGraph.root !== null) {
1393
2530
  const allScoped = new Set([
1394
2531
  ...tenantGraph.root !== null ? [tenantGraph.root] : [],
@@ -1396,28 +2533,51 @@ function createDb(options) {
1396
2533
  ...tenantGraph.indirectlyScoped,
1397
2534
  ...tenantGraph.shared
1398
2535
  ]);
1399
- for (const [key, entry] of Object.entries(tables)) {
2536
+ for (const [key, entry] of Object.entries(models)) {
1400
2537
  if (!allScoped.has(key)) {
1401
2538
  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
2539
  }
1403
2540
  }
1404
2541
  }
1405
- const tablesRegistry = tables;
2542
+ const modelsRegistry = models;
1406
2543
  let driver = null;
2544
+ let sqliteDriver = null;
1407
2545
  let replicaDrivers = [];
1408
2546
  let replicaIndex = 0;
1409
2547
  const queryFn = (() => {
1410
2548
  if (options._queryFn) {
1411
2549
  return options._queryFn;
1412
2550
  }
2551
+ if (dialect === "sqlite" && options.d1) {
2552
+ const tableSchema = buildTableSchema(models);
2553
+ sqliteDriver = createSqliteDriver(options.d1, tableSchema);
2554
+ return async (sqlStr, params) => {
2555
+ if (!sqliteDriver) {
2556
+ throw new Error("SQLite driver not initialized");
2557
+ }
2558
+ const rows = await sqliteDriver.query(sqlStr, params);
2559
+ return { rows, rowCount: rows.length };
2560
+ };
2561
+ }
1413
2562
  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
- }
2563
+ let initialized = false;
2564
+ const initPostgres = async () => {
2565
+ if (initialized)
2566
+ return;
2567
+ const { createPostgresDriver } = await import("./shared/chunk-2gd1fqcw.js");
2568
+ driver = createPostgresDriver(options.url, options.pool);
2569
+ const replicas = options.pool?.replicas;
2570
+ if (replicas && replicas.length > 0) {
2571
+ replicaDrivers = replicas.map((replicaUrl) => createPostgresDriver(replicaUrl, options.pool));
2572
+ }
2573
+ initialized = true;
2574
+ };
1419
2575
  return async (sqlStr, params) => {
2576
+ await initPostgres();
1420
2577
  if (replicaDrivers.length === 0) {
2578
+ if (!driver) {
2579
+ throw new Error("Database driver not initialized");
2580
+ }
1421
2581
  return driver.queryFn(sqlStr, params);
1422
2582
  }
1423
2583
  if (isReadQuery(sqlStr)) {
@@ -1425,127 +2585,260 @@ function createDb(options) {
1425
2585
  replicaIndex = (replicaIndex + 1) % replicaDrivers.length;
1426
2586
  try {
1427
2587
  return await targetReplica.queryFn(sqlStr, params);
1428
- } catch (err) {
1429
- console.warn("[vertz/db] replica query failed, falling back to primary:", err.message);
2588
+ } catch (err4) {
2589
+ console.warn("[vertz/db] replica query failed, falling back to primary:", err4.message);
1430
2590
  }
1431
2591
  }
2592
+ if (!driver) {
2593
+ throw new Error("Database driver not initialized");
2594
+ }
1432
2595
  return driver.queryFn(sqlStr, params);
1433
2596
  };
1434
2597
  }
1435
2598
  return async () => {
1436
- throw new Error("db.query() requires a connected postgres driver. " + "Provide a `url` to connect to PostgreSQL, or `_queryFn` for testing.");
2599
+ 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
2600
  };
1438
2601
  })();
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();
2602
+ function implGet(name, opts) {
2603
+ return (async () => {
2604
+ try {
2605
+ const entry = resolveModel(models, name);
2606
+ const result = await get(queryFn, entry.table, opts, dialectObj);
2607
+ if (result !== null && opts?.include) {
2608
+ const rows = await loadRelations(queryFn, [result], entry.relations, opts.include, 0, modelsRegistry, entry.table);
2609
+ return ok2(rows[0] ?? null);
2610
+ }
2611
+ return ok2(result);
2612
+ } catch (e) {
2613
+ return err3(toReadError(e));
1448
2614
  }
1449
- await Promise.all(replicaDrivers.map((r) => r.close()));
1450
- },
1451
- async isHealthy() {
1452
- if (driver) {
1453
- return driver.isHealthy();
2615
+ })();
2616
+ }
2617
+ function implGetRequired(name, opts) {
2618
+ return (async () => {
2619
+ try {
2620
+ const entry = resolveModel(models, name);
2621
+ const result = await get(queryFn, entry.table, opts, dialectObj);
2622
+ if (result === null) {
2623
+ return err3({
2624
+ code: "NotFound",
2625
+ message: `Record not found in table ${name}`,
2626
+ table: name
2627
+ });
2628
+ }
2629
+ if (opts?.include) {
2630
+ const rows = await loadRelations(queryFn, [result], entry.relations, opts.include, 0, modelsRegistry, entry.table);
2631
+ return ok2(rows[0]);
2632
+ }
2633
+ return ok2(result);
2634
+ } catch (e) {
2635
+ return err3(toReadError(e));
1454
2636
  }
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;
2637
+ })();
2638
+ }
2639
+ function implList(name, opts) {
2640
+ return (async () => {
2641
+ try {
2642
+ const entry = resolveModel(models, name);
2643
+ const results = await list(queryFn, entry.table, opts, dialectObj);
2644
+ if (opts?.include && results.length > 0) {
2645
+ const withRelations = await loadRelations(queryFn, results, entry.relations, opts.include, 0, modelsRegistry, entry.table);
2646
+ return ok2(withRelations);
2647
+ }
2648
+ return ok2(results);
2649
+ } catch (e) {
2650
+ return err3(toReadError(e));
1463
2651
  }
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];
2652
+ })();
2653
+ }
2654
+ function implListAndCount(name, opts) {
2655
+ return (async () => {
2656
+ try {
2657
+ const entry = resolveModel(models, name);
2658
+ const { data, total } = await listAndCount(queryFn, entry.table, opts, dialectObj);
2659
+ if (opts?.include && data.length > 0) {
2660
+ const withRelations = await loadRelations(queryFn, data, entry.relations, opts.include, 0, modelsRegistry, entry.table);
2661
+ return ok2({ data: withRelations, total });
2662
+ }
2663
+ return ok2({ data, total });
2664
+ } catch (e) {
2665
+ return err3(toReadError(e));
1472
2666
  }
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);
2667
+ })();
2668
+ }
2669
+ function implCreate(name, opts) {
2670
+ return (async () => {
2671
+ try {
2672
+ const entry = resolveModel(models, name);
2673
+ const result = await create(queryFn, entry.table, opts, dialectObj);
2674
+ return ok2(result);
2675
+ } catch (e) {
2676
+ return err3(toWriteError(e));
1480
2677
  }
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);
2678
+ })();
2679
+ }
2680
+ function implCreateMany(name, opts) {
2681
+ return (async () => {
2682
+ try {
2683
+ const entry = resolveModel(models, name);
2684
+ const result = await createMany(queryFn, entry.table, opts, dialectObj);
2685
+ return ok2(result);
2686
+ } catch (e) {
2687
+ return err3(toWriteError(e));
2688
+ }
2689
+ })();
2690
+ }
2691
+ function implCreateManyAndReturn(name, opts) {
2692
+ return (async () => {
2693
+ try {
2694
+ const entry = resolveModel(models, name);
2695
+ const result = await createManyAndReturn(queryFn, entry.table, opts, dialectObj);
2696
+ return ok2(result);
2697
+ } catch (e) {
2698
+ return err3(toWriteError(e));
2699
+ }
2700
+ })();
2701
+ }
2702
+ function implUpdate(name, opts) {
2703
+ return (async () => {
2704
+ try {
2705
+ const entry = resolveModel(models, name);
2706
+ const result = await update(queryFn, entry.table, opts, dialectObj);
2707
+ return ok2(result);
2708
+ } catch (e) {
2709
+ return err3(toWriteError(e));
2710
+ }
2711
+ })();
2712
+ }
2713
+ function implUpdateMany(name, opts) {
2714
+ return (async () => {
2715
+ try {
2716
+ const entry = resolveModel(models, name);
2717
+ const result = await updateMany(queryFn, entry.table, opts, dialectObj);
2718
+ return ok2(result);
2719
+ } catch (e) {
2720
+ return err3(toWriteError(e));
2721
+ }
2722
+ })();
2723
+ }
2724
+ function implUpsert(name, opts) {
2725
+ return (async () => {
2726
+ try {
2727
+ const entry = resolveModel(models, name);
2728
+ const result = await upsert(queryFn, entry.table, opts, dialectObj);
2729
+ return ok2(result);
2730
+ } catch (e) {
2731
+ return err3(toWriteError(e));
2732
+ }
2733
+ })();
2734
+ }
2735
+ function implDelete(name, opts) {
2736
+ return (async () => {
2737
+ try {
2738
+ const entry = resolveModel(models, name);
2739
+ const result = await deleteOne(queryFn, entry.table, opts, dialectObj);
2740
+ return ok2(result);
2741
+ } catch (e) {
2742
+ return err3(toWriteError(e));
2743
+ }
2744
+ })();
2745
+ }
2746
+ function implDeleteMany(name, opts) {
2747
+ return (async () => {
2748
+ try {
2749
+ const entry = resolveModel(models, name);
2750
+ const result = await deleteMany(queryFn, entry.table, opts, dialectObj);
2751
+ return ok2(result);
2752
+ } catch (e) {
2753
+ return err3(toWriteError(e));
2754
+ }
2755
+ })();
2756
+ }
2757
+ function implCount(name, opts) {
2758
+ return (async () => {
2759
+ try {
2760
+ const entry = resolveModel(models, name);
2761
+ const result = await count(queryFn, entry.table, opts);
2762
+ return ok2(result);
2763
+ } catch (e) {
2764
+ return err3(toReadError(e));
2765
+ }
2766
+ })();
2767
+ }
2768
+ function implAggregate(name, opts) {
2769
+ return (async () => {
2770
+ try {
2771
+ const entry = resolveModel(models, name);
2772
+ const result = await aggregate(queryFn, entry.table, opts);
2773
+ return ok2(result);
2774
+ } catch (e) {
2775
+ return err3(toReadError(e));
2776
+ }
2777
+ })();
2778
+ }
2779
+ function implGroupBy(name, opts) {
2780
+ return (async () => {
2781
+ try {
2782
+ const entry = resolveModel(models, name);
2783
+ const result = await groupBy(queryFn, entry.table, opts);
2784
+ return ok2(result);
2785
+ } catch (e) {
2786
+ return err3(toReadError(e));
2787
+ }
2788
+ })();
2789
+ }
2790
+ const client = {};
2791
+ for (const name of Object.keys(models)) {
2792
+ client[name] = {
2793
+ get: (opts) => implGet(name, opts),
2794
+ getOrThrow: (opts) => implGetRequired(name, opts),
2795
+ list: (opts) => implList(name, opts),
2796
+ listAndCount: (opts) => implListAndCount(name, opts),
2797
+ create: (opts) => implCreate(name, opts),
2798
+ createMany: (opts) => implCreateMany(name, opts),
2799
+ createManyAndReturn: (opts) => implCreateManyAndReturn(name, opts),
2800
+ update: (opts) => implUpdate(name, opts),
2801
+ updateMany: (opts) => implUpdateMany(name, opts),
2802
+ upsert: (opts) => implUpsert(name, opts),
2803
+ delete: (opts) => implDelete(name, opts),
2804
+ deleteMany: (opts) => implDeleteMany(name, opts),
2805
+ count: (opts) => implCount(name, opts),
2806
+ aggregate: (opts) => implAggregate(name, opts),
2807
+ groupBy: (opts) => implGroupBy(name, opts)
2808
+ };
2809
+ }
2810
+ client.query = async (fragment) => {
2811
+ try {
2812
+ const result = await executeQuery(queryFn, fragment.sql, fragment.params);
2813
+ return ok2(result);
2814
+ } catch (e) {
2815
+ return err3(toReadError(e, fragment.sql));
2816
+ }
2817
+ };
2818
+ client.close = async () => {
2819
+ if (driver) {
2820
+ await driver.close();
2821
+ }
2822
+ if (sqliteDriver) {
2823
+ await sqliteDriver.close();
2824
+ }
2825
+ await Promise.all(replicaDrivers.map((r) => r.close()));
2826
+ };
2827
+ client.isHealthy = async () => {
2828
+ if (driver) {
2829
+ return driver.isHealthy();
1547
2830
  }
2831
+ if (sqliteDriver) {
2832
+ return sqliteDriver.isHealthy();
2833
+ }
2834
+ return true;
2835
+ };
2836
+ client._internals = {
2837
+ models,
2838
+ dialect: dialectObj,
2839
+ tenantGraph
1548
2840
  };
2841
+ return client;
1549
2842
  }
1550
2843
  // src/schema/column.ts
1551
2844
  function cloneWith(source, metaOverrides) {
@@ -1554,8 +2847,12 @@ function cloneWith(source, metaOverrides) {
1554
2847
  function createColumnWithMeta(meta) {
1555
2848
  const col = {
1556
2849
  _meta: meta,
1557
- primary() {
1558
- return cloneWith(this, { primary: true, hasDefault: true });
2850
+ primary(options) {
2851
+ const meta2 = { primary: true, hasDefault: true };
2852
+ if (options?.generate) {
2853
+ meta2.generate = options.generate;
2854
+ }
2855
+ return cloneWith(this, meta2);
1559
2856
  },
1560
2857
  unique() {
1561
2858
  return cloneWith(this, { unique: true });
@@ -1566,11 +2863,17 @@ function createColumnWithMeta(meta) {
1566
2863
  default(value) {
1567
2864
  return cloneWith(this, { hasDefault: true, defaultValue: value });
1568
2865
  },
1569
- sensitive() {
1570
- return cloneWith(this, { sensitive: true });
2866
+ is(flag) {
2867
+ return createColumnWithMeta({
2868
+ ...this._meta,
2869
+ _annotations: { ...this._meta._annotations, [flag]: true }
2870
+ });
2871
+ },
2872
+ readOnly() {
2873
+ return cloneWith(this, { isReadOnly: true });
1571
2874
  },
1572
- hidden() {
1573
- return cloneWith(this, { hidden: true });
2875
+ autoUpdate() {
2876
+ return cloneWith(this, { isAutoUpdate: true, isReadOnly: true });
1574
2877
  },
1575
2878
  check(sql) {
1576
2879
  return cloneWith(this, { check: sql });
@@ -1590,8 +2893,9 @@ function defaultMeta(sqlType) {
1590
2893
  unique: false,
1591
2894
  nullable: false,
1592
2895
  hasDefault: false,
1593
- sensitive: false,
1594
- hidden: false,
2896
+ _annotations: {},
2897
+ isReadOnly: false,
2898
+ isAutoUpdate: false,
1595
2899
  isTenant: false,
1596
2900
  references: null,
1597
2901
  check: null
@@ -1610,8 +2914,9 @@ function createSerialColumn() {
1610
2914
  unique: false,
1611
2915
  nullable: false,
1612
2916
  hasDefault: true,
1613
- sensitive: false,
1614
- hidden: false,
2917
+ _annotations: {},
2918
+ isReadOnly: false,
2919
+ isAutoUpdate: false,
1615
2920
  isTenant: false,
1616
2921
  references: null,
1617
2922
  check: null
@@ -1624,14 +2929,101 @@ function createTenantColumn(targetTableName) {
1624
2929
  unique: false,
1625
2930
  nullable: false,
1626
2931
  hasDefault: false,
1627
- sensitive: false,
1628
- hidden: false,
2932
+ _annotations: {},
2933
+ isReadOnly: false,
2934
+ isAutoUpdate: false,
1629
2935
  isTenant: true,
1630
2936
  references: { table: targetTableName, column: "id" },
1631
2937
  check: null
1632
2938
  });
1633
2939
  }
1634
2940
 
2941
+ // src/schema/model-schemas.ts
2942
+ function deriveSchemas(table) {
2943
+ const hiddenCols = getColumnNamesWithAnnotation(table, "hidden");
2944
+ const readOnlyCols = getColumnNamesWhere(table, "isReadOnly");
2945
+ const primaryCols = getColumnNamesWhere(table, "primary");
2946
+ const allCols = new Set(Object.keys(table._columns));
2947
+ const defaultCols = getColumnNamesWhere(table, "hasDefault");
2948
+ const responseCols = setDifference(allCols, hiddenCols);
2949
+ const inputCols = setDifference(setDifference(allCols, readOnlyCols), primaryCols);
2950
+ const requiredCols = getRequiredInputColumns(table, inputCols, defaultCols);
2951
+ return {
2952
+ response: {
2953
+ parse(value) {
2954
+ return { ok: true, data: pickKeys(value, responseCols) };
2955
+ }
2956
+ },
2957
+ createInput: {
2958
+ parse(value) {
2959
+ const data = value;
2960
+ const missing = requiredCols.filter((col) => !(col in data) || data[col] === undefined);
2961
+ if (missing.length > 0) {
2962
+ return {
2963
+ ok: false,
2964
+ error: new Error(`Missing required fields: ${missing.join(", ")}`)
2965
+ };
2966
+ }
2967
+ return { ok: true, data: pickKeys(value, inputCols) };
2968
+ }
2969
+ },
2970
+ updateInput: {
2971
+ parse(value) {
2972
+ return { ok: true, data: pickKeys(value, inputCols) };
2973
+ }
2974
+ }
2975
+ };
2976
+ }
2977
+ function pickKeys(value, allowed) {
2978
+ const data = value;
2979
+ const result = {};
2980
+ for (const [key, val] of Object.entries(data)) {
2981
+ if (allowed.has(key)) {
2982
+ result[key] = val;
2983
+ }
2984
+ }
2985
+ return result;
2986
+ }
2987
+ function setDifference(a, b) {
2988
+ const result = new Set;
2989
+ for (const item of a) {
2990
+ if (!b.has(item)) {
2991
+ result.add(item);
2992
+ }
2993
+ }
2994
+ return result;
2995
+ }
2996
+ function getRequiredInputColumns(_table, allowed, defaults) {
2997
+ return [...allowed].filter((key) => !defaults.has(key));
2998
+ }
2999
+ function getColumnNamesWhere(table, flag) {
3000
+ const result = new Set;
3001
+ for (const [key, col] of Object.entries(table._columns)) {
3002
+ if (col._meta[flag]) {
3003
+ result.add(key);
3004
+ }
3005
+ }
3006
+ return result;
3007
+ }
3008
+ function getColumnNamesWithAnnotation(table, annotation) {
3009
+ const result = new Set;
3010
+ for (const [key, col] of Object.entries(table._columns)) {
3011
+ if (col._meta._annotations[annotation]) {
3012
+ result.add(key);
3013
+ }
3014
+ }
3015
+ return result;
3016
+ }
3017
+
3018
+ // src/schema/model.ts
3019
+ function createModel(table, relations) {
3020
+ return {
3021
+ table,
3022
+ relations: relations ?? {},
3023
+ schemas: deriveSchemas(table)
3024
+ };
3025
+ }
3026
+
1635
3027
  // src/schema/relation.ts
1636
3028
  function createOneRelation(target, foreignKey) {
1637
3029
  return {
@@ -1663,9 +3055,10 @@ function createManyRelation(target, foreignKey) {
1663
3055
  }
1664
3056
 
1665
3057
  // src/schema/table.ts
1666
- function createIndex(columns) {
3058
+ function createIndex(columns, options) {
1667
3059
  return {
1668
- columns: Array.isArray(columns) ? columns : [columns]
3060
+ columns: Array.isArray(columns) ? columns : [columns],
3061
+ ...options
1669
3062
  };
1670
3063
  }
1671
3064
  function createTable(name, columns, options) {
@@ -1689,10 +3082,13 @@ function createTableInternal(name, columns, indexes, shared) {
1689
3082
  get $update() {
1690
3083
  return;
1691
3084
  },
1692
- get $not_sensitive() {
3085
+ get $response() {
3086
+ return;
3087
+ },
3088
+ get $create_input() {
1693
3089
  return;
1694
3090
  },
1695
- get $not_hidden() {
3091
+ get $update_input() {
1696
3092
  return;
1697
3093
  },
1698
3094
  shared() {
@@ -1718,7 +3114,13 @@ var d = {
1718
3114
  timestamp: () => createColumn("timestamp with time zone"),
1719
3115
  date: () => createColumn("date"),
1720
3116
  time: () => createColumn("time"),
1721
- jsonb: (opts) => createColumn("jsonb", opts?.validator ? { validator: opts.validator } : {}),
3117
+ jsonb: (schemaOrOpts) => {
3118
+ if (schemaOrOpts && "parse" in schemaOrOpts && !("validator" in schemaOrOpts)) {
3119
+ return createColumn("jsonb", { validator: schemaOrOpts });
3120
+ }
3121
+ const opts = schemaOrOpts;
3122
+ return createColumn("jsonb", opts?.validator ? { validator: opts.validator } : {});
3123
+ },
1722
3124
  textArray: () => createColumn("text[]"),
1723
3125
  integerArray: () => createColumn("integer[]"),
1724
3126
  enum: (name, valuesOrSchema) => {
@@ -1738,50 +3140,24 @@ var d = {
1738
3140
  entry: (table, relations = {}) => ({
1739
3141
  table,
1740
3142
  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"
3143
+ }),
3144
+ model: (table, relations = {}) => createModel(table, relations)
1758
3145
  };
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 };
3146
+ // src/schema/define-annotations.ts
3147
+ function defineAnnotations(...annotations) {
3148
+ const result = {};
3149
+ for (const annotation of annotations) {
3150
+ result[annotation] = annotation;
1780
3151
  }
1781
- if (error instanceof ConnectionError) {
1782
- return { status: 503, body };
3152
+ return Object.freeze(result);
3153
+ }
3154
+ // src/schema/enum-registry.ts
3155
+ function createEnumRegistry(definitions) {
3156
+ const registry = {};
3157
+ for (const [name, values] of Object.entries(definitions)) {
3158
+ registry[name] = { name, values };
1783
3159
  }
1784
- return { status: 500, body };
3160
+ return registry;
1785
3161
  }
1786
3162
  // src/schema/registry.ts
1787
3163
  function createRegistry(tables, relationsCallback) {
@@ -1809,195 +3185,39 @@ function createRegistry(tables, relationsCallback) {
1809
3185
  }
1810
3186
  return result;
1811
3187
  }
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
3188
  export {
3189
+ toWriteError,
3190
+ toReadError,
1982
3191
  resolveErrorCode,
3192
+ reset,
1983
3193
  push,
1984
3194
  parsePgError,
3195
+ parseMigrationName,
1985
3196
  migrateStatus,
1986
3197
  migrateDev,
1987
3198
  migrateDeploy,
1988
- generateTypes,
1989
- generateClient,
3199
+ generateId,
1990
3200
  formatDiagnostic,
1991
3201
  explainError,
1992
3202
  diagnoseError,
1993
- defineDomain,
3203
+ detectSchemaDrift,
3204
+ defineAnnotations,
3205
+ defaultSqliteDialect,
3206
+ defaultPostgresDialect,
1994
3207
  dbErrorToHttpError,
1995
3208
  d,
3209
+ createSnapshot,
1996
3210
  createRegistry,
1997
3211
  createEnumRegistry,
1998
3212
  createDb,
3213
+ createDatabaseBridgeAdapter,
3214
+ createD1Driver,
3215
+ createD1Adapter,
1999
3216
  computeTenantGraph,
3217
+ baseline,
2000
3218
  UniqueConstraintError,
3219
+ SqliteDialect,
3220
+ PostgresDialect,
2001
3221
  PgCodeToName,
2002
3222
  NotNullError,
2003
3223
  NotFoundError,