@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
@@ -0,0 +1,1056 @@
1
+ import { n as buildWhere, r as formatDbError, t as DatabaseSchemaManager } from "./schema-manager-a8RlxIE2.js";
2
+ import { t as convertUniqueIndexesToInlineConstraints } from "./duckdb-schema-utils-B-XBP8QF.js";
3
+ import { a as validateTableName, i as validateIndexName, n as generateCreateIndexStatement, r as validateColumnName, t as generateAddColumnStatement } from "./alter-utils-K7tiuj5L.js";
4
+ import { resolveSchemas } from "../index.js";
5
+ import { DatabaseError } from "@happyvertical/utils";
6
+ import { readdir } from "node:fs/promises";
7
+ import { basename, extname, join } from "node:path";
8
+ import { tmpdir } from "node:os";
9
+ //#region src/duckdb.ts
10
+ /**
11
+ * Creates tables from provided schema definitions
12
+ *
13
+ * DuckDB has a known limitation (issue #12684) where ON CONFLICT clauses
14
+ * fail with UNIQUE INDEX but work with inline UNIQUE constraints.
15
+ * This function converts UNIQUE indexes to inline constraints for compatibility.
16
+ *
17
+ * @param connection - DuckDB connection
18
+ * @param schemas - Schema definitions to create
19
+ */
20
+ async function createTablesFromSchemas(connection, schemas) {
21
+ for (const [tableName, schema] of Object.entries(schemas)) try {
22
+ if ((await connection.runAndReadAll(`SELECT name FROM sqlite_master WHERE type='table' AND name='${tableName}'`)).getRowObjects().length > 0) {
23
+ console.log(`[duckdb] Table ${tableName} already exists, skipping`);
24
+ continue;
25
+ }
26
+ console.log(`[duckdb] Creating table ${tableName} from provided schema`);
27
+ const transformed = convertUniqueIndexesToInlineConstraints(schema.ddl, schema.indexes);
28
+ await connection.run(transformed.ddl);
29
+ if (transformed.indexes && transformed.indexes.length > 0) for (const indexSQL of transformed.indexes) try {
30
+ await connection.run(indexSQL);
31
+ } catch (error) {
32
+ console.warn(`[duckdb] Failed to create index for ${tableName}: ${error instanceof Error ? error.message : String(error)}`);
33
+ }
34
+ if (schema.triggers && schema.triggers.length > 0) for (const triggerSQL of schema.triggers) try {
35
+ await connection.run(triggerSQL);
36
+ } catch (error) {
37
+ console.warn(`[duckdb] Failed to create trigger for ${tableName}: ${error instanceof Error ? error.message : String(error)}`);
38
+ }
39
+ } catch (error) {
40
+ throw new DatabaseError(`Failed to create table ${tableName} from schema`, {
41
+ tableName,
42
+ schema,
43
+ originalError: error instanceof Error ? error.message : String(error)
44
+ });
45
+ }
46
+ }
47
+ /**
48
+ * Resolves a DuckDB URL, handling :memory: variants to prevent file leakage.
49
+ *
50
+ * DuckDB interprets any string as a file path unless it's exactly ':memory:'.
51
+ * URLs like ':memory:12345' create files named ':memory:12345' in the working directory.
52
+ *
53
+ * This function:
54
+ * - Returns ':memory:' unchanged (pure in-memory database)
55
+ * - Converts ':memory:*' patterns to temp files in os.tmpdir()
56
+ * - Returns other URLs unchanged (file paths, etc.)
57
+ *
58
+ * @param url - The database URL to resolve
59
+ * @returns The resolved URL safe for DuckDB
60
+ */
61
+ function resolveDuckDBUrl(url) {
62
+ if (url === ":memory:") return url;
63
+ if (url.startsWith(":memory:")) {
64
+ const suffix = url.slice(8);
65
+ return join(tmpdir(), `duckdb-memory-${suffix}.db`);
66
+ }
67
+ return url;
68
+ }
69
+ /**
70
+ * Creates a DuckDB connection instance
71
+ *
72
+ * @param options - DuckDB connection options
73
+ * @returns Promise resolving to a DuckDB connection
74
+ */
75
+ async function createDuckDBConnection(options) {
76
+ const { url = ":memory:", dataDir = "./data", autoRegisterJSON = true } = options;
77
+ const schemas = resolveSchemas(options.schemas) ?? {};
78
+ const resolvedUrl = resolveDuckDBUrl(url);
79
+ try {
80
+ const { DuckDBInstance } = await import(
81
+ /* @vite-ignore */
82
+ "@duckdb/node-api"
83
+ );
84
+ const connection = await (await DuckDBInstance.create(resolvedUrl)).connect();
85
+ if (schemas && Object.keys(schemas).length > 0) await createTablesFromSchemas(connection, schemas);
86
+ if (autoRegisterJSON && dataDir) await registerJSONFiles(connection, dataDir);
87
+ return connection;
88
+ } catch (error) {
89
+ const errorMessage = error instanceof Error ? error.message : String(error);
90
+ throw new DatabaseError(`Failed to create DuckDB connection: ${errorMessage}`, {
91
+ url,
92
+ originalError: errorMessage
93
+ });
94
+ }
95
+ }
96
+ /**
97
+ * Scans the data directory and registers JSON files as queryable tables
98
+ *
99
+ * @param connection - DuckDB connection
100
+ * @param dataDir - Directory containing JSON files
101
+ */
102
+ async function registerJSONFiles(connection, dataDir) {
103
+ try {
104
+ const jsonFiles = (await readdir(dataDir)).filter((file) => extname(file).toLowerCase() === ".json");
105
+ for (const file of jsonFiles) {
106
+ const filePath = join(dataDir, file);
107
+ const tableName = basename(file, ".json");
108
+ await connection.run(`CREATE OR REPLACE VIEW ${tableName} AS SELECT * FROM read_json('${filePath}', auto_detect=true, format='auto', timestampformat='iso', dateformat='iso')`);
109
+ }
110
+ } catch (error) {
111
+ if (error.code !== "ENOENT") throw new DatabaseError(`Failed to register JSON files from ${dataDir}`, {
112
+ dataDir,
113
+ originalError: error instanceof Error ? error.message : String(error)
114
+ });
115
+ }
116
+ }
117
+ /**
118
+ * Converts DuckDB-specific type representations to JavaScript types
119
+ *
120
+ * Handles two cases:
121
+ * 1. JavaScript BigInt values → converted to numbers
122
+ * 2. DuckDB timestamp objects ({ micros: number }) → converted to Date objects
123
+ *
124
+ * DuckDB represents TIMESTAMP values as objects with a single property
125
+ * called 'micros' containing microseconds since Unix epoch. This function
126
+ * converts them to JavaScript Date objects for proper serialization.
127
+ *
128
+ * @param obj - Object that may contain BigInt or timestamp values
129
+ * @returns Object with DuckDB types converted to JavaScript types
130
+ */
131
+ function convertBigInts(obj) {
132
+ if (obj === null || obj === void 0) return obj;
133
+ if (typeof obj === "bigint") return Number(obj);
134
+ if (typeof obj === "object" && !Array.isArray(obj) && "micros" in obj && Object.keys(obj).length === 1 && (typeof obj.micros === "number" || typeof obj.micros === "bigint")) {
135
+ const milliseconds = (typeof obj.micros === "bigint" ? Number(obj.micros) : obj.micros) / 1e3;
136
+ return new Date(milliseconds);
137
+ }
138
+ if (Array.isArray(obj)) return obj.map(convertBigInts);
139
+ if (typeof obj === "object") {
140
+ const result = {};
141
+ for (const key in obj) result[key] = convertBigInts(obj[key]);
142
+ return result;
143
+ }
144
+ return obj;
145
+ }
146
+ /**
147
+ * Creates a DuckDB database adapter
148
+ *
149
+ * @param options - DuckDB connection options
150
+ * @returns Database interface for DuckDB
151
+ */
152
+ async function getDatabase(options = {}) {
153
+ const connection = await createDuckDBConnection(options);
154
+ const writeStrategy = options.writeStrategy || "none";
155
+ const dataDir = options.dataDir || "./data";
156
+ const url = resolveDuckDBUrl(options.url || ":memory:");
157
+ /**
158
+ * Inserts one or more records into a table
159
+ *
160
+ * @param table - Table name
161
+ * @param data - Single record or array of records to insert
162
+ * @returns Promise resolving to operation result
163
+ * @throws Error if the insert operation fails
164
+ */
165
+ const insert = async (table, data) => {
166
+ const records = Array.isArray(data) ? data : [data];
167
+ if (records.length === 0) return {
168
+ operation: "insert",
169
+ affected: 0
170
+ };
171
+ const keys = Object.keys(records[0]);
172
+ const values = [];
173
+ let paramIdx = 1;
174
+ const placeholders = records.map((record) => {
175
+ return `(${keys.map((key) => {
176
+ const value = record[key];
177
+ if (value === null || value === void 0) return "NULL";
178
+ else if (value === "" && typeof value === "string") {
179
+ values.push(value);
180
+ return `CAST($${paramIdx++} AS TEXT)`;
181
+ } else if (value instanceof Date) {
182
+ values.push(value.toISOString());
183
+ return `$${paramIdx++}`;
184
+ } else if (Array.isArray(value)) {
185
+ values.push(JSON.stringify(value));
186
+ return `CAST($${paramIdx++} AS JSON)`;
187
+ } else if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype) {
188
+ values.push(JSON.stringify(value));
189
+ return `CAST($${paramIdx++} AS JSON)`;
190
+ } else {
191
+ values.push(value);
192
+ return `$${paramIdx++}`;
193
+ }
194
+ }).join(", ")})`;
195
+ }).join(", ");
196
+ const sql = `INSERT INTO ${table} (${keys.join(", ")}) VALUES ${placeholders}`;
197
+ try {
198
+ await connection.run(sql, values);
199
+ const affected = records.length;
200
+ if (writeStrategy === "immediate") await exportTableToJSON(connection, table, dataDir);
201
+ return {
202
+ operation: "insert",
203
+ affected
204
+ };
205
+ } catch (e) {
206
+ throw new DatabaseError("Failed to insert records into table", {
207
+ table,
208
+ sql,
209
+ values,
210
+ originalError: formatDbError(e)
211
+ });
212
+ }
213
+ };
214
+ /**
215
+ * Retrieves a single record matching the where criteria
216
+ *
217
+ * @param table - Table name
218
+ * @param where - Criteria to match records
219
+ * @returns Promise resolving to matching record or null if not found
220
+ * @throws Error if the query fails
221
+ */
222
+ const get = async (table, where) => {
223
+ const { sql: whereClause, values } = buildWhere(where, 1, "duckdb");
224
+ const sql = `SELECT * FROM ${table} ${whereClause} LIMIT 1`;
225
+ try {
226
+ const rows = (await connection.runAndReadAll(sql, values)).getRowObjects();
227
+ return rows.length > 0 ? convertBigInts(rows[0]) : null;
228
+ } catch (e) {
229
+ throw new DatabaseError("Failed to retrieve record from table", {
230
+ table,
231
+ sql,
232
+ values,
233
+ originalError: formatDbError(e)
234
+ });
235
+ }
236
+ };
237
+ /**
238
+ * Retrieves multiple records matching the where criteria
239
+ *
240
+ * @param table - Table name
241
+ * @param where - Criteria to match records
242
+ * @returns Promise resolving to array of matching records
243
+ * @throws Error if the query fails
244
+ */
245
+ const list = async (table, where) => {
246
+ const { sql: whereClause, values } = buildWhere(where, 1, "duckdb");
247
+ const sql = `SELECT * FROM ${table} ${whereClause}`;
248
+ try {
249
+ return convertBigInts((await connection.runAndReadAll(sql, values)).getRowObjects());
250
+ } catch (e) {
251
+ throw new DatabaseError("Failed to list records from table", {
252
+ table,
253
+ sql,
254
+ values,
255
+ originalError: formatDbError(e)
256
+ });
257
+ }
258
+ };
259
+ /**
260
+ * Updates records matching the where criteria
261
+ *
262
+ * @param table - Table name
263
+ * @param where - Criteria to match records to update
264
+ * @param data - New data to set
265
+ * @returns Promise resolving to operation result
266
+ * @throws Error if the update operation fails
267
+ */
268
+ const update = async (table, where, data) => {
269
+ const keys = Object.keys(data);
270
+ const setClause = keys.map((key, idx) => `${key} = $${idx + 1}`).join(", ");
271
+ const { sql: whereClause, values: whereValues } = buildWhere(where, keys.length + 1, "duckdb");
272
+ const sql = `UPDATE ${table} SET ${setClause} ${whereClause}`;
273
+ const values = [...Object.values(data), ...whereValues];
274
+ try {
275
+ await connection.run(sql, values);
276
+ if (writeStrategy === "immediate") await exportTableToJSON(connection, table, dataDir);
277
+ return {
278
+ operation: "update",
279
+ affected: 1
280
+ };
281
+ } catch (e) {
282
+ throw new DatabaseError("Failed to update records in table", {
283
+ table,
284
+ sql,
285
+ values,
286
+ originalError: formatDbError(e)
287
+ });
288
+ }
289
+ };
290
+ const buildDuckDBValueExpression = (value, values, paramIdx) => {
291
+ if (value === null || value === void 0) return "NULL";
292
+ if (value === "" && typeof value === "string") {
293
+ values.push(value);
294
+ return `CAST($${paramIdx.value++} AS TEXT)`;
295
+ }
296
+ if (value instanceof Date) {
297
+ values.push(value.toISOString());
298
+ return `$${paramIdx.value++}`;
299
+ }
300
+ if (Array.isArray(value)) {
301
+ values.push(JSON.stringify(value));
302
+ return `CAST($${paramIdx.value++} AS JSON)`;
303
+ }
304
+ if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype) {
305
+ values.push(JSON.stringify(value));
306
+ return `CAST($${paramIdx.value++} AS JSON)`;
307
+ }
308
+ values.push(value);
309
+ return `$${paramIdx.value++}`;
310
+ };
311
+ const executeNullAwareDuckDBUpsert = async (table, conflictColumns, data) => {
312
+ const conflictWhere = Object.fromEntries(conflictColumns.map((col) => [col, data[col]]));
313
+ if (!await get(table, conflictWhere)) return insert(table, data);
314
+ const keys = Object.keys(data);
315
+ const values = [];
316
+ const paramIdx = { value: 1 };
317
+ const assignments = keys.map((key) => {
318
+ return `"${key}" = ${buildDuckDBValueExpression(data[key], values, paramIdx)}`;
319
+ });
320
+ const { sql: whereClause, values: whereValues } = buildWhere(conflictWhere, paramIdx.value, "duckdb");
321
+ const sql = `UPDATE ${table} SET ${assignments.join(", ")} ${whereClause}`;
322
+ try {
323
+ await connection.run(sql, [...values, ...whereValues]);
324
+ if (writeStrategy === "immediate") await exportTableToJSON(connection, table, dataDir);
325
+ return {
326
+ operation: "upsert",
327
+ affected: 1
328
+ };
329
+ } catch (e) {
330
+ throw new DatabaseError("Failed to upsert record into table", {
331
+ table,
332
+ sql,
333
+ values: [...values, ...whereValues],
334
+ conflictColumns,
335
+ originalError: formatDbError(e)
336
+ });
337
+ }
338
+ };
339
+ /**
340
+ * Inserts a record or updates it if it already exists (UPSERT)
341
+ *
342
+ * @param table - Table name
343
+ * @param conflictColumns - Columns that define the uniqueness constraint
344
+ * @param data - Data to insert or update
345
+ * @returns Promise resolving to operation result
346
+ * @throws Error if the upsert operation fails
347
+ */
348
+ const upsert = async (table, conflictColumns, data, options) => {
349
+ const missingColumns = conflictColumns.filter((col) => !(col in data));
350
+ if (missingColumns.length > 0) throw new DatabaseError("Conflict columns missing from data", {
351
+ table,
352
+ conflictColumns,
353
+ missingColumns,
354
+ availableColumns: Object.keys(data),
355
+ 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."
356
+ });
357
+ if (!options?.nullsDistinct && conflictColumns.some((col) => data[col] === null)) return executeNullAwareDuckDBUpsert(table, conflictColumns, data);
358
+ const keys = Object.keys(data);
359
+ const dataValues = Object.values(data);
360
+ const placeholders = [];
361
+ const values = [];
362
+ let paramIdx = 1;
363
+ for (const value of dataValues) if (value === null || value === void 0) placeholders.push("NULL");
364
+ else if (value === "" && typeof value === "string") {
365
+ placeholders.push(`CAST($${paramIdx} AS TEXT)`);
366
+ values.push(value);
367
+ paramIdx++;
368
+ } else if (value instanceof Date) {
369
+ placeholders.push(`$${paramIdx}`);
370
+ values.push(value.toISOString());
371
+ paramIdx++;
372
+ } else if (Array.isArray(value)) {
373
+ placeholders.push(`CAST($${paramIdx} AS JSON)`);
374
+ values.push(JSON.stringify(value));
375
+ paramIdx++;
376
+ } else if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype) {
377
+ placeholders.push(`CAST($${paramIdx} AS JSON)`);
378
+ values.push(JSON.stringify(value));
379
+ paramIdx++;
380
+ } else {
381
+ placeholders.push(`$${paramIdx}`);
382
+ values.push(value);
383
+ paramIdx++;
384
+ }
385
+ const updateSetParts = [];
386
+ for (let i = 0; i < keys.length; i++) {
387
+ const key = keys[i];
388
+ const value = dataValues[i];
389
+ if (value === null || value === void 0) updateSetParts.push(`${key} = NULL`);
390
+ else if (value === "" && typeof value === "string") {
391
+ updateSetParts.push(`${key} = CAST($${paramIdx} AS TEXT)`);
392
+ values.push(value);
393
+ paramIdx++;
394
+ } else if (value instanceof Date) {
395
+ updateSetParts.push(`${key} = $${paramIdx}`);
396
+ values.push(value.toISOString());
397
+ paramIdx++;
398
+ } else if (Array.isArray(value)) {
399
+ updateSetParts.push(`${key} = CAST($${paramIdx} AS JSON)`);
400
+ values.push(JSON.stringify(value));
401
+ paramIdx++;
402
+ } else if (typeof value === "object" && value !== null && Object.getPrototypeOf(value) === Object.prototype) {
403
+ updateSetParts.push(`${key} = CAST($${paramIdx} AS JSON)`);
404
+ values.push(JSON.stringify(value));
405
+ paramIdx++;
406
+ } else {
407
+ updateSetParts.push(`${key} = $${paramIdx}`);
408
+ values.push(value);
409
+ paramIdx++;
410
+ }
411
+ }
412
+ const conflict = conflictColumns.map((col) => `"${col}"`).join(", ");
413
+ const quotedKeys = keys.map((key) => `"${key}"`).join(", ");
414
+ const quotedUpdateSetParts = keys.map((key, i) => {
415
+ const part = updateSetParts[i];
416
+ return `"${key}" = ${part.substring(part.indexOf("=") + 1).trim()}`;
417
+ });
418
+ const sql = `INSERT INTO ${table} (${quotedKeys}) VALUES (${placeholders.join(", ")}) ON CONFLICT(${conflict}) DO UPDATE SET ${quotedUpdateSetParts.join(", ")}`;
419
+ try {
420
+ await connection.run(sql, values);
421
+ if (writeStrategy === "immediate") await exportTableToJSON(connection, table, dataDir);
422
+ return {
423
+ operation: "upsert",
424
+ affected: 1
425
+ };
426
+ } catch (e) {
427
+ throw new DatabaseError("Failed to upsert record into table", {
428
+ table,
429
+ sql,
430
+ values,
431
+ conflictColumns,
432
+ originalError: formatDbError(e)
433
+ });
434
+ }
435
+ };
436
+ /**
437
+ * Gets a record matching the where criteria or inserts it if not found
438
+ *
439
+ * @param table - Table name
440
+ * @param where - Criteria to match existing record
441
+ * @param data - Data to insert if no record found
442
+ * @returns Promise resolving to the record (either retrieved or newly inserted)
443
+ * @throws Error if the operation fails
444
+ */
445
+ const getOrInsert = async (table, where, data) => {
446
+ const result = await get(table, where);
447
+ if (result) return result;
448
+ await insert(table, data);
449
+ const inserted = await get(table, where);
450
+ if (!inserted) throw new DatabaseError("Failed to insert and retrieve record", {
451
+ table,
452
+ where,
453
+ data
454
+ });
455
+ return inserted;
456
+ };
457
+ /**
458
+ * Deletes records from a table matching the where criteria
459
+ *
460
+ * @param table - Table name
461
+ * @param where - Criteria to match records for deletion
462
+ * @returns Promise resolving to operation result with count of deleted rows
463
+ * @throws Error if the delete operation fails
464
+ */
465
+ const deleteRecords = async (table, where) => {
466
+ validateTableName(table);
467
+ if (Object.keys(where).length === 0) throw new DatabaseError("DELETE requires at least one WHERE condition to prevent accidental deletion of all records", { table });
468
+ const { sql: whereClause, values } = buildWhere(where, 1, "duckdb");
469
+ const sql = `DELETE FROM ${table} ${whereClause}`;
470
+ try {
471
+ await connection.run(sql, values);
472
+ if (writeStrategy === "immediate") await exportTableToJSON(connection, table, dataDir);
473
+ return {
474
+ operation: "delete",
475
+ affected: 1
476
+ };
477
+ } catch (e) {
478
+ throw new DatabaseError("Failed to delete records from table", {
479
+ table,
480
+ sql,
481
+ values,
482
+ originalError: formatDbError(e)
483
+ });
484
+ }
485
+ };
486
+ /**
487
+ * Counts records in a table matching the where criteria
488
+ *
489
+ * @param table - Table name
490
+ * @param where - Criteria to match records (optional, counts all if omitted)
491
+ * @returns Promise resolving to count of matching records
492
+ * @throws Error if the count operation fails
493
+ */
494
+ const count = async (table, where) => {
495
+ validateTableName(table);
496
+ try {
497
+ if (!where || Object.keys(where).length === 0) {
498
+ const rows = (await connection.runAndReadAll(`SELECT COUNT(*) as count FROM ${table}`)).getRowObjects();
499
+ return Number(rows[0]?.count) || 0;
500
+ }
501
+ const { sql: whereClause, values } = buildWhere(where, 1, "duckdb");
502
+ const sql = `SELECT COUNT(*) as count FROM ${table} ${whereClause}`;
503
+ const rows = (await connection.runAndReadAll(sql, values)).getRowObjects();
504
+ return Number(rows[0]?.count) || 0;
505
+ } catch (e) {
506
+ throw new DatabaseError("Failed to count records in table", {
507
+ table,
508
+ where,
509
+ originalError: formatDbError(e)
510
+ });
511
+ }
512
+ };
513
+ /**
514
+ * Checks if a table exists in the database
515
+ *
516
+ * @param tableName - Name of the table to check
517
+ * @returns Promise resolving to boolean indicating if the table exists
518
+ */
519
+ const tableExists = async (tableName) => {
520
+ try {
521
+ await connection.runAndReadAll(`SELECT * FROM ${tableName} LIMIT 1`);
522
+ return true;
523
+ } catch {
524
+ return false;
525
+ }
526
+ };
527
+ /**
528
+ * Exports a table to a JSON file (for write-back strategies)
529
+ *
530
+ * @param connection - DuckDB connection
531
+ * @param table - Table name
532
+ * @param dataDir - Directory to write JSON file
533
+ */
534
+ const exportTableToJSON = async (connection, table, dataDir) => {
535
+ const filePath = join(dataDir, `${table}.json`);
536
+ await connection.run(`COPY (SELECT * FROM ${table}) TO '${filePath}' (FORMAT JSON, ARRAY true)`);
537
+ };
538
+ /**
539
+ * Manual export method for 'manual' write strategy
540
+ *
541
+ * @param table - Table name to export
542
+ * @returns Promise that resolves when export completes
543
+ */
544
+ const exportTable = async (table) => {
545
+ if (writeStrategy === "none") throw new DatabaseError("Cannot export table: write strategy is set to none", {
546
+ table,
547
+ writeStrategy
548
+ });
549
+ await exportTableToJSON(connection, table, dataDir);
550
+ };
551
+ /**
552
+ * Creates a table-specific interface for simplified table operations
553
+ *
554
+ * @param tableName - Table name
555
+ * @returns TableInterface for the specified table
556
+ */
557
+ const table = (tableName) => ({
558
+ insert: (data) => insert(tableName, data),
559
+ get: (where) => get(tableName, where),
560
+ list: (where) => list(tableName, where)
561
+ });
562
+ /**
563
+ * Parses a tagged template literal into a SQL query and values
564
+ *
565
+ * @param strings - Template strings
566
+ * @param vars - Variables to interpolate into the query
567
+ * @returns Object with SQL query and values array
568
+ */
569
+ const parseTemplate = (strings, ...vars) => {
570
+ let sql = strings[0];
571
+ const values = [];
572
+ for (let i = 0; i < vars.length; i++) {
573
+ values.push(vars[i]);
574
+ sql += `$${i + 1}${strings[i + 1]}`;
575
+ }
576
+ return {
577
+ sql,
578
+ values
579
+ };
580
+ };
581
+ /**
582
+ * Executes a SQL query using template literals and returns multiple rows
583
+ *
584
+ * @param strings - Template strings
585
+ * @param vars - Variables to interpolate into the query
586
+ * @returns Promise resolving to array of result records
587
+ * @throws Error if the query fails
588
+ */
589
+ const many = async (strings, ...vars) => {
590
+ const { sql, values } = parseTemplate(strings, ...vars);
591
+ try {
592
+ return convertBigInts((await connection.runAndReadAll(sql, values)).getRowObjects());
593
+ } catch (e) {
594
+ throw new DatabaseError("Failed to execute many query", {
595
+ sql,
596
+ values,
597
+ originalError: formatDbError(e)
598
+ });
599
+ }
600
+ };
601
+ /**
602
+ * Executes a SQL query using template literals and returns a single row
603
+ *
604
+ * @param strings - Template strings
605
+ * @param vars - Variables to interpolate into the query
606
+ * @returns Promise resolving to a single result record or null
607
+ * @throws Error if the query fails
608
+ */
609
+ const single = async (strings, ...vars) => {
610
+ const { sql, values } = parseTemplate(strings, ...vars);
611
+ try {
612
+ const rows = (await connection.runAndReadAll(sql, values)).getRowObjects();
613
+ return rows[0] ? convertBigInts(rows[0]) : null;
614
+ } catch (e) {
615
+ throw new DatabaseError("Failed to execute single query", {
616
+ sql,
617
+ values,
618
+ originalError: formatDbError(e)
619
+ });
620
+ }
621
+ };
622
+ /**
623
+ * Executes a SQL query using template literals and returns a single value
624
+ *
625
+ * @param strings - Template strings
626
+ * @param vars - Variables to interpolate into the query
627
+ * @returns Promise resolving to a single value (first column of first row)
628
+ * @throws Error if the query fails
629
+ */
630
+ const pluck = async (strings, ...vars) => {
631
+ const { sql, values } = parseTemplate(strings, ...vars);
632
+ try {
633
+ const rows = (await connection.runAndReadAll(sql, values)).getRowObjects();
634
+ if (rows.length === 0) return null;
635
+ const firstRow = rows[0];
636
+ const firstKey = Object.keys(firstRow)[0];
637
+ return convertBigInts(firstRow[firstKey]);
638
+ } catch (e) {
639
+ throw new DatabaseError("Failed to execute pluck query", {
640
+ sql,
641
+ values,
642
+ originalError: formatDbError(e)
643
+ });
644
+ }
645
+ };
646
+ /**
647
+ * Executes a SQL query using template literals without returning results
648
+ *
649
+ * @param strings - Template strings
650
+ * @param vars - Variables to interpolate into the query
651
+ * @returns Promise that resolves when the query completes
652
+ * @throws Error if the query fails
653
+ */
654
+ const execute = async (strings, ...vars) => {
655
+ const { sql, values } = parseTemplate(strings, ...vars);
656
+ try {
657
+ await connection.run(sql, values);
658
+ } catch (e) {
659
+ throw new DatabaseError("Failed to execute query", {
660
+ sql,
661
+ values,
662
+ originalError: formatDbError(e)
663
+ });
664
+ }
665
+ };
666
+ /**
667
+ * Executes a raw SQL query with parameterized values
668
+ *
669
+ * @param str - SQL query string
670
+ * @param values - Variables to use as parameters
671
+ * @returns Promise resolving to query result with rows and metadata
672
+ * @throws Error if the query fails
673
+ */
674
+ const query = async (str, ...values) => {
675
+ const sql = str;
676
+ const args = Array.isArray(values[0]) ? values[0] : values;
677
+ try {
678
+ const rows = convertBigInts((await connection.runAndReadAll(sql, args)).getRowObjects());
679
+ return {
680
+ command: sql.split(" ")[0].toUpperCase(),
681
+ rowCount: rows.length,
682
+ oid: null,
683
+ fields: rows.length > 0 ? Object.keys(rows[0]).map((name) => ({
684
+ name,
685
+ tableID: 0,
686
+ columnID: 0,
687
+ dataTypeID: 0,
688
+ dataTypeSize: -1,
689
+ dataTypeModifier: -1,
690
+ format: "text"
691
+ })) : [],
692
+ rows
693
+ };
694
+ } catch (e) {
695
+ throw new DatabaseError("Failed to execute raw query", {
696
+ sql,
697
+ args,
698
+ originalError: formatDbError(e)
699
+ });
700
+ }
701
+ };
702
+ const oo = many;
703
+ const oO = single;
704
+ const ox = pluck;
705
+ const xx = execute;
706
+ /**
707
+ * Synchronizes database schema with provided SQL DDL
708
+ *
709
+ * DuckDB requires UNIQUE constraints to be defined **inline** in CREATE TABLE
710
+ * statements for ON CONFLICT clauses to work. This method automatically
711
+ * transforms UNIQUE indexes to inline constraints before executing the schema.
712
+ *
713
+ * @param schema - SQL schema definition with CREATE TABLE and CREATE INDEX statements
714
+ * @returns Promise that resolves when schema is synchronized
715
+ */
716
+ const syncSchema = async (schema) => {
717
+ const statements = schema.trim().split(";").filter((stmt) => stmt.trim() !== "");
718
+ const createTableStatements = [];
719
+ const indexStatements = [];
720
+ for (const stmt of statements) {
721
+ const trimmed = stmt.trim();
722
+ if (!trimmed) continue;
723
+ if (trimmed.startsWith("CREATE INDEX") || trimmed.startsWith("CREATE UNIQUE INDEX")) indexStatements.push(trimmed);
724
+ else if (trimmed.startsWith("CREATE TABLE")) createTableStatements.push(trimmed);
725
+ }
726
+ for (const ddl of createTableStatements) {
727
+ const { ddl: transformedDDL, indexes: remainingIndexes } = convertUniqueIndexesToInlineConstraints(ddl, indexStatements);
728
+ try {
729
+ await connection.run(transformedDDL);
730
+ } catch (e) {
731
+ console.error("Schema sync error (CREATE TABLE):", e);
732
+ }
733
+ for (const indexSQL of remainingIndexes) try {
734
+ await connection.run(indexSQL);
735
+ } catch (e) {
736
+ console.error("Schema sync error (CREATE INDEX):", e);
737
+ }
738
+ }
739
+ };
740
+ /**
741
+ * Initialize database schemas from JSON manifest
742
+ * Supports dependency resolution and schema overrides
743
+ *
744
+ * @param options - Schema initialization options
745
+ * @returns Promise that resolves when schemas are initialized
746
+ */
747
+ const initializeSchemas = async (options) => {
748
+ const schemaManager = new DatabaseSchemaManager();
749
+ const currentDb = {
750
+ url,
751
+ client: connection,
752
+ query,
753
+ insert,
754
+ update,
755
+ upsert,
756
+ get,
757
+ list,
758
+ getOrInsert,
759
+ delete: deleteRecords,
760
+ count,
761
+ table,
762
+ tableExists,
763
+ many,
764
+ single,
765
+ pluck,
766
+ execute,
767
+ oo,
768
+ oO,
769
+ ox,
770
+ xx,
771
+ syncSchema
772
+ };
773
+ await schemaManager.initializeSchemas(currentDb, options);
774
+ };
775
+ /**
776
+ * Retrieves the schema information for a table
777
+ *
778
+ * @param table - Table name
779
+ * @returns Promise resolving to table schema info or null if table doesn't exist
780
+ * @throws Error if the query fails
781
+ */
782
+ const getTableSchema = async (table) => {
783
+ validateTableName(table);
784
+ try {
785
+ if (!await tableExists(table)) return null;
786
+ const columnRows = await many`
787
+ SELECT
788
+ column_name,
789
+ data_type,
790
+ is_nullable,
791
+ column_default
792
+ FROM information_schema.columns
793
+ WHERE table_name = ${table}
794
+ ORDER BY ordinal_position
795
+ `;
796
+ const pkRows = await many`
797
+ SELECT column_name
798
+ FROM duckdb_constraints() con
799
+ JOIN duckdb_columns() cols
800
+ ON con.table_oid = cols.table_oid
801
+ WHERE con.table_name = ${table}
802
+ AND con.constraint_type = 'PRIMARY KEY'
803
+ AND cols.column_name = ANY(con.constraint_column_names)
804
+ `;
805
+ const pkColumns = new Set(pkRows.map((row) => row.column_name));
806
+ const columns = {};
807
+ for (const row of columnRows) {
808
+ const colName = row.column_name;
809
+ columns[colName] = {
810
+ type: row.data_type,
811
+ primaryKey: pkColumns.has(colName),
812
+ notNull: row.is_nullable === "NO",
813
+ defaultValue: row.column_default
814
+ };
815
+ }
816
+ const indexRows = await many`
817
+ SELECT
818
+ index_name,
819
+ sql
820
+ FROM duckdb_indexes()
821
+ WHERE table_name = ${table}
822
+ AND NOT is_primary
823
+ `;
824
+ const indexes = [];
825
+ for (const row of indexRows) {
826
+ const indexName = row.index_name;
827
+ const indexDef = row.sql || "";
828
+ const colMatch = indexDef.match(/\(([^)]+)\)/);
829
+ const indexColumns = colMatch ? colMatch[1].split(",").map((col) => col.trim()) : [];
830
+ const isUnique = indexDef.toUpperCase().includes("UNIQUE");
831
+ indexes.push({
832
+ name: indexName,
833
+ columns: indexColumns,
834
+ unique: isUnique
835
+ });
836
+ }
837
+ const fkRows = await many`
838
+ SELECT
839
+ constraint_column_names,
840
+ constraint_column_indexes,
841
+ constraint_text
842
+ FROM duckdb_constraints()
843
+ WHERE table_name = ${table}
844
+ AND constraint_type = 'FOREIGN KEY'
845
+ `;
846
+ const foreignKeys = [];
847
+ for (const fkRow of fkRows) {
848
+ const fkText = fkRow.constraint_text || "";
849
+ const referencesMatch = fkText.match(/REFERENCES\s+(\w+)\s*\(([^)]+)\)/i);
850
+ if (referencesMatch && Array.isArray(fkRow.constraint_column_names) && fkRow.constraint_column_names.length > 0) {
851
+ const columnName = fkRow.constraint_column_names[0];
852
+ const referencedTable = referencesMatch[1];
853
+ const referencedColumn = referencesMatch[2].trim();
854
+ const onDeleteMatch = fkText.match(/ON DELETE\s+(\w+)/i);
855
+ const onUpdateMatch = fkText.match(/ON UPDATE\s+(\w+)/i);
856
+ foreignKeys.push({
857
+ column: columnName,
858
+ referencesTable: referencedTable,
859
+ referencesColumn: referencedColumn,
860
+ onDelete: onDeleteMatch ? onDeleteMatch[1] : void 0,
861
+ onUpdate: onUpdateMatch ? onUpdateMatch[1] : void 0
862
+ });
863
+ }
864
+ }
865
+ return {
866
+ tableName: table,
867
+ columns,
868
+ indexes,
869
+ foreignKeys
870
+ };
871
+ } catch (e) {
872
+ throw new DatabaseError("Failed to retrieve table schema", {
873
+ table,
874
+ originalError: formatDbError(e)
875
+ });
876
+ }
877
+ };
878
+ /**
879
+ * ALTER TABLE operations for schema evolution
880
+ */
881
+ const alterTable = {
882
+ /**
883
+ * Adds a new column to an existing table
884
+ *
885
+ * @param table - Table name
886
+ * @param column - Column definition with name
887
+ * @returns Promise that resolves when column is added
888
+ * @throws Error if the alter operation fails
889
+ */
890
+ addColumn: async (table, column) => {
891
+ validateTableName(table);
892
+ validateColumnName(column.name);
893
+ try {
894
+ const sql = generateAddColumnStatement(table, column, "duckdb");
895
+ await connection.run(sql);
896
+ } catch (e) {
897
+ throw new DatabaseError("Failed to add column to table", {
898
+ table,
899
+ column: column.name,
900
+ originalError: formatDbError(e)
901
+ });
902
+ }
903
+ },
904
+ /**
905
+ * Adds a new index to an existing table
906
+ *
907
+ * @param table - Table name
908
+ * @param index - Index definition
909
+ * @returns Promise that resolves when index is created
910
+ * @throws Error if the create index operation fails
911
+ */
912
+ addIndex: async (table, index) => {
913
+ validateTableName(table);
914
+ validateIndexName(index.name);
915
+ for (const col of index.columns) validateColumnName(col);
916
+ try {
917
+ const sql = generateCreateIndexStatement(table, index);
918
+ await connection.run(sql);
919
+ } catch (e) {
920
+ throw new DatabaseError("Failed to create index on table", {
921
+ table,
922
+ index: index.name,
923
+ originalError: formatDbError(e)
924
+ });
925
+ }
926
+ }
927
+ };
928
+ /**
929
+ * Executes a callback within a database transaction
930
+ * Automatically commits on success or rolls back on error
931
+ *
932
+ * @param callback - Function to execute within transaction
933
+ * @returns Promise resolving to callback result
934
+ */
935
+ const transaction = async (callback) => {
936
+ try {
937
+ await connection.run("BEGIN TRANSACTION");
938
+ const result = await callback({
939
+ url,
940
+ client: connection,
941
+ insert,
942
+ get,
943
+ list,
944
+ update,
945
+ upsert,
946
+ getOrInsert,
947
+ delete: deleteRecords,
948
+ count,
949
+ table,
950
+ many,
951
+ single,
952
+ pluck,
953
+ execute,
954
+ query,
955
+ oo,
956
+ oO,
957
+ ox,
958
+ xx,
959
+ tableExists,
960
+ syncSchema,
961
+ transaction
962
+ });
963
+ await connection.run("COMMIT");
964
+ return result;
965
+ } catch (error) {
966
+ await connection.run("ROLLBACK");
967
+ throw error;
968
+ }
969
+ };
970
+ /**
971
+ * Begins a new transaction and returns a handle for manual control
972
+ *
973
+ * Unlike transaction(), this gives you explicit control over commit/rollback.
974
+ * Ideal for test isolation where you want to rollback after each test.
975
+ *
976
+ * @returns Promise resolving to a TransactionHandle
977
+ */
978
+ const beginTransaction = async () => {
979
+ await connection.run("BEGIN TRANSACTION");
980
+ let active = true;
981
+ const commit = async () => {
982
+ if (!active) throw new DatabaseError("Transaction already ended", {});
983
+ await connection.run("COMMIT");
984
+ active = false;
985
+ };
986
+ const rollback = async () => {
987
+ if (!active) throw new DatabaseError("Transaction already ended", {});
988
+ await connection.run("ROLLBACK");
989
+ active = false;
990
+ };
991
+ const isActive = () => active;
992
+ return {
993
+ url,
994
+ client: connection,
995
+ insert,
996
+ get,
997
+ list,
998
+ update,
999
+ upsert,
1000
+ getOrInsert,
1001
+ delete: deleteRecords,
1002
+ count,
1003
+ table,
1004
+ many,
1005
+ single,
1006
+ pluck,
1007
+ execute,
1008
+ query,
1009
+ oo,
1010
+ oO,
1011
+ ox,
1012
+ xx,
1013
+ tableExists,
1014
+ syncSchema,
1015
+ transaction,
1016
+ commit,
1017
+ rollback,
1018
+ isActive
1019
+ };
1020
+ };
1021
+ return {
1022
+ url,
1023
+ requiresSchemaCheck: true,
1024
+ client: connection,
1025
+ query,
1026
+ insert,
1027
+ update,
1028
+ upsert,
1029
+ get,
1030
+ list,
1031
+ getOrInsert,
1032
+ delete: deleteRecords,
1033
+ count,
1034
+ table,
1035
+ tableExists,
1036
+ many,
1037
+ single,
1038
+ pluck,
1039
+ execute,
1040
+ oo,
1041
+ oO,
1042
+ ox,
1043
+ xx,
1044
+ syncSchema,
1045
+ initializeSchemas,
1046
+ transaction,
1047
+ beginTransaction,
1048
+ getTableSchema,
1049
+ alterTable,
1050
+ exportTable
1051
+ };
1052
+ }
1053
+ //#endregion
1054
+ export { getDatabase };
1055
+
1056
+ //# sourceMappingURL=duckdb-DykBBzd6.js.map