@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,1045 @@
1
+ import { n as buildWhere, r as formatDbError, t as DatabaseSchemaManager } from "./schema-manager-a8RlxIE2.js";
2
+ import { a as validateTableName, i as validateIndexName, n as generateCreateIndexStatement, r as validateColumnName, t as generateAddColumnStatement } from "./alter-utils-K7tiuj5L.js";
3
+ import { resolveSchemas } from "../index.js";
4
+ import { DatabaseError } from "@happyvertical/utils";
5
+ //#region src/sqlite.ts
6
+ /**
7
+ * Connection cache for in-memory databases with memoryId
8
+ * Enables sharing of :memory: databases across multiple getDatabase() calls
9
+ */
10
+ var memoryConnectionCache = /* @__PURE__ */ new Map();
11
+ /**
12
+ * Pending connection promises to handle concurrent getDatabase() calls
13
+ * Prevents creating duplicate connections when parallel calls happen
14
+ */
15
+ var pendingConnections = /* @__PURE__ */ new Map();
16
+ var NULL_AWARE_UPSERT_MAX_ATTEMPTS = 8;
17
+ var NULL_AWARE_UPSERT_BASE_DELAY_MS = 25;
18
+ var nullAwareUpsertLocks = /* @__PURE__ */ new Map();
19
+ function delay(ms) {
20
+ return new Promise((resolve) => setTimeout(resolve, ms));
21
+ }
22
+ function isRetriableSqliteTransactionError(error) {
23
+ const formatted = formatDbError(error).toLowerCase();
24
+ return formatted.includes("busy") || formatted.includes("locked") || formatted.includes("database is locked");
25
+ }
26
+ async function withNullAwareUpsertLock(lockKey, callback) {
27
+ const previous = nullAwareUpsertLocks.get(lockKey) ?? Promise.resolve();
28
+ let release = () => {};
29
+ const current = new Promise((resolve) => {
30
+ release = resolve;
31
+ });
32
+ const next = previous.catch(() => {}).then(() => current);
33
+ nullAwareUpsertLocks.set(lockKey, next);
34
+ await previous.catch(() => {});
35
+ try {
36
+ return await callback();
37
+ } finally {
38
+ release();
39
+ if (nullAwareUpsertLocks.get(lockKey) === next) nullAwareUpsertLocks.delete(lockKey);
40
+ }
41
+ }
42
+ /**
43
+ * Generates a unique identifier for in-memory databases
44
+ * @returns A unique database identifier string
45
+ */
46
+ function generateDbId() {
47
+ return `memory-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
48
+ }
49
+ /**
50
+ * Creates a LibSQL client using the default client implementation
51
+ * Supports in-memory databases and remote LibSQL URLs
52
+ *
53
+ * @param options - SQLite connection options
54
+ * @returns Promise resolving to a LibSQL client instance
55
+ */
56
+ async function createLibSQLClient(options) {
57
+ const { url = ":memory:", authToken, encryptionKey } = options;
58
+ let libsqlUrl = url;
59
+ if (url !== ":memory:" && !url.startsWith("http://") && !url.startsWith("https://") && !url.startsWith("libsql://") && !url.startsWith("file:")) {
60
+ const { resolve } = await import("node:path");
61
+ libsqlUrl = `file://${resolve(url)}`;
62
+ }
63
+ try {
64
+ const { createClient } = await import(
65
+ /* @vite-ignore */
66
+ "@libsql/client"
67
+ );
68
+ return createClient({
69
+ url: libsqlUrl,
70
+ authToken,
71
+ encryptionKey
72
+ });
73
+ } catch (error) {
74
+ const errorMessage = error instanceof Error ? error.message : String(error);
75
+ if (errorMessage?.includes("URL_SCHEME_NOT_SUPPORTED")) throw new DatabaseError(`Unsupported URL scheme. Use ':memory:' for in-memory databases or 'libsql://' for remote LibSQL databases. Original: ${url}, Converted: ${libsqlUrl}`, {
76
+ url: libsqlUrl,
77
+ originalError: errorMessage
78
+ });
79
+ throw new DatabaseError(`Failed to create LibSQL client: ${errorMessage}`, {
80
+ url: libsqlUrl,
81
+ originalError: errorMessage
82
+ });
83
+ }
84
+ }
85
+ function hasNativeSqliteCapabilities(options) {
86
+ return Boolean(options.capabilities?.notifications || options.capabilities?.vector);
87
+ }
88
+ /**
89
+ * Creates tables from provided schema definitions
90
+ *
91
+ * @param client - LibSQL client
92
+ * @param schemas - Schema definitions to create
93
+ */
94
+ async function createTablesFromSchemas(client, schemas) {
95
+ for (const [tableName, schema] of Object.entries(schemas)) try {
96
+ if ((await client.execute({
97
+ sql: `SELECT name FROM sqlite_master WHERE type='table' AND name=?`,
98
+ args: [tableName]
99
+ })).rows.length > 0) {
100
+ console.log(`[sqlite] Table ${tableName} already exists, skipping`);
101
+ continue;
102
+ }
103
+ console.log(`[sqlite] Creating table ${tableName} from provided schema`);
104
+ await client.execute(schema.ddl);
105
+ if (schema.indexes && schema.indexes.length > 0) for (const indexSQL of schema.indexes) try {
106
+ await client.execute(indexSQL);
107
+ } catch (error) {
108
+ console.warn(`[sqlite] Failed to create index for ${tableName}: ${error instanceof Error ? error.message : String(error)}`);
109
+ }
110
+ if (schema.triggers && schema.triggers.length > 0) for (const triggerSQL of schema.triggers) try {
111
+ await client.execute(triggerSQL);
112
+ } catch (error) {
113
+ console.warn(`[sqlite] Failed to create trigger for ${tableName}: ${error instanceof Error ? error.message : String(error)}`);
114
+ }
115
+ } catch (error) {
116
+ console.error(`[sqlite] Error creating table ${tableName}:`, error instanceof Error ? error.message : String(error));
117
+ console.error(`[sqlite] DDL was:`, schema.ddl?.substring(0, 200) + "...");
118
+ throw new DatabaseError(`Failed to create table ${tableName} from schema: ${error instanceof Error ? error.message : String(error)}`, {
119
+ tableName,
120
+ schema,
121
+ originalError: error instanceof Error ? error.message : String(error)
122
+ });
123
+ }
124
+ }
125
+ /**
126
+ * Creates a SQLite database adapter
127
+ *
128
+ * @param options - SQLite connection options
129
+ * @returns Database interface for SQLite
130
+ */
131
+ async function getDatabase(options = {}) {
132
+ const url = options.url || ":memory:";
133
+ if (url === ":memory:" && !options.dbid) options.dbid = generateDbId();
134
+ if (hasNativeSqliteCapabilities(options)) return (await import("./sqlite-native-BGtpx7Ro.js")).getNativeSqliteDatabase(options);
135
+ if (options.dbid) {
136
+ const cached = memoryConnectionCache.get(options.dbid);
137
+ if (cached) return cached;
138
+ const pending = pendingConnections.get(options.dbid);
139
+ if (pending) return pending;
140
+ }
141
+ const connectionPromise = (async () => {
142
+ const client = await createLibSQLClient(options);
143
+ const resolvedSchemas = resolveSchemas(options.schemas);
144
+ if (resolvedSchemas && Object.keys(resolvedSchemas).length > 0) await createTablesFromSchemas(client, resolvedSchemas);
145
+ /**
146
+ * Serializes a value for SQLite storage
147
+ * Converts objects and arrays to JSON strings
148
+ * Converts Dates to ISO strings
149
+ * Passes through primitives unchanged
150
+ */
151
+ const serializeValue = (value) => {
152
+ if (value === null || value === void 0) return value;
153
+ if (value instanceof Date) return value.toISOString();
154
+ if (typeof value === "object") return JSON.stringify(value);
155
+ return value;
156
+ };
157
+ /**
158
+ * Serializes all values in an object
159
+ */
160
+ const serializeRecord = (record) => {
161
+ const serialized = {};
162
+ for (const [key, value] of Object.entries(record)) {
163
+ if (value === void 0) continue;
164
+ serialized[key] = serializeValue(value);
165
+ }
166
+ return serialized;
167
+ };
168
+ const hasNullConflictValue = (conflictColumns, data) => conflictColumns.some((col) => data[col] === null);
169
+ const buildNullAwareUpsertLockKey = (table, conflictColumns, data) => {
170
+ const conflictValues = conflictColumns.map((col) => `${col}:${JSON.stringify(data[col])}`).join("|");
171
+ return `${url}:${table}:${conflictValues}`;
172
+ };
173
+ const validateUpsertConflictColumns = (table, conflictColumns, serializedData) => {
174
+ const missingColumns = conflictColumns.filter((col) => !(col in serializedData));
175
+ if (missingColumns.length > 0) throw new DatabaseError("Conflict columns missing from data", {
176
+ table,
177
+ conflictColumns,
178
+ missingColumns,
179
+ availableColumns: Object.keys(serializedData),
180
+ hint: "All columns specified in ON CONFLICT must be present in the data being inserted. Undefined values are filtered out during serialization - consider using null or an empty string instead."
181
+ });
182
+ };
183
+ const executeStandardUpsert = async (table, conflictColumns, serializedData) => {
184
+ const keys = Object.keys(serializedData);
185
+ const values = Object.values(serializedData);
186
+ const placeholders = keys.map(() => "?").join(", ");
187
+ const updateSet = keys.map((key) => `${key} = excluded.${key}`).join(", ");
188
+ const conflict = conflictColumns.join(", ");
189
+ const sql = `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders}) ON CONFLICT(${conflict}) DO UPDATE SET ${updateSet}`;
190
+ return {
191
+ operation: "upsert",
192
+ affected: (await client.execute({
193
+ sql,
194
+ args: values
195
+ })).rowsAffected
196
+ };
197
+ };
198
+ const executeNullAwareUpsertAttempt = async (executor, table, conflictColumns, serializedData) => {
199
+ const keys = Object.keys(serializedData);
200
+ const values = Object.values(serializedData);
201
+ const updateSet = keys.map((key) => `${key} = ?`).join(", ");
202
+ const whereClause = conflictColumns.map((col) => `${col} IS ?`).join(" AND ");
203
+ const whereValues = conflictColumns.map((col) => serializedData[col]);
204
+ const updateSql = `UPDATE ${table} SET ${updateSet} WHERE ${whereClause}`;
205
+ const updateResult = await executor.execute({
206
+ sql: updateSql,
207
+ args: [...values, ...whereValues]
208
+ });
209
+ if ((updateResult.rowsAffected ?? 0) > 0) return {
210
+ operation: "upsert",
211
+ affected: updateResult.rowsAffected
212
+ };
213
+ const placeholders = keys.map(() => "?").join(", ");
214
+ const insertSql = `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders})`;
215
+ return {
216
+ operation: "upsert",
217
+ affected: (await executor.execute({
218
+ sql: insertSql,
219
+ args: values
220
+ })).rowsAffected
221
+ };
222
+ };
223
+ const executeNullAwareUpsertWithRetry = async (table, conflictColumns, serializedData) => {
224
+ if (url === ":memory:") return executeNullAwareUpsertAttempt(client, table, conflictColumns, serializedData);
225
+ let lastError;
226
+ for (let attempt = 0; attempt < NULL_AWARE_UPSERT_MAX_ATTEMPTS; attempt++) {
227
+ let transaction;
228
+ try {
229
+ transaction = await client.transaction("write");
230
+ const result = await executeNullAwareUpsertAttempt(transaction, table, conflictColumns, serializedData);
231
+ await transaction.commit();
232
+ return result;
233
+ } catch (error) {
234
+ lastError = error;
235
+ if (transaction && !transaction.closed) try {
236
+ await transaction.rollback();
237
+ } catch (_rollbackError) {}
238
+ } finally {
239
+ transaction?.close();
240
+ }
241
+ if (lastError && attempt < NULL_AWARE_UPSERT_MAX_ATTEMPTS - 1 && isRetriableSqliteTransactionError(lastError)) {
242
+ await delay(NULL_AWARE_UPSERT_BASE_DELAY_MS * 2 ** attempt);
243
+ continue;
244
+ }
245
+ break;
246
+ }
247
+ throw lastError;
248
+ };
249
+ const executeUpsert = async (table, conflictColumns, data, options, acquireTransaction) => {
250
+ const serializedData = serializeRecord(data);
251
+ validateUpsertConflictColumns(table, conflictColumns, serializedData);
252
+ if (options?.nullsDistinct || !hasNullConflictValue(conflictColumns, serializedData)) return executeStandardUpsert(table, conflictColumns, serializedData);
253
+ if (!acquireTransaction) return executeNullAwareUpsertAttempt(client, table, conflictColumns, serializedData);
254
+ return withNullAwareUpsertLock(buildNullAwareUpsertLockKey(table, conflictColumns, serializedData), () => executeNullAwareUpsertWithRetry(table, conflictColumns, serializedData));
255
+ };
256
+ /**
257
+ * Inserts one or more records into a table
258
+ *
259
+ * @param table - Table name
260
+ * @param data - Single record or array of records to insert
261
+ * @returns Promise resolving to operation result
262
+ * @throws Error if the insert operation fails
263
+ */
264
+ const insert = async (table, data) => {
265
+ let sql;
266
+ let values;
267
+ if (Array.isArray(data)) {
268
+ const serializedRecords = data.map((record) => serializeRecord(record));
269
+ const keys = Object.keys(serializedRecords[0]);
270
+ const placeholders = serializedRecords.map(() => `(${keys.map(() => "?").join(", ")})`).join(", ");
271
+ sql = `INSERT INTO ${table} (${keys.join(", ")}) VALUES ${placeholders}`;
272
+ const flattenedValues = [];
273
+ for (const record of serializedRecords) flattenedValues.push(...Object.values(record));
274
+ values = flattenedValues;
275
+ } else {
276
+ const serializedData = serializeRecord(data);
277
+ const keys = Object.keys(serializedData);
278
+ const placeholders = keys.map(() => "?").join(", ");
279
+ sql = `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders})`;
280
+ values = Object.values(serializedData);
281
+ }
282
+ try {
283
+ return {
284
+ operation: "insert",
285
+ affected: (await client.execute({
286
+ sql,
287
+ args: values
288
+ })).rowsAffected
289
+ };
290
+ } catch (e) {
291
+ throw new DatabaseError("Failed to insert records into table", {
292
+ table,
293
+ sql,
294
+ values,
295
+ originalError: formatDbError(e)
296
+ });
297
+ }
298
+ };
299
+ /**
300
+ * Retrieves a single record matching the where criteria
301
+ *
302
+ * @param table - Table name
303
+ * @param where - Criteria to match records
304
+ * @returns Promise resolving to matching record or null if not found
305
+ * @throws Error if the query fails
306
+ */
307
+ const get = async (table, where) => {
308
+ const { sql: whereClause, values } = buildWhere(where, 1, "sqlite");
309
+ if (!whereClause) throw new DatabaseError("GET requires at least one WHERE condition to prevent returning an arbitrary record", { table });
310
+ const sql = `SELECT * FROM ${table} ${whereClause}`;
311
+ try {
312
+ return (await client.execute({
313
+ sql,
314
+ args: values
315
+ })).rows[0] || null;
316
+ } catch (e) {
317
+ throw new DatabaseError("Failed to retrieve record from table", {
318
+ table,
319
+ sql,
320
+ values,
321
+ originalError: formatDbError(e)
322
+ });
323
+ }
324
+ };
325
+ /**
326
+ * Retrieves multiple records matching the where criteria
327
+ *
328
+ * @param table - Table name
329
+ * @param where - Criteria to match records
330
+ * @returns Promise resolving to array of matching records
331
+ * @throws Error if the query fails
332
+ */
333
+ const list = async (table, where) => {
334
+ const { sql: whereClause, values } = buildWhere(where, 1, "sqlite");
335
+ const sql = `SELECT * FROM ${table} ${whereClause}`;
336
+ try {
337
+ return (await client.execute({
338
+ sql,
339
+ args: values
340
+ })).rows;
341
+ } catch (e) {
342
+ throw new DatabaseError("Failed to list records from table", {
343
+ table,
344
+ sql,
345
+ values,
346
+ originalError: formatDbError(e)
347
+ });
348
+ }
349
+ };
350
+ /**
351
+ * Updates records matching the where criteria
352
+ *
353
+ * @param table - Table name
354
+ * @param where - Criteria to match records to update
355
+ * @param data - New data to set
356
+ * @returns Promise resolving to operation result
357
+ * @throws Error if the update operation fails
358
+ */
359
+ const update = async (table, where, data) => {
360
+ const serializedData = serializeRecord(data);
361
+ const keys = Object.keys(serializedData);
362
+ const values = Object.values(serializedData);
363
+ const setClause = keys.map((key, i) => `${key} = $${i + 1}`).join(", ");
364
+ const { sql: whereClause, values: whereValues } = buildWhere(where, values.length + 1, "sqlite");
365
+ if (!whereClause) throw new DatabaseError("UPDATE requires at least one WHERE condition to prevent accidental update of all records", { table });
366
+ const sql = `UPDATE ${table} SET ${setClause} ${whereClause}`;
367
+ try {
368
+ return {
369
+ operation: "update",
370
+ affected: (await client.execute({
371
+ sql,
372
+ args: [...values, ...whereValues]
373
+ })).rowsAffected
374
+ };
375
+ } catch (e) {
376
+ throw new DatabaseError("Failed to update records in table", {
377
+ table,
378
+ sql,
379
+ values: [...values, ...whereValues],
380
+ originalError: formatDbError(e)
381
+ });
382
+ }
383
+ };
384
+ /**
385
+ * Inserts a record or updates it if it already exists (UPSERT)
386
+ *
387
+ * @param table - Table name
388
+ * @param conflictColumns - Columns that define the uniqueness constraint
389
+ * @param data - Data to insert or update
390
+ * @returns Promise resolving to operation result
391
+ * @throws Error if the upsert operation fails
392
+ */
393
+ const upsert = async (table, conflictColumns, data, options) => {
394
+ try {
395
+ return await executeUpsert(table, conflictColumns, data, options, true);
396
+ } catch (e) {
397
+ if (e instanceof DatabaseError && e.message === "Conflict columns missing from data") throw e;
398
+ throw new DatabaseError("Failed to upsert record into table", {
399
+ table,
400
+ values: data,
401
+ conflictColumns,
402
+ originalError: formatDbError(e)
403
+ });
404
+ }
405
+ };
406
+ const upsertInCurrentTransaction = async (table, conflictColumns, data, options) => {
407
+ try {
408
+ return await executeUpsert(table, conflictColumns, data, options, false);
409
+ } catch (e) {
410
+ if (e instanceof DatabaseError && e.message === "Conflict columns missing from data") throw e;
411
+ throw new DatabaseError("Failed to upsert record into table", {
412
+ table,
413
+ values: data,
414
+ conflictColumns,
415
+ originalError: formatDbError(e)
416
+ });
417
+ }
418
+ };
419
+ /**
420
+ * Gets a record matching the where criteria or inserts it if not found
421
+ *
422
+ * @param table - Table name
423
+ * @param where - Criteria to match existing record
424
+ * @param data - Data to insert if no record found
425
+ * @returns Promise resolving to the record (either retrieved or newly inserted)
426
+ * @throws Error if the operation fails or if the record cannot be retrieved after insert
427
+ */
428
+ const getOrInsert = async (table, where, data) => {
429
+ const result = await get(table, where);
430
+ if (result) return result;
431
+ await insert(table, data);
432
+ const inserted = await get(table, where);
433
+ if (!inserted) throw new DatabaseError("Failed to insert and retrieve record", {
434
+ table,
435
+ where,
436
+ data
437
+ });
438
+ return inserted;
439
+ };
440
+ /**
441
+ * Deletes records from a table matching the where criteria
442
+ *
443
+ * @param table - Table name
444
+ * @param where - Criteria to match records for deletion
445
+ * @returns Promise resolving to operation result with count of deleted rows
446
+ * @throws Error if the delete operation fails
447
+ */
448
+ const deleteRecords = async (table, where) => {
449
+ validateTableName(table);
450
+ if (Object.keys(where).length === 0) throw new DatabaseError("DELETE requires at least one WHERE condition to prevent accidental deletion of all records", { table });
451
+ const { sql: whereClause, values } = buildWhere(where, 1, "sqlite");
452
+ try {
453
+ await client.execute({
454
+ sql: `DELETE FROM ${table} ${whereClause}`,
455
+ args: values
456
+ });
457
+ return {
458
+ operation: "delete",
459
+ affected: 1
460
+ };
461
+ } catch (e) {
462
+ throw new DatabaseError("Failed to delete records from table", {
463
+ table,
464
+ where,
465
+ originalError: e
466
+ });
467
+ }
468
+ };
469
+ /**
470
+ * Counts records in a table matching the where criteria
471
+ *
472
+ * @param table - Table name
473
+ * @param where - Criteria to match records (optional, counts all if omitted)
474
+ * @returns Promise resolving to count of matching records
475
+ * @throws Error if the count operation fails
476
+ */
477
+ const count = async (table, where) => {
478
+ validateTableName(table);
479
+ try {
480
+ if (!where || Object.keys(where).length === 0) {
481
+ const result = await pluck`SELECT COUNT(*) FROM ${table}`;
482
+ return Number(result) || 0;
483
+ }
484
+ const { sql: whereClause, values } = buildWhere(where, 1, "sqlite");
485
+ const result = await client.execute({
486
+ sql: `SELECT COUNT(*) as count FROM ${table} ${whereClause}`,
487
+ args: values
488
+ });
489
+ return Number(result.rows[0]?.count) || 0;
490
+ } catch (e) {
491
+ throw new DatabaseError("Failed to count records in table", {
492
+ table,
493
+ where,
494
+ originalError: e
495
+ });
496
+ }
497
+ };
498
+ /**
499
+ * Checks if a table exists in the database
500
+ *
501
+ * @param tableName - Name of the table to check
502
+ * @returns Promise resolving to boolean indicating if the table exists
503
+ */
504
+ const tableExists = async (tableName) => {
505
+ return !!await pluck`SELECT name FROM sqlite_master WHERE type='table' AND name=${tableName}`;
506
+ };
507
+ /**
508
+ * Synchronizes database schema with provided SQL DDL
509
+ * Creates tables if they don't exist and adds missing columns
510
+ *
511
+ * @param schema - SQL schema definition with CREATE TABLE statements
512
+ * @returns Promise that resolves when schema is synchronized
513
+ */
514
+ const syncSchema = async (schema) => {
515
+ console.log("[sqlite.syncSchema] Starting schema sync for dbid:", options.dbid);
516
+ console.log("[sqlite.syncSchema] Schema length:", schema.length, "chars");
517
+ const reservedKeywords = /* @__PURE__ */ new Set([
518
+ "references",
519
+ "order",
520
+ "group",
521
+ "table",
522
+ "index",
523
+ "select",
524
+ "from",
525
+ "where",
526
+ "join",
527
+ "left",
528
+ "right",
529
+ "inner",
530
+ "outer",
531
+ "on",
532
+ "as",
533
+ "and",
534
+ "or",
535
+ "not",
536
+ "like",
537
+ "in",
538
+ "between",
539
+ "case",
540
+ "when",
541
+ "then",
542
+ "else",
543
+ "end",
544
+ "union",
545
+ "distinct",
546
+ "having",
547
+ "limit",
548
+ "offset",
549
+ "default",
550
+ "check",
551
+ "unique",
552
+ "primary",
553
+ "foreign",
554
+ "key",
555
+ "constraint",
556
+ "cascade"
557
+ ]);
558
+ let normalizedSchema = schema.replace(/\s+/g, " ").replace(/DEFAULT CAST\(([^)]+)\s+AS\s+\w+\)/gi, "DEFAULT $1").trim();
559
+ normalizedSchema = normalizedSchema.replace(/"(\w+)"/g, (match, identifier) => {
560
+ return reservedKeywords.has(identifier.toLowerCase()) ? match : identifier;
561
+ });
562
+ console.log("[sqlite.syncSchema] Normalized schema:", normalizedSchema);
563
+ const commands = normalizedSchema.split(";").map((cmd) => cmd.trim()).filter((cmd) => cmd.length > 0);
564
+ console.log("[sqlite.syncSchema] Found", commands.length, "commands to process");
565
+ for (const command of commands) try {
566
+ console.log("[sqlite.syncSchema] Executing:", `${command.substring(0, 50)}...`);
567
+ await client.execute(command);
568
+ console.log("[sqlite.syncSchema] Successfully executed command");
569
+ } catch (error) {
570
+ console.error("[sqlite.syncSchema] Failed to execute command:", command);
571
+ console.error("[sqlite.syncSchema] Error:", error);
572
+ throw error;
573
+ }
574
+ };
575
+ /**
576
+ * Executes a callback within a database transaction
577
+ * Automatically commits on success or rolls back on error
578
+ *
579
+ * @param callback - Function to execute within transaction
580
+ * @returns Promise resolving to callback result
581
+ */
582
+ const transaction = async (callback) => {
583
+ try {
584
+ await client.execute({
585
+ sql: "BEGIN TRANSACTION",
586
+ args: []
587
+ });
588
+ const result = await callback({
589
+ url,
590
+ client,
591
+ insert,
592
+ get,
593
+ list,
594
+ update,
595
+ upsert: upsertInCurrentTransaction,
596
+ getOrInsert,
597
+ delete: deleteRecords,
598
+ count,
599
+ table,
600
+ many,
601
+ single,
602
+ pluck,
603
+ execute,
604
+ query,
605
+ oo: many,
606
+ oO: single,
607
+ ox: pluck,
608
+ xx: execute,
609
+ tableExists,
610
+ syncSchema,
611
+ transaction
612
+ });
613
+ await client.execute({
614
+ sql: "COMMIT",
615
+ args: []
616
+ });
617
+ return result;
618
+ } catch (error) {
619
+ await client.execute({
620
+ sql: "ROLLBACK",
621
+ args: []
622
+ });
623
+ throw error;
624
+ }
625
+ };
626
+ /**
627
+ * Begins a new transaction and returns a handle for manual control
628
+ *
629
+ * Unlike transaction(), this gives you explicit control over commit/rollback.
630
+ * Ideal for test isolation where you want to rollback after each test.
631
+ *
632
+ * @returns Promise resolving to a TransactionHandle
633
+ */
634
+ const beginTransaction = async () => {
635
+ await client.execute({
636
+ sql: "BEGIN TRANSACTION",
637
+ args: []
638
+ });
639
+ let active = true;
640
+ const commit = async () => {
641
+ if (!active) throw new DatabaseError("Transaction already ended", {});
642
+ await client.execute({
643
+ sql: "COMMIT",
644
+ args: []
645
+ });
646
+ active = false;
647
+ };
648
+ const rollback = async () => {
649
+ if (!active) throw new DatabaseError("Transaction already ended", {});
650
+ await client.execute({
651
+ sql: "ROLLBACK",
652
+ args: []
653
+ });
654
+ active = false;
655
+ };
656
+ const isActive = () => active;
657
+ return {
658
+ url,
659
+ client,
660
+ insert,
661
+ get,
662
+ list,
663
+ update,
664
+ upsert: upsertInCurrentTransaction,
665
+ getOrInsert,
666
+ delete: deleteRecords,
667
+ count,
668
+ table,
669
+ many,
670
+ single,
671
+ pluck,
672
+ execute,
673
+ query,
674
+ oo: many,
675
+ oO: single,
676
+ ox: pluck,
677
+ xx: execute,
678
+ tableExists,
679
+ syncSchema,
680
+ transaction,
681
+ commit,
682
+ rollback,
683
+ isActive
684
+ };
685
+ };
686
+ /**
687
+ * Creates a table-specific interface for simplified table operations
688
+ *
689
+ * @param tableName - Table name
690
+ * @returns TableInterface for the specified table
691
+ */
692
+ const table = (tableName) => ({
693
+ insert: (data) => insert(tableName, data),
694
+ get: (where) => get(tableName, where),
695
+ list: (where) => list(tableName, where)
696
+ });
697
+ /**
698
+ * Parses a tagged template literal into a SQL query and values
699
+ *
700
+ * @param strings - Template strings
701
+ * @param vars - Variables to interpolate into the query
702
+ * @returns Object with SQL query and values array
703
+ */
704
+ const parseTemplate = (strings, ...vars) => {
705
+ let sql = strings[0];
706
+ const values = [];
707
+ for (let i = 0; i < vars.length; i++) {
708
+ values.push(vars[i]);
709
+ sql += `?${strings[i + 1]}`;
710
+ }
711
+ return {
712
+ sql,
713
+ values
714
+ };
715
+ };
716
+ /**
717
+ * Executes a SQL query using template literals and returns a single value
718
+ *
719
+ * @param strings - Template strings
720
+ * @param vars - Variables to interpolate into the query
721
+ * @returns Promise resolving to a single value (first column of first row)
722
+ * @throws Error if the query fails
723
+ */
724
+ const pluck = async (strings, ...vars) => {
725
+ const { sql, values } = parseTemplate(strings, ...vars);
726
+ try {
727
+ const result = await client.execute({
728
+ sql,
729
+ args: values
730
+ });
731
+ return result.rows[0]?.[Object.keys(result.rows[0])[0]] ?? null;
732
+ } catch (e) {
733
+ throw new DatabaseError("Failed to execute pluck query", {
734
+ sql,
735
+ values,
736
+ originalError: formatDbError(e)
737
+ });
738
+ }
739
+ };
740
+ /**
741
+ * Executes a SQL query using template literals and returns a single row
742
+ *
743
+ * @param strings - Template strings
744
+ * @param vars - Variables to interpolate into the query
745
+ * @returns Promise resolving to a single result record or null
746
+ * @throws Error if the query fails
747
+ */
748
+ const single = async (strings, ...vars) => {
749
+ const { sql, values } = parseTemplate(strings, ...vars);
750
+ try {
751
+ return (await client.execute({
752
+ sql,
753
+ args: values
754
+ })).rows[0] || null;
755
+ } catch (e) {
756
+ throw new DatabaseError("Failed to execute single query", {
757
+ sql,
758
+ values,
759
+ originalError: formatDbError(e)
760
+ });
761
+ }
762
+ };
763
+ /**
764
+ * Executes a SQL query using template literals and returns multiple rows
765
+ *
766
+ * @param strings - Template strings
767
+ * @param vars - Variables to interpolate into the query
768
+ * @returns Promise resolving to array of result records
769
+ * @throws Error if the query fails
770
+ */
771
+ const many = async (strings, ...vars) => {
772
+ const { sql, values } = parseTemplate(strings, ...vars);
773
+ try {
774
+ return (await client.execute({
775
+ sql,
776
+ args: values
777
+ })).rows;
778
+ } catch (e) {
779
+ throw new DatabaseError("Failed to execute many query", {
780
+ sql,
781
+ values,
782
+ originalError: formatDbError(e)
783
+ });
784
+ }
785
+ };
786
+ /**
787
+ * Executes a SQL query using template literals without returning results
788
+ *
789
+ * @param strings - Template strings
790
+ * @param vars - Variables to interpolate into the query
791
+ * @returns Promise that resolves when the query completes
792
+ * @throws Error if the query fails
793
+ */
794
+ const execute = async (strings, ...vars) => {
795
+ const { sql, values } = parseTemplate(strings, ...vars);
796
+ try {
797
+ await client.execute({
798
+ sql,
799
+ args: values
800
+ });
801
+ } catch (e) {
802
+ throw new DatabaseError("Failed to execute query", {
803
+ sql,
804
+ values,
805
+ originalError: formatDbError(e)
806
+ });
807
+ }
808
+ };
809
+ /**
810
+ * Executes a raw SQL query with parameterized values
811
+ *
812
+ * @param str - SQL query string
813
+ * @param values - Variables to use as parameters
814
+ * @returns Promise resolving to query result with rows and metadata
815
+ * @throws Error if the query fails
816
+ */
817
+ const query = async (str, ...values) => {
818
+ const sql = str;
819
+ const args = Array.isArray(values[0]) ? values[0] : values;
820
+ try {
821
+ const result = await client.execute({
822
+ sql,
823
+ args
824
+ });
825
+ return {
826
+ command: sql.split(" ")[0].toUpperCase(),
827
+ rowCount: result.rowsAffected ?? result.rows.length,
828
+ oid: null,
829
+ fields: Object.keys(result.rows[0] || {}).map((name) => ({
830
+ name,
831
+ tableID: 0,
832
+ columnID: 0,
833
+ dataTypeID: 0,
834
+ dataTypeSize: -1,
835
+ dataTypeModifier: -1,
836
+ format: "text"
837
+ })),
838
+ rows: result.rows
839
+ };
840
+ } catch (e) {
841
+ throw new DatabaseError("Failed to execute raw query", {
842
+ sql,
843
+ args,
844
+ originalError: formatDbError(e)
845
+ });
846
+ }
847
+ };
848
+ const oo = many;
849
+ const oO = single;
850
+ const ox = pluck;
851
+ const xx = execute;
852
+ /**
853
+ * Initialize database schemas from JSON manifest
854
+ * Supports dependency resolution and schema overrides
855
+ *
856
+ * @param options - Schema initialization options
857
+ * @returns Promise that resolves when schemas are initialized
858
+ */
859
+ const initializeSchemas = async (options) => {
860
+ const schemaManager = new DatabaseSchemaManager();
861
+ const currentDb = {
862
+ url,
863
+ client,
864
+ query,
865
+ insert,
866
+ update,
867
+ upsert,
868
+ get,
869
+ list,
870
+ getOrInsert,
871
+ delete: deleteRecords,
872
+ count,
873
+ table,
874
+ tableExists,
875
+ many,
876
+ single,
877
+ pluck,
878
+ execute,
879
+ oo,
880
+ oO,
881
+ ox,
882
+ xx,
883
+ syncSchema,
884
+ transaction
885
+ };
886
+ await schemaManager.initializeSchemas(currentDb, options);
887
+ };
888
+ /**
889
+ * Retrieves the schema information for a table
890
+ *
891
+ * @param table - Table name
892
+ * @returns Promise resolving to table schema info or null if table doesn't exist
893
+ * @throws Error if the query fails
894
+ */
895
+ const getTableSchema = async (table) => {
896
+ validateTableName(table);
897
+ try {
898
+ if (!await tableExists(table)) return null;
899
+ const columnRows = await many`SELECT * FROM pragma_table_info(${table})`;
900
+ const columns = {};
901
+ for (const row of columnRows) {
902
+ const colName = row.name;
903
+ columns[colName] = {
904
+ type: row.type,
905
+ primaryKey: row.pk === 1,
906
+ notNull: row.notnull === 1,
907
+ defaultValue: row.dflt_value
908
+ };
909
+ }
910
+ const indexRows = await many`
911
+ SELECT name, sql FROM sqlite_master
912
+ WHERE type = 'index'
913
+ AND tbl_name = ${table}
914
+ AND name NOT LIKE 'sqlite_%'
915
+ `;
916
+ const indexes = [];
917
+ for (const row of indexRows) {
918
+ const indexName = row.name;
919
+ const indexInfoRows = await many`SELECT * FROM pragma_index_info(${indexName})`;
920
+ const indexColumns = [];
921
+ for (const infoRow of indexInfoRows) indexColumns.push(infoRow.name);
922
+ const indexListRow = await single`SELECT * FROM pragma_index_list(${table}) WHERE name = ${indexName}`;
923
+ indexes.push({
924
+ name: indexName,
925
+ columns: indexColumns,
926
+ unique: indexListRow?.unique === 1
927
+ });
928
+ }
929
+ const fkRows = await many`SELECT * FROM pragma_foreign_key_list(${table})`;
930
+ const foreignKeys = [];
931
+ for (const fkRow of fkRows) foreignKeys.push({
932
+ column: fkRow.from,
933
+ referencesTable: fkRow.table,
934
+ referencesColumn: fkRow.to,
935
+ onDelete: fkRow.on_delete,
936
+ onUpdate: fkRow.on_update
937
+ });
938
+ return {
939
+ tableName: table,
940
+ columns,
941
+ indexes,
942
+ foreignKeys
943
+ };
944
+ } catch (e) {
945
+ throw new DatabaseError("Failed to retrieve table schema", {
946
+ table,
947
+ originalError: formatDbError(e)
948
+ });
949
+ }
950
+ };
951
+ return {
952
+ url,
953
+ client,
954
+ query,
955
+ insert,
956
+ update,
957
+ upsert,
958
+ get,
959
+ list,
960
+ getOrInsert,
961
+ delete: deleteRecords,
962
+ count,
963
+ table,
964
+ tableExists,
965
+ many,
966
+ single,
967
+ pluck,
968
+ execute,
969
+ oo,
970
+ oO,
971
+ ox,
972
+ xx,
973
+ syncSchema,
974
+ initializeSchemas,
975
+ transaction,
976
+ beginTransaction,
977
+ getTableSchema,
978
+ alterTable: {
979
+ /**
980
+ * Adds a new column to an existing table
981
+ *
982
+ * @param table - Table name
983
+ * @param column - Column definition with name
984
+ * @returns Promise that resolves when column is added
985
+ * @throws Error if the alter operation fails
986
+ */
987
+ addColumn: async (table, column) => {
988
+ validateTableName(table);
989
+ validateColumnName(column.name);
990
+ try {
991
+ const sql = generateAddColumnStatement(table, column, "sqlite");
992
+ await client.execute({
993
+ sql,
994
+ args: []
995
+ });
996
+ } catch (e) {
997
+ throw new DatabaseError("Failed to add column to table", {
998
+ table,
999
+ column: column.name,
1000
+ originalError: formatDbError(e)
1001
+ });
1002
+ }
1003
+ },
1004
+ /**
1005
+ * Adds a new index to an existing table
1006
+ *
1007
+ * @param table - Table name
1008
+ * @param index - Index definition
1009
+ * @returns Promise that resolves when index is created
1010
+ * @throws Error if the create index operation fails
1011
+ */
1012
+ addIndex: async (table, index) => {
1013
+ validateTableName(table);
1014
+ validateIndexName(index.name);
1015
+ for (const col of index.columns) validateColumnName(col);
1016
+ try {
1017
+ const sql = generateCreateIndexStatement(table, index);
1018
+ await client.execute({
1019
+ sql,
1020
+ args: []
1021
+ });
1022
+ } catch (e) {
1023
+ throw new DatabaseError("Failed to create index on table", {
1024
+ table,
1025
+ index: index.name,
1026
+ originalError: formatDbError(e)
1027
+ });
1028
+ }
1029
+ }
1030
+ }
1031
+ };
1032
+ })();
1033
+ if (options.dbid) pendingConnections.set(options.dbid, connectionPromise);
1034
+ try {
1035
+ const db = await connectionPromise;
1036
+ if (options.dbid) memoryConnectionCache.set(options.dbid, db);
1037
+ return db;
1038
+ } finally {
1039
+ if (options.dbid) pendingConnections.delete(options.dbid);
1040
+ }
1041
+ }
1042
+ //#endregion
1043
+ export { getDatabase };
1044
+
1045
+ //# sourceMappingURL=sqlite-YgU9LCVy.js.map