@happyvertical/sql 0.79.0 → 0.80.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.
Files changed (33) hide show
  1. package/AGENT.md +1 -1
  2. package/dist/backup/index.js +210 -188
  3. package/dist/backup/index.js.map +1 -1
  4. package/dist/chunks/alter-utils-K7tiuj5L.js +135 -0
  5. package/dist/chunks/alter-utils-K7tiuj5L.js.map +1 -0
  6. package/dist/chunks/duckdb-DykBBzd6.js +1056 -0
  7. package/dist/chunks/duckdb-DykBBzd6.js.map +1 -0
  8. package/dist/chunks/duckdb-schema-utils-B-XBP8QF.js +115 -0
  9. package/dist/chunks/duckdb-schema-utils-B-XBP8QF.js.map +1 -0
  10. package/dist/chunks/postgres-cli-DzZcXSIG.js +272 -0
  11. package/dist/chunks/postgres-cli-DzZcXSIG.js.map +1 -0
  12. package/dist/chunks/schema-manager-a8RlxIE2.js +450 -0
  13. package/dist/chunks/schema-manager-a8RlxIE2.js.map +1 -0
  14. package/dist/chunks/sqlite-YgU9LCVy.js +1045 -0
  15. package/dist/chunks/sqlite-YgU9LCVy.js.map +1 -0
  16. package/dist/chunks/sqlite-native-BGtpx7Ro.js +902 -0
  17. package/dist/chunks/sqlite-native-BGtpx7Ro.js.map +1 -0
  18. package/dist/cli/claude-context.js +17 -17
  19. package/dist/cli/claude-context.js.map +1 -1
  20. package/dist/index.js +3244 -35
  21. package/dist/index.js.map +1 -1
  22. package/metadata.json +2 -2
  23. package/package.json +5 -5
  24. package/dist/chunks/duckdb-B6m78QdL.js +0 -924
  25. package/dist/chunks/duckdb-B6m78QdL.js.map +0 -1
  26. package/dist/chunks/index-CzdYvRFp.js +0 -3692
  27. package/dist/chunks/index-CzdYvRFp.js.map +0 -1
  28. package/dist/chunks/postgres-cli-Cr6bBZ3q.js +0 -209
  29. package/dist/chunks/postgres-cli-Cr6bBZ3q.js.map +0 -1
  30. package/dist/chunks/sqlite-CPCNeJIH.js +0 -937
  31. package/dist/chunks/sqlite-CPCNeJIH.js.map +0 -1
  32. package/dist/chunks/sqlite-native-DqVPKNUu.js +0 -1129
  33. package/dist/chunks/sqlite-native-DqVPKNUu.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,36 +1,3245 @@
1
- import "@happyvertical/utils";
2
- import { D, P, h, i, d, j, k, l, m, n, e, o, p, f, q, r, s, t, u, w } from "./chunks/index-CzdYvRFp.js";
3
- import { a, b, c, d as d2, e as e2, f as f2, i as i2, p as p2, r as r2, g } from "./chunks/postgres-cli-Cr6bBZ3q.js";
4
- export {
5
- D as DatabaseSchemaManager,
6
- P as PACKAGE_VERSION_INITIALIZED,
7
- a as assertCanExportDatabase,
8
- b as assertCanImportDatabase,
9
- h as bucketExpr,
10
- i as buildAggregate,
11
- d as buildWhere,
12
- j as checkExpectedTables,
13
- k as checkRelationship,
14
- l as checkUniqueColumn,
15
- m as clearConnectionCache,
16
- n as clearPostgresConnectionCache,
17
- e as convertUniqueIndexesToInlineConstraints,
18
- c as createPostgresDatabase,
19
- d2 as databaseNameFromUrl,
20
- o as default,
21
- e2 as dropPostgresDatabase,
22
- f2 as dumpPostgresDatabase,
23
- p as escapeSqlValue,
24
- f as formatDbError,
25
- q as getDatabase,
26
- i2 as isLocalDatabaseUrl,
27
- p2 as postgresEnvFromUrl,
28
- r2 as redactDatabaseUrl,
29
- r as resolveSchemas,
30
- g as restorePostgresDatabase,
31
- s as runDoctor,
32
- t as syncSchema,
33
- u as tableExists,
34
- w as validateColumnName
1
+ import { i as parseConditionKey, n as buildWhere, r as formatDbError, t as DatabaseSchemaManager } from "./chunks/schema-manager-a8RlxIE2.js";
2
+ import { t as convertUniqueIndexesToInlineConstraints } from "./chunks/duckdb-schema-utils-B-XBP8QF.js";
3
+ import { a as dropPostgresDatabase, c as postgresEnvFromUrl, i as databaseNameFromUrl, l as redactDatabaseUrl, n as assertCanImportDatabase, o as dumpPostgresDatabase, r as createPostgresDatabase, s as isLocalDatabaseUrl, t as assertCanExportDatabase, u as restorePostgresDatabase } from "./chunks/postgres-cli-DzZcXSIG.js";
4
+ import { a as validateTableName$1, i as validateIndexName, n as generateCreateIndexStatement, r as validateColumnName$1, t as generateAddColumnStatement } from "./chunks/alter-utils-K7tiuj5L.js";
5
+ import { DatabaseError, loadEnvConfig } from "@happyvertical/utils";
6
+ import { mkdir, readdir } from "node:fs/promises";
7
+ import { basename, extname, join } from "node:path";
8
+ import { Pool } from "pg";
9
+ //#region \0rolldown/runtime.js
10
+ var __defProp = Object.defineProperty;
11
+ var __exportAll = (all, no_symbols) => {
12
+ let target = {};
13
+ for (var name in all) __defProp(target, name, {
14
+ get: all[name],
15
+ enumerable: true
16
+ });
17
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
18
+ return target;
35
19
  };
36
- //# sourceMappingURL=index.js.map
20
+ //#endregion
21
+ //#region src/aggregate.ts
22
+ var VALID_DIRECTIONS = /* @__PURE__ */ new Set(["ASC", "DESC"]);
23
+ var VALID_BUCKET_UNITS = /* @__PURE__ */ new Set([
24
+ "minute",
25
+ "hour",
26
+ "day",
27
+ "week",
28
+ "month",
29
+ "quarter",
30
+ "year"
31
+ ]);
32
+ function validateSqlIdentifier(identifier) {
33
+ if (!/^[a-zA-Z0-9_.]+$/.test(identifier)) throw new Error(`Invalid SQL identifier: ${identifier}`);
34
+ return identifier;
35
+ }
36
+ function normalizeAdapter(adapterType) {
37
+ return adapterType ?? "sqlite";
38
+ }
39
+ function placeholder(index) {
40
+ return `$${index}`;
41
+ }
42
+ function bucketExpr(adapterType, unit, column) {
43
+ if (!VALID_BUCKET_UNITS.has(unit)) throw new Error(`Invalid aggregate bucket unit: ${unit}`);
44
+ const col = validateSqlIdentifier(column);
45
+ const adapter = normalizeAdapter(adapterType);
46
+ if (adapter === "postgres" || adapter === "duckdb" || adapter === "json") return `date_trunc('${unit}', ${col})`;
47
+ switch (unit) {
48
+ case "minute": return `strftime('%Y-%m-%d %H:%M:00', ${col})`;
49
+ case "hour": return `strftime('%Y-%m-%d %H:00:00', ${col})`;
50
+ case "day": return `strftime('%Y-%m-%d 00:00:00', ${col})`;
51
+ case "week": return `strftime('%Y-%m-%d 00:00:00', date(${col}, printf('-%d days', (CAST(strftime('%w', ${col}) AS INTEGER) + 6) % 7)))`;
52
+ case "month": return `strftime('%Y-%m-01 00:00:00', ${col})`;
53
+ case "quarter": return `strftime('%Y-%m-%d 00:00:00', date(strftime('%Y-', ${col}) || printf('%02d', ((CAST(strftime('%m', ${col}) AS INTEGER) - 1) / 3) * 3 + 1) || '-01'))`;
54
+ case "year": return `strftime('%Y-01-01 00:00:00', ${col})`;
55
+ }
56
+ }
57
+ function aggregateExpression(expr) {
58
+ const normalizedFn = String(expr.fn).toLowerCase();
59
+ const fn = validateSqlIdentifier(String(expr.fn)).toUpperCase();
60
+ if (normalizedFn === "count" && !expr.column) {
61
+ if (expr.distinct) throw new Error(`Aggregate ${expr.fn} with distinct requires a column`);
62
+ return "COUNT(*)";
63
+ }
64
+ if (!expr.column) throw new Error(`Aggregate ${expr.fn} requires a column`);
65
+ const col = validateSqlIdentifier(expr.column);
66
+ return `${fn}(${expr.distinct ? "DISTINCT " : ""}${col})`;
67
+ }
68
+ function selectExpression(expr, adapterType) {
69
+ if ("column" in expr && !("fn" in expr) && !("bucket" in expr)) return {
70
+ expression: validateSqlIdentifier(expr.column),
71
+ alias: validateSqlIdentifier(expr.as ?? expr.column),
72
+ groupingCandidate: true
73
+ };
74
+ if ("bucket" in expr) return {
75
+ expression: bucketExpr(adapterType, expr.bucket, expr.column),
76
+ alias: validateSqlIdentifier(expr.as),
77
+ groupingCandidate: true
78
+ };
79
+ return {
80
+ expression: aggregateExpression(expr),
81
+ alias: validateSqlIdentifier(expr.as),
82
+ groupingCandidate: false
83
+ };
84
+ }
85
+ function validateWhereShape(where) {
86
+ if (!where) return;
87
+ const validateKey = (key) => {
88
+ const { field } = parseConditionKey(key);
89
+ if (field) validateSqlIdentifier(field);
90
+ };
91
+ if (Array.isArray(where)) {
92
+ for (const andGroup of where) for (const condition of andGroup) for (const key of Object.keys(condition)) validateKey(key);
93
+ return;
94
+ }
95
+ for (const key of Object.keys(where)) validateKey(key);
96
+ }
97
+ function expandHavingKey(key, expressionByAlias) {
98
+ const parsed = parseConditionKey(key);
99
+ return `${expressionByAlias.get(parsed.field) ?? validateSqlIdentifier(parsed.field)} ${parsed.operator}`;
100
+ }
101
+ function expandHaving(having, expressionByAlias) {
102
+ const expandCondition = (condition) => Object.fromEntries(Object.entries(condition).map(([key, value]) => [expandHavingKey(key, expressionByAlias), value]));
103
+ if (Array.isArray(having)) return having.map((andGroup) => andGroup.map((condition) => expandCondition(condition)));
104
+ return expandCondition(having);
105
+ }
106
+ function buildHaving(having, expressionByAlias, startIndex, adapterType) {
107
+ if (!having) return {
108
+ sql: "",
109
+ values: []
110
+ };
111
+ const built = buildWhere(expandHaving(having, expressionByAlias), startIndex, adapterType);
112
+ return {
113
+ sql: built.sql ? built.sql.replace(/^WHERE/, "HAVING") : "",
114
+ values: built.values
115
+ };
116
+ }
117
+ function buildOrderBy(orderBy) {
118
+ if (!orderBy) return "";
119
+ return `ORDER BY ${(Array.isArray(orderBy) ? orderBy : [orderBy]).map((item) => {
120
+ const [field, direction = "ASC"] = item.trim().split(/\s+/);
121
+ const normalizedDirection = direction.toUpperCase();
122
+ if (!VALID_DIRECTIONS.has(normalizedDirection)) throw new Error(`Invalid aggregate order direction: ${direction}`);
123
+ return `${validateSqlIdentifier(field)} ${normalizedDirection}`;
124
+ }).join(", ")}`;
125
+ }
126
+ function buildAggregate(spec, startIndex = 1, adapterType) {
127
+ const from = validateSqlIdentifier(spec.from);
128
+ if (spec.select.length === 0) throw new Error("AggregateSpec.select must contain at least one expression");
129
+ validateWhereShape(spec.where);
130
+ const expressionByAlias = /* @__PURE__ */ new Map();
131
+ const groupingAliases = [];
132
+ const selectSql = spec.select.map((expr) => {
133
+ const selected = selectExpression(expr, adapterType);
134
+ expressionByAlias.set(selected.alias, selected.expression);
135
+ if (selected.groupingCandidate) groupingAliases.push(selected.alias);
136
+ return `${selected.expression} AS ${selected.alias}`;
137
+ });
138
+ const where = spec.where ? buildWhere(spec.where, startIndex, adapterType) : {
139
+ sql: "",
140
+ values: []
141
+ };
142
+ let nextParamIndex = startIndex + where.values.length;
143
+ const groupByItems = spec.groupBy ?? groupingAliases;
144
+ const groupBySql = groupByItems.length > 0 ? `GROUP BY ${groupByItems.map((item) => expressionByAlias.has(item) ? expressionByAlias.get(item) : validateSqlIdentifier(item)).join(", ")}` : "";
145
+ const having = buildHaving(spec.having, expressionByAlias, nextParamIndex, adapterType);
146
+ nextParamIndex += having.values.length;
147
+ const orderBySql = buildOrderBy(spec.orderBy);
148
+ const pagingValues = [];
149
+ const pagingSql = [];
150
+ if (spec.limit !== void 0) {
151
+ pagingSql.push(`LIMIT ${placeholder(nextParamIndex++)}`);
152
+ pagingValues.push(spec.limit);
153
+ } else if (spec.offset !== void 0 && normalizeAdapter(adapterType) === "sqlite") pagingSql.push("LIMIT -1");
154
+ if (spec.offset !== void 0) {
155
+ pagingSql.push(`OFFSET ${placeholder(nextParamIndex++)}`);
156
+ pagingValues.push(spec.offset);
157
+ }
158
+ return {
159
+ sql: [
160
+ `SELECT ${selectSql.join(", ")}`,
161
+ `FROM ${from}`,
162
+ where.sql,
163
+ groupBySql,
164
+ having.sql,
165
+ orderBySql,
166
+ pagingSql.join(" ")
167
+ ].filter(Boolean).join(" "),
168
+ values: [
169
+ ...where.values,
170
+ ...having.values,
171
+ ...pagingValues
172
+ ]
173
+ };
174
+ }
175
+ //#endregion
176
+ //#region src/doctor.ts
177
+ /**
178
+ * Database integrity-check framework.
179
+ *
180
+ * The runner is a thin loop over user-supplied checks; the value here is
181
+ * the reusable check builders (expected tables, unique columns, FK-like
182
+ * relationships) that codify the common shapes of integrity bugs that
183
+ * sneak past schema migrations.
184
+ *
185
+ * No SMRT awareness — checks operate on the `DatabaseInterface` from this
186
+ * package and on plain table/column names.
187
+ *
188
+ * **Postgres-only today.** The bundled check builders and
189
+ * `listPublicTables` use Postgres syntax (`pg_tables`, `::int`/`::text`
190
+ * casts). The shape is portable in principle — `DoctorContext` carries a
191
+ * generic `DatabaseInterface` so apps can add engine-aware checks — but
192
+ * the out-of-the-box runner expects a Postgres connection. `runDoctor`
193
+ * throws early if pointed at a non-Postgres URL rather than failing
194
+ * mid-execution with a confusing `pg_tables`-doesn't-exist error.
195
+ */
196
+ /**
197
+ * Execute every check against the public-schema tables of `db` and
198
+ * partition the results by severity. Returns the aggregate; callers
199
+ * format/exit as they see fit.
200
+ *
201
+ * The current table set is enumerated once and shared across checks so
202
+ * each check can cheaply decide whether its target tables exist.
203
+ */
204
+ async function runDoctor(options) {
205
+ assertPostgresAdapter(options.db);
206
+ const tables = await listPublicTables(options.db);
207
+ const issues = [];
208
+ for (const check of options.checks) {
209
+ const checkIssues = await check({
210
+ db: options.db,
211
+ tables
212
+ });
213
+ issues.push(...checkIssues);
214
+ }
215
+ return {
216
+ failures: issues.filter((issue) => issue.level === "fail"),
217
+ warnings: issues.filter((issue) => issue.level === "warn"),
218
+ issues
219
+ };
220
+ }
221
+ /**
222
+ * Throw early if the supplied database isn't Postgres. The check builders
223
+ * and `listPublicTables` use Postgres syntax; pointing them at SQLite or
224
+ * DuckDB would fail mid-execution with a confusing "table pg_tables does
225
+ * not exist" error from the adapter. Fail fast with a useful message
226
+ * instead.
227
+ */
228
+ function assertPostgresAdapter(db) {
229
+ const url = String(db.url ?? "");
230
+ if (!url.startsWith("postgres://") && !url.startsWith("postgresql://")) throw new Error(`runDoctor requires a Postgres database connection. Got URL: ${url ? redactDatabaseUrl(url) : "(empty)"}. The bundled check builders use Postgres-specific syntax (pg_tables, ::int/::text casts).`);
231
+ }
232
+ /**
233
+ * Emit a 'warn' for each table in `expected` that's missing.
234
+ *
235
+ * Marked as `warn` rather than `fail` because a missing table often
236
+ * indicates a migration that hasn't been run yet, not active data
237
+ * corruption — callers can promote to fail by mapping the issues.
238
+ */
239
+ function checkExpectedTables(expected) {
240
+ return async ({ tables }) => {
241
+ const issues = [];
242
+ for (const table of expected) if (!tables.has(table)) issues.push({
243
+ level: "warn",
244
+ message: `Missing expected table: ${table}`
245
+ });
246
+ return issues;
247
+ };
248
+ }
249
+ /**
250
+ * Emit a 'fail' for each value that appears more than once in
251
+ * `(table, column)` (ignoring NULL and empty-string values).
252
+ *
253
+ * Useful for catching slug/key collisions that the schema didn't
254
+ * declare a UNIQUE constraint for (or where the constraint was added
255
+ * after data already existed).
256
+ */
257
+ function checkUniqueColumn(spec) {
258
+ return async ({ db, tables }) => {
259
+ if (!tables.has(spec.table)) return [];
260
+ return (await db.query(`
261
+ SELECT ${quoteIdentifier(spec.column)} AS value, count(*)::text AS count
262
+ FROM ${quoteIdentifier(spec.table)}
263
+ WHERE NULLIF(trim(${quoteIdentifier(spec.column)}::text), '') IS NOT NULL
264
+ GROUP BY ${quoteIdentifier(spec.column)}
265
+ HAVING count(*) > 1
266
+ ORDER BY count DESC, ${quoteIdentifier(spec.column)}
267
+ `)).rows.map((row) => ({
268
+ level: "fail",
269
+ message: `Duplicate ${spec.label} "${String(row.value)}" in ${spec.table} (${countValueFromRow(row.count)} rows).`
270
+ }));
271
+ };
272
+ }
273
+ /**
274
+ * Emit a 'fail' counting orphan rows where the child column points at a
275
+ * parent row that doesn't exist (FK-shaped check, but doesn't require an
276
+ * actual FK constraint to be declared — useful for soft references like
277
+ * tag-id-or-slug lookup columns).
278
+ */
279
+ function checkRelationship(spec) {
280
+ return async ({ db, tables }) => {
281
+ if (!tables.has(spec.childTable) || !tables.has(spec.parentTable)) return [];
282
+ const parentColumn = spec.parentColumn ?? "id";
283
+ const orphanCount = countValueFromRow((await db.query(`
284
+ SELECT count(*)::text AS count
285
+ FROM ${quoteIdentifier(spec.childTable)} child
286
+ LEFT JOIN ${quoteIdentifier(spec.parentTable)} parent
287
+ ON parent.${quoteIdentifier(parentColumn)} = child.${quoteIdentifier(spec.childColumn)}
288
+ WHERE NULLIF(trim(child.${quoteIdentifier(spec.childColumn)}::text), '') IS NOT NULL
289
+ AND parent.${quoteIdentifier(parentColumn)} IS NULL
290
+ `)).rows[0]?.count);
291
+ if (orphanCount === "0") return [];
292
+ return [{
293
+ level: "fail",
294
+ message: `${spec.label}: ${orphanCount} orphaned row${orphanCount === "1" ? "" : "s"}.`
295
+ }];
296
+ };
297
+ }
298
+ async function listPublicTables(db) {
299
+ const result = await db.query("SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename");
300
+ return new Set(result.rows.map((row) => String(row.tablename ?? "")));
301
+ }
302
+ function quoteIdentifier(value) {
303
+ return `"${value.replace(/"/gu, "\"\"")}"`;
304
+ }
305
+ function countValueFromRow(value) {
306
+ if (typeof value === "string") return value;
307
+ if (typeof value === "number" || typeof value === "bigint") return String(value);
308
+ return "0";
309
+ }
310
+ //#endregion
311
+ //#region src/shared/types.ts
312
+ /**
313
+ * Resolves a SchemasOption to its concrete value.
314
+ *
315
+ * Adapters that need schemas (JSON, DuckDB) call this to unwrap the
316
+ * lazy function. Adapters that don't (Postgres, SQLite) simply ignore
317
+ * the `schemas` option without calling this.
318
+ */
319
+ function resolveSchemas(schemas) {
320
+ if (!schemas) return void 0;
321
+ if (typeof schemas === "function") return schemas();
322
+ return schemas;
323
+ }
324
+ //#endregion
325
+ //#region src/json.ts
326
+ var json_exports = /* @__PURE__ */ __exportAll({
327
+ clearConnectionCache: () => clearConnectionCache,
328
+ getDatabase: () => getDatabase$2
329
+ });
330
+ /**
331
+ * Connection cache for in-memory DuckDB instances keyed by data directory URL
332
+ * Enables sharing of in-memory databases across multiple getDatabase() calls
333
+ * with the same data directory.
334
+ *
335
+ * Uses globalThis to ensure cache is shared across all module instances,
336
+ * fixing the lost update bug in monorepos with workspace symlinks.
337
+ *
338
+ * @see https://github.com/happyvertical/sdk/issues/678
339
+ */
340
+ globalThis.__haveSqlMemoryConnectionCache ??= /* @__PURE__ */ new Map();
341
+ var memoryConnectionCache = globalThis.__haveSqlMemoryConnectionCache;
342
+ /**
343
+ * Pending connection promises to handle concurrent getDatabase() calls
344
+ * Prevents creating duplicate connections when parallel calls happen.
345
+ *
346
+ * Uses globalThis to ensure cache is shared across all module instances.
347
+ *
348
+ * @see https://github.com/happyvertical/sdk/issues/678
349
+ */
350
+ globalThis.__haveSqlPendingConnections ??= /* @__PURE__ */ new Map();
351
+ var pendingConnections$1 = globalThis.__haveSqlPendingConnections;
352
+ /**
353
+ * Clears all cached connections
354
+ * Useful for testing to ensure test isolation
355
+ */
356
+ function clearConnectionCache() {
357
+ memoryConnectionCache.clear();
358
+ pendingConnections$1.clear();
359
+ }
360
+ /**
361
+ * Creates a JSON database connection using DuckDB in-memory engine
362
+ *
363
+ * This adapter uses DuckDB as a query engine for JSON files, storing everything
364
+ * in-memory and writing changes back to JSON files. No WAL files or persistent
365
+ * database files are created.
366
+ *
367
+ * @param options - JSON database options
368
+ * @returns Promise resolving to a DuckDB connection
369
+ */
370
+ async function createJSONConnection(options) {
371
+ const { url, autoRegister = true, eagerLoadTables = true } = options;
372
+ const schemas = resolveSchemas(options.schemas) ?? {};
373
+ if (!url) throw new DatabaseError("url is required for JSON adapter", { options });
374
+ const pendingJSONFiles = /* @__PURE__ */ new Map();
375
+ const inferredSchemaTables = /* @__PURE__ */ new Set();
376
+ try {
377
+ const { DuckDBInstance } = await import(
378
+ /* @vite-ignore */
379
+ "@duckdb/node-api"
380
+ );
381
+ const connection = await (await DuckDBInstance.create(":memory:")).connect();
382
+ try {
383
+ await mkdir(url, { recursive: true });
384
+ } catch (_error) {}
385
+ if (autoRegister) await loadJSONTables(connection, url, schemas, pendingJSONFiles, eagerLoadTables, inferredSchemaTables);
386
+ return {
387
+ connection,
388
+ pendingJSONFiles,
389
+ inferredSchemaTables
390
+ };
391
+ } catch (error) {
392
+ const errorMessage = error instanceof Error ? error.message : String(error);
393
+ throw new DatabaseError(`Failed to create JSON database connection: ${errorMessage}`, {
394
+ url,
395
+ originalError: errorMessage
396
+ });
397
+ }
398
+ }
399
+ /**
400
+ * Loads JSON data into an existing table
401
+ *
402
+ * This function only loads data - it assumes the table already exists with proper schema.
403
+ * Used after DDL creates the table to load persisted JSON data.
404
+ *
405
+ * @param connection - DuckDB connection
406
+ * @param tableName - Name of the table to load data into
407
+ * @param filePath - Path to the JSON file
408
+ */
409
+ async function loadJSONData(connection, tableName, filePath) {
410
+ try {
411
+ const { readFile } = await import("node:fs/promises");
412
+ const jsonContent = await readFile(filePath, "utf-8");
413
+ const records = JSON.parse(jsonContent);
414
+ if (Array.isArray(records) && records.length > 0) {
415
+ await insertRecordsWithCast(connection, tableName, records);
416
+ console.log(`[json-adapter] Loaded ${records.length} records into ${tableName} from JSON`);
417
+ }
418
+ } catch (error) {
419
+ console.warn(`[json-adapter] Could not load data for ${tableName}: ${error instanceof Error ? error.message : String(error)}`);
420
+ }
421
+ }
422
+ /**
423
+ * Scans the data directory and registers JSON files for deferred loading
424
+ *
425
+ * Unlike the DuckDB adapter which creates views, this creates actual tables
426
+ * so that indexes can be added.
427
+ *
428
+ * Schema resolution priority:
429
+ * 1. Explicit schemas provided via options.schemas - table created immediately
430
+ * 2. Schema file (.schema.sql) in data directory - table created with schema
431
+ * 3. All other tables - DEFERRED until syncSchema() provides DDL
432
+ *
433
+ * IMPORTANT: Tables without explicit schemas are NOT auto-created from JSON.
434
+ * This ensures UNIQUE constraints from syncSchema() are in place before data
435
+ * is loaded, which is required for UPSERT operations to work correctly.
436
+ *
437
+ * For users who want auto-inference, use the inferSchemaFromJSON() method
438
+ * after database creation.
439
+ *
440
+ * @param connection - DuckDB connection
441
+ * @param dataDir - Directory containing JSON files
442
+ * @param providedSchemas - Explicit schema definitions provided by caller (optional)
443
+ * @param pendingJSONFiles - Map to store JSON files pending deferred loading
444
+ * @param eagerLoadTables - Whether to eagerly create tables with inferred schemas
445
+ * @param inferredSchemaTables - Set to track tables created with inferred schemas
446
+ */
447
+ async function loadJSONTables(connection, dataDir, providedSchemas = {}, pendingJSONFiles = /* @__PURE__ */ new Map(), eagerLoadTables = true, inferredSchemaTables = /* @__PURE__ */ new Set()) {
448
+ try {
449
+ const jsonFiles = (await readdir(dataDir)).filter((file) => extname(file).toLowerCase() === ".json");
450
+ for (const file of jsonFiles) {
451
+ const filePath = join(dataDir, file);
452
+ const tableName = basename(file, ".json");
453
+ const schema = providedSchemas[tableName];
454
+ if (schema) {
455
+ console.log(`[json-adapter] Creating ${tableName} with provided schema definition`);
456
+ await createTableFromSchema(connection, tableName, schema);
457
+ await loadJSONData(connection, tableName, filePath);
458
+ } else {
459
+ const schemaPath = join(dataDir, `${tableName}.schema.sql`);
460
+ const { readFile, access } = await import("node:fs/promises");
461
+ const { constants } = await import("node:fs");
462
+ let schemaExists = false;
463
+ try {
464
+ await access(schemaPath, constants.F_OK);
465
+ schemaExists = true;
466
+ } catch {
467
+ schemaExists = false;
468
+ }
469
+ if (schemaExists) try {
470
+ const schemaDDL = await readFile(schemaPath, "utf-8");
471
+ await connection.run(schemaDDL);
472
+ await loadJSONData(connection, tableName, filePath);
473
+ } catch (error) {
474
+ console.warn(`[json-adapter] Could not load ${tableName} with schema file: ${error instanceof Error ? error.message : String(error)}`);
475
+ }
476
+ else {
477
+ if (eagerLoadTables) try {
478
+ await connection.run(`CREATE TABLE IF NOT EXISTS "${tableName}" AS SELECT * FROM read_json_auto('${filePath}')`);
479
+ inferredSchemaTables.add(tableName);
480
+ console.log(`[json-adapter] Eagerly created ${tableName} with inferred schema (no UNIQUE constraints)`);
481
+ } catch (error) {
482
+ console.warn(`[json-adapter] Could not eagerly load ${tableName}: ${error instanceof Error ? error.message : String(error)}`);
483
+ }
484
+ else console.log(`[json-adapter] Deferring ${tableName} - will load after syncSchema() creates table`);
485
+ pendingJSONFiles.set(tableName, filePath);
486
+ }
487
+ }
488
+ }
489
+ } catch (error) {
490
+ if (error.code !== "ENOENT") {
491
+ console.error("[json-adapter] Error loading JSON tables:", error);
492
+ throw new DatabaseError(`Failed to load JSON tables from ${dataDir}`, {
493
+ dataDir,
494
+ originalError: error instanceof Error ? error.message : String(error),
495
+ stack: error instanceof Error ? error.stack : void 0
496
+ });
497
+ }
498
+ }
499
+ }
500
+ /**
501
+ * Converts DuckDB-specific type representations to JavaScript types
502
+ *
503
+ * Handles three cases:
504
+ * 1. JavaScript BigInt values → converted to numbers
505
+ * 2. DuckDB hugeint objects ({ hugeint: number }) → converted to strings
506
+ * 3. DuckDB timestamp objects ({ micros: number }) → converted to Date objects
507
+ *
508
+ * DuckDB represents HUGEINT (128-bit integers) as objects with a single
509
+ * property called 'hugeint'. When UUID strings (stored as TEXT) are
510
+ * exported to JSON via COPY TO, DuckDB sometimes converts them to hugeint
511
+ * representation. This function converts them back to strings.
512
+ *
513
+ * DuckDB represents TIMESTAMP values as objects with a single property
514
+ * called 'micros' containing microseconds since Unix epoch. This function
515
+ * converts them to JavaScript Date objects for proper round-trip serialization.
516
+ *
517
+ * @param obj - Object that may contain BigInt, hugeint, or timestamp values
518
+ * @returns Object with DuckDB types converted to JavaScript types
519
+ */
520
+ function convertBigInts(obj) {
521
+ if (obj === null || obj === void 0) return obj;
522
+ if (typeof obj === "bigint") return Number(obj);
523
+ if (typeof obj === "object" && !Array.isArray(obj) && "hugeint" in obj && Object.keys(obj).length === 1) return String(obj.hugeint);
524
+ if (typeof obj === "object" && !Array.isArray(obj) && "micros" in obj && Object.keys(obj).length === 1 && (typeof obj.micros === "number" || typeof obj.micros === "bigint")) {
525
+ const milliseconds = (typeof obj.micros === "bigint" ? Number(obj.micros) : obj.micros) / 1e3;
526
+ return new Date(milliseconds);
527
+ }
528
+ if (Array.isArray(obj)) return obj.map(convertBigInts);
529
+ if (typeof obj === "object") {
530
+ const result = {};
531
+ for (const key in obj) result[key] = convertBigInts(obj[key]);
532
+ return result;
533
+ }
534
+ return obj;
535
+ }
536
+ /**
537
+ * Creates a table from a schema definition with proper typing
538
+ *
539
+ * DuckDB has issues with type inference when DEFAULT values are empty strings.
540
+ * This function explicitly casts all DEFAULT values to their column types to
541
+ * prevent DuckDB from inferring ANY type.
542
+ *
543
+ * Additionally, DuckDB has a known limitation (issue #12684) where ON CONFLICT
544
+ * clauses fail with UNIQUE INDEX but work with inline UNIQUE constraints.
545
+ * This function converts UNIQUE indexes to inline constraints for compatibility.
546
+ *
547
+ * @param connection - DuckDB connection
548
+ * @param tableName - Name of the table to create
549
+ * @param schema - Schema definition with DDL and indexes
550
+ */
551
+ async function createTableFromSchema(connection, tableName, schema) {
552
+ const ddlLines = schema.ddl.split("\n");
553
+ const createTableEnd = ddlLines.findIndex((line) => {
554
+ const trimmed = line.trim();
555
+ return trimmed === ")" || trimmed.startsWith(");");
556
+ });
557
+ if (createTableEnd === -1) throw new DatabaseError("Invalid schema DDL - no closing parenthesis", {
558
+ tableName,
559
+ ddl: schema.ddl
560
+ });
561
+ let createTableSQL = ddlLines.slice(0, createTableEnd + 1).join("\n");
562
+ createTableSQL = createTableSQL.replace(/\b(\w+)\s+(TEXT|VARCHAR)\s+DEFAULT\s+''/g, "$1 $2 DEFAULT CAST('' AS $2)");
563
+ createTableSQL = createTableSQL.replace(/\b(\w+)\s+(TEXT|VARCHAR)\s+DEFAULT\s+NULL/g, "$1 $2 DEFAULT CAST(NULL AS $2)");
564
+ const transformed = convertUniqueIndexesToInlineConstraints(createTableSQL, schema.indexes);
565
+ createTableSQL = transformed.ddl;
566
+ const indexesToCreate = transformed.indexes;
567
+ try {
568
+ await connection.run(createTableSQL);
569
+ let existingColumns = /* @__PURE__ */ new Set();
570
+ try {
571
+ const columns = (await connection.runAndReadAll(`DESCRIBE ${tableName}`)).getRowObjects();
572
+ existingColumns = new Set(columns.map((col) => col.column_name.toLowerCase()));
573
+ console.log(`[json-adapter] Created ${tableName} with schema:`, { columns: columns.map((col) => ({
574
+ name: col.column_name,
575
+ type: col.column_type,
576
+ null: col.null
577
+ })) });
578
+ } catch (error) {
579
+ console.warn(`[json-adapter] Could not verify schema for ${tableName}: ${error instanceof Error ? error.message : String(error)}`);
580
+ }
581
+ if (schema.fields && existingColumns.size > 0) {
582
+ const fieldEntries = schema.fields instanceof Map ? Array.from(schema.fields.entries()) : Object.entries(schema.fields);
583
+ for (const [fieldName, fieldDef] of fieldEntries) {
584
+ const columnName = fieldName.toLowerCase();
585
+ if (!existingColumns.has(columnName)) {
586
+ const sqlType = fieldDef?.type || "TEXT";
587
+ try {
588
+ await connection.run(`ALTER TABLE ${tableName} ADD COLUMN "${fieldName}" ${sqlType}`);
589
+ console.log(`[json-adapter] Added missing column '${fieldName}' (${sqlType}) to ${tableName}`);
590
+ } catch (alterError) {
591
+ console.warn(`[json-adapter] Could not add column '${fieldName}' to ${tableName}: ${alterError instanceof Error ? alterError.message : String(alterError)}`);
592
+ }
593
+ }
594
+ }
595
+ }
596
+ if (indexesToCreate && indexesToCreate.length > 0) for (const indexSQL of indexesToCreate) try {
597
+ await connection.run(indexSQL);
598
+ } catch (error) {
599
+ console.warn(`[json-adapter] Failed to create index: ${error instanceof Error ? error.message : String(error)}`);
600
+ }
601
+ } catch (error) {
602
+ throw new DatabaseError("Failed to create table from schema", {
603
+ tableName,
604
+ sql: createTableSQL,
605
+ originalError: error instanceof Error ? error.message : String(error)
606
+ });
607
+ }
608
+ }
609
+ /**
610
+ * Inserts records with proper CAST handling for empty strings
611
+ *
612
+ * This helper avoids using read_json() with auto_detect, which causes DuckDB
613
+ * to re-infer column types from data (causing ANY type for empty strings).
614
+ * Instead, it uses parameterized INSERT with CAST to maintain table schema.
615
+ *
616
+ * @param connection - DuckDB connection
617
+ * @param tableName - Name of the table to insert into
618
+ * @param records - Array of records to insert
619
+ */
620
+ async function insertRecordsWithCast(connection, tableName, records) {
621
+ if (records.length === 0) return;
622
+ const keys = Object.keys(records[0]);
623
+ const values = [];
624
+ let paramIdx = 1;
625
+ const placeholders = records.map((record) => {
626
+ return `(${keys.map((key) => {
627
+ const value = record[key];
628
+ if (value === null) return "NULL";
629
+ else if (value === "" && typeof value === "string") {
630
+ values.push(value);
631
+ return `CAST($${paramIdx++} AS TEXT)`;
632
+ } else if (value instanceof Date) {
633
+ values.push(value.toISOString());
634
+ return `CAST($${paramIdx++} AS TIMESTAMP)`;
635
+ } else if (Array.isArray(value)) {
636
+ values.push(JSON.stringify(value));
637
+ return `CAST($${paramIdx++} AS JSON)`;
638
+ } else if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype) {
639
+ values.push(JSON.stringify(value));
640
+ return `CAST($${paramIdx++} AS JSON)`;
641
+ } else {
642
+ values.push(value);
643
+ return `$${paramIdx++}`;
644
+ }
645
+ }).join(", ")})`;
646
+ }).join(", ");
647
+ const sql = `INSERT INTO ${tableName} (${keys.join(", ")}) VALUES ${placeholders}`;
648
+ await connection.run(sql, values);
649
+ }
650
+ /**
651
+ * Validates that a table name is valid (alphanumeric + underscores only)
652
+ *
653
+ * @param table - Table name to validate
654
+ * @throws DatabaseError if table name is invalid
655
+ */
656
+ function validateTableName(table) {
657
+ if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table)) throw new DatabaseError(`Invalid table name: ${table}`, { table });
658
+ }
659
+ /**
660
+ * Creates a JSON database adapter using DuckDB in-memory engine
661
+ *
662
+ * This adapter provides SQL query capabilities over JSON files without creating
663
+ * persistent database files or WAL files. All data is stored in-memory during
664
+ * runtime and written back to JSON files based on the write strategy.
665
+ *
666
+ * @param options - JSON database options
667
+ * @returns Database interface for JSON files
668
+ */
669
+ async function getDatabase$2(options) {
670
+ const cacheKey = options.dbid || `json:${options.url}:${options.writeStrategy || "immediate"}:${options.autoRegister !== false}`;
671
+ if (options.clearCache) {
672
+ memoryConnectionCache.delete(cacheKey);
673
+ pendingConnections$1.delete(cacheKey);
674
+ }
675
+ const cached = memoryConnectionCache.get(cacheKey);
676
+ if (cached) return cached;
677
+ const pending = pendingConnections$1.get(cacheKey);
678
+ if (pending) return pending;
679
+ const connectionPromise = (async () => {
680
+ const { connection, pendingJSONFiles, inferredSchemaTables } = await createJSONConnection(options);
681
+ const writeStrategy = options.writeStrategy || "immediate";
682
+ const { url } = options;
683
+ /**
684
+ * Inserts one or more records into a table
685
+ *
686
+ * @param table - Table name
687
+ * @param data - Single record or array of records to insert
688
+ * @returns Promise resolving to operation result
689
+ * @throws Error if the insert operation fails
690
+ */
691
+ const insert = async (table, data) => {
692
+ validateTableName(table);
693
+ if (writeStrategy === "none") throw new DatabaseError("Cannot insert: write strategy is set to none (read-only mode)", {
694
+ table,
695
+ writeStrategy
696
+ });
697
+ const records = Array.isArray(data) ? data : [data];
698
+ if (records.length === 0) return {
699
+ operation: "insert",
700
+ affected: 0
701
+ };
702
+ const keys = Object.keys(records[0]);
703
+ const values = [];
704
+ let paramIdx = 1;
705
+ const placeholders = records.map((record) => {
706
+ return `(${keys.map((key) => {
707
+ const value = record[key];
708
+ if (value === null) return "NULL";
709
+ else if (value === "" && typeof value === "string") {
710
+ values.push(value);
711
+ return `CAST($${paramIdx++} AS TEXT)`;
712
+ } else if (value instanceof Date) {
713
+ values.push(value.toISOString());
714
+ return `CAST($${paramIdx++} AS TIMESTAMP)`;
715
+ } else if (Array.isArray(value)) {
716
+ values.push(JSON.stringify(value));
717
+ return `CAST($${paramIdx++} AS JSON)`;
718
+ } else if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype) {
719
+ values.push(JSON.stringify(value));
720
+ return `CAST($${paramIdx++} AS JSON)`;
721
+ } else {
722
+ values.push(value);
723
+ return `$${paramIdx++}`;
724
+ }
725
+ }).join(", ")})`;
726
+ }).join(", ");
727
+ const sql = `INSERT INTO "${table}" (${keys.join(", ")}) VALUES ${placeholders}`;
728
+ try {
729
+ await connection.run(sql, values);
730
+ const affected = records.length;
731
+ if (writeStrategy === "immediate") await exportTableToJSON(connection, table, url);
732
+ return {
733
+ operation: "insert",
734
+ affected
735
+ };
736
+ } catch (e) {
737
+ throw new DatabaseError("Failed to insert records into table", {
738
+ table,
739
+ sql,
740
+ values,
741
+ originalError: formatDbError(e)
742
+ });
743
+ }
744
+ };
745
+ /**
746
+ * Retrieves a single record matching the where criteria
747
+ *
748
+ * @param table - Table name
749
+ * @param where - Criteria to match records
750
+ * @returns Promise resolving to matching record or null if not found
751
+ * @throws Error if the query fails
752
+ */
753
+ const get = async (table, where) => {
754
+ validateTableName(table);
755
+ const { sql: whereClause, values } = buildWhere(where, 1, "json");
756
+ const sql = `SELECT * FROM "${table}" ${whereClause} LIMIT 1`;
757
+ try {
758
+ const rows = (await connection.runAndReadAll(sql, values)).getRowObjects();
759
+ return rows.length > 0 ? convertBigInts(rows[0]) : null;
760
+ } catch (e) {
761
+ throw new DatabaseError("Failed to retrieve record from table", {
762
+ table,
763
+ sql,
764
+ values,
765
+ originalError: formatDbError(e)
766
+ });
767
+ }
768
+ };
769
+ /**
770
+ * Retrieves multiple records matching the where criteria
771
+ *
772
+ * @param table - Table name
773
+ * @param where - Criteria to match records
774
+ * @returns Promise resolving to array of matching records
775
+ * @throws Error if the query fails
776
+ */
777
+ const list = async (table, where) => {
778
+ validateTableName(table);
779
+ const { sql: whereClause, values } = buildWhere(where, 1, "json");
780
+ const sql = `SELECT * FROM "${table}" ${whereClause}`;
781
+ try {
782
+ return convertBigInts((await connection.runAndReadAll(sql, values)).getRowObjects());
783
+ } catch (e) {
784
+ throw new DatabaseError("Failed to list records from table", {
785
+ table,
786
+ sql,
787
+ values,
788
+ originalError: formatDbError(e)
789
+ });
790
+ }
791
+ };
792
+ /**
793
+ * Updates records matching the where criteria
794
+ *
795
+ * @param table - Table name
796
+ * @param where - Criteria to match records to update
797
+ * @param data - New data to set
798
+ * @returns Promise resolving to operation result
799
+ * @throws Error if the update operation fails
800
+ */
801
+ const update = async (table, where, data) => {
802
+ validateTableName(table);
803
+ if (writeStrategy === "none") throw new DatabaseError("Cannot update: write strategy is set to none (read-only mode)", {
804
+ table,
805
+ writeStrategy
806
+ });
807
+ const keys = Object.keys(data);
808
+ const setClause = keys.map((key, idx) => `${key} = $${idx + 1}`).join(", ");
809
+ const { sql: whereClause, values: whereValues } = buildWhere(where, keys.length + 1, "json");
810
+ const dataValues = Object.values(data).map((value) => value instanceof Date ? value.toISOString() : value);
811
+ const sql = `UPDATE "${table}" SET ${setClause} ${whereClause}`;
812
+ const values = [...dataValues, ...whereValues];
813
+ try {
814
+ await connection.run(sql, values);
815
+ if (writeStrategy === "immediate") await exportTableToJSON(connection, table, url);
816
+ return {
817
+ operation: "update",
818
+ affected: 1
819
+ };
820
+ } catch (e) {
821
+ throw new DatabaseError("Failed to update records in table", {
822
+ table,
823
+ sql,
824
+ values,
825
+ originalError: formatDbError(e)
826
+ });
827
+ }
828
+ };
829
+ const buildJSONValueExpression = (value, values, paramIdx) => {
830
+ if (value === null) return "NULL";
831
+ if (value === "" && typeof value === "string") {
832
+ values.push(value);
833
+ return `CAST($${paramIdx.value++} AS TEXT)`;
834
+ }
835
+ if (value instanceof Date) {
836
+ values.push(value.toISOString());
837
+ return `CAST($${paramIdx.value++} AS TIMESTAMP)`;
838
+ }
839
+ if (Array.isArray(value)) {
840
+ values.push(JSON.stringify(value));
841
+ return `CAST($${paramIdx.value++} AS JSON)`;
842
+ }
843
+ if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype) {
844
+ values.push(JSON.stringify(value));
845
+ return `CAST($${paramIdx.value++} AS JSON)`;
846
+ }
847
+ values.push(value);
848
+ return `$${paramIdx.value++}`;
849
+ };
850
+ const executeNullAwareJSONUpsert = async (table, conflictColumns, data) => {
851
+ const conflictWhere = Object.fromEntries(conflictColumns.map((col) => [col, data[col]]));
852
+ if (!await get(table, conflictWhere)) return insert(table, data);
853
+ const keys = Object.keys(data);
854
+ const values = [];
855
+ const paramIdx = { value: 1 };
856
+ const assignments = keys.map((key) => {
857
+ return `"${key}" = ${buildJSONValueExpression(data[key], values, paramIdx)}`;
858
+ });
859
+ const { sql: whereClause, values: whereValues } = buildWhere(conflictWhere, paramIdx.value, "json");
860
+ const sql = `UPDATE "${table}" SET ${assignments.join(", ")} ${whereClause}`;
861
+ try {
862
+ await connection.run(sql, [...values, ...whereValues]);
863
+ if (writeStrategy === "immediate") await exportTableToJSON(connection, table, url);
864
+ return {
865
+ operation: "upsert",
866
+ affected: 1
867
+ };
868
+ } catch (e) {
869
+ throw new DatabaseError("Failed to upsert record into table", {
870
+ table,
871
+ sql,
872
+ values: [...values, ...whereValues],
873
+ conflictColumns,
874
+ originalError: formatDbError(e)
875
+ });
876
+ }
877
+ };
878
+ /**
879
+ * Inserts a record or updates it if it already exists (UPSERT)
880
+ *
881
+ * @param table - Table name
882
+ * @param conflictColumns - Columns that define the uniqueness constraint
883
+ * @param data - Data to insert or update
884
+ * @returns Promise resolving to operation result
885
+ * @throws Error if the upsert operation fails
886
+ */
887
+ const upsert = async (table, conflictColumns, data, options) => {
888
+ validateTableName(table);
889
+ if (writeStrategy === "none") throw new DatabaseError("Cannot upsert: write strategy is set to none (read-only mode)", {
890
+ table,
891
+ writeStrategy
892
+ });
893
+ const missingColumns = conflictColumns.filter((col) => !(col in data));
894
+ if (missingColumns.length > 0) throw new DatabaseError("Conflict columns missing from data", {
895
+ table,
896
+ conflictColumns,
897
+ missingColumns,
898
+ availableColumns: Object.keys(data),
899
+ hint: "All columns specified in ON CONFLICT must be present in the data being inserted. Undefined values should be replaced with null or an appropriate default."
900
+ });
901
+ if (!options?.nullsDistinct && conflictColumns.some((col) => data[col] === null)) return executeNullAwareJSONUpsert(table, conflictColumns, data);
902
+ const keys = Object.keys(data);
903
+ const dataValues = Object.values(data);
904
+ const placeholders = [];
905
+ const values = [];
906
+ let paramIdx = 1;
907
+ for (const value of dataValues) if (value === null) placeholders.push("NULL");
908
+ else if (value === "" && typeof value === "string") {
909
+ placeholders.push(`CAST($${paramIdx} AS TEXT)`);
910
+ values.push(value);
911
+ paramIdx++;
912
+ } else if (value instanceof Date) {
913
+ placeholders.push(`CAST($${paramIdx} AS TIMESTAMP)`);
914
+ values.push(value.toISOString());
915
+ paramIdx++;
916
+ } else if (Array.isArray(value)) {
917
+ placeholders.push(`CAST($${paramIdx} AS JSON)`);
918
+ values.push(JSON.stringify(value));
919
+ paramIdx++;
920
+ } else if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype) {
921
+ placeholders.push(`CAST($${paramIdx} AS JSON)`);
922
+ values.push(JSON.stringify(value));
923
+ paramIdx++;
924
+ } else {
925
+ placeholders.push(`$${paramIdx}`);
926
+ values.push(value);
927
+ paramIdx++;
928
+ }
929
+ const updateSetParts = [];
930
+ for (let i = 0; i < keys.length; i++) {
931
+ const key = keys[i];
932
+ const value = dataValues[i];
933
+ if (value === null) updateSetParts.push(`${key} = NULL`);
934
+ else if (value === "" && typeof value === "string") {
935
+ updateSetParts.push(`${key} = CAST($${paramIdx} AS TEXT)`);
936
+ values.push(value);
937
+ paramIdx++;
938
+ } else if (value instanceof Date) {
939
+ updateSetParts.push(`${key} = CAST($${paramIdx} AS TIMESTAMP)`);
940
+ values.push(value.toISOString());
941
+ paramIdx++;
942
+ } else if (Array.isArray(value)) {
943
+ updateSetParts.push(`${key} = CAST($${paramIdx} AS JSON)`);
944
+ values.push(JSON.stringify(value));
945
+ paramIdx++;
946
+ } else if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype) {
947
+ updateSetParts.push(`${key} = CAST($${paramIdx} AS JSON)`);
948
+ values.push(JSON.stringify(value));
949
+ paramIdx++;
950
+ } else {
951
+ updateSetParts.push(`${key} = $${paramIdx}`);
952
+ values.push(value);
953
+ paramIdx++;
954
+ }
955
+ }
956
+ const conflict = conflictColumns.map((col) => `"${col}"`).join(", ");
957
+ const quotedKeys = keys.map((key) => `"${key}"`).join(", ");
958
+ const quotedUpdateSetParts = keys.map((key, i) => {
959
+ const part = updateSetParts[i];
960
+ return `"${key}" = ${part.substring(part.indexOf("=") + 1).trim()}`;
961
+ });
962
+ const sql = `INSERT INTO "${table}" (${quotedKeys}) VALUES (${placeholders.join(", ")}) ON CONFLICT(${conflict}) DO UPDATE SET ${quotedUpdateSetParts.join(", ")}`;
963
+ try {
964
+ await connection.run(sql, values);
965
+ if (writeStrategy === "immediate") await exportTableToJSON(connection, table, url);
966
+ return {
967
+ operation: "upsert",
968
+ affected: 1
969
+ };
970
+ } catch (e) {
971
+ console.error("UPSERT failed:", {
972
+ table,
973
+ sql,
974
+ values,
975
+ valueTypes: values.map((v) => `${typeof v} (${v})`),
976
+ conflictColumns,
977
+ error: formatDbError(e)
978
+ });
979
+ throw new DatabaseError("Failed to upsert record into table", {
980
+ table,
981
+ sql,
982
+ values,
983
+ conflictColumns,
984
+ originalError: formatDbError(e)
985
+ });
986
+ }
987
+ };
988
+ /**
989
+ * Gets a record matching the where criteria or inserts it if not found
990
+ *
991
+ * @param table - Table name
992
+ * @param where - Criteria to match existing record
993
+ * @param data - Data to insert if no record found
994
+ * @returns Promise resolving to the record (either retrieved or newly inserted)
995
+ * @throws Error if the operation fails
996
+ */
997
+ const getOrInsert = async (table, where, data) => {
998
+ const result = await get(table, where);
999
+ if (result) return result;
1000
+ await insert(table, data);
1001
+ const inserted = await get(table, where);
1002
+ if (!inserted) throw new DatabaseError("Failed to insert and retrieve record", {
1003
+ table,
1004
+ where,
1005
+ data
1006
+ });
1007
+ return inserted;
1008
+ };
1009
+ /**
1010
+ * Deletes records from a table matching the where criteria
1011
+ *
1012
+ * @param table - Table name
1013
+ * @param where - Criteria to match records for deletion
1014
+ * @returns Promise resolving to operation result with count of deleted rows
1015
+ * @throws Error if the delete operation fails
1016
+ */
1017
+ const deleteRecords = async (table, where) => {
1018
+ validateTableName(table);
1019
+ if (writeStrategy === "none") throw new DatabaseError("Cannot delete: write strategy is set to none (read-only mode)", {
1020
+ table,
1021
+ writeStrategy
1022
+ });
1023
+ if (Object.keys(where).length === 0) throw new DatabaseError("DELETE requires at least one WHERE condition to prevent accidental deletion of all records", { table });
1024
+ const { sql: whereClause, values } = buildWhere(where, 1, "json");
1025
+ const sql = `DELETE FROM "${table}" ${whereClause}`;
1026
+ try {
1027
+ await connection.run(sql, values);
1028
+ if (writeStrategy === "immediate") await exportTableToJSON(connection, table, url);
1029
+ return {
1030
+ operation: "delete",
1031
+ affected: 1
1032
+ };
1033
+ } catch (e) {
1034
+ throw new DatabaseError("Failed to delete records from table", {
1035
+ table,
1036
+ sql,
1037
+ values,
1038
+ originalError: formatDbError(e)
1039
+ });
1040
+ }
1041
+ };
1042
+ /**
1043
+ * Counts records in a table matching the where criteria
1044
+ *
1045
+ * @param table - Table name
1046
+ * @param where - Criteria to match records (optional, counts all if omitted)
1047
+ * @returns Promise resolving to count of matching records
1048
+ * @throws Error if the count operation fails
1049
+ */
1050
+ const count = async (table, where) => {
1051
+ validateTableName(table);
1052
+ try {
1053
+ if (!where || Object.keys(where).length === 0) {
1054
+ const rows = (await connection.runAndReadAll(`SELECT COUNT(*) as count FROM "${table}"`)).getRowObjects();
1055
+ return Number(rows[0]?.count) || 0;
1056
+ }
1057
+ const { sql: whereClause, values } = buildWhere(where, 1, "json");
1058
+ const sql = `SELECT COUNT(*) as count FROM "${table}" ${whereClause}`;
1059
+ const rows = (await connection.runAndReadAll(sql, values)).getRowObjects();
1060
+ return Number(rows[0]?.count) || 0;
1061
+ } catch (e) {
1062
+ throw new DatabaseError("Failed to count records in table", {
1063
+ table,
1064
+ where,
1065
+ originalError: formatDbError(e)
1066
+ });
1067
+ }
1068
+ };
1069
+ /**
1070
+ * Checks if a table exists in the database
1071
+ *
1072
+ * @param tableName - Name of the table to check
1073
+ * @returns Promise resolving to boolean indicating if the table exists
1074
+ */
1075
+ const tableExists = async (tableName) => {
1076
+ try {
1077
+ await connection.runAndReadAll(`SELECT * FROM ${tableName} LIMIT 1`);
1078
+ return true;
1079
+ } catch {
1080
+ return false;
1081
+ }
1082
+ };
1083
+ /**
1084
+ * Exports a table to JSON and schema files
1085
+ *
1086
+ * Exports two files:
1087
+ * 1. {table}.json - Table data with columns cast appropriately
1088
+ * 2. {table}.schema.sql - CREATE TABLE statement to preserve constraints
1089
+ *
1090
+ * Note: JSON columns are NOT cast to TEXT to preserve object structure.
1091
+ * Other columns are cast to TEXT to prevent DuckDB from converting UUIDs
1092
+ * and other TEXT values to hugeint representation in JSON output.
1093
+ *
1094
+ * @param connection - DuckDB connection
1095
+ * @param table - Table name
1096
+ * @param dataDir - Directory to write files
1097
+ */
1098
+ const exportTableToJSON = async (connection, table, dataDir) => {
1099
+ const filePath = join(dataDir, `${table}.json`);
1100
+ const schemaPath = join(dataDir, `${table}.schema.sql`);
1101
+ try {
1102
+ const rows = (await connection.runAndReadAll(`SELECT sql FROM sqlite_master WHERE type='table' AND name='${table}'`)).getRowObjects();
1103
+ if (rows.length > 0 && rows[0].sql) {
1104
+ const { writeFile } = await import("node:fs/promises");
1105
+ await writeFile(schemaPath, rows[0].sql, "utf-8");
1106
+ }
1107
+ } catch (error) {
1108
+ console.warn(`[json-adapter] Could not export schema for ${table}: ${error instanceof Error ? error.message : String(error)}`);
1109
+ }
1110
+ const columnInfo = (await connection.runAndReadAll(`SELECT column_name, data_type FROM information_schema.columns WHERE table_name = '${table}' ORDER BY ordinal_position`)).getRowObjects();
1111
+ if (columnInfo.length === 0) {
1112
+ await connection.run(`COPY (SELECT * FROM "${table}") TO '${filePath}' (FORMAT JSON, ARRAY true)`);
1113
+ return;
1114
+ }
1115
+ const sql = `COPY (SELECT ${columnInfo.map(({ column_name, data_type }) => {
1116
+ const upperType = data_type.toUpperCase();
1117
+ if (upperType === "JSON" || upperType.startsWith("STRUCT")) return `"${column_name}"`;
1118
+ if ([
1119
+ "DOUBLE",
1120
+ "REAL",
1121
+ "FLOAT",
1122
+ "INTEGER",
1123
+ "BIGINT",
1124
+ "SMALLINT",
1125
+ "TINYINT",
1126
+ "DECIMAL",
1127
+ "NUMERIC",
1128
+ "HUGEINT",
1129
+ "UBIGINT",
1130
+ "UINTEGER",
1131
+ "USMALLINT",
1132
+ "UTINYINT"
1133
+ ].some((t) => upperType.includes(t))) return `"${column_name}"`;
1134
+ if (upperType === "BOOLEAN" || upperType === "BOOL") return `"${column_name}"`;
1135
+ return `CAST("${column_name}" AS TEXT) AS "${column_name}"`;
1136
+ }).join(", ")} FROM "${table}") TO '${filePath}' (FORMAT JSON, ARRAY true)`;
1137
+ await connection.run(sql);
1138
+ };
1139
+ /**
1140
+ * Manual export method for 'manual' write strategy
1141
+ *
1142
+ * Allows explicit control over when tables are written back to JSON files.
1143
+ *
1144
+ * @param table - Table name to export
1145
+ * @returns Promise that resolves when export completes
1146
+ */
1147
+ const exportTable = async (table) => {
1148
+ if (writeStrategy === "none") throw new DatabaseError("Cannot export table: write strategy is set to none (read-only mode)", {
1149
+ table,
1150
+ writeStrategy
1151
+ });
1152
+ await exportTableToJSON(connection, table, url);
1153
+ };
1154
+ /**
1155
+ * Creates a table by inferring schema from a JSON file
1156
+ *
1157
+ * This utility is for users who want automatic schema inference from JSON.
1158
+ * Note: Inferred schemas do NOT include UNIQUE constraints, so UPSERT
1159
+ * operations will fail. Use syncSchema() with explicit DDL for UPSERT support.
1160
+ *
1161
+ * @param tableName - Name of the table to create
1162
+ * @param jsonPath - Optional path to JSON file (defaults to {dataDir}/{tableName}.json)
1163
+ * @returns Promise that resolves when table is created
1164
+ * @throws Error if JSON file doesn't exist or table creation fails
1165
+ */
1166
+ const inferSchemaFromJSON = async (tableName, jsonPath) => {
1167
+ validateTableName(tableName);
1168
+ const filePath = jsonPath || join(url, `${tableName}.json`);
1169
+ try {
1170
+ const { access } = await import("node:fs/promises");
1171
+ const { constants } = await import("node:fs");
1172
+ await access(filePath, constants.F_OK);
1173
+ console.log(`[json-adapter] Inferring schema for ${tableName} from JSON file`);
1174
+ await connection.run(`CREATE TABLE "${tableName}" AS SELECT * FROM read_json_auto('${filePath}')`);
1175
+ pendingJSONFiles.delete(tableName);
1176
+ console.log(`[json-adapter] Created ${tableName} with inferred schema (no UNIQUE constraints)`);
1177
+ } catch (error) {
1178
+ throw new DatabaseError(`Failed to infer schema from JSON for table ${tableName}`, {
1179
+ tableName,
1180
+ jsonPath: filePath,
1181
+ originalError: error instanceof Error ? error.message : String(error)
1182
+ });
1183
+ }
1184
+ };
1185
+ /**
1186
+ * Creates a table-specific interface for simplified table operations
1187
+ *
1188
+ * @param tableName - Table name
1189
+ * @returns TableInterface for the specified table
1190
+ */
1191
+ const table = (tableName) => ({
1192
+ insert: (data) => insert(tableName, data),
1193
+ get: (where) => get(tableName, where),
1194
+ list: (where) => list(tableName, where)
1195
+ });
1196
+ /**
1197
+ * Parses a tagged template literal into a SQL query and values
1198
+ *
1199
+ * @param strings - Template strings
1200
+ * @param vars - Variables to interpolate into the query
1201
+ * @returns Object with SQL query and values array
1202
+ */
1203
+ const parseTemplate = (strings, ...vars) => {
1204
+ let sql = strings[0];
1205
+ const values = [];
1206
+ for (let i = 0; i < vars.length; i++) {
1207
+ values.push(vars[i]);
1208
+ sql += `$${i + 1}${strings[i + 1]}`;
1209
+ }
1210
+ return {
1211
+ sql,
1212
+ values
1213
+ };
1214
+ };
1215
+ /**
1216
+ * Executes a SQL query using template literals and returns multiple rows
1217
+ *
1218
+ * @param strings - Template strings
1219
+ * @param vars - Variables to interpolate into the query
1220
+ * @returns Promise resolving to array of result records
1221
+ * @throws Error if the query fails
1222
+ */
1223
+ const many = async (strings, ...vars) => {
1224
+ const { sql, values } = parseTemplate(strings, ...vars);
1225
+ try {
1226
+ return convertBigInts((await connection.runAndReadAll(sql, values)).getRowObjects());
1227
+ } catch (e) {
1228
+ throw new DatabaseError("Failed to execute many query", {
1229
+ sql,
1230
+ values,
1231
+ originalError: formatDbError(e)
1232
+ });
1233
+ }
1234
+ };
1235
+ /**
1236
+ * Executes a SQL query using template literals and returns a single row
1237
+ *
1238
+ * @param strings - Template strings
1239
+ * @param vars - Variables to interpolate into the query
1240
+ * @returns Promise resolving to a single result record or null
1241
+ * @throws Error if the query fails
1242
+ */
1243
+ const single = async (strings, ...vars) => {
1244
+ const { sql, values } = parseTemplate(strings, ...vars);
1245
+ try {
1246
+ const rows = (await connection.runAndReadAll(sql, values)).getRowObjects();
1247
+ return rows[0] ? convertBigInts(rows[0]) : null;
1248
+ } catch (e) {
1249
+ throw new DatabaseError("Failed to execute single query", {
1250
+ sql,
1251
+ values,
1252
+ originalError: formatDbError(e)
1253
+ });
1254
+ }
1255
+ };
1256
+ /**
1257
+ * Executes a SQL query using template literals and returns a single value
1258
+ *
1259
+ * @param strings - Template strings
1260
+ * @param vars - Variables to interpolate into the query
1261
+ * @returns Promise resolving to a single value (first column of first row)
1262
+ * @throws Error if the query fails
1263
+ */
1264
+ const pluck = async (strings, ...vars) => {
1265
+ const { sql, values } = parseTemplate(strings, ...vars);
1266
+ try {
1267
+ const rows = (await connection.runAndReadAll(sql, values)).getRowObjects();
1268
+ if (rows.length === 0) return null;
1269
+ const firstRow = rows[0];
1270
+ const firstKey = Object.keys(firstRow)[0];
1271
+ return convertBigInts(firstRow[firstKey]);
1272
+ } catch (e) {
1273
+ throw new DatabaseError("Failed to execute pluck query", {
1274
+ sql,
1275
+ values,
1276
+ originalError: formatDbError(e)
1277
+ });
1278
+ }
1279
+ };
1280
+ /**
1281
+ * Executes a SQL query using template literals without returning results
1282
+ *
1283
+ * @param strings - Template strings
1284
+ * @param vars - Variables to interpolate into the query
1285
+ * @returns Promise that resolves when the query completes
1286
+ * @throws Error if the query fails
1287
+ */
1288
+ const execute = async (strings, ...vars) => {
1289
+ const { sql, values } = parseTemplate(strings, ...vars);
1290
+ try {
1291
+ await connection.run(sql, values);
1292
+ const createTableMatch = sql.match(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["']?(\w+)["']?/i);
1293
+ if (createTableMatch) {
1294
+ const tableName = createTableMatch[1];
1295
+ const pendingFile = pendingJSONFiles.get(tableName);
1296
+ if (pendingFile) {
1297
+ await loadJSONData(connection, tableName, pendingFile);
1298
+ pendingJSONFiles.delete(tableName);
1299
+ } else if (writeStrategy !== "none") try {
1300
+ const { access } = await import("node:fs/promises");
1301
+ const jsonFilePath = join(url, `${tableName}.json`);
1302
+ try {
1303
+ await access(jsonFilePath);
1304
+ } catch {
1305
+ await exportTableToJSON(connection, tableName, url);
1306
+ }
1307
+ } catch (error) {
1308
+ console.warn(`[json-adapter] Could not export newly created table ${tableName}: ${error instanceof Error ? error.message : String(error)}`);
1309
+ }
1310
+ }
1311
+ } catch (e) {
1312
+ throw new DatabaseError("Failed to execute query", {
1313
+ sql,
1314
+ values,
1315
+ originalError: formatDbError(e)
1316
+ });
1317
+ }
1318
+ };
1319
+ /**
1320
+ * Executes a raw SQL query with parameterized values
1321
+ *
1322
+ * @param str - SQL query string
1323
+ * @param values - Variables to use as parameters
1324
+ * @returns Promise resolving to query result with rows and metadata
1325
+ * @throws Error if the query fails
1326
+ */
1327
+ const query = async (str, ...values) => {
1328
+ const sql = str;
1329
+ const args = Array.isArray(values[0]) ? values[0] : values;
1330
+ try {
1331
+ const rows = convertBigInts((await connection.runAndReadAll(sql, args)).getRowObjects());
1332
+ return {
1333
+ command: sql.split(" ")[0].toUpperCase(),
1334
+ rowCount: rows.length,
1335
+ oid: null,
1336
+ fields: rows.length > 0 ? Object.keys(rows[0]).map((name) => ({
1337
+ name,
1338
+ tableID: 0,
1339
+ columnID: 0,
1340
+ dataTypeID: 0,
1341
+ dataTypeSize: -1,
1342
+ dataTypeModifier: -1,
1343
+ format: "text"
1344
+ })) : [],
1345
+ rows
1346
+ };
1347
+ } catch (e) {
1348
+ throw new DatabaseError("Failed to execute raw query", {
1349
+ sql,
1350
+ args,
1351
+ originalError: formatDbError(e)
1352
+ });
1353
+ }
1354
+ };
1355
+ const oo = many;
1356
+ const oO = single;
1357
+ const ox = pluck;
1358
+ const xx = execute;
1359
+ /**
1360
+ * Synchronizes database schema with provided SQL DDL
1361
+ *
1362
+ * Filters out CREATE TRIGGER statements since triggers are not supported
1363
+ * in the JSON adapter (timestamps managed at application level).
1364
+ *
1365
+ * @param schema - SQL schema definition with CREATE TABLE statements
1366
+ * @returns Promise that resolves when schema is synchronized
1367
+ */
1368
+ const syncSchema = async (schema) => {
1369
+ const statements = schema.trim().split(";").filter((stmt) => stmt.trim() !== "");
1370
+ const createTableStatements = [];
1371
+ const indexStatements = [];
1372
+ const otherStatements = [];
1373
+ for (const stmt of statements) {
1374
+ const trimmed = stmt.trim();
1375
+ if (!trimmed) continue;
1376
+ const upperTrimmed = trimmed.toUpperCase();
1377
+ if (upperTrimmed.startsWith("CREATE TRIGGER")) {
1378
+ console.warn("[json-adapter] Skipping trigger creation - timestamps managed at application level");
1379
+ continue;
1380
+ }
1381
+ if (upperTrimmed.startsWith("CREATE INDEX") || upperTrimmed.startsWith("CREATE UNIQUE INDEX")) indexStatements.push(trimmed);
1382
+ else if (upperTrimmed.startsWith("CREATE TABLE")) createTableStatements.push(trimmed);
1383
+ else otherStatements.push(trimmed);
1384
+ }
1385
+ for (const ddl of createTableStatements) {
1386
+ const { ddl: transformedDDL, indexes: remainingIndexes } = convertUniqueIndexesToInlineConstraints(ddl, indexStatements);
1387
+ const createTableMatch = transformedDDL.match(/CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?["']?(\w+)["']?/i);
1388
+ const tableName = createTableMatch ? createTableMatch[1] : null;
1389
+ if (tableName && inferredSchemaTables.has(tableName)) try {
1390
+ await connection.run(`DROP TABLE IF EXISTS "${tableName}"`);
1391
+ inferredSchemaTables.delete(tableName);
1392
+ console.log(`[json-adapter] Dropped inferred-schema table ${tableName} for schema upgrade`);
1393
+ } catch (error) {
1394
+ console.warn(`[json-adapter] Could not drop inferred-schema table ${tableName}: ${error instanceof Error ? error.message : String(error)}`);
1395
+ }
1396
+ try {
1397
+ await connection.run(transformedDDL);
1398
+ if (tableName) {
1399
+ const pendingFile = pendingJSONFiles.get(tableName);
1400
+ if (pendingFile) {
1401
+ await loadJSONData(connection, tableName, pendingFile);
1402
+ pendingJSONFiles.delete(tableName);
1403
+ } else if (writeStrategy !== "none") try {
1404
+ const { access } = await import("node:fs/promises");
1405
+ const jsonFilePath = join(url, `${tableName}.json`);
1406
+ try {
1407
+ await access(jsonFilePath);
1408
+ } catch {
1409
+ await exportTableToJSON(connection, tableName, url);
1410
+ }
1411
+ } catch (error) {
1412
+ console.warn(`[json-adapter] Could not export newly created table ${tableName}: ${error instanceof Error ? error.message : String(error)}`);
1413
+ }
1414
+ }
1415
+ } catch (e) {
1416
+ console.error("Schema sync error (CREATE TABLE):", e);
1417
+ }
1418
+ for (const indexSQL of remainingIndexes) try {
1419
+ await connection.run(indexSQL);
1420
+ } catch (e) {
1421
+ console.error("Schema sync error (CREATE INDEX):", e);
1422
+ }
1423
+ }
1424
+ for (const stmt of otherStatements) try {
1425
+ await connection.run(stmt);
1426
+ } catch (e) {
1427
+ console.error("Schema sync error:", e);
1428
+ }
1429
+ };
1430
+ /**
1431
+ * Initialize database schemas from JSON manifest
1432
+ * Supports dependency resolution and schema overrides
1433
+ *
1434
+ * @param options - Schema initialization options
1435
+ * @returns Promise that resolves when schemas are initialized
1436
+ */
1437
+ const initializeSchemas = async (options) => {
1438
+ const schemaManager = new DatabaseSchemaManager();
1439
+ const currentDb = {
1440
+ url,
1441
+ client: connection,
1442
+ query,
1443
+ insert,
1444
+ update,
1445
+ upsert,
1446
+ get,
1447
+ list,
1448
+ getOrInsert,
1449
+ delete: deleteRecords,
1450
+ count,
1451
+ table,
1452
+ tableExists,
1453
+ many,
1454
+ single,
1455
+ pluck,
1456
+ execute,
1457
+ oo,
1458
+ oO,
1459
+ ox,
1460
+ xx,
1461
+ syncSchema
1462
+ };
1463
+ await schemaManager.initializeSchemas(currentDb, options);
1464
+ };
1465
+ /**
1466
+ * Executes a callback within a database transaction
1467
+ * Automatically commits on success or rolls back on error
1468
+ *
1469
+ * @param callback - Function to execute within transaction
1470
+ * @returns Promise resolving to callback result
1471
+ */
1472
+ const transaction = async (callback) => {
1473
+ try {
1474
+ await connection.run("BEGIN TRANSACTION");
1475
+ const result = await callback({
1476
+ url,
1477
+ client: connection,
1478
+ insert,
1479
+ get,
1480
+ list,
1481
+ update,
1482
+ upsert,
1483
+ getOrInsert,
1484
+ delete: deleteRecords,
1485
+ count,
1486
+ table,
1487
+ many,
1488
+ single,
1489
+ pluck,
1490
+ execute,
1491
+ query,
1492
+ oo,
1493
+ oO,
1494
+ ox,
1495
+ xx,
1496
+ tableExists,
1497
+ syncSchema,
1498
+ transaction
1499
+ });
1500
+ await connection.run("COMMIT");
1501
+ return result;
1502
+ } catch (error) {
1503
+ await connection.run("ROLLBACK");
1504
+ throw error;
1505
+ }
1506
+ };
1507
+ /**
1508
+ * Begins a new transaction and returns a handle for manual control
1509
+ *
1510
+ * Unlike transaction(), this gives you explicit control over commit/rollback.
1511
+ * Ideal for test isolation where you want to rollback after each test.
1512
+ *
1513
+ * @returns Promise resolving to a TransactionHandle
1514
+ */
1515
+ const beginTransaction = async () => {
1516
+ await connection.run("BEGIN TRANSACTION");
1517
+ let active = true;
1518
+ const commit = async () => {
1519
+ if (!active) throw new DatabaseError("Transaction already ended", {});
1520
+ await connection.run("COMMIT");
1521
+ active = false;
1522
+ };
1523
+ const rollback = async () => {
1524
+ if (!active) throw new DatabaseError("Transaction already ended", {});
1525
+ await connection.run("ROLLBACK");
1526
+ active = false;
1527
+ };
1528
+ const isActive = () => active;
1529
+ return {
1530
+ url,
1531
+ client: connection,
1532
+ insert,
1533
+ get,
1534
+ list,
1535
+ update,
1536
+ upsert,
1537
+ getOrInsert,
1538
+ delete: deleteRecords,
1539
+ count,
1540
+ table,
1541
+ many,
1542
+ single,
1543
+ pluck,
1544
+ execute,
1545
+ query,
1546
+ oo,
1547
+ oO,
1548
+ ox,
1549
+ xx,
1550
+ tableExists,
1551
+ syncSchema,
1552
+ transaction,
1553
+ commit,
1554
+ rollback,
1555
+ isActive
1556
+ };
1557
+ };
1558
+ return {
1559
+ url,
1560
+ requiresSchemaCheck: true,
1561
+ client: connection,
1562
+ query,
1563
+ insert,
1564
+ update,
1565
+ upsert,
1566
+ get,
1567
+ list,
1568
+ getOrInsert,
1569
+ delete: deleteRecords,
1570
+ count,
1571
+ table,
1572
+ tableExists,
1573
+ many,
1574
+ single,
1575
+ pluck,
1576
+ execute,
1577
+ oo,
1578
+ oO,
1579
+ ox,
1580
+ xx,
1581
+ syncSchema,
1582
+ initializeSchemas,
1583
+ transaction,
1584
+ beginTransaction,
1585
+ exportTable,
1586
+ inferSchemaFromJSON
1587
+ };
1588
+ })();
1589
+ pendingConnections$1.set(cacheKey, connectionPromise);
1590
+ try {
1591
+ const db = await connectionPromise;
1592
+ memoryConnectionCache.set(cacheKey, db);
1593
+ return db;
1594
+ } finally {
1595
+ pendingConnections$1.delete(cacheKey);
1596
+ }
1597
+ }
1598
+ //#endregion
1599
+ //#region src/postgres.ts
1600
+ var postgres_exports = /* @__PURE__ */ __exportAll({
1601
+ clearPostgresConnectionCache: () => clearPostgresConnectionCache,
1602
+ getDatabase: () => getDatabase$1
1603
+ });
1604
+ /**
1605
+ * Module-level cache for PostgreSQL connections.
1606
+ * Keyed by dbid (or derived from connection URL).
1607
+ * Prevents creating multiple pg.Pool instances for the same database.
1608
+ */
1609
+ var connectionCache = /* @__PURE__ */ new Map();
1610
+ var pendingConnections = /* @__PURE__ */ new Map();
1611
+ /**
1612
+ * Clears the PostgreSQL connection cache.
1613
+ * Useful for test isolation and reconnection scenarios.
1614
+ */
1615
+ function clearPostgresConnectionCache() {
1616
+ connectionCache.clear();
1617
+ pendingConnections.clear();
1618
+ }
1619
+ /**
1620
+ * Operator for the given distance metric in pgvector
1621
+ */
1622
+ function vectorOperator(metric) {
1623
+ switch (metric) {
1624
+ case "cosine": return "<=>";
1625
+ case "l2": return "<->";
1626
+ case "ip": return "<#>";
1627
+ default: return "<=>";
1628
+ }
1629
+ }
1630
+ function getMaxPostgresParameterIndex(sql) {
1631
+ let maxIndex = 0;
1632
+ for (const match of sql.matchAll(/\$(\d+)/g)) {
1633
+ const index = Number(match[1]);
1634
+ if (Number.isSafeInteger(index) && index > maxIndex) maxIndex = index;
1635
+ }
1636
+ return maxIndex;
1637
+ }
1638
+ function usesSingleArrayParameter(sql) {
1639
+ const identifier = String.raw`(?:"[^"]+"|[a-z_][\w$]*)`;
1640
+ const qualifiedIdentifier = String.raw`${identifier}(?:\s*\.\s*${identifier})?`;
1641
+ const arrayType = String.raw`${qualifiedIdentifier}(?:\s*\([^)]*\))?\s*\[\]`;
1642
+ return new RegExp(String.raw`\$1\s*::\s*${arrayType}`, "i").test(sql) || new RegExp(String.raw`\bCAST\s*\(\s*\$1\s+AS\s+${arrayType}\s*\)`, "i").test(sql) || /\b(?:ANY|ALL|SOME)\s*\(\s*\$1\s*\)/i.test(sql);
1643
+ }
1644
+ function normalizeRawQueryValues(sql, values) {
1645
+ if (values.length !== 1 || !Array.isArray(values[0])) return values;
1646
+ const valuesArray = values[0];
1647
+ const maxParameterIndex = getMaxPostgresParameterIndex(sql);
1648
+ if (maxParameterIndex === 1 && usesSingleArrayParameter(sql)) return values;
1649
+ if (maxParameterIndex === 0 || maxParameterIndex === valuesArray.length) return valuesArray;
1650
+ return values;
1651
+ }
1652
+ function rewriteQuestionMarkPlaceholders(sql) {
1653
+ let output = "";
1654
+ let count = 0;
1655
+ let index = 0;
1656
+ const copyQuoted = (quote, escaped = false) => {
1657
+ output += quote;
1658
+ index += 1;
1659
+ while (index < sql.length) {
1660
+ const char = sql[index];
1661
+ output += char;
1662
+ index += 1;
1663
+ if (escaped && char === "\\" && index < sql.length) {
1664
+ output += sql[index];
1665
+ index += 1;
1666
+ continue;
1667
+ }
1668
+ if (char === quote) {
1669
+ if (sql[index] === quote) {
1670
+ output += sql[index];
1671
+ index += 1;
1672
+ continue;
1673
+ }
1674
+ break;
1675
+ }
1676
+ }
1677
+ };
1678
+ const copyUntil = (end) => {
1679
+ const endIndex = sql.indexOf(end, index + end.length);
1680
+ if (endIndex === -1) {
1681
+ output += sql.slice(index);
1682
+ index = sql.length;
1683
+ return;
1684
+ }
1685
+ output += sql.slice(index, endIndex + end.length);
1686
+ index = endIndex + end.length;
1687
+ };
1688
+ while (index < sql.length) {
1689
+ const char = sql[index];
1690
+ const next = sql[index + 1];
1691
+ if ((char === "E" || char === "e") && next === "'") {
1692
+ output += char;
1693
+ index += 1;
1694
+ copyQuoted("'", true);
1695
+ continue;
1696
+ }
1697
+ if (char === "'") {
1698
+ copyQuoted("'");
1699
+ continue;
1700
+ }
1701
+ if (char === "\"") {
1702
+ copyQuoted("\"");
1703
+ continue;
1704
+ }
1705
+ if (char === "-" && next === "-") {
1706
+ const endIndex = sql.indexOf("\n", index + 2);
1707
+ if (endIndex === -1) {
1708
+ output += sql.slice(index);
1709
+ index = sql.length;
1710
+ } else {
1711
+ output += sql.slice(index, endIndex + 1);
1712
+ index = endIndex + 1;
1713
+ }
1714
+ continue;
1715
+ }
1716
+ if (char === "/" && next === "*") {
1717
+ copyUntil("*/");
1718
+ continue;
1719
+ }
1720
+ if (char === "$") {
1721
+ const tag = sql.slice(index).match(/^\$[A-Za-z_][\w$]*\$|^\$\$/);
1722
+ if (tag) {
1723
+ copyUntil(tag[0]);
1724
+ continue;
1725
+ }
1726
+ }
1727
+ if (char === "?" && next !== "|" && next !== "&") {
1728
+ count += 1;
1729
+ output += `$${count}`;
1730
+ index += 1;
1731
+ continue;
1732
+ }
1733
+ output += char;
1734
+ index += 1;
1735
+ }
1736
+ return {
1737
+ sql: output,
1738
+ count
1739
+ };
1740
+ }
1741
+ function usesSingleQuestionArrayParameter(sql) {
1742
+ const identifier = String.raw`(?:"[^"]+"|[a-z_][\w$]*)`;
1743
+ const qualifiedIdentifier = String.raw`${identifier}(?:\s*\.\s*${identifier})?`;
1744
+ const arrayType = String.raw`${qualifiedIdentifier}(?:\s*\([^)]*\))?\s*\[\]`;
1745
+ return new RegExp(String.raw`\?\s*::\s*${arrayType}`, "i").test(sql) || new RegExp(String.raw`\bCAST\s*\(\s*\?\s+AS\s+${arrayType}\s*\)`, "i").test(sql) || /\b(?:ANY|ALL|SOME)\s*\(\s*\?\s*\)/i.test(sql);
1746
+ }
1747
+ function normalizeQuestionMarkQueryValues(sql, values, placeholderCount) {
1748
+ if (values.length !== 1 || !Array.isArray(values[0])) return values;
1749
+ if (placeholderCount === 1 && usesSingleQuestionArrayParameter(sql)) return values;
1750
+ if (placeholderCount === values[0].length) return values[0];
1751
+ return values;
1752
+ }
1753
+ function normalizePostgresRawQuery(sql, values) {
1754
+ if (getMaxPostgresParameterIndex(sql) > 0) return {
1755
+ sql,
1756
+ values: normalizeRawQueryValues(sql, values)
1757
+ };
1758
+ const rewritten = rewriteQuestionMarkPlaceholders(sql);
1759
+ const placeholderCount = rewritten.count;
1760
+ const queryValues = normalizeQuestionMarkQueryValues(sql, values, placeholderCount);
1761
+ if (placeholderCount > 0 && placeholderCount === queryValues.length) return {
1762
+ sql: rewritten.sql,
1763
+ values: queryValues
1764
+ };
1765
+ return {
1766
+ sql,
1767
+ values: normalizeRawQueryValues(sql, values)
1768
+ };
1769
+ }
1770
+ /**
1771
+ * pgvector index operator class for CREATE INDEX
1772
+ */
1773
+ function vectorOpsClass(metric) {
1774
+ switch (metric) {
1775
+ case "cosine": return "vector_cosine_ops";
1776
+ case "l2": return "vector_l2_ops";
1777
+ case "ip": return "vector_ip_ops";
1778
+ default: return "vector_cosine_ops";
1779
+ }
1780
+ }
1781
+ /**
1782
+ * Convert a number[] to a pgvector literal string: '[0.1,0.2,0.3]'
1783
+ */
1784
+ function toVectorLiteral(embedding) {
1785
+ if (embedding.length === 0) throw new DatabaseError("Vector embedding must not be empty", {});
1786
+ for (let i = 0; i < embedding.length; i++) if (!Number.isFinite(embedding[i])) throw new DatabaseError("Vector embedding contains invalid value (NaN or Infinity)", {
1787
+ index: i,
1788
+ value: embedding[i]
1789
+ });
1790
+ return `[${embedding.join(",")}]`;
1791
+ }
1792
+ /**
1793
+ * Creates vector capabilities backed by pgvector for a PostgreSQL pool.
1794
+ *
1795
+ * @param pool - PostgreSQL connection pool
1796
+ * @returns VectorCapabilities implementation
1797
+ */
1798
+ function createVectorCapabilities(pool) {
1799
+ return {
1800
+ async ensureColumn(table, column, dimensions) {
1801
+ if (!Number.isInteger(dimensions) || dimensions <= 0) throw new DatabaseError("Vector dimensions must be a positive integer", { dimensions });
1802
+ try {
1803
+ await pool.query("CREATE EXTENSION IF NOT EXISTS vector");
1804
+ await pool.query(`ALTER TABLE "${table}" ADD COLUMN IF NOT EXISTS "${column}" vector(${dimensions})`);
1805
+ } catch (e) {
1806
+ throw new DatabaseError("Failed to ensure vector column", {
1807
+ table,
1808
+ column,
1809
+ dimensions,
1810
+ originalError: formatDbError(e)
1811
+ });
1812
+ }
1813
+ },
1814
+ async ensureIndex(table, column, options) {
1815
+ const metric = options?.metric || "cosine";
1816
+ const indexType = options?.type || "hnsw";
1817
+ const opsClass = vectorOpsClass(metric);
1818
+ const indexName = `idx_${table}_${column}_${metric}_${indexType}`;
1819
+ try {
1820
+ await pool.query(`CREATE INDEX IF NOT EXISTS "${indexName}" ON "${table}" USING ${indexType} ("${column}" ${opsClass})`);
1821
+ } catch (e) {
1822
+ throw new DatabaseError("Failed to ensure vector index", {
1823
+ table,
1824
+ column,
1825
+ indexName,
1826
+ originalError: formatDbError(e)
1827
+ });
1828
+ }
1829
+ },
1830
+ async upsertVector(table, where, column, embedding) {
1831
+ const keys = Object.keys(where);
1832
+ const values = Object.values(where);
1833
+ const conditions = keys.map((key, i) => `"${key}" IS NOT DISTINCT FROM $${i + 1}`).join(" AND ");
1834
+ const vectorParam = `$${values.length + 1}`;
1835
+ try {
1836
+ await pool.query(`UPDATE "${table}" SET "${column}" = ${vectorParam}::vector WHERE ${conditions}`, [...values, toVectorLiteral(embedding)]);
1837
+ } catch (e) {
1838
+ throw new DatabaseError("Failed to upsert vector", {
1839
+ table,
1840
+ column,
1841
+ where,
1842
+ originalError: formatDbError(e)
1843
+ });
1844
+ }
1845
+ },
1846
+ async search(table, column, embedding, options) {
1847
+ const metric = options?.metric || "cosine";
1848
+ const limit = options?.limit || 10;
1849
+ const op = vectorOperator(metric);
1850
+ const queryParams = [toVectorLiteral(embedding)];
1851
+ let whereClause = `"${column}" IS NOT NULL`;
1852
+ if (options?.where) {
1853
+ whereClause += ` AND (${options.where})`;
1854
+ if (options.params) queryParams.push(...options.params);
1855
+ }
1856
+ const limitParam = `$${queryParams.length + 1}`;
1857
+ queryParams.push(limit);
1858
+ const sql = `SELECT *, ("${column}" ${op} $1::vector) AS distance FROM "${table}" WHERE ${whereClause} ORDER BY "${column}" ${op} $1::vector LIMIT ${limitParam}`;
1859
+ try {
1860
+ return (await pool.query(sql, queryParams)).rows.map((row) => ({
1861
+ ...row,
1862
+ id: row.id,
1863
+ distance: Number.parseFloat(row.distance)
1864
+ }));
1865
+ } catch (e) {
1866
+ throw new DatabaseError("Failed to execute vector search", {
1867
+ table,
1868
+ column,
1869
+ metric,
1870
+ limit,
1871
+ originalError: formatDbError(e)
1872
+ });
1873
+ }
1874
+ }
1875
+ };
1876
+ }
1877
+ /**
1878
+ * Creates a PostgreSQL database adapter
1879
+ *
1880
+ * Loads configuration from environment variables with backward compatibility:
1881
+ * - First checks HAVE_SQL_* environment variables (new standard)
1882
+ * - Falls back to SQLOO_* environment variables (legacy)
1883
+ * - User-provided options always take precedence
1884
+ *
1885
+ * Environment variables:
1886
+ * - HAVE_SQL_URL / SQLOO_URL → Connection string (takes precedence)
1887
+ * - HAVE_SQL_DATABASE / SQLOO_DATABASE → Database name
1888
+ * - HAVE_SQL_HOST / SQLOO_HOST → Host (default: 'localhost')
1889
+ * - HAVE_SQL_USER / SQLOO_USER → Username
1890
+ * - HAVE_SQL_PASSWORD / SQLOO_PASSWORD → Password
1891
+ * - HAVE_SQL_PORT / SQLOO_PORT → Port (default: 5432)
1892
+ *
1893
+ * @param options - PostgreSQL connection options
1894
+ * @returns Database interface for PostgreSQL
1895
+ */
1896
+ async function getDatabase$1(options = {}) {
1897
+ const envUrl = process.env.HAVE_SQL_URL || process.env.SQLOO_URL || null;
1898
+ const envDatabase = process.env.HAVE_SQL_DATABASE || process.env.SQLOO_DATABASE || null;
1899
+ const envHost = process.env.HAVE_SQL_HOST || process.env.SQLOO_HOST || "localhost";
1900
+ const envUser = process.env.HAVE_SQL_USER || process.env.SQLOO_USER || "";
1901
+ const envPortRaw = process.env.HAVE_SQL_PORT || process.env.SQLOO_PORT;
1902
+ const envPort = envPortRaw ? Number(envPortRaw) || 5432 : 5432;
1903
+ const derivedHost = options.host || envHost;
1904
+ const derivedPort = options.port || envPort;
1905
+ const derivedDatabase = options.database || envDatabase;
1906
+ const derivedUser = options.user || envUser;
1907
+ const cacheKey = options.dbid || options.url || (derivedDatabase ? `pg:${derivedUser ? `${derivedUser}@` : ""}${derivedHost}:${derivedPort}/${derivedDatabase}` : envUrl || null);
1908
+ if (cacheKey) {
1909
+ const cached = connectionCache.get(cacheKey);
1910
+ if (cached) return cached;
1911
+ const pending = pendingConnections.get(cacheKey);
1912
+ if (pending) return pending;
1913
+ }
1914
+ const connectionPromise = createDatabase(options, cacheKey);
1915
+ if (cacheKey) {
1916
+ pendingConnections.set(cacheKey, connectionPromise);
1917
+ connectionPromise.finally(() => {
1918
+ if (cacheKey) pendingConnections.delete(cacheKey);
1919
+ });
1920
+ }
1921
+ return connectionPromise;
1922
+ }
1923
+ /**
1924
+ * Internal: creates the actual database connection and caches it.
1925
+ */
1926
+ async function createDatabase(options, cacheKey) {
1927
+ const config = loadEnvConfig(options, {
1928
+ packageName: "sql",
1929
+ schema: {
1930
+ url: "string",
1931
+ database: "string",
1932
+ host: "string",
1933
+ port: "number",
1934
+ user: "string",
1935
+ password: "string"
1936
+ }
1937
+ });
1938
+ if (!config.url && !config.host && !config.database && !config.user) {
1939
+ const legacyConfig = loadEnvConfig({}, {
1940
+ prefix: "SQLOO",
1941
+ schema: {
1942
+ url: "string",
1943
+ database: "string",
1944
+ host: "string",
1945
+ port: "number",
1946
+ user: "string",
1947
+ password: "string"
1948
+ }
1949
+ });
1950
+ for (const [key, value] of Object.entries(legacyConfig)) if (config[key] === void 0 && value !== void 0) config[key] = value;
1951
+ }
1952
+ const { database, host = "localhost", user, password, port = 5432 } = config;
1953
+ const url = config.url || `postgresql://${user}${password ? `:${password}` : ""}@${host}:${port}/${database || "postgres"}`;
1954
+ const poolMax = options.max ?? 20;
1955
+ const pool = new Pool(config.url ? {
1956
+ connectionString: config.url,
1957
+ max: poolMax
1958
+ } : {
1959
+ host,
1960
+ user,
1961
+ password,
1962
+ port,
1963
+ database,
1964
+ max: poolMax
1965
+ });
1966
+ const originalEnd = pool.end.bind(pool);
1967
+ const evictFromCache = () => {
1968
+ for (const [key, cached] of connectionCache.entries()) if (cached.client === pool) connectionCache.delete(key);
1969
+ };
1970
+ pool.end = (callback) => {
1971
+ evictFromCache();
1972
+ if (callback) return originalEnd(callback);
1973
+ return originalEnd();
1974
+ };
1975
+ const client = pool;
1976
+ /**
1977
+ * Serializes a value for database storage
1978
+ * Converts objects and arrays to JSON strings
1979
+ * Converts Dates to ISO strings
1980
+ * Passes through primitives unchanged
1981
+ */
1982
+ const serializeValue = (value) => {
1983
+ if (value === null || value === void 0) return value;
1984
+ if (value instanceof Date) return value.toISOString();
1985
+ if (typeof value === "object") return JSON.stringify(value);
1986
+ return value;
1987
+ };
1988
+ /**
1989
+ * Serializes all values in an object for database storage
1990
+ */
1991
+ const serializeRecord = (record) => {
1992
+ const serialized = {};
1993
+ for (const [key, value] of Object.entries(record)) {
1994
+ if (value === void 0) continue;
1995
+ serialized[key] = serializeValue(value);
1996
+ }
1997
+ return serialized;
1998
+ };
1999
+ let serverVersionNumPromise;
2000
+ const nullsNotDistinctIndexCache = /* @__PURE__ */ new Map();
2001
+ const hasNullConflictValue = (conflictColumns, data) => conflictColumns.some((col) => data[col] === null);
2002
+ const validateUpsertConflictColumns = (table, conflictColumns, serializedData) => {
2003
+ const missingColumns = conflictColumns.filter((col) => !(col in serializedData));
2004
+ if (missingColumns.length > 0) throw new DatabaseError("Conflict columns missing from data", {
2005
+ table,
2006
+ conflictColumns,
2007
+ missingColumns,
2008
+ availableColumns: Object.keys(serializedData),
2009
+ hint: "All columns specified in ON CONFLICT must be present in the data being inserted. Undefined values should be replaced with null or an appropriate default."
2010
+ });
2011
+ };
2012
+ const getServerVersionNum = async () => {
2013
+ serverVersionNumPromise ??= client.query("SHOW server_version_num").then((result) => Number(result.rows[0]?.server_version_num) || 0);
2014
+ return serverVersionNumPromise;
2015
+ };
2016
+ const hasNativeNullsNotDistinctIndex = async (table, conflictColumns) => {
2017
+ if (await getServerVersionNum() < 15e4) return false;
2018
+ const cacheKey = `${table}:${conflictColumns.join(",")}`;
2019
+ let cached = nullsNotDistinctIndexCache.get(cacheKey);
2020
+ if (!cached) {
2021
+ cached = client.query(`
2022
+ SELECT 1
2023
+ FROM pg_index i
2024
+ WHERE i.indrelid = to_regclass($1)
2025
+ AND i.indisunique
2026
+ AND i.indnullsnotdistinct
2027
+ AND ARRAY(
2028
+ SELECT a.attname::text
2029
+ FROM unnest(i.indkey) WITH ORDINALITY AS cols(attnum, ord)
2030
+ JOIN pg_attribute a
2031
+ ON a.attrelid = i.indrelid
2032
+ AND a.attnum = cols.attnum
2033
+ ORDER BY cols.ord
2034
+ ) = $2::text[]
2035
+ LIMIT 1
2036
+ `, [table, conflictColumns]).then((result) => result.rows.length > 0).catch(() => false);
2037
+ nullsNotDistinctIndexCache.set(cacheKey, cached);
2038
+ }
2039
+ return cached;
2040
+ };
2041
+ const buildPostgresUpsertParts = (conflictColumns, serializedData) => {
2042
+ const keys = Object.keys(serializedData);
2043
+ return {
2044
+ keys,
2045
+ values: Object.values(serializedData),
2046
+ placeholders: keys.map((_, i) => `$${i + 1}`).join(", "),
2047
+ updateSet: keys.map((key, i) => `${key} = $${i + 1}`).join(", "),
2048
+ conflict: conflictColumns.join(", ")
2049
+ };
2050
+ };
2051
+ const executeStandardPostgresUpsert = async (executor, table, conflictColumns, serializedData) => {
2052
+ const { keys, values, placeholders, updateSet, conflict } = buildPostgresUpsertParts(conflictColumns, serializedData);
2053
+ const sql = `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders}) ON CONFLICT(${conflict}) DO UPDATE SET ${updateSet}`;
2054
+ return {
2055
+ operation: "upsert",
2056
+ affected: (await executor.query(sql, values)).rowCount ?? 0
2057
+ };
2058
+ };
2059
+ const executeNullAwarePostgresUpsert = async (executor, table, conflictColumns, serializedData) => {
2060
+ const { keys, values, placeholders } = buildPostgresUpsertParts(conflictColumns, serializedData);
2061
+ const lockKey = conflictColumns.map((col) => `${col}:${JSON.stringify(serializedData[col])}`).join("|");
2062
+ await executor.query("SELECT pg_advisory_xact_lock(hashtext($1), hashtext($2))", [`@happyvertical/sql.upsert:${table}`, lockKey]);
2063
+ const updateSet = keys.map((key, i) => `${key} = $${i + 1}`).join(", ");
2064
+ const whereStart = values.length + 1;
2065
+ const whereClause = conflictColumns.map((col, i) => `${col} IS NOT DISTINCT FROM $${whereStart + i}`).join(" AND ");
2066
+ const whereValues = conflictColumns.map((col) => serializedData[col]);
2067
+ const updateSql = `UPDATE ${table} SET ${updateSet} WHERE ${whereClause}`;
2068
+ const updateResult = await executor.query(updateSql, [...values, ...whereValues]);
2069
+ if ((updateResult.rowCount ?? 0) > 0) return {
2070
+ operation: "upsert",
2071
+ affected: updateResult.rowCount ?? 0
2072
+ };
2073
+ const insertSql = `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders})`;
2074
+ return {
2075
+ operation: "upsert",
2076
+ affected: (await executor.query(insertSql, values)).rowCount ?? 0
2077
+ };
2078
+ };
2079
+ const executeNullAwarePostgresUpsertInTransaction = async (table, conflictColumns, serializedData) => {
2080
+ const txClient = await client.connect();
2081
+ try {
2082
+ await txClient.query("BEGIN");
2083
+ const result = await executeNullAwarePostgresUpsert(txClient, table, conflictColumns, serializedData);
2084
+ await txClient.query("COMMIT");
2085
+ return result;
2086
+ } catch (error) {
2087
+ await txClient.query("ROLLBACK");
2088
+ throw error;
2089
+ } finally {
2090
+ txClient.release();
2091
+ }
2092
+ };
2093
+ const executePostgresUpsert = async (executor, table, conflictColumns, data, options, acquireTransaction) => {
2094
+ const serializedData = serializeRecord(data);
2095
+ validateUpsertConflictColumns(table, conflictColumns, serializedData);
2096
+ if (options?.nullsDistinct || !hasNullConflictValue(conflictColumns, serializedData)) return executeStandardPostgresUpsert(executor, table, conflictColumns, serializedData);
2097
+ if (await hasNativeNullsNotDistinctIndex(table, conflictColumns)) return executeStandardPostgresUpsert(executor, table, conflictColumns, serializedData);
2098
+ if (!acquireTransaction) return executeNullAwarePostgresUpsert(executor, table, conflictColumns, serializedData);
2099
+ return executeNullAwarePostgresUpsertInTransaction(table, conflictColumns, serializedData);
2100
+ };
2101
+ /**
2102
+ * Inserts one or more records into a table
2103
+ *
2104
+ * @param table - Table name
2105
+ * @param data - Single record or array of records to insert
2106
+ * @returns Promise resolving to operation result
2107
+ * @throws Error if the insert operation fails
2108
+ *
2109
+ * @example Single record insert:
2110
+ * ```typescript
2111
+ * await db.insert('users', {
2112
+ * name: 'John Doe',
2113
+ * email: 'john@example.com'
2114
+ * });
2115
+ * ```
2116
+ *
2117
+ * @example Multiple record insert:
2118
+ * ```typescript
2119
+ * await db.insert('users', [
2120
+ * { name: 'John', email: 'john@example.com' },
2121
+ * { name: 'Jane', email: 'jane@example.com' }
2122
+ * ]);
2123
+ * ```
2124
+ */
2125
+ const insert = async (table, data) => {
2126
+ if (Array.isArray(data)) {
2127
+ const serializedRecords = data.map((record) => serializeRecord(record));
2128
+ const keys = Object.keys(serializedRecords[0]);
2129
+ const placeholders = serializedRecords.map((_, i) => `(${keys.map((_, j) => `$${i * keys.length + j + 1}`).join(", ")})`).join(", ");
2130
+ const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES ${placeholders}`;
2131
+ const values = serializedRecords.reduce((acc, row) => acc.concat(Object.values(row)), []);
2132
+ return {
2133
+ operation: "insert",
2134
+ affected: (await client.query(query, values)).rowCount ?? 0
2135
+ };
2136
+ }
2137
+ const serializedData = serializeRecord(data);
2138
+ const keys = Object.keys(serializedData);
2139
+ const values = Object.values(serializedData);
2140
+ const placeholders = keys.map((_, i) => `$${i + 1}`).join(", ");
2141
+ const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders})`;
2142
+ return {
2143
+ operation: "insert",
2144
+ affected: (await client.query(query, values)).rowCount ?? 0
2145
+ };
2146
+ };
2147
+ /**
2148
+ * Retrieves a single record matching the where criteria
2149
+ *
2150
+ * @param table - Table name
2151
+ * @param where - Criteria to match records
2152
+ * @returns Promise resolving to query result
2153
+ */
2154
+ const get = async (table, where) => {
2155
+ const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2156
+ if (!whereClause) throw new DatabaseError("GET requires at least one WHERE condition to prevent returning an arbitrary record", { table });
2157
+ const query = `SELECT * FROM ${table} ${whereClause}`;
2158
+ try {
2159
+ return (await client.query(query, values)).rows[0] || null;
2160
+ } catch (e) {
2161
+ throw new DatabaseError("Failed to retrieve record from table", {
2162
+ table,
2163
+ sql: query,
2164
+ values,
2165
+ originalError: formatDbError(e)
2166
+ });
2167
+ }
2168
+ };
2169
+ /**
2170
+ * Retrieves multiple records matching the where criteria
2171
+ *
2172
+ * @param table - Table name
2173
+ * @param where - Criteria to match records
2174
+ * @returns Promise resolving to array of records
2175
+ */
2176
+ const list = async (table, where) => {
2177
+ const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2178
+ const query = `SELECT * FROM ${table} ${whereClause}`;
2179
+ try {
2180
+ return (await client.query(query, values)).rows;
2181
+ } catch (e) {
2182
+ throw new DatabaseError("Failed to list records from table", {
2183
+ table,
2184
+ sql: query,
2185
+ values,
2186
+ originalError: formatDbError(e)
2187
+ });
2188
+ }
2189
+ };
2190
+ /**
2191
+ * Updates records matching the where criteria
2192
+ *
2193
+ * @param table - Table name
2194
+ * @param where - Criteria to match records to update
2195
+ * @param data - New data to set
2196
+ * @returns Promise resolving to operation result
2197
+ */
2198
+ const update = async (table, where, data) => {
2199
+ const serializedData = serializeRecord(data);
2200
+ const keys = Object.keys(serializedData);
2201
+ const values = Object.values(serializedData);
2202
+ const setClause = keys.map((key, i) => `${key} = $${i + 1}`).join(", ");
2203
+ const { sql: whereClause, values: whereValues } = buildWhere(where, values.length + 1, "postgres");
2204
+ if (!whereClause) throw new DatabaseError("UPDATE requires at least one WHERE condition to prevent accidental update of all records", { table });
2205
+ const sql = `UPDATE ${table} SET ${setClause} ${whereClause}`;
2206
+ try {
2207
+ return {
2208
+ operation: "update",
2209
+ affected: (await client.query(sql, [...values, ...whereValues])).rowCount ?? 0
2210
+ };
2211
+ } catch (e) {
2212
+ throw new DatabaseError("Failed to update records in table", {
2213
+ table,
2214
+ sql,
2215
+ values: [...values, ...whereValues],
2216
+ originalError: formatDbError(e)
2217
+ });
2218
+ }
2219
+ };
2220
+ /**
2221
+ * Inserts a record or updates it if it already exists (UPSERT)
2222
+ *
2223
+ * @param table - Table name
2224
+ * @param conflictColumns - Columns that define the uniqueness constraint
2225
+ * @param data - Data to insert or update
2226
+ * @returns Promise resolving to operation result
2227
+ * @throws Error if the upsert operation fails
2228
+ */
2229
+ const upsert = async (table, conflictColumns, data, options) => {
2230
+ try {
2231
+ return await executePostgresUpsert(client, table, conflictColumns, data, options, true);
2232
+ } catch (e) {
2233
+ if (e instanceof DatabaseError && e.message === "Conflict columns missing from data") throw e;
2234
+ throw new DatabaseError("Failed to upsert record into table", {
2235
+ table,
2236
+ values: data,
2237
+ conflictColumns,
2238
+ originalError: formatDbError(e)
2239
+ });
2240
+ }
2241
+ };
2242
+ /**
2243
+ * Gets a record matching the where criteria or inserts it if not found
2244
+ *
2245
+ * @param table - Table name
2246
+ * @param where - Criteria to match existing record
2247
+ * @returns Promise resolving to the query result or insert result
2248
+ */
2249
+ const getOrInsert = async (table, where, data) => {
2250
+ const result = await get(table, where);
2251
+ if (result) return result;
2252
+ await insert(table, data);
2253
+ const inserted = await get(table, where);
2254
+ if (!inserted) throw new DatabaseError("Failed to insert and retrieve record", {
2255
+ table,
2256
+ where,
2257
+ data
2258
+ });
2259
+ return inserted;
2260
+ };
2261
+ /**
2262
+ * Deletes records from a table matching the where criteria
2263
+ *
2264
+ * @param table - Table name
2265
+ * @param where - Criteria to match records for deletion
2266
+ * @returns Promise resolving to operation result with count of deleted rows
2267
+ * @throws Error if the delete operation fails
2268
+ */
2269
+ const deleteRecords = async (table, where) => {
2270
+ validateTableName$1(table);
2271
+ if (Object.keys(where).length === 0) throw new DatabaseError("DELETE requires at least one WHERE condition to prevent accidental deletion of all records", { table });
2272
+ const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2273
+ try {
2274
+ return {
2275
+ operation: "delete",
2276
+ affected: (await client.query(`DELETE FROM ${table} ${whereClause}`, values)).rowCount ?? 0
2277
+ };
2278
+ } catch (e) {
2279
+ throw new DatabaseError("Failed to delete records from table", {
2280
+ table,
2281
+ where,
2282
+ originalError: formatDbError(e)
2283
+ });
2284
+ }
2285
+ };
2286
+ /**
2287
+ * Counts records in a table matching the where criteria
2288
+ *
2289
+ * @param table - Table name
2290
+ * @param where - Criteria to match records (optional, counts all if omitted)
2291
+ * @returns Promise resolving to count of matching records
2292
+ * @throws Error if the count operation fails
2293
+ */
2294
+ const count = async (table, where) => {
2295
+ validateTableName$1(table);
2296
+ try {
2297
+ if (!where || Object.keys(where).length === 0) {
2298
+ const result = await client.query(`SELECT COUNT(*) as count FROM ${table}`);
2299
+ return Number(result.rows[0]?.count) || 0;
2300
+ }
2301
+ const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2302
+ const result = await client.query(`SELECT COUNT(*) as count FROM ${table} ${whereClause}`, values);
2303
+ return Number(result.rows[0]?.count) || 0;
2304
+ } catch (e) {
2305
+ throw new DatabaseError("Failed to count records in table", {
2306
+ table,
2307
+ where,
2308
+ originalError: formatDbError(e)
2309
+ });
2310
+ }
2311
+ };
2312
+ /**
2313
+ * Creates a table-specific interface for simplified table operations
2314
+ *
2315
+ * @param tableName - Table name
2316
+ * @returns TableMethods interface for the specified table
2317
+ */
2318
+ const table = (tableName) => {
2319
+ return {
2320
+ insert: (data) => insert(tableName, data),
2321
+ get: (data) => get(tableName, data),
2322
+ list: (data) => list(tableName, data)
2323
+ };
2324
+ };
2325
+ /**
2326
+ * Parses a tagged template literal into a SQL query and values
2327
+ *
2328
+ * @param strings - Template strings
2329
+ * @param vars - Variables to interpolate into the query
2330
+ * @returns Object with SQL query and values array
2331
+ */
2332
+ const parseTemplate = (strings, ...vars) => {
2333
+ let sql = strings[0];
2334
+ const values = [];
2335
+ for (let i = 0; i < vars.length; i++) {
2336
+ values.push(vars[i]);
2337
+ sql += `$${i + 1}${strings[i + 1]}`;
2338
+ }
2339
+ return {
2340
+ sql,
2341
+ values
2342
+ };
2343
+ };
2344
+ /**
2345
+ * Executes a SQL query using template literals and returns a single value
2346
+ *
2347
+ * @param strings - Template strings
2348
+ * @param vars - Variables to interpolate into the query
2349
+ * @returns Promise resolving to a single value (first column of first row)
2350
+ */
2351
+ const pluck = async (strings, ...vars) => {
2352
+ const { sql, values } = parseTemplate(strings, ...vars);
2353
+ try {
2354
+ const firstRow = (await client.query(sql, values)).rows[0];
2355
+ if (!firstRow) return null;
2356
+ return Object.values(firstRow)[0];
2357
+ } catch (e) {
2358
+ throw new DatabaseError("Failed to execute pluck query", {
2359
+ sql,
2360
+ values,
2361
+ originalError: formatDbError(e)
2362
+ });
2363
+ }
2364
+ };
2365
+ /**
2366
+ * Executes a SQL query using template literals and returns a single row
2367
+ *
2368
+ * @param strings - Template strings
2369
+ * @param vars - Variables to interpolate into the query
2370
+ * @returns Promise resolving to a single result record or null
2371
+ */
2372
+ const single = async (strings, ...vars) => {
2373
+ const { sql, values } = parseTemplate(strings, ...vars);
2374
+ try {
2375
+ return (await client.query(sql, values)).rows[0] || null;
2376
+ } catch (e) {
2377
+ throw new DatabaseError("Failed to execute single query", {
2378
+ sql,
2379
+ values,
2380
+ originalError: formatDbError(e)
2381
+ });
2382
+ }
2383
+ };
2384
+ /**
2385
+ * Executes a SQL query using template literals and returns multiple rows
2386
+ *
2387
+ * @param strings - Template strings
2388
+ * @param vars - Variables to interpolate into the query
2389
+ * @returns Promise resolving to array of result records
2390
+ */
2391
+ const many = async (strings, ...vars) => {
2392
+ const { sql, values } = parseTemplate(strings, ...vars);
2393
+ try {
2394
+ return (await client.query(sql, values)).rows;
2395
+ } catch (e) {
2396
+ throw new DatabaseError("Failed to execute many query", {
2397
+ sql,
2398
+ values,
2399
+ originalError: formatDbError(e)
2400
+ });
2401
+ }
2402
+ };
2403
+ /**
2404
+ * Executes a SQL query using template literals without returning results
2405
+ *
2406
+ * @param strings - Template strings
2407
+ * @param vars - Variables to interpolate into the query
2408
+ * @returns Promise that resolves when the query completes
2409
+ */
2410
+ const execute = async (strings, ...vars) => {
2411
+ const { sql, values } = parseTemplate(strings, ...vars);
2412
+ try {
2413
+ await client.query(sql, values);
2414
+ } catch (e) {
2415
+ throw new DatabaseError("Failed to execute query", {
2416
+ sql,
2417
+ values,
2418
+ originalError: formatDbError(e)
2419
+ });
2420
+ }
2421
+ };
2422
+ /**
2423
+ * Executes a raw SQL query with parameterized values
2424
+ *
2425
+ * Uses PostgreSQL-native placeholders ($1, $2, ...). Legacy ? placeholders
2426
+ * are converted only when the placeholder count matches the supplied values,
2427
+ * so Postgres operators such as JSONB ? remain intact.
2428
+ *
2429
+ * @param sql - SQL query string
2430
+ * @param values - Variables to use as parameters
2431
+ * @returns Promise resolving to query result with rows and count
2432
+ */
2433
+ const query = async (sql, ...values) => {
2434
+ const query = normalizePostgresRawQuery(sql, values);
2435
+ try {
2436
+ const result = await client.query(query.sql, query.values);
2437
+ return {
2438
+ rows: result.rows,
2439
+ rowCount: result.rowCount ?? 0
2440
+ };
2441
+ } catch (e) {
2442
+ throw new DatabaseError("Failed to execute raw query", {
2443
+ sql: query.sql,
2444
+ values: query.values,
2445
+ originalError: formatDbError(e)
2446
+ });
2447
+ }
2448
+ };
2449
+ /**
2450
+ * Acquire a pinned connection from the pool for session-scoped state such as
2451
+ * `pg_advisory_lock`. Every query on the returned handle runs on the same
2452
+ * client until {@link SessionHandle.release} drops the connection (which
2453
+ * frees any session locks it held). The connection is held for the handle's
2454
+ * lifetime, so hold one session per process, not per operation.
2455
+ */
2456
+ const acquireSession = async () => {
2457
+ const sessionClient = await pool.connect();
2458
+ let released = false;
2459
+ let lost = false;
2460
+ sessionClient.on("error", () => {
2461
+ lost = true;
2462
+ });
2463
+ return {
2464
+ query: async (sql, ...values) => {
2465
+ if (released) throw new DatabaseError("Session has been released", {});
2466
+ if (lost) throw new DatabaseError("Session connection was lost", {});
2467
+ const normalized = normalizePostgresRawQuery(sql, values);
2468
+ try {
2469
+ const result = await sessionClient.query(normalized.sql, normalized.values);
2470
+ return {
2471
+ rows: result.rows,
2472
+ rowCount: result.rowCount ?? 0
2473
+ };
2474
+ } catch (e) {
2475
+ throw new DatabaseError("Failed to execute session query", {
2476
+ sql: normalized.sql,
2477
+ values: normalized.values,
2478
+ originalError: formatDbError(e)
2479
+ });
2480
+ }
2481
+ },
2482
+ isActive: () => !released && !lost,
2483
+ release: async () => {
2484
+ if (released) return;
2485
+ released = true;
2486
+ if (!lost) {
2487
+ try {
2488
+ await sessionClient.query("ROLLBACK");
2489
+ } catch {}
2490
+ try {
2491
+ await sessionClient.query("SELECT pg_advisory_unlock_all()");
2492
+ } catch {}
2493
+ }
2494
+ sessionClient.release(true);
2495
+ }
2496
+ };
2497
+ };
2498
+ /**
2499
+ * Checks if a table exists in the database
2500
+ *
2501
+ * @param tableName - Name of the table to check
2502
+ * @returns Promise resolving to boolean indicating if the table exists
2503
+ */
2504
+ const tableExists = async (tableName) => {
2505
+ return (await client.query(`SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = $1 AND table_schema = 'public')`, [tableName])).rows[0].exists;
2506
+ };
2507
+ /**
2508
+ * Synchronizes database schema with provided SQL DDL
2509
+ * Creates tables if they don't exist and adds missing columns
2510
+ * Also executes CREATE INDEX statements
2511
+ *
2512
+ * @param schema - SQL schema definition with CREATE TABLE and CREATE INDEX statements
2513
+ * @returns Promise that resolves when schema is synchronized
2514
+ */
2515
+ const syncSchema = async (schema) => {
2516
+ const commands = schema.trim().split(";").filter((command) => command.trim() !== "");
2517
+ const createIndexRegex = /CREATE (UNIQUE )?INDEX (IF NOT EXISTS )?"?(\w+)"? ON "?(\w+)"?\s*\(([^)]+)\)/i;
2518
+ const indexNames = [];
2519
+ for (const command of commands) {
2520
+ const indexMatch = command.trim().match(createIndexRegex);
2521
+ if (indexMatch) indexNames.push(indexMatch[3]);
2522
+ }
2523
+ const existingIndexes = /* @__PURE__ */ new Set();
2524
+ if (indexNames.length > 0) {
2525
+ const result = await client.query(`SELECT indexname FROM pg_indexes
2526
+ WHERE schemaname = 'public' AND indexname = ANY($1::text[])`, [indexNames]);
2527
+ for (const row of result.rows) existingIndexes.add(row.indexname);
2528
+ }
2529
+ for (const command of commands) {
2530
+ const trimmedCommand = command.trim();
2531
+ const tableMatch = trimmedCommand.match(/CREATE TABLE (IF NOT EXISTS )?"?(\w+)"? \(([\s\S]+)\)/i);
2532
+ if (tableMatch) {
2533
+ const tableName = tableMatch[2];
2534
+ const columns = tableMatch[3].trim().split(",\n");
2535
+ if (!await tableExists(tableName)) await client.query(trimmedCommand);
2536
+ else for (const column of columns) {
2537
+ const columnDef = column.trim();
2538
+ const columnMatch = columnDef.match(/"?(\w+)"?\s+(\w+[^,]*)/);
2539
+ if (columnMatch) {
2540
+ const columnName = columnMatch[1];
2541
+ if (columnName.toUpperCase() === "PRIMARY" || columnName.toUpperCase() === "FOREIGN" || columnName.toUpperCase() === "UNIQUE" || columnName.toUpperCase() === "CHECK" || columnName.toUpperCase() === "CONSTRAINT") continue;
2542
+ try {
2543
+ if (!(await client.query(`SELECT EXISTS (
2544
+ SELECT 1 FROM information_schema.columns
2545
+ WHERE table_name = $1
2546
+ AND column_name = $2
2547
+ AND table_schema = 'public'
2548
+ )`, [tableName, columnName])).rows[0].exists) {
2549
+ const alterCommand = `ALTER TABLE "${tableName}" ADD COLUMN ${columnDef}`;
2550
+ await client.query(alterCommand);
2551
+ }
2552
+ } catch (error) {
2553
+ console.error(`Error adding column ${columnName} to ${tableName}:`, error);
2554
+ }
2555
+ }
2556
+ }
2557
+ continue;
2558
+ }
2559
+ const indexMatch = trimmedCommand.match(createIndexRegex);
2560
+ if (indexMatch) {
2561
+ const indexName = indexMatch[3];
2562
+ const indexTableName = indexMatch[4];
2563
+ if (!existingIndexes.has(indexName)) try {
2564
+ await client.query(trimmedCommand);
2565
+ existingIndexes.add(indexName);
2566
+ } catch (error) {
2567
+ console.warn(`Warning: Failed to create index ${indexName} on ${indexTableName}:`, error instanceof Error ? error.message : String(error));
2568
+ }
2569
+ continue;
2570
+ }
2571
+ if (trimmedCommand.toUpperCase().startsWith("CREATE ") && !trimmedCommand.toUpperCase().includes("CREATE TABLE")) try {
2572
+ await client.query(trimmedCommand);
2573
+ } catch (error) {
2574
+ console.warn(`Warning: DDL statement may have failed:`, error instanceof Error ? error.message : String(error));
2575
+ }
2576
+ }
2577
+ };
2578
+ /**
2579
+ * Executes a callback within a database transaction
2580
+ * Automatically commits on success or rolls back on error
2581
+ *
2582
+ * @param callback - Function to execute within transaction
2583
+ * @returns Promise resolving to callback result
2584
+ */
2585
+ const transaction = async (callback) => {
2586
+ const txClient = await client.connect();
2587
+ try {
2588
+ await txClient.query("BEGIN");
2589
+ const txDb = {
2590
+ url,
2591
+ client: txClient,
2592
+ insert: async (table, data) => {
2593
+ if (Array.isArray(data)) {
2594
+ const keys = Object.keys(data[0]);
2595
+ const placeholders = data.map((_, i) => `(${keys.map((_, j) => `$${i * keys.length + j + 1}`).join(", ")})`).join(", ");
2596
+ const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES ${placeholders}`;
2597
+ const values = data.reduce((acc, row) => acc.concat(Object.values(row)), []);
2598
+ return {
2599
+ operation: "insert",
2600
+ affected: (await txClient.query(query, values)).rowCount ?? 0
2601
+ };
2602
+ }
2603
+ const keys = Object.keys(data);
2604
+ const values = Object.values(data);
2605
+ const placeholders = keys.map((_, i) => `$${i + 1}`).join(", ");
2606
+ const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders})`;
2607
+ return {
2608
+ operation: "insert",
2609
+ affected: (await txClient.query(query, values)).rowCount ?? 0
2610
+ };
2611
+ },
2612
+ get: async (table, where) => {
2613
+ const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2614
+ if (!whereClause) throw new DatabaseError("GET requires at least one WHERE condition to prevent returning an arbitrary record", { table });
2615
+ const query = `SELECT * FROM ${table} ${whereClause}`;
2616
+ return (await txClient.query(query, values)).rows[0] || null;
2617
+ },
2618
+ list: async (table, where) => {
2619
+ const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2620
+ const query = `SELECT * FROM ${table} ${whereClause}`;
2621
+ return (await txClient.query(query, values)).rows;
2622
+ },
2623
+ update: async (table, where, data) => {
2624
+ const keys = Object.keys(data);
2625
+ const values = Object.values(data);
2626
+ const setClause = keys.map((key, i) => `${key} = $${i + 1}`).join(", ");
2627
+ const { sql: whereClause, values: whereValues } = buildWhere(where, values.length + 1, "postgres");
2628
+ if (!whereClause) throw new DatabaseError("UPDATE requires at least one WHERE condition to prevent accidental update of all records", { table });
2629
+ const sql = `UPDATE ${table} SET ${setClause} ${whereClause}`;
2630
+ return {
2631
+ operation: "update",
2632
+ affected: (await txClient.query(sql, [...values, ...whereValues])).rowCount ?? 0
2633
+ };
2634
+ },
2635
+ upsert: async (table, conflictColumns, data, options) => executePostgresUpsert(txClient, table, conflictColumns, data, options, false),
2636
+ getOrInsert: async (table, where, data) => {
2637
+ const result = await txDb.get(table, where);
2638
+ if (result) return result;
2639
+ await txDb.insert(table, data);
2640
+ const inserted = await txDb.get(table, where);
2641
+ if (!inserted) throw new DatabaseError("Failed to insert and retrieve record", {
2642
+ table,
2643
+ where,
2644
+ data
2645
+ });
2646
+ return inserted;
2647
+ },
2648
+ delete: async (table, where) => {
2649
+ validateTableName$1(table);
2650
+ if (Object.keys(where).length === 0) throw new DatabaseError("DELETE requires at least one WHERE condition to prevent accidental deletion of all records", { table });
2651
+ const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2652
+ return {
2653
+ operation: "delete",
2654
+ affected: (await txClient.query(`DELETE FROM ${table} ${whereClause}`, values)).rowCount ?? 0
2655
+ };
2656
+ },
2657
+ count: async (table, where) => {
2658
+ validateTableName$1(table);
2659
+ if (!where || Object.keys(where).length === 0) {
2660
+ const result = await txClient.query(`SELECT COUNT(*) as count FROM ${table}`);
2661
+ return Number(result.rows[0]?.count) || 0;
2662
+ }
2663
+ const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2664
+ const result = await txClient.query(`SELECT COUNT(*) as count FROM ${table} ${whereClause}`, values);
2665
+ return Number(result.rows[0]?.count) || 0;
2666
+ },
2667
+ table: (tableName) => ({
2668
+ insert: (data) => txDb.insert(tableName, data),
2669
+ get: (data) => txDb.get(tableName, data),
2670
+ list: (data) => txDb.list(tableName, data)
2671
+ }),
2672
+ many: async (strings, ...vars) => {
2673
+ const { sql, values } = parseTemplate(strings, ...vars);
2674
+ return (await txClient.query(sql, values)).rows;
2675
+ },
2676
+ single: async (strings, ...vars) => {
2677
+ const { sql, values } = parseTemplate(strings, ...vars);
2678
+ return (await txClient.query(sql, values)).rows[0] || null;
2679
+ },
2680
+ pluck: async (strings, ...vars) => {
2681
+ const { sql, values } = parseTemplate(strings, ...vars);
2682
+ const firstRow = (await txClient.query(sql, values)).rows[0];
2683
+ if (!firstRow) return null;
2684
+ return Object.values(firstRow)[0];
2685
+ },
2686
+ execute: async (strings, ...vars) => {
2687
+ const { sql, values } = parseTemplate(strings, ...vars);
2688
+ await txClient.query(sql, values);
2689
+ },
2690
+ query: async (sql, ...values) => {
2691
+ const query = normalizePostgresRawQuery(sql, values);
2692
+ const result = await txClient.query(query.sql, query.values);
2693
+ return {
2694
+ rows: result.rows,
2695
+ rowCount: result.rowCount ?? 0
2696
+ };
2697
+ },
2698
+ oo: async (strings, ...vars) => {
2699
+ const { sql, values } = parseTemplate(strings, ...vars);
2700
+ return (await txClient.query(sql, values)).rows;
2701
+ },
2702
+ oO: async (strings, ...vars) => {
2703
+ const { sql, values } = parseTemplate(strings, ...vars);
2704
+ return (await txClient.query(sql, values)).rows[0] || null;
2705
+ },
2706
+ ox: async (strings, ...vars) => {
2707
+ const { sql, values } = parseTemplate(strings, ...vars);
2708
+ const firstRow = (await txClient.query(sql, values)).rows[0];
2709
+ if (!firstRow) return null;
2710
+ return Object.values(firstRow)[0];
2711
+ },
2712
+ xx: async (strings, ...vars) => {
2713
+ const { sql, values } = parseTemplate(strings, ...vars);
2714
+ await txClient.query(sql, values);
2715
+ },
2716
+ tableExists,
2717
+ syncSchema,
2718
+ transaction
2719
+ };
2720
+ const result = await callback(txDb);
2721
+ await txClient.query("COMMIT");
2722
+ return result;
2723
+ } catch (error) {
2724
+ await txClient.query("ROLLBACK");
2725
+ throw error;
2726
+ } finally {
2727
+ txClient.release();
2728
+ }
2729
+ };
2730
+ /**
2731
+ * Begins a new transaction and returns a handle for manual control
2732
+ *
2733
+ * Unlike transaction(), this gives you explicit control over commit/rollback.
2734
+ * Ideal for test isolation where you want to rollback after each test.
2735
+ *
2736
+ * @returns Promise resolving to a TransactionHandle
2737
+ */
2738
+ const beginTransaction = async () => {
2739
+ const txClient = await client.connect();
2740
+ await txClient.query("BEGIN");
2741
+ let active = true;
2742
+ const commit = async () => {
2743
+ if (!active) throw new DatabaseError("Transaction already ended", {});
2744
+ await txClient.query("COMMIT");
2745
+ active = false;
2746
+ txClient.release();
2747
+ };
2748
+ const rollback = async () => {
2749
+ if (!active) throw new DatabaseError("Transaction already ended", {});
2750
+ await txClient.query("ROLLBACK");
2751
+ active = false;
2752
+ txClient.release();
2753
+ };
2754
+ const isActive = () => active;
2755
+ const txHandle = {
2756
+ url,
2757
+ client: txClient,
2758
+ insert: async (table, data) => {
2759
+ if (Array.isArray(data)) {
2760
+ const keys = Object.keys(data[0]);
2761
+ const placeholders = data.map((_, i) => `(${keys.map((_, j) => `$${i * keys.length + j + 1}`).join(", ")})`).join(", ");
2762
+ const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES ${placeholders}`;
2763
+ const values = data.reduce((acc, row) => acc.concat(Object.values(row)), []);
2764
+ return {
2765
+ operation: "insert",
2766
+ affected: (await txClient.query(query, values)).rowCount ?? 0
2767
+ };
2768
+ }
2769
+ const keys = Object.keys(data);
2770
+ const values = Object.values(data);
2771
+ const placeholders = keys.map((_, i) => `$${i + 1}`).join(", ");
2772
+ const query = `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders})`;
2773
+ return {
2774
+ operation: "insert",
2775
+ affected: (await txClient.query(query, values)).rowCount ?? 0
2776
+ };
2777
+ },
2778
+ get: async (table, where) => {
2779
+ const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2780
+ if (!whereClause) throw new DatabaseError("GET requires at least one WHERE condition to prevent returning an arbitrary record", { table });
2781
+ const query = `SELECT * FROM ${table} ${whereClause}`;
2782
+ return (await txClient.query(query, values)).rows[0] || null;
2783
+ },
2784
+ list: async (table, where) => {
2785
+ const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2786
+ const query = `SELECT * FROM ${table} ${whereClause}`;
2787
+ return (await txClient.query(query, values)).rows;
2788
+ },
2789
+ update: async (table, where, data) => {
2790
+ const keys = Object.keys(data);
2791
+ const values = Object.values(data);
2792
+ const setClause = keys.map((key, i) => `${key} = $${i + 1}`).join(", ");
2793
+ const { sql: whereClause, values: whereValues } = buildWhere(where, values.length + 1, "postgres");
2794
+ if (!whereClause) throw new DatabaseError("UPDATE requires at least one WHERE condition to prevent accidental update of all records", { table });
2795
+ const sql = `UPDATE ${table} SET ${setClause} ${whereClause}`;
2796
+ return {
2797
+ operation: "update",
2798
+ affected: (await txClient.query(sql, [...values, ...whereValues])).rowCount ?? 0
2799
+ };
2800
+ },
2801
+ upsert: async (table, conflictColumns, data, options) => executePostgresUpsert(txClient, table, conflictColumns, data, options, false),
2802
+ getOrInsert: async (table, where, data) => {
2803
+ const result = await txHandle.get(table, where);
2804
+ if (result) return result;
2805
+ await txHandle.insert(table, data);
2806
+ const inserted = await txHandle.get(table, where);
2807
+ if (!inserted) throw new DatabaseError("Failed to insert and retrieve record", {
2808
+ table,
2809
+ where,
2810
+ data
2811
+ });
2812
+ return inserted;
2813
+ },
2814
+ delete: async (table, where) => {
2815
+ validateTableName$1(table);
2816
+ if (Object.keys(where).length === 0) throw new DatabaseError("DELETE requires at least one WHERE condition to prevent accidental deletion of all records", { table });
2817
+ const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2818
+ return {
2819
+ operation: "delete",
2820
+ affected: (await txClient.query(`DELETE FROM ${table} ${whereClause}`, values)).rowCount ?? 0
2821
+ };
2822
+ },
2823
+ count: async (table, where) => {
2824
+ validateTableName$1(table);
2825
+ if (!where || Object.keys(where).length === 0) {
2826
+ const result = await txClient.query(`SELECT COUNT(*) as count FROM ${table}`);
2827
+ return Number(result.rows[0]?.count) || 0;
2828
+ }
2829
+ const { sql: whereClause, values } = buildWhere(where, 1, "postgres");
2830
+ const result = await txClient.query(`SELECT COUNT(*) as count FROM ${table} ${whereClause}`, values);
2831
+ return Number(result.rows[0]?.count) || 0;
2832
+ },
2833
+ table: (tableName) => ({
2834
+ insert: (data) => txHandle.insert(tableName, data),
2835
+ get: (data) => txHandle.get(tableName, data),
2836
+ list: (data) => txHandle.list(tableName, data)
2837
+ }),
2838
+ many: async (strings, ...vars) => {
2839
+ const { sql, values } = parseTemplate(strings, ...vars);
2840
+ return (await txClient.query(sql, values)).rows;
2841
+ },
2842
+ single: async (strings, ...vars) => {
2843
+ const { sql, values } = parseTemplate(strings, ...vars);
2844
+ return (await txClient.query(sql, values)).rows[0] || null;
2845
+ },
2846
+ pluck: async (strings, ...vars) => {
2847
+ const { sql, values } = parseTemplate(strings, ...vars);
2848
+ const firstRow = (await txClient.query(sql, values)).rows[0];
2849
+ if (!firstRow) return null;
2850
+ return Object.values(firstRow)[0];
2851
+ },
2852
+ execute: async (strings, ...vars) => {
2853
+ const { sql, values } = parseTemplate(strings, ...vars);
2854
+ await txClient.query(sql, values);
2855
+ },
2856
+ query: async (sql, ...values) => {
2857
+ const query = normalizePostgresRawQuery(sql, values);
2858
+ const result = await txClient.query(query.sql, query.values);
2859
+ return {
2860
+ rows: result.rows,
2861
+ rowCount: result.rowCount ?? 0
2862
+ };
2863
+ },
2864
+ oo: async (strings, ...vars) => {
2865
+ const { sql, values } = parseTemplate(strings, ...vars);
2866
+ return (await txClient.query(sql, values)).rows;
2867
+ },
2868
+ oO: async (strings, ...vars) => {
2869
+ const { sql, values } = parseTemplate(strings, ...vars);
2870
+ return (await txClient.query(sql, values)).rows[0] || null;
2871
+ },
2872
+ ox: async (strings, ...vars) => {
2873
+ const { sql, values } = parseTemplate(strings, ...vars);
2874
+ const firstRow = (await txClient.query(sql, values)).rows[0];
2875
+ if (!firstRow) return null;
2876
+ return Object.values(firstRow)[0];
2877
+ },
2878
+ xx: async (strings, ...vars) => {
2879
+ const { sql, values } = parseTemplate(strings, ...vars);
2880
+ await txClient.query(sql, values);
2881
+ },
2882
+ tableExists,
2883
+ syncSchema,
2884
+ transaction,
2885
+ commit,
2886
+ rollback,
2887
+ isActive
2888
+ };
2889
+ return txHandle;
2890
+ };
2891
+ const oo = many;
2892
+ const oO = single;
2893
+ const ox = pluck;
2894
+ const xx = execute;
2895
+ /**
2896
+ * Initialize database schemas from JSON manifest
2897
+ * Supports dependency resolution and schema overrides
2898
+ *
2899
+ * @param options - Schema initialization options
2900
+ * @returns Promise that resolves when schemas are initialized
2901
+ */
2902
+ const initializeSchemas = async (options) => {
2903
+ const schemaManager = new DatabaseSchemaManager();
2904
+ const currentDb = {
2905
+ url,
2906
+ client,
2907
+ insert,
2908
+ update,
2909
+ upsert,
2910
+ get,
2911
+ getOrInsert,
2912
+ delete: deleteRecords,
2913
+ count,
2914
+ list,
2915
+ table,
2916
+ many,
2917
+ single,
2918
+ pluck,
2919
+ execute,
2920
+ query,
2921
+ oo,
2922
+ oO,
2923
+ ox,
2924
+ xx,
2925
+ tableExists,
2926
+ syncSchema,
2927
+ transaction
2928
+ };
2929
+ await schemaManager.initializeSchemas(currentDb, options);
2930
+ };
2931
+ /**
2932
+ * Retrieves the schema information for a table
2933
+ *
2934
+ * @param table - Table name
2935
+ * @returns Promise resolving to table schema info or null if table doesn't exist
2936
+ * @throws Error if the query fails
2937
+ */
2938
+ const getTableSchema = async (table) => {
2939
+ validateTableName$1(table);
2940
+ try {
2941
+ if (!await tableExists(table)) return null;
2942
+ const columnRows = await many`
2943
+ SELECT
2944
+ column_name,
2945
+ data_type,
2946
+ is_nullable,
2947
+ column_default
2948
+ FROM information_schema.columns
2949
+ WHERE table_name = ${table}
2950
+ AND table_schema = 'public'
2951
+ ORDER BY ordinal_position
2952
+ `;
2953
+ const pkRows = await many`
2954
+ SELECT kcu.column_name
2955
+ FROM information_schema.table_constraints tc
2956
+ JOIN information_schema.key_column_usage kcu
2957
+ ON tc.constraint_name = kcu.constraint_name
2958
+ AND tc.table_schema = kcu.table_schema
2959
+ WHERE tc.table_name = ${table}
2960
+ AND tc.table_schema = 'public'
2961
+ AND tc.constraint_type = 'PRIMARY KEY'
2962
+ `;
2963
+ const pkColumns = new Set(pkRows.map((row) => row.column_name));
2964
+ const columns = {};
2965
+ for (const row of columnRows) {
2966
+ const colName = row.column_name;
2967
+ columns[colName] = {
2968
+ type: row.data_type,
2969
+ primaryKey: pkColumns.has(colName),
2970
+ notNull: row.is_nullable === "NO",
2971
+ defaultValue: row.column_default
2972
+ };
2973
+ }
2974
+ const indexRows = await many`
2975
+ SELECT
2976
+ indexname,
2977
+ indexdef
2978
+ FROM pg_indexes
2979
+ WHERE tablename = ${table}
2980
+ AND schemaname = 'public'
2981
+ AND indexname NOT LIKE '%_pkey'
2982
+ `;
2983
+ const indexes = [];
2984
+ for (const row of indexRows) {
2985
+ const indexName = row.indexname;
2986
+ const indexDef = row.indexdef;
2987
+ const colMatch = indexDef.match(/\(([^)]+)\)/);
2988
+ const indexColumns = colMatch ? colMatch[1].split(",").map((col) => col.trim()) : [];
2989
+ const isUnique = indexDef.toUpperCase().includes("UNIQUE");
2990
+ indexes.push({
2991
+ name: indexName,
2992
+ columns: indexColumns,
2993
+ unique: isUnique
2994
+ });
2995
+ }
2996
+ const fkRows = await many`
2997
+ SELECT
2998
+ kcu.column_name,
2999
+ ccu.table_name AS foreign_table_name,
3000
+ ccu.column_name AS foreign_column_name,
3001
+ rc.delete_rule,
3002
+ rc.update_rule
3003
+ FROM information_schema.table_constraints AS tc
3004
+ JOIN information_schema.key_column_usage AS kcu
3005
+ ON tc.constraint_name = kcu.constraint_name
3006
+ AND tc.table_schema = kcu.table_schema
3007
+ JOIN information_schema.constraint_column_usage AS ccu
3008
+ ON ccu.constraint_name = tc.constraint_name
3009
+ AND ccu.table_schema = tc.table_schema
3010
+ JOIN information_schema.referential_constraints AS rc
3011
+ ON rc.constraint_name = tc.constraint_name
3012
+ AND rc.constraint_schema = tc.table_schema
3013
+ WHERE tc.constraint_type = 'FOREIGN KEY'
3014
+ AND tc.table_name = ${table}
3015
+ AND tc.table_schema = 'public'
3016
+ `;
3017
+ const foreignKeys = [];
3018
+ for (const fkRow of fkRows) foreignKeys.push({
3019
+ column: fkRow.column_name,
3020
+ referencesTable: fkRow.foreign_table_name,
3021
+ referencesColumn: fkRow.foreign_column_name,
3022
+ onDelete: fkRow.delete_rule,
3023
+ onUpdate: fkRow.update_rule
3024
+ });
3025
+ return {
3026
+ tableName: table,
3027
+ columns,
3028
+ indexes,
3029
+ foreignKeys
3030
+ };
3031
+ } catch (e) {
3032
+ throw new DatabaseError("Failed to retrieve table schema", {
3033
+ table,
3034
+ originalError: formatDbError(e)
3035
+ });
3036
+ }
3037
+ };
3038
+ const db = {
3039
+ url,
3040
+ client,
3041
+ insert,
3042
+ update,
3043
+ upsert,
3044
+ get,
3045
+ getOrInsert,
3046
+ delete: deleteRecords,
3047
+ count,
3048
+ list,
3049
+ table,
3050
+ many,
3051
+ single,
3052
+ pluck,
3053
+ execute,
3054
+ query,
3055
+ oo,
3056
+ oO,
3057
+ ox,
3058
+ xx,
3059
+ tableExists,
3060
+ syncSchema,
3061
+ initializeSchemas,
3062
+ transaction,
3063
+ beginTransaction,
3064
+ acquireSession,
3065
+ getTableSchema,
3066
+ alterTable: {
3067
+ /**
3068
+ * Adds a new column to an existing table
3069
+ *
3070
+ * @param table - Table name
3071
+ * @param column - Column definition with name
3072
+ * @returns Promise that resolves when column is added
3073
+ * @throws Error if the alter operation fails
3074
+ */
3075
+ addColumn: async (table, column) => {
3076
+ validateTableName$1(table);
3077
+ validateColumnName$1(column.name);
3078
+ try {
3079
+ const sql = generateAddColumnStatement(table, column, "postgres");
3080
+ await client.query(sql);
3081
+ } catch (e) {
3082
+ throw new DatabaseError("Failed to add column to table", {
3083
+ table,
3084
+ column: column.name,
3085
+ originalError: formatDbError(e)
3086
+ });
3087
+ }
3088
+ },
3089
+ /**
3090
+ * Adds a new index to an existing table
3091
+ *
3092
+ * @param table - Table name
3093
+ * @param index - Index definition
3094
+ * @returns Promise that resolves when index is created
3095
+ * @throws Error if the create index operation fails
3096
+ */
3097
+ addIndex: async (table, index) => {
3098
+ validateTableName$1(table);
3099
+ validateIndexName(index.name);
3100
+ for (const col of index.columns) validateColumnName$1(col);
3101
+ try {
3102
+ const sql = generateCreateIndexStatement(table, index);
3103
+ await client.query(sql);
3104
+ } catch (e) {
3105
+ throw new DatabaseError("Failed to create index on table", {
3106
+ table,
3107
+ index: index.name,
3108
+ originalError: formatDbError(e)
3109
+ });
3110
+ }
3111
+ }
3112
+ },
3113
+ vector: createVectorCapabilities(pool)
3114
+ };
3115
+ const finalCacheKey = cacheKey || url;
3116
+ connectionCache.set(finalCacheKey, db);
3117
+ return db;
3118
+ }
3119
+ //#endregion
3120
+ //#region src/index.ts
3121
+ /**
3122
+ * Checks if the provided value is a database instance rather than configuration options
3123
+ *
3124
+ * @param value - Value to check
3125
+ * @returns True if the value appears to be a DatabaseInterface instance
3126
+ */
3127
+ function isDatabaseInstance(value) {
3128
+ return value && typeof value === "object" && typeof value.client !== "undefined" && typeof value.insert === "function" && typeof value.get === "function" && typeof value.query === "function";
3129
+ }
3130
+ /**
3131
+ * Creates a database connection based on the provided options, or returns an existing database instance
3132
+ *
3133
+ * Loads configuration from environment variables using the HAVE_SQL_* pattern:
3134
+ * - HAVE_SQL_TYPE → type ('sqlite' | 'postgres' | 'duckdb' | 'json')
3135
+ * - HAVE_SQL_URL → url (connection string)
3136
+ * - HAVE_SQL_HOST → host (database server hostname)
3137
+ * - HAVE_SQL_PORT → port (database server port number)
3138
+ * - HAVE_SQL_DATABASE → database (database name)
3139
+ * - HAVE_SQL_USER → user (authentication username)
3140
+ * - HAVE_SQL_PASSWORD → password (authentication password)
3141
+ *
3142
+ * User-provided options always take precedence over environment variables.
3143
+ *
3144
+ * ## Lazy schemas
3145
+ *
3146
+ * The `schemas` option accepts either an eagerly-built record or a lazy
3147
+ * function (`() => Record<string, SchemaProvider>`). Adapters that manage
3148
+ * tables via migrations (Postgres, SQLite) never resolve the function, so
3149
+ * there is zero cost for callers that pass schemas uniformly regardless of
3150
+ * adapter type. Only JSON and DuckDB adapters call the function.
3151
+ *
3152
+ * ```typescript
3153
+ * // Caller doesn't need to know the adapter type:
3154
+ * await getDatabase({
3155
+ * ...config,
3156
+ * schemas: () => ObjectRegistry.getAllSchemas(),
3157
+ * });
3158
+ * ```
3159
+ *
3160
+ * @param options - Configuration options for the database connection or an existing database instance
3161
+ * @returns Promise resolving to a DatabaseInterface implementation
3162
+ * @throws Error if the database type is invalid
3163
+ */
3164
+ async function getDatabase(options = {}) {
3165
+ if (isDatabaseInstance(options)) return options;
3166
+ Object.assign(options, loadEnvConfig(options, {
3167
+ packageName: "sql",
3168
+ schema: {
3169
+ type: "string",
3170
+ url: "string",
3171
+ host: "string",
3172
+ port: "number",
3173
+ database: "string",
3174
+ user: "string",
3175
+ password: "string"
3176
+ }
3177
+ }));
3178
+ if (!options.type && (options.url?.startsWith("file:") || options.url === ":memory:")) options.type = "sqlite";
3179
+ if (options.type === "postgres") return (await Promise.resolve().then(() => postgres_exports)).getDatabase(options);
3180
+ if (options.type === "sqlite") return (await import("./chunks/sqlite-YgU9LCVy.js")).getDatabase(options);
3181
+ if (options.type === "duckdb") return (await import("./chunks/duckdb-DykBBzd6.js")).getDatabase(options);
3182
+ if (options.type === "json") return (await Promise.resolve().then(() => json_exports)).getDatabase(options);
3183
+ throw new Error("Invalid database type");
3184
+ }
3185
+ /**
3186
+ * Synchronizes a SQL schema definition with a database
3187
+ * Creates tables if they don't exist and adds missing columns to existing tables
3188
+ *
3189
+ * @param options - Object containing database and schema
3190
+ * @param options.db - Database interface to use
3191
+ * @param options.schema - SQL schema definition
3192
+ * @throws Error if db or schema are missing or if the database doesn't support syncSchema
3193
+ */
3194
+ async function syncSchema(options) {
3195
+ const { db, schema } = options;
3196
+ if (!db || !schema) throw new Error("db and schema are required");
3197
+ if (db.syncSchema) await db.syncSchema(schema);
3198
+ else throw new Error("Database adapter does not support schema synchronization");
3199
+ }
3200
+ /**
3201
+ * Checks if a table exists in the database
3202
+ *
3203
+ * @param db - Database interface to use
3204
+ * @param tableName - Name of the table to check
3205
+ * @returns Promise resolving to boolean indicating if the table exists
3206
+ */
3207
+ async function tableExists(db, tableName) {
3208
+ return db.tableExists(tableName);
3209
+ }
3210
+ /**
3211
+ * Escapes and formats a value for use in SQL queries
3212
+ *
3213
+ * @param value - Value to escape
3214
+ * @returns String representation of the value safe for SQL use
3215
+ */
3216
+ function escapeSqlValue(value) {
3217
+ if (value === null) return "NULL";
3218
+ if (value instanceof Date) return `'${value.toISOString()}'`;
3219
+ if (typeof value === "number") return value.toString();
3220
+ if (typeof value === "boolean") return value ? "1" : "0";
3221
+ return `'${String(value).replace(/'/g, "''")}'`;
3222
+ }
3223
+ /**
3224
+ * Validates a column name for use in SQL queries
3225
+ *
3226
+ * @param column - Column name to validate
3227
+ * @returns The validated column name
3228
+ * @throws Error if the column name contains invalid characters
3229
+ */
3230
+ function validateColumnName(column) {
3231
+ if (!/^[a-zA-Z0-9_.]+$/.test(column)) throw new Error(`Invalid column name: ${column}`);
3232
+ return column;
3233
+ }
3234
+ var src_default = {
3235
+ getDatabase,
3236
+ syncSchema,
3237
+ tableExists,
3238
+ buildWhere
3239
+ };
3240
+ /** @internal */
3241
+ var PACKAGE_VERSION_INITIALIZED = true;
3242
+ //#endregion
3243
+ export { DatabaseSchemaManager, PACKAGE_VERSION_INITIALIZED, assertCanExportDatabase, assertCanImportDatabase, bucketExpr, buildAggregate, buildWhere, checkExpectedTables, checkRelationship, checkUniqueColumn, clearConnectionCache, clearPostgresConnectionCache, convertUniqueIndexesToInlineConstraints, createPostgresDatabase, databaseNameFromUrl, src_default as default, dropPostgresDatabase, dumpPostgresDatabase, escapeSqlValue, formatDbError, getDatabase, isLocalDatabaseUrl, postgresEnvFromUrl, redactDatabaseUrl, resolveSchemas, restorePostgresDatabase, runDoctor, syncSchema, tableExists, validateColumnName };
3244
+
3245
+ //# sourceMappingURL=index.js.map