@happyvertical/sql 0.89.2 → 0.89.4

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 (36) hide show
  1. package/AGENT.md +1 -0
  2. package/README.md +143 -9
  3. package/dist/chunks/{connection-cache-BU3GBQxu.js → connection-cache-Da5UcY5u.js} +116 -4
  4. package/dist/chunks/{connection-cache-BU3GBQxu.js.map → connection-cache-Da5UcY5u.js.map} +1 -1
  5. package/dist/chunks/{duckdb-hbOqNSuA.js → duckdb-DfikVgov.js} +3 -3
  6. package/dist/chunks/{duckdb-hbOqNSuA.js.map → duckdb-DfikVgov.js.map} +1 -1
  7. package/dist/chunks/sqlite-Cz2NMiDs.js +2619 -0
  8. package/dist/chunks/sqlite-Cz2NMiDs.js.map +1 -0
  9. package/dist/chunks/{sqlite-connection-cache-Cnsr2tuN.js → sqlite-connection-cache-BAakj2HO.js} +9 -4
  10. package/dist/chunks/sqlite-connection-cache-BAakj2HO.js.map +1 -0
  11. package/dist/chunks/{sqlite-native-CdNLXZ60.js → sqlite-native-ByGwGEbS.js} +4 -4
  12. package/dist/chunks/{sqlite-native-CdNLXZ60.js.map → sqlite-native-ByGwGEbS.js.map} +1 -1
  13. package/dist/chunks/{types-uVFHgaSU.js → types-CwWJ-yju.js} +8 -2
  14. package/dist/chunks/types-CwWJ-yju.js.map +1 -0
  15. package/dist/index.d.ts +2 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +185 -15
  18. package/dist/index.js.map +1 -1
  19. package/dist/json.d.ts.map +1 -1
  20. package/dist/secure-sqlite-client.d.ts +81 -0
  21. package/dist/secure-sqlite-client.d.ts.map +1 -0
  22. package/dist/shared/connection-cache.d.ts +13 -1
  23. package/dist/shared/connection-cache.d.ts.map +1 -1
  24. package/dist/shared/sqlite-connection-cache.d.ts +1 -1
  25. package/dist/shared/sqlite-connection-cache.d.ts.map +1 -1
  26. package/dist/shared/transaction-lock.d.ts +2 -0
  27. package/dist/shared/transaction-lock.d.ts.map +1 -1
  28. package/dist/shared/types.d.ts +6 -2
  29. package/dist/shared/types.d.ts.map +1 -1
  30. package/dist/sqlite.d.ts +35 -0
  31. package/dist/sqlite.d.ts.map +1 -1
  32. package/package.json +4 -3
  33. package/dist/chunks/sqlite-D3sUjQ5Q.js +0 -1136
  34. package/dist/chunks/sqlite-D3sUjQ5Q.js.map +0 -1
  35. package/dist/chunks/sqlite-connection-cache-Cnsr2tuN.js.map +0 -1
  36. package/dist/chunks/types-uVFHgaSU.js.map +0 -1
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
- import { _ as resolveInsertColumns, a as generateAddColumnStatement, c as validateColumnName$1, d as validateTableName$1, f as DatabaseSchemaManager, g as raw, h as parseConditionKey, i as escapeStringLiteral, l as validateColumnNames, m as formatDbError, n as validateDatabaseCacheOptions, o as generateCreateIndexStatement, p as buildWhere, r as escapeQuotedIdentifier, s as quotePostgresColumnName, t as ConnectionCache, u as validateIndexName, v as unwrapRawKey, y as wrapDatabaseError } from "./chunks/connection-cache-BU3GBQxu.js";
1
+ import { _ as raw, a as escapeStringLiteral, b as wrapDatabaseError, c as quotePostgresColumnName, d as validateIndexName, f as validateTableName$1, g as parseConditionKey, h as formatDbError, i as escapeQuotedIdentifier, l as validateColumnName$1, m as buildWhere, n as enableCachedValidation, o as generateAddColumnStatement, p as DatabaseSchemaManager, r as validateDatabaseCacheOptions, s as generateCreateIndexStatement, t as ConnectionCache, u as validateColumnNames, v as resolveInsertColumns, y as unwrapRawKey } from "./chunks/connection-cache-Da5UcY5u.js";
2
2
  import { n as throwWithDuckDBCleanup, r as convertUniqueIndexesToInlineConstraints, t as createDuckDBResourceCloser } from "./chunks/duckdb-resources-Cci6Lo6H.js";
3
3
  import { a as dropPostgresDatabase, c as postgresEnvFromUrl, i as databaseNameFromUrl, l as redactDatabaseUrl, n as assertCanImportDatabase, o as dumpPostgresDatabase, r as createPostgresDatabase, s as isLocalDatabaseUrl, t as assertCanExportDatabase, u as restorePostgresDatabase } from "./chunks/postgres-cli-DzZcXSIG.js";
4
4
  import { t as redactDatabaseUrl$1 } from "./chunks/redact-database-url-CfMDvSdy.js";
5
- import { n as resolveSchemas, r as createTransactionLock, t as NestedTransactionError } from "./chunks/types-uVFHgaSU.js";
5
+ import { i as createTransactionLock, n as resolveSchemas, t as NestedTransactionError } from "./chunks/types-CwWJ-yju.js";
6
6
  import { DatabaseError, loadEnvConfig } from "@happyvertical/utils";
7
- import { mkdir, readdir } from "node:fs/promises";
7
+ import { mkdir, readdir, stat } from "node:fs/promises";
8
8
  import { basename, extname, join } from "node:path";
9
9
  import { AsyncLocalStorage } from "node:async_hooks";
10
10
  import { webcrypto } from "node:crypto";
@@ -316,6 +316,130 @@ var json_exports = /* @__PURE__ */ __exportAll({
316
316
  */
317
317
  globalThis.__haveSqlJSONConnectionCache ??= new ConnectionCache();
318
318
  var memoryConnectionCache = globalThis.__haveSqlJSONConnectionCache;
319
+ enableCachedValidation(memoryConnectionCache);
320
+ var JSON_SOURCE_CACHE_STATE = Symbol.for("@happyvertical/sql/json-source-cache-state/v1");
321
+ var SOURCE_SNAPSHOT_ATTEMPTS = 3;
322
+ function isMissingPathError(error) {
323
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
324
+ }
325
+ async function listJSONSourceFiles(dataDir) {
326
+ try {
327
+ return (await readdir(dataDir)).filter((file) => {
328
+ const lower = file.toLowerCase();
329
+ return lower.endsWith(".json") || lower.endsWith(".schema.sql");
330
+ }).sort();
331
+ } catch (error) {
332
+ if (isMissingPathError(error)) return null;
333
+ throw error;
334
+ }
335
+ }
336
+ /**
337
+ * Captures the JSON adapter's file-backed inputs without reading their contents.
338
+ * Directory membership plus nanosecond timestamps, size and inode detect edits,
339
+ * replacements, additions and removals while keeping cache hits inexpensive.
340
+ */
341
+ async function getJSONSourceFingerprint(dataDir) {
342
+ const statFiles = (files) => Promise.all(files.map(async (file) => {
343
+ const info = await stat(join(dataDir, file), { bigint: true });
344
+ return [
345
+ file,
346
+ info.size.toString(),
347
+ info.mtimeNs.toString(),
348
+ info.ctimeNs.toString(),
349
+ info.ino.toString()
350
+ ];
351
+ }));
352
+ const sameFiles = (left, right) => right !== null && left.length === right.length && left.every((file, index) => file === right[index]);
353
+ for (let attempt = 0; attempt < SOURCE_SNAPSHOT_ATTEMPTS; attempt += 1) {
354
+ const filesBefore = await listJSONSourceFiles(dataDir);
355
+ if (filesBefore === null) return "directory:missing";
356
+ try {
357
+ const entriesBefore = await statFiles(filesBefore);
358
+ if (!sameFiles(filesBefore, await listJSONSourceFiles(dataDir))) continue;
359
+ const entriesAfter = await statFiles(filesBefore);
360
+ const filesAfter = await listJSONSourceFiles(dataDir);
361
+ const fingerprintAfter = JSON.stringify(entriesAfter);
362
+ if (sameFiles(filesBefore, filesAfter) && JSON.stringify(entriesBefore) === fingerprintAfter) return fingerprintAfter;
363
+ } catch (error) {
364
+ if (!isMissingPathError(error)) throw error;
365
+ }
366
+ }
367
+ throw new DatabaseError("JSON source files changed while checking connection cache freshness", { dataDir });
368
+ }
369
+ function getJSONSourceCacheState(db) {
370
+ return db[JSON_SOURCE_CACHE_STATE];
371
+ }
372
+ function setJSONSourceCacheState(db, state) {
373
+ Object.defineProperty(db, JSON_SOURCE_CACHE_STATE, {
374
+ configurable: false,
375
+ enumerable: false,
376
+ value: state,
377
+ writable: false
378
+ });
379
+ }
380
+ async function waitForJSONSourceWrites(state) {
381
+ while (state.activeWrites.size > 0) await Promise.allSettled([...state.activeWrites]);
382
+ }
383
+ async function isCachedJSONDatabaseFresh(db) {
384
+ const state = getJSONSourceCacheState(db);
385
+ if (!state) return false;
386
+ if (!state.trackExternalChanges) return true;
387
+ while (true) {
388
+ await waitForJSONSourceWrites(state);
389
+ const fingerprint = await getJSONSourceFingerprint(state.dataDir);
390
+ if (state.activeWrites.size === 0) return fingerprint === state.fingerprint;
391
+ }
392
+ }
393
+ async function trackJSONSourceWrite(db, table, write) {
394
+ const state = getJSONSourceCacheState(db);
395
+ if (!state) return write();
396
+ if (state.closing) throw new DatabaseError("JSON database connection is closing");
397
+ const previousWrite = state.sourceWriteTail;
398
+ let releaseWrite;
399
+ const currentWrite = new Promise((resolve) => {
400
+ releaseWrite = resolve;
401
+ });
402
+ state.sourceWriteTail = previousWrite.then(() => currentWrite, () => currentWrite);
403
+ const operation = (async () => {
404
+ await previousWrite.catch(() => {});
405
+ let succeeded = false;
406
+ let fingerprintBeforeWrite;
407
+ try {
408
+ if (state.trackExternalChanges) {
409
+ fingerprintBeforeWrite = await getJSONSourceFingerprint(state.dataDir);
410
+ if (fingerprintBeforeWrite !== state.fingerprint) {
411
+ state.fingerprint = void 0;
412
+ throw new DatabaseError("JSON source files changed externally before the adapter could persist its write; acquire a fresh database and retry", {
413
+ dataDir: state.dataDir,
414
+ table
415
+ });
416
+ }
417
+ }
418
+ const result = await write();
419
+ succeeded = true;
420
+ return result;
421
+ } finally {
422
+ if (succeeded && state.trackExternalChanges) try {
423
+ const fingerprintAfterWrite = await getJSONSourceFingerprint(state.dataDir);
424
+ const ownedFiles = /* @__PURE__ */ new Set([`${table}.json`, `${table}.schema.sql`]);
425
+ const excludeOwnedFiles = (fingerprint) => JSON.stringify(JSON.parse(fingerprint).filter(([file]) => !ownedFiles.has(file)));
426
+ state.fingerprint = fingerprintBeforeWrite !== void 0 && excludeOwnedFiles(fingerprintBeforeWrite) === excludeOwnedFiles(fingerprintAfterWrite) ? fingerprintAfterWrite : void 0;
427
+ } catch (error) {
428
+ state.fingerprint = void 0;
429
+ console.warn(`[json-adapter] Could not refresh source fingerprint: ${error instanceof Error ? error.message : String(error)}`);
430
+ }
431
+ else if (!succeeded) state.fingerprint = void 0;
432
+ releaseWrite();
433
+ }
434
+ })();
435
+ const activeWrite = operation.then(() => void 0, () => void 0);
436
+ state.activeWrites.add(activeWrite);
437
+ try {
438
+ return await operation;
439
+ } finally {
440
+ state.activeWrites.delete(activeWrite);
441
+ }
442
+ }
319
443
  /**
320
444
  * Clears and closes all cached connections.
321
445
  * Callers must await completion before opening a replacement connection.
@@ -746,13 +870,53 @@ function validateTableName(table) {
746
870
  */
747
871
  async function getDatabase$2(options) {
748
872
  const cacheKey = options.dbid || `json:${options.url}:${options.writeStrategy || "immediate"}:${options.autoRegister !== false}`;
749
- return memoryConnectionCache.getOrCreate(cacheKey, options, () => createJSONDatabase(options), closeJSONDatabase);
873
+ return memoryConnectionCache.getOrCreate(cacheKey, options, () => createTrackedJSONDatabase(options), closeJSONDatabase, isCachedJSONDatabaseFresh);
874
+ }
875
+ async function createTrackedJSONDatabase(options) {
876
+ const trackExternalChanges = options.autoRegister !== false;
877
+ const dataDir = options.url;
878
+ if (!trackExternalChanges || !dataDir) {
879
+ const db = await createJSONDatabase(options);
880
+ setJSONSourceCacheState(db, {
881
+ activeWrites: /* @__PURE__ */ new Set(),
882
+ closing: false,
883
+ dataDir: dataDir ?? "",
884
+ fingerprint: void 0,
885
+ sourceWriteTail: Promise.resolve(),
886
+ trackExternalChanges: false
887
+ });
888
+ return db;
889
+ }
890
+ for (let attempt = 0; attempt < SOURCE_SNAPSHOT_ATTEMPTS; attempt += 1) {
891
+ const before = await getJSONSourceFingerprint(dataDir);
892
+ const db = await createJSONDatabase(options);
893
+ try {
894
+ const after = await getJSONSourceFingerprint(dataDir);
895
+ if (before === after) {
896
+ setJSONSourceCacheState(db, {
897
+ activeWrites: /* @__PURE__ */ new Set(),
898
+ closing: false,
899
+ dataDir,
900
+ fingerprint: after,
901
+ sourceWriteTail: Promise.resolve(),
902
+ trackExternalChanges: true
903
+ });
904
+ return db;
905
+ }
906
+ } catch (error) {
907
+ await closeJSONDatabase(db);
908
+ throw error;
909
+ }
910
+ await closeJSONDatabase(db);
911
+ }
912
+ throw new DatabaseError("JSON source files did not remain stable while creating the database", { dataDir });
750
913
  }
751
914
  async function createJSONDatabase(options) {
752
915
  return (async () => {
753
916
  const { instance, connection, pendingJSONFiles, inferredSchemaTables, tableLoadErrors, reportLoadError } = await createJSONConnection(options);
754
917
  const writeStrategy = options.writeStrategy || "immediate";
755
918
  const { url } = options;
919
+ let db;
756
920
  const connectionLock = createTransactionLock("json", options.transactionQueueTimeout);
757
921
  /**
758
922
  * Inserts one or more records into a table
@@ -803,7 +967,7 @@ async function createJSONDatabase(options) {
803
967
  try {
804
968
  await connection.run(sql, values);
805
969
  const affected = records.length;
806
- if (writeStrategy === "immediate") await exportTableToJSON(connection, table, url);
970
+ if (writeStrategy === "immediate") await persistTableToJSON(table);
807
971
  return {
808
972
  operation: "insert",
809
973
  affected
@@ -891,7 +1055,7 @@ async function createJSONDatabase(options) {
891
1055
  const values = [...dataValues, ...whereValues];
892
1056
  try {
893
1057
  await connection.run(sql, values);
894
- if (writeStrategy === "immediate") await exportTableToJSON(connection, table, url);
1058
+ if (writeStrategy === "immediate") await persistTableToJSON(table);
895
1059
  return {
896
1060
  operation: "update",
897
1061
  affected: 1
@@ -940,7 +1104,7 @@ async function createJSONDatabase(options) {
940
1104
  const sql = `UPDATE "${table}" SET ${assignments.join(", ")} ${whereClause}`;
941
1105
  try {
942
1106
  await connection.run(sql, [...values, ...whereValues]);
943
- if (writeStrategy === "immediate") await exportTableToJSON(connection, table, url);
1107
+ if (writeStrategy === "immediate") await persistTableToJSON(table);
944
1108
  return {
945
1109
  operation: "upsert",
946
1110
  affected: 1
@@ -1045,7 +1209,7 @@ async function createJSONDatabase(options) {
1045
1209
  const sql = `INSERT INTO "${table}" (${quotedKeys}) VALUES (${placeholders.join(", ")}) ON CONFLICT(${conflict}) DO UPDATE SET ${quotedUpdateSetParts.join(", ")}`;
1046
1210
  try {
1047
1211
  await connection.run(sql, values);
1048
- if (writeStrategy === "immediate") await exportTableToJSON(connection, table, url);
1212
+ if (writeStrategy === "immediate") await persistTableToJSON(table);
1049
1213
  return {
1050
1214
  operation: "upsert",
1051
1215
  affected: 1
@@ -1108,7 +1272,7 @@ async function createJSONDatabase(options) {
1108
1272
  const sql = `DELETE FROM "${table}" ${whereClause}`;
1109
1273
  try {
1110
1274
  await connection.run(sql, values);
1111
- if (writeStrategy === "immediate") await exportTableToJSON(connection, table, url);
1275
+ if (writeStrategy === "immediate") await persistTableToJSON(table);
1112
1276
  return {
1113
1277
  operation: "delete",
1114
1278
  affected: 1
@@ -1221,6 +1385,7 @@ async function createJSONDatabase(options) {
1221
1385
  }).join(", ")} FROM "${table}") TO '${filePath}' (FORMAT JSON, ARRAY true)`;
1222
1386
  await connection.run(sql);
1223
1387
  };
1388
+ const persistTableToJSON = (table) => trackJSONSourceWrite(db, table, () => exportTableToJSON(connection, table, url));
1224
1389
  /**
1225
1390
  * Manual export method for 'manual' write strategy
1226
1391
  *
@@ -1234,7 +1399,7 @@ async function createJSONDatabase(options) {
1234
1399
  table,
1235
1400
  writeStrategy
1236
1401
  });
1237
- await exportTableToJSON(connection, table, url);
1402
+ await persistTableToJSON(table);
1238
1403
  };
1239
1404
  /**
1240
1405
  * Returns the table-load failures collected for this connection
@@ -1413,7 +1578,7 @@ async function createJSONDatabase(options) {
1413
1578
  try {
1414
1579
  await access(jsonFilePath);
1415
1580
  } catch {
1416
- await exportTableToJSON(connection, tableName, url);
1581
+ await persistTableToJSON(tableName);
1417
1582
  }
1418
1583
  } catch (error) {
1419
1584
  console.warn(`[json-adapter] Could not export newly created table ${tableName}: ${error instanceof Error ? error.message : String(error)}`);
@@ -1516,7 +1681,7 @@ async function createJSONDatabase(options) {
1516
1681
  try {
1517
1682
  await access(jsonFilePath);
1518
1683
  } catch {
1519
- await exportTableToJSON(connection, tableName, url);
1684
+ await persistTableToJSON(tableName);
1520
1685
  }
1521
1686
  } catch (error) {
1522
1687
  console.warn(`[json-adapter] Could not export newly created table ${tableName}: ${error instanceof Error ? error.message : String(error)}`);
@@ -1673,7 +1838,7 @@ async function createJSONDatabase(options) {
1673
1838
  };
1674
1839
  };
1675
1840
  const closeResources = createDuckDBResourceCloser(connection, instance);
1676
- const db = {
1841
+ db = {
1677
1842
  url,
1678
1843
  requiresSchemaCheck: true,
1679
1844
  client: connection,
@@ -1704,6 +1869,11 @@ async function createJSONDatabase(options) {
1704
1869
  inferSchemaFromJSON,
1705
1870
  getTableLoadErrors,
1706
1871
  close: async () => {
1872
+ const state = getJSONSourceCacheState(db);
1873
+ if (state) {
1874
+ state.closing = true;
1875
+ await waitForJSONSourceWrites(state);
1876
+ }
1707
1877
  memoryConnectionCache.forget(db);
1708
1878
  await closeResources();
1709
1879
  }
@@ -3375,8 +3545,8 @@ async function getDatabase(options = {}) {
3375
3545
  }));
3376
3546
  if (!options.type && (options.url?.startsWith("file:") || options.url === ":memory:")) options.type = "sqlite";
3377
3547
  if (options.type === "postgres") return (await Promise.resolve().then(() => postgres_exports)).getDatabase(options);
3378
- if (options.type === "sqlite") return (await import("./chunks/sqlite-D3sUjQ5Q.js")).getDatabase(options);
3379
- if (options.type === "duckdb") return (await import("./chunks/duckdb-hbOqNSuA.js")).getDatabase(options);
3548
+ if (options.type === "sqlite") return (await import("./chunks/sqlite-Cz2NMiDs.js")).getDatabase(options);
3549
+ if (options.type === "duckdb") return (await import("./chunks/duckdb-DfikVgov.js")).getDatabase(options);
3380
3550
  if (options.type === "json") return (await Promise.resolve().then(() => json_exports)).getDatabase(options);
3381
3551
  throw new Error("Invalid database type");
3382
3552
  }