@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.
- package/AGENT.md +1 -0
- package/README.md +143 -9
- package/dist/chunks/{connection-cache-BU3GBQxu.js → connection-cache-Da5UcY5u.js} +116 -4
- package/dist/chunks/{connection-cache-BU3GBQxu.js.map → connection-cache-Da5UcY5u.js.map} +1 -1
- package/dist/chunks/{duckdb-hbOqNSuA.js → duckdb-DfikVgov.js} +3 -3
- package/dist/chunks/{duckdb-hbOqNSuA.js.map → duckdb-DfikVgov.js.map} +1 -1
- package/dist/chunks/sqlite-Cz2NMiDs.js +2619 -0
- package/dist/chunks/sqlite-Cz2NMiDs.js.map +1 -0
- package/dist/chunks/{sqlite-connection-cache-Cnsr2tuN.js → sqlite-connection-cache-BAakj2HO.js} +9 -4
- package/dist/chunks/sqlite-connection-cache-BAakj2HO.js.map +1 -0
- package/dist/chunks/{sqlite-native-CdNLXZ60.js → sqlite-native-ByGwGEbS.js} +4 -4
- package/dist/chunks/{sqlite-native-CdNLXZ60.js.map → sqlite-native-ByGwGEbS.js.map} +1 -1
- package/dist/chunks/{types-uVFHgaSU.js → types-CwWJ-yju.js} +8 -2
- package/dist/chunks/types-CwWJ-yju.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +185 -15
- package/dist/index.js.map +1 -1
- package/dist/json.d.ts.map +1 -1
- package/dist/secure-sqlite-client.d.ts +81 -0
- package/dist/secure-sqlite-client.d.ts.map +1 -0
- package/dist/shared/connection-cache.d.ts +13 -1
- package/dist/shared/connection-cache.d.ts.map +1 -1
- package/dist/shared/sqlite-connection-cache.d.ts +1 -1
- package/dist/shared/sqlite-connection-cache.d.ts.map +1 -1
- package/dist/shared/transaction-lock.d.ts +2 -0
- package/dist/shared/transaction-lock.d.ts.map +1 -1
- package/dist/shared/types.d.ts +6 -2
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/sqlite.d.ts +35 -0
- package/dist/sqlite.d.ts.map +1 -1
- package/package.json +4 -3
- package/dist/chunks/sqlite-D3sUjQ5Q.js +0 -1136
- package/dist/chunks/sqlite-D3sUjQ5Q.js.map +0 -1
- package/dist/chunks/sqlite-connection-cache-Cnsr2tuN.js.map +0 -1
- package/dist/chunks/types-uVFHgaSU.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { _ as
|
|
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 {
|
|
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, () =>
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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-
|
|
3379
|
-
if (options.type === "duckdb") return (await import("./chunks/duckdb-
|
|
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
|
}
|