@happyvertical/sql 0.89.3 → 0.89.5

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