@happyvertical/sql 0.89.3 → 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 +129 -9
- package/dist/chunks/{duckdb-Ba6QVk6i.js → duckdb-DfikVgov.js} +2 -2
- package/dist/chunks/{duckdb-Ba6QVk6i.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-DwlyWaqG.js → sqlite-connection-cache-BAakj2HO.js} +8 -3
- package/dist/chunks/sqlite-connection-cache-BAakj2HO.js.map +1 -0
- package/dist/chunks/{sqlite-native-DBbBMvTD.js → sqlite-native-ByGwGEbS.js} +3 -3
- package/dist/chunks/{sqlite-native-DBbBMvTD.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 +3 -3
- package/dist/index.js.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/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/sqlite.d.ts +35 -0
- package/dist/sqlite.d.ts.map +1 -1
- package/package.json +4 -3
- package/dist/chunks/sqlite-BPI6YDpW.js +0 -1136
- package/dist/chunks/sqlite-BPI6YDpW.js.map +0 -1
- package/dist/chunks/sqlite-connection-cache-DwlyWaqG.js.map +0 -1
- package/dist/chunks/types-uVFHgaSU.js.map +0 -1
|
@@ -0,0 +1,2619 @@
|
|
|
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 { i as createTransactionLock, n as resolveSchemas } from "./types-CwWJ-yju.js";
|
|
4
|
+
import { t as getCachedSqliteDatabase } from "./sqlite-connection-cache-BAakj2HO.js";
|
|
5
|
+
import { DatabaseError } from "@happyvertical/utils";
|
|
6
|
+
import { execFile } from "node:child_process";
|
|
7
|
+
import { lstat, open, unlink } from "node:fs/promises";
|
|
8
|
+
import { dirname, parse, relative, resolve, sep } from "node:path";
|
|
9
|
+
import { AsyncLocalStorage, createHook, executionAsyncId } from "node:async_hooks";
|
|
10
|
+
import { createHash } from "node:crypto";
|
|
11
|
+
import { fileURLToPath } from "node:url";
|
|
12
|
+
import { types } from "node:util";
|
|
13
|
+
//#region src/secure-sqlite-client.ts
|
|
14
|
+
/**
|
|
15
|
+
* @internal Parses the stable ACL section emitted by macOS `/bin/ls`.
|
|
16
|
+
* Returns true only when the ACL grants authority; deny-only ACLs are
|
|
17
|
+
* restrictive and do not weaken trusted-parent custody.
|
|
18
|
+
*/
|
|
19
|
+
function parseDarwinAclListing(listing) {
|
|
20
|
+
const permissionMarker = /^[bcdlps-][rwxStTs-]{9}([+@ ])/.exec(listing)?.[1];
|
|
21
|
+
if (!permissionMarker) throw new Error("macOS ACL inspection returned an unrecognized listing");
|
|
22
|
+
const aclEntries = listing.split("\n").slice(1).filter(Boolean);
|
|
23
|
+
if (aclEntries.length === 0) {
|
|
24
|
+
if (permissionMarker === "+") throw new Error("macOS ACL inspection returned no ACL entries");
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (permissionMarker !== "+" && permissionMarker !== "@") throw new Error("macOS ACL inspection returned entries without a marker");
|
|
28
|
+
let grantsAuthority = false;
|
|
29
|
+
for (const entry of aclEntries) {
|
|
30
|
+
const parsed = /^\s+\d+:\s+.+\s+(allow|deny)\s+[A-Za-z_,]+\s*$/.exec(entry);
|
|
31
|
+
if (!parsed) throw new Error("macOS ACL inspection returned an unrecognized entry");
|
|
32
|
+
if (parsed[1] === "allow") grantsAuthority = true;
|
|
33
|
+
}
|
|
34
|
+
return grantsAuthority;
|
|
35
|
+
}
|
|
36
|
+
function inspectDarwinAcl(filePath) {
|
|
37
|
+
return new Promise((resolveInspection, reject) => {
|
|
38
|
+
execFile("/bin/ls", [
|
|
39
|
+
"-lde",
|
|
40
|
+
"--",
|
|
41
|
+
filePath
|
|
42
|
+
], {
|
|
43
|
+
encoding: "utf8",
|
|
44
|
+
env: { LC_ALL: "C" },
|
|
45
|
+
timeout: 5e3
|
|
46
|
+
}, (error, stdout) => {
|
|
47
|
+
if (error) {
|
|
48
|
+
reject(error);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
try {
|
|
52
|
+
resolveInspection(parseDarwinAclListing(stdout));
|
|
53
|
+
} catch (parseError) {
|
|
54
|
+
reject(parseError);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
var defaultRuntime = {
|
|
60
|
+
platform: process.platform,
|
|
61
|
+
nodeVersion: process.versions.node,
|
|
62
|
+
currentUid: () => {
|
|
63
|
+
if (!process.getuid) throw new Error("process.getuid() is unavailable");
|
|
64
|
+
return process.getuid();
|
|
65
|
+
},
|
|
66
|
+
inspectDarwinAcl,
|
|
67
|
+
loadDriver: async () => {
|
|
68
|
+
return await import(
|
|
69
|
+
/* @vite-ignore */
|
|
70
|
+
"node:sqlite"
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
var GROUP_OR_WORLD_WRITE = 18;
|
|
75
|
+
var STICKY_BIT = 512;
|
|
76
|
+
var MINIMUM_NODE_VERSION = [
|
|
77
|
+
24,
|
|
78
|
+
18,
|
|
79
|
+
0
|
|
80
|
+
];
|
|
81
|
+
function validateNodeVersion(runtime) {
|
|
82
|
+
const version = runtime.nodeVersion ?? defaultRuntime.nodeVersion ?? "";
|
|
83
|
+
const match = /^v?(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(version);
|
|
84
|
+
const parsed = match?.slice(1, 4).map(Number);
|
|
85
|
+
const isPrerelease = Boolean(match?.[4]);
|
|
86
|
+
if (!parsed || parsed.some((part) => !Number.isSafeInteger(part)) || isPrerelease || parsed[0] < MINIMUM_NODE_VERSION[0] || parsed[0] === MINIMUM_NODE_VERSION[0] && parsed[1] < MINIMUM_NODE_VERSION[1] || parsed[0] === MINIMUM_NODE_VERSION[0] && parsed[1] === MINIMUM_NODE_VERSION[1] && parsed[2] < MINIMUM_NODE_VERSION[2]) throw new DatabaseError("Secure SQLite requires Node.js 24.18.0 or newer", {
|
|
87
|
+
actualVersion: version || "unknown",
|
|
88
|
+
minimumVersion: MINIMUM_NODE_VERSION.join("."),
|
|
89
|
+
hint: "Upgrade the runtime before requesting trusted-parent secure SQLite. Default LibSQL mode remains available on older supported runtimes."
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function pathComponents(path) {
|
|
93
|
+
const root = parse(path).root;
|
|
94
|
+
const components = path.slice(root.length).split(sep).filter(Boolean);
|
|
95
|
+
const paths = [root];
|
|
96
|
+
for (const component of components) paths.push(resolve(paths.at(-1) ?? root, component));
|
|
97
|
+
return paths;
|
|
98
|
+
}
|
|
99
|
+
async function validateDarwinAcl(filePath, runtime) {
|
|
100
|
+
if (runtime.platform !== "darwin") return;
|
|
101
|
+
let hasAcl;
|
|
102
|
+
try {
|
|
103
|
+
const inspect = runtime.inspectDarwinAcl ?? defaultRuntime.inspectDarwinAcl;
|
|
104
|
+
if (!inspect) throw new Error("macOS ACL inspector is unavailable");
|
|
105
|
+
hasAcl = await inspect(filePath);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
throw new DatabaseError("Secure SQLite acquisition cannot inspect macOS access control lists", {
|
|
108
|
+
path: filePath,
|
|
109
|
+
originalError: error instanceof Error ? error.message : String(error)
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (hasAcl) throw new DatabaseError("Secure SQLite custody path contains a permissive macOS access control list", {
|
|
113
|
+
path: filePath,
|
|
114
|
+
hint: "Remove ACL entries that grant authority from the application-custodied path before requesting secure acquisition."
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
async function validateTrustedParentCustody(filePath, options, runtime) {
|
|
118
|
+
if (options.custody !== "trusted-parent") throw new DatabaseError("Secure SQLite acquisition requires trusted-parent custody", {});
|
|
119
|
+
let currentUid;
|
|
120
|
+
try {
|
|
121
|
+
currentUid = (runtime.currentUid ?? defaultRuntime.currentUid)?.() ?? -1;
|
|
122
|
+
} catch (error) {
|
|
123
|
+
throw new DatabaseError("Secure SQLite acquisition cannot verify the current user", { originalError: error instanceof Error ? error.message : String(error) });
|
|
124
|
+
}
|
|
125
|
+
const databaseParent = dirname(filePath);
|
|
126
|
+
const custodyRoot = resolve(options.root ?? databaseParent);
|
|
127
|
+
const relativeDatabasePath = relative(custodyRoot, filePath);
|
|
128
|
+
if (!relativeDatabasePath || relativeDatabasePath === ".." || relativeDatabasePath.startsWith(`..${sep}`) || parse(relativeDatabasePath).root) throw new DatabaseError("Secure SQLite database must be beneath its trusted custody root", {
|
|
129
|
+
path: filePath,
|
|
130
|
+
custodyRoot
|
|
131
|
+
});
|
|
132
|
+
const custodyRelative = relative(custodyRoot, databaseParent);
|
|
133
|
+
const custodiedParents = /* @__PURE__ */ new Set([custodyRoot, ...pathComponents(databaseParent).filter((path) => {
|
|
134
|
+
const child = relative(custodyRoot, path);
|
|
135
|
+
return child === "" || !child.startsWith(`..${sep}`) && child !== "..";
|
|
136
|
+
})]);
|
|
137
|
+
if (custodyRelative === ".." || custodyRelative.startsWith(`..${sep}`) || parse(custodyRelative).root) throw new DatabaseError("Secure SQLite parent must be beneath its trusted custody root", {
|
|
138
|
+
path: databaseParent,
|
|
139
|
+
custodyRoot
|
|
140
|
+
});
|
|
141
|
+
for (const componentPath of pathComponents(databaseParent)) {
|
|
142
|
+
let stats;
|
|
143
|
+
try {
|
|
144
|
+
stats = await lstat(componentPath);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
throw new DatabaseError("Secure SQLite custody chain contains an inaccessible component", {
|
|
147
|
+
path: componentPath,
|
|
148
|
+
originalError: error instanceof Error ? error.message : String(error)
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
if (stats.isSymbolicLink() || !stats.isDirectory()) throw new DatabaseError("Secure SQLite custody chain must contain only real directories", { path: componentPath });
|
|
152
|
+
await validateDarwinAcl(componentPath, runtime);
|
|
153
|
+
const componentUid = runtime.pathOwnerUid?.(componentPath, stats.uid) ?? stats.uid;
|
|
154
|
+
if (custodiedParents.has(componentPath)) {
|
|
155
|
+
if (componentUid !== currentUid) throw new DatabaseError("Secure SQLite custody directory is not owned by the current user", {
|
|
156
|
+
path: componentPath,
|
|
157
|
+
expectedUid: currentUid,
|
|
158
|
+
actualUid: componentUid
|
|
159
|
+
});
|
|
160
|
+
if ((stats.mode & GROUP_OR_WORLD_WRITE) !== 0) throw new DatabaseError("Secure SQLite custody directory is group/world writable", { path: componentPath });
|
|
161
|
+
} else {
|
|
162
|
+
if (componentUid !== currentUid && componentUid !== 0) throw new DatabaseError("Secure SQLite ancestor is owned by an untrusted user", {
|
|
163
|
+
path: componentPath,
|
|
164
|
+
expectedUid: currentUid,
|
|
165
|
+
actualUid: componentUid
|
|
166
|
+
});
|
|
167
|
+
if ((stats.mode & GROUP_OR_WORLD_WRITE) !== 0 && (stats.mode & STICKY_BIT) === 0) throw new DatabaseError("Secure SQLite ancestor permits replacement by another principal", { path: componentPath });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
const leaf = await lstat(filePath);
|
|
172
|
+
if (leaf.isSymbolicLink() || !leaf.isFile()) throw new DatabaseError("Secure SQLite database leaf must be a regular file", { path: filePath });
|
|
173
|
+
const leafUid = runtime.pathOwnerUid?.(filePath, leaf.uid) ?? leaf.uid;
|
|
174
|
+
if (leafUid !== currentUid) throw new DatabaseError("Secure SQLite database leaf is not owned by the current user", {
|
|
175
|
+
path: filePath,
|
|
176
|
+
expectedUid: currentUid,
|
|
177
|
+
actualUid: leafUid
|
|
178
|
+
});
|
|
179
|
+
if ((leaf.mode & GROUP_OR_WORLD_WRITE) !== 0) throw new DatabaseError("Secure SQLite database leaf is group/world writable", { path: filePath });
|
|
180
|
+
await validateDarwinAcl(filePath, runtime);
|
|
181
|
+
return true;
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (error.code !== "ENOENT") throw error;
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function leafIdentity(stats) {
|
|
188
|
+
return {
|
|
189
|
+
birthtimeNs: stats.birthtimeNs,
|
|
190
|
+
ctimeNs: stats.ctimeNs,
|
|
191
|
+
dev: stats.dev,
|
|
192
|
+
ino: stats.ino
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
function sameLeafIdentity(left, right) {
|
|
196
|
+
return left.dev === right.dev && left.ino === right.ino && left.birthtimeNs === right.birthtimeNs && left.ctimeNs === right.ctimeNs;
|
|
197
|
+
}
|
|
198
|
+
async function createSecureLeaf(filePath, runtime) {
|
|
199
|
+
let handle;
|
|
200
|
+
try {
|
|
201
|
+
handle = await (runtime.openLeaf ?? open)(filePath, "wx", 384);
|
|
202
|
+
} catch (error) {
|
|
203
|
+
if (error.code === "EEXIST") return void 0;
|
|
204
|
+
throw new DatabaseError("Secure SQLite could not create the database leaf", {
|
|
205
|
+
path: filePath,
|
|
206
|
+
originalError: error instanceof Error ? error.message : String(error)
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
let identity;
|
|
210
|
+
let acquisitionError;
|
|
211
|
+
try {
|
|
212
|
+
identity = leafIdentity(await handle.stat({ bigint: true }));
|
|
213
|
+
} catch (error) {
|
|
214
|
+
acquisitionError = error;
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
await handle.close();
|
|
218
|
+
} catch (error) {
|
|
219
|
+
acquisitionError = acquisitionError ? new AggregateError([acquisitionError, error], "Secure SQLite leaf inspection and close failed") : error;
|
|
220
|
+
}
|
|
221
|
+
if (acquisitionError) {
|
|
222
|
+
const cleanupError = identity ? await removeCreatedLeafIfUnchanged(filePath, identity, runtime) : void 0;
|
|
223
|
+
throw new DatabaseError("Secure SQLite could not close the created leaf", {
|
|
224
|
+
path: filePath,
|
|
225
|
+
originalError: acquisitionError instanceof Error ? acquisitionError.message : String(acquisitionError),
|
|
226
|
+
...!identity ? { cleanupSkipped: "leaf identity unavailable" } : {},
|
|
227
|
+
...cleanupError ? { cleanupError } : {}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
return identity;
|
|
231
|
+
}
|
|
232
|
+
async function removeCreatedLeafIfUnchanged(filePath, identity, runtime) {
|
|
233
|
+
try {
|
|
234
|
+
if (!sameLeafIdentity(leafIdentity(await (runtime.lstatLeaf ?? lstat)(filePath, { bigint: true })), identity)) return;
|
|
235
|
+
await unlink(filePath);
|
|
236
|
+
return;
|
|
237
|
+
} catch (error) {
|
|
238
|
+
if (error.code === "ENOENT") return void 0;
|
|
239
|
+
return error instanceof Error ? error.message : String(error);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
function resolveSecureFilePath(url) {
|
|
243
|
+
if (url === ":memory:" || url.startsWith("file::memory:") || /^(?:https?|libsql):/i.test(url)) throw new DatabaseError("Secure SQLite acquisition requires a local file-backed database", { hint: "Use a local path or 'file:...' URL, or disable secureFile for memory and remote databases." });
|
|
244
|
+
if (url.startsWith("file://")) {
|
|
245
|
+
const parsed = new URL(url);
|
|
246
|
+
if (parsed.search || parsed.hash) throw new DatabaseError("Secure SQLite acquisition does not support file URLs with query parameters or fragments", {});
|
|
247
|
+
return resolve(fileURLToPath(parsed));
|
|
248
|
+
}
|
|
249
|
+
if (url.startsWith("file:")) {
|
|
250
|
+
const path = url.slice(5);
|
|
251
|
+
if (!path || path.includes("?") || path.includes("#")) throw new DatabaseError("Secure SQLite acquisition requires a plain file path without query parameters or fragments", {});
|
|
252
|
+
return resolve(path);
|
|
253
|
+
}
|
|
254
|
+
return resolve(url);
|
|
255
|
+
}
|
|
256
|
+
function normalizeExactInteger(value) {
|
|
257
|
+
if (typeof value !== "bigint") return value;
|
|
258
|
+
if (value >= BigInt(Number.MIN_SAFE_INTEGER) && value <= BigInt(Number.MAX_SAFE_INTEGER)) return Number(value);
|
|
259
|
+
return value;
|
|
260
|
+
}
|
|
261
|
+
function normalizeExactRow(row) {
|
|
262
|
+
return Object.fromEntries(Object.entries(row).map(([key, value]) => [key, normalizeExactInteger(value)]));
|
|
263
|
+
}
|
|
264
|
+
function toExactBigInt(value, metric) {
|
|
265
|
+
if (typeof value === "bigint") return value;
|
|
266
|
+
if (!Number.isSafeInteger(value)) throw new DatabaseError(`Secure SQLite returned an inexact ${metric} metric`, {
|
|
267
|
+
metric,
|
|
268
|
+
value
|
|
269
|
+
});
|
|
270
|
+
return BigInt(value);
|
|
271
|
+
}
|
|
272
|
+
/** @internal Exact public-contract range guard used by deterministic tests. */
|
|
273
|
+
function toPublicRowCount(value) {
|
|
274
|
+
const exact = toExactBigInt(value, "changes");
|
|
275
|
+
if (exact < 0n || exact > BigInt(Number.MAX_SAFE_INTEGER)) throw new DatabaseError("Secure SQLite changes exceed the public safe-integer row-count range", {
|
|
276
|
+
changes: exact.toString(),
|
|
277
|
+
maxSafeInteger: Number.MAX_SAFE_INTEGER
|
|
278
|
+
});
|
|
279
|
+
return Number(exact);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* node:sqlite does not bind named parameters from the positional array used by
|
|
283
|
+
* the existing LibSQL adapter. Translate executable parameters to SQLite
|
|
284
|
+
* numeric slots while preserving SQLite's first-occurrence allocation and
|
|
285
|
+
* reuse rules, including Tcl-style `$name::suffix(...)` parameters. Quoted
|
|
286
|
+
* text, identifiers, and comments remain untouched.
|
|
287
|
+
*/
|
|
288
|
+
function normalizeNodeSqlitePlaceholders(sql) {
|
|
289
|
+
let normalized = "";
|
|
290
|
+
let index = 0;
|
|
291
|
+
let quote;
|
|
292
|
+
let largestParameterIndex = 0;
|
|
293
|
+
const namedParameterIndexes = /* @__PURE__ */ new Map();
|
|
294
|
+
while (index < sql.length) {
|
|
295
|
+
const char = sql[index];
|
|
296
|
+
const next = sql[index + 1];
|
|
297
|
+
if (quote) {
|
|
298
|
+
normalized += char;
|
|
299
|
+
if (char === quote) {
|
|
300
|
+
if (next === quote && quote !== "]") {
|
|
301
|
+
normalized += next;
|
|
302
|
+
index += 2;
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
quote = void 0;
|
|
306
|
+
}
|
|
307
|
+
index += 1;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (char === "'" || char === "\"" || char === "`" || char === "[") {
|
|
311
|
+
quote = char === "[" ? "]" : char;
|
|
312
|
+
normalized += char;
|
|
313
|
+
index += 1;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (char === "-" && next === "-") {
|
|
317
|
+
const end = sql.indexOf("\n", index + 2);
|
|
318
|
+
if (end === -1) return normalized + sql.slice(index);
|
|
319
|
+
normalized += sql.slice(index, end + 1);
|
|
320
|
+
index = end + 1;
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
if (char === "/" && next === "*") {
|
|
324
|
+
const end = sql.indexOf("*/", index + 2);
|
|
325
|
+
if (end === -1) return normalized + sql.slice(index);
|
|
326
|
+
normalized += sql.slice(index, end + 2);
|
|
327
|
+
index = end + 2;
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
if (char === "?") {
|
|
331
|
+
let end = index + 1;
|
|
332
|
+
while (/[0-9]/.test(sql[end] ?? "")) end += 1;
|
|
333
|
+
if (end > index + 1) {
|
|
334
|
+
largestParameterIndex = Math.max(largestParameterIndex, Number(sql.slice(index + 1, end)));
|
|
335
|
+
normalized += sql.slice(index, end);
|
|
336
|
+
} else {
|
|
337
|
+
largestParameterIndex += 1;
|
|
338
|
+
normalized += `?${largestParameterIndex}`;
|
|
339
|
+
}
|
|
340
|
+
index = end;
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
if ((char === "$" || char === ":" || char === "@") && /[A-Za-z0-9_]/.test(next ?? "") && !/[A-Za-z0-9_$:@]/.test(sql[index - 1] ?? "")) {
|
|
344
|
+
let end = index + 2;
|
|
345
|
+
while (/[A-Za-z0-9_]/.test(sql[end] ?? "")) end += 1;
|
|
346
|
+
if (char === "$") {
|
|
347
|
+
while (sql.slice(end, end + 2) === "::" && /[A-Za-z0-9_]/.test(sql[end + 2] ?? "")) {
|
|
348
|
+
end += 3;
|
|
349
|
+
while (/[A-Za-z0-9_]/.test(sql[end] ?? "")) end += 1;
|
|
350
|
+
}
|
|
351
|
+
if (sql[end] === "(") {
|
|
352
|
+
const closingParenthesis = sql.indexOf(")", end + 1);
|
|
353
|
+
if (closingParenthesis !== -1) end = closingParenthesis + 1;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
const token = sql.slice(index, end);
|
|
357
|
+
let parameterIndex = namedParameterIndexes.get(token);
|
|
358
|
+
if (parameterIndex === void 0) {
|
|
359
|
+
largestParameterIndex += 1;
|
|
360
|
+
parameterIndex = largestParameterIndex;
|
|
361
|
+
namedParameterIndexes.set(token, parameterIndex);
|
|
362
|
+
}
|
|
363
|
+
normalized += `?${parameterIndex}`;
|
|
364
|
+
index = end;
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
normalized += char;
|
|
368
|
+
index += 1;
|
|
369
|
+
}
|
|
370
|
+
return normalized;
|
|
371
|
+
}
|
|
372
|
+
function executeNodeSqlite(database, statement) {
|
|
373
|
+
const sql = normalizeNodeSqlitePlaceholders(typeof statement === "string" ? statement : statement.sql);
|
|
374
|
+
const args = (typeof statement === "string" ? [] : statement.args ?? []).map((value) => typeof value === "boolean" ? Number(value) : value);
|
|
375
|
+
const prepared = database.prepare(sql);
|
|
376
|
+
prepared.setReadBigInts(true);
|
|
377
|
+
if (prepared.columns().length > 0) {
|
|
378
|
+
const beforeStatement = database.prepare("SELECT total_changes() AS totalChanges");
|
|
379
|
+
beforeStatement.setReadBigInts(true);
|
|
380
|
+
const before = toExactBigInt(beforeStatement.all()[0]?.totalChanges, "totalChanges");
|
|
381
|
+
const rows = prepared.all(...args).map(normalizeExactRow);
|
|
382
|
+
const afterStatement = database.prepare("SELECT total_changes() AS totalChanges, changes() AS changes, last_insert_rowid() AS lastInsertRowid");
|
|
383
|
+
afterStatement.setReadBigInts(true);
|
|
384
|
+
const metrics = afterStatement.all()[0];
|
|
385
|
+
const changed = toExactBigInt(metrics?.totalChanges, "totalChanges") > before;
|
|
386
|
+
return {
|
|
387
|
+
rows,
|
|
388
|
+
rowsAffected: changed ? toPublicRowCount(metrics?.changes) : 0,
|
|
389
|
+
...changed ? { lastInsertRowid: toExactBigInt(metrics?.lastInsertRowid, "lastInsertRowid") } : {}
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
const result = prepared.run(...args);
|
|
393
|
+
const rowsAffected = toPublicRowCount(result.changes);
|
|
394
|
+
return {
|
|
395
|
+
rows: [],
|
|
396
|
+
rowsAffected,
|
|
397
|
+
...rowsAffected > 0 ? { lastInsertRowid: toExactBigInt(result.lastInsertRowid, "lastInsertRowid") } : {}
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function createClient(database) {
|
|
401
|
+
let closePromise;
|
|
402
|
+
let executionTail = Promise.resolve();
|
|
403
|
+
let state = "open";
|
|
404
|
+
const executeStatement = async (statement) => {
|
|
405
|
+
return executeNodeSqlite(database, statement);
|
|
406
|
+
};
|
|
407
|
+
const enqueue = (statement, owner) => {
|
|
408
|
+
if (owner) {
|
|
409
|
+
if (!owner.active || state === "closed") return Promise.reject(new DatabaseError("Secure SQLite transaction reservation is closed", {}));
|
|
410
|
+
const pending = owner.tail.then(() => {
|
|
411
|
+
if (!owner.active || state === "closed") throw new DatabaseError("Secure SQLite transaction reservation is closed", {});
|
|
412
|
+
owner.beforeExecute?.();
|
|
413
|
+
return executeStatement(statement);
|
|
414
|
+
});
|
|
415
|
+
owner.tail = pending.then(() => void 0, () => void 0);
|
|
416
|
+
return pending;
|
|
417
|
+
}
|
|
418
|
+
if (state !== "open") return Promise.reject(new DatabaseError("Secure SQLite client is closing or closed", {}));
|
|
419
|
+
const pending = executionTail.then(() => executeStatement(statement));
|
|
420
|
+
executionTail = pending.then(() => void 0, () => void 0);
|
|
421
|
+
return pending;
|
|
422
|
+
};
|
|
423
|
+
const acquireReservation = async () => {
|
|
424
|
+
if (state !== "open") throw new DatabaseError("Secure SQLite client is closing or closed", {});
|
|
425
|
+
const owner = {
|
|
426
|
+
active: false,
|
|
427
|
+
tail: Promise.resolve()
|
|
428
|
+
};
|
|
429
|
+
let markAcquired = () => {};
|
|
430
|
+
let markReleased = () => {};
|
|
431
|
+
const acquired = new Promise((resolveAcquired) => {
|
|
432
|
+
markAcquired = resolveAcquired;
|
|
433
|
+
});
|
|
434
|
+
const released = new Promise((resolveReleased) => {
|
|
435
|
+
markReleased = resolveReleased;
|
|
436
|
+
});
|
|
437
|
+
const reservation = executionTail.then(async () => {
|
|
438
|
+
owner.active = true;
|
|
439
|
+
markAcquired();
|
|
440
|
+
await released;
|
|
441
|
+
await owner.tail;
|
|
442
|
+
owner.active = false;
|
|
443
|
+
});
|
|
444
|
+
executionTail = reservation.then(() => void 0, () => void 0);
|
|
445
|
+
await acquired;
|
|
446
|
+
let releaseStarted = false;
|
|
447
|
+
return {
|
|
448
|
+
owner,
|
|
449
|
+
release: async () => {
|
|
450
|
+
if (!releaseStarted) {
|
|
451
|
+
releaseStarted = true;
|
|
452
|
+
markReleased();
|
|
453
|
+
}
|
|
454
|
+
await reservation;
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
};
|
|
458
|
+
const execute = (statement) => enqueue(statement);
|
|
459
|
+
return {
|
|
460
|
+
transactionReservation: "exclusive",
|
|
461
|
+
execute,
|
|
462
|
+
transaction: async () => {
|
|
463
|
+
const { owner, release } = await acquireReservation();
|
|
464
|
+
try {
|
|
465
|
+
await enqueue("BEGIN IMMEDIATE", owner);
|
|
466
|
+
} catch (error) {
|
|
467
|
+
await release();
|
|
468
|
+
throw error;
|
|
469
|
+
}
|
|
470
|
+
let transactionClosed = false;
|
|
471
|
+
let transactionEnding = false;
|
|
472
|
+
let reservationReleased = false;
|
|
473
|
+
let automaticRollbackError;
|
|
474
|
+
const releaseReservation = async () => {
|
|
475
|
+
if (reservationReleased) return;
|
|
476
|
+
reservationReleased = true;
|
|
477
|
+
await release();
|
|
478
|
+
};
|
|
479
|
+
const detectAutomaticRollback = () => {
|
|
480
|
+
if (transactionClosed || database.isTransaction) return;
|
|
481
|
+
transactionClosed = true;
|
|
482
|
+
automaticRollbackError = new DatabaseError("Secure SQLite transaction ended automatically", { hint: "A statement conflict policy such as ON CONFLICT ROLLBACK ended the transaction. No later accepted work was executed." });
|
|
483
|
+
releaseReservation().catch(() => {});
|
|
484
|
+
};
|
|
485
|
+
const executeInTransaction = async (statement) => {
|
|
486
|
+
detectAutomaticRollback();
|
|
487
|
+
if (transactionClosed) throw automaticRollbackError ?? new DatabaseError("Secure SQLite transaction reservation is closed", {});
|
|
488
|
+
try {
|
|
489
|
+
return await enqueue(statement, owner);
|
|
490
|
+
} finally {
|
|
491
|
+
detectAutomaticRollback();
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
owner.beforeExecute = () => {
|
|
495
|
+
detectAutomaticRollback();
|
|
496
|
+
if (transactionClosed) throw automaticRollbackError ?? new DatabaseError("Secure SQLite transaction reservation is closed", {});
|
|
497
|
+
};
|
|
498
|
+
const end = async (sql) => {
|
|
499
|
+
if (transactionEnding) throw new DatabaseError("Secure SQLite transaction is already ending", {});
|
|
500
|
+
if (transactionClosed) {
|
|
501
|
+
await releaseReservation();
|
|
502
|
+
if (sql === "COMMIT" && automaticRollbackError) throw automaticRollbackError;
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
transactionEnding = true;
|
|
506
|
+
if (sql === "COMMIT") try {
|
|
507
|
+
await enqueue(sql, owner);
|
|
508
|
+
transactionClosed = true;
|
|
509
|
+
await releaseReservation();
|
|
510
|
+
return;
|
|
511
|
+
} finally {
|
|
512
|
+
transactionEnding = false;
|
|
513
|
+
}
|
|
514
|
+
try {
|
|
515
|
+
await enqueue(sql, owner);
|
|
516
|
+
transactionClosed = true;
|
|
517
|
+
await releaseReservation();
|
|
518
|
+
} catch (rollbackError) {
|
|
519
|
+
transactionClosed = true;
|
|
520
|
+
state = "closing";
|
|
521
|
+
let closeError;
|
|
522
|
+
try {
|
|
523
|
+
await releaseReservation();
|
|
524
|
+
database.close();
|
|
525
|
+
} catch (error) {
|
|
526
|
+
closeError = error;
|
|
527
|
+
} finally {
|
|
528
|
+
state = "closed";
|
|
529
|
+
}
|
|
530
|
+
if (closeError) throw new AggregateError([rollbackError, closeError], "Secure SQLite rollback and connection invalidation both failed", { cause: rollbackError });
|
|
531
|
+
throw rollbackError;
|
|
532
|
+
} finally {
|
|
533
|
+
transactionEnding = false;
|
|
534
|
+
}
|
|
535
|
+
};
|
|
536
|
+
return {
|
|
537
|
+
get closed() {
|
|
538
|
+
return transactionClosed;
|
|
539
|
+
},
|
|
540
|
+
execute: executeInTransaction,
|
|
541
|
+
commit: () => end("COMMIT"),
|
|
542
|
+
rollback: () => end("ROLLBACK"),
|
|
543
|
+
close: () => {
|
|
544
|
+
if (!transactionClosed) end("ROLLBACK").catch(() => {});
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
},
|
|
548
|
+
close: async () => {
|
|
549
|
+
if (state === "closed") return;
|
|
550
|
+
if (!closePromise) {
|
|
551
|
+
state = "closing";
|
|
552
|
+
closePromise = executionTail.then(() => database.close());
|
|
553
|
+
}
|
|
554
|
+
try {
|
|
555
|
+
await closePromise;
|
|
556
|
+
state = "closed";
|
|
557
|
+
} catch (error) {
|
|
558
|
+
state = "open";
|
|
559
|
+
closePromise = void 0;
|
|
560
|
+
throw error;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Opens a local SQLite file under an explicit trusted-parent custody contract.
|
|
567
|
+
*
|
|
568
|
+
* Static path components and their ownership/mode are checked before the
|
|
569
|
+
* built-in `node:sqlite` driver opens the file. Other principals cannot replace
|
|
570
|
+
* a path beneath the caller-custodied parent. Hostile processes running as the
|
|
571
|
+
* same account are explicitly outside this boundary.
|
|
572
|
+
*
|
|
573
|
+
* @internal Call through `getDatabase()` with typed `secureFile` custody.
|
|
574
|
+
*/
|
|
575
|
+
async function createSecureSqliteClient(url, options, runtime = defaultRuntime) {
|
|
576
|
+
validateNodeVersion(runtime);
|
|
577
|
+
if (runtime.platform !== "darwin" && runtime.platform !== "linux") throw new DatabaseError(`Secure SQLite acquisition is unsupported on ${runtime.platform}`, { hint: "secureFile requires built-in node:sqlite on macOS or Linux. Disable secureFile only if trusted-parent custody is not required." });
|
|
578
|
+
const filePath = resolveSecureFilePath(url);
|
|
579
|
+
const leafExisted = await validateTrustedParentCustody(filePath, options, runtime);
|
|
580
|
+
let nodeSqlite;
|
|
581
|
+
try {
|
|
582
|
+
nodeSqlite = await runtime.loadDriver();
|
|
583
|
+
} catch (error) {
|
|
584
|
+
throw new DatabaseError("Secure SQLite acquisition could not load the node:sqlite driver", {
|
|
585
|
+
hint: "Run @happyvertical/sql on its supported Node.js version with built-in node:sqlite available.",
|
|
586
|
+
originalError: error instanceof Error ? error.message : String(error)
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
let createdLeaf;
|
|
590
|
+
if (!leafExisted) {
|
|
591
|
+
createdLeaf = await createSecureLeaf(filePath, runtime);
|
|
592
|
+
try {
|
|
593
|
+
if (!await validateTrustedParentCustody(filePath, options, runtime)) throw new DatabaseError("Secure SQLite database leaf disappeared during acquisition", { path: filePath });
|
|
594
|
+
if (createdLeaf) {
|
|
595
|
+
let current;
|
|
596
|
+
try {
|
|
597
|
+
current = await (runtime.lstatLeaf ?? lstat)(filePath, { bigint: true });
|
|
598
|
+
} catch (error) {
|
|
599
|
+
throw new DatabaseError("Secure SQLite database leaf changed during acquisition", {
|
|
600
|
+
path: filePath,
|
|
601
|
+
originalError: error instanceof Error ? error.message : String(error)
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
if (!sameLeafIdentity(leafIdentity(current), createdLeaf)) throw new DatabaseError("Secure SQLite database leaf changed during acquisition", { path: filePath });
|
|
605
|
+
}
|
|
606
|
+
} catch (error) {
|
|
607
|
+
if (createdLeaf) await removeCreatedLeafIfUnchanged(filePath, createdLeaf, runtime);
|
|
608
|
+
throw error;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
let database;
|
|
612
|
+
try {
|
|
613
|
+
database = new nodeSqlite.DatabaseSync(filePath, {
|
|
614
|
+
enableForeignKeyConstraints: false,
|
|
615
|
+
enableDoubleQuotedStringLiterals: true
|
|
616
|
+
});
|
|
617
|
+
} catch (error) {
|
|
618
|
+
let cleanupError;
|
|
619
|
+
if (createdLeaf) cleanupError = await removeCreatedLeafIfUnchanged(filePath, createdLeaf, runtime);
|
|
620
|
+
throw new DatabaseError("Secure SQLite acquisition rejected the database path", {
|
|
621
|
+
path: filePath,
|
|
622
|
+
hint: "Ensure the database path and every ancestor are real directories/files, not symbolic links, and that the file is writable.",
|
|
623
|
+
originalError: error instanceof Error ? error.message : String(error),
|
|
624
|
+
...cleanupError ? { cleanupError } : {}
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
return createClient(database);
|
|
628
|
+
}
|
|
629
|
+
//#endregion
|
|
630
|
+
//#region src/sqlite.ts
|
|
631
|
+
/**
|
|
632
|
+
* Connection cache for in-memory databases with memoryId
|
|
633
|
+
* Enables sharing of :memory: databases across multiple getDatabase() calls
|
|
634
|
+
*/
|
|
635
|
+
var NULL_AWARE_UPSERT_MAX_ATTEMPTS = 8;
|
|
636
|
+
var NULL_AWARE_UPSERT_BASE_DELAY_MS = 25;
|
|
637
|
+
var nullAwareUpsertLocks = /* @__PURE__ */ new Map();
|
|
638
|
+
var MANAGED_TRANSACTION_CONTROL_KEYWORDS = /* @__PURE__ */ new Set([
|
|
639
|
+
"begin",
|
|
640
|
+
"commit",
|
|
641
|
+
"end",
|
|
642
|
+
"release",
|
|
643
|
+
"rollback",
|
|
644
|
+
"savepoint"
|
|
645
|
+
]);
|
|
646
|
+
function firstSqlKeyword(sql) {
|
|
647
|
+
let offset = 0;
|
|
648
|
+
while (offset < sql.length) {
|
|
649
|
+
const remaining = sql.slice(offset);
|
|
650
|
+
const whitespace = remaining.match(/^\s+/)?.[0];
|
|
651
|
+
if (whitespace) {
|
|
652
|
+
offset += whitespace.length;
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
if (remaining.startsWith("--")) {
|
|
656
|
+
const newline = remaining.indexOf("\n", 2);
|
|
657
|
+
if (newline === -1) return void 0;
|
|
658
|
+
offset += newline + 1;
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
if (remaining.startsWith("/*")) {
|
|
662
|
+
const end = remaining.indexOf("*/", 2);
|
|
663
|
+
if (end === -1) return void 0;
|
|
664
|
+
offset += end + 2;
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
667
|
+
if (remaining.startsWith(";")) {
|
|
668
|
+
offset += 1;
|
|
669
|
+
continue;
|
|
670
|
+
}
|
|
671
|
+
return remaining.match(/^[A-Za-z]+/)?.[0]?.toLowerCase();
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
function prepareManagedStatement(statement) {
|
|
675
|
+
const sql = typeof statement === "string" ? statement : statement?.sql;
|
|
676
|
+
if (typeof sql === "string" && MANAGED_TRANSACTION_CONTROL_KEYWORDS.has(firstSqlKeyword(sql) ?? "")) throw new DatabaseError("Transaction-control SQL is managed by the SQLite transaction scope", { hint: "Use transaction(), beginTransaction(), commit(), or rollback() instead of issuing BEGIN, COMMIT, ROLLBACK, SAVEPOINT, or RELEASE as raw SQL." });
|
|
677
|
+
if (typeof statement === "object" && statement !== null) {
|
|
678
|
+
const args = statement.args;
|
|
679
|
+
return {
|
|
680
|
+
sql,
|
|
681
|
+
args: Array.isArray(args) ? [...args] : args
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
return statement;
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Orders secure adapter calls at public invocation time.
|
|
688
|
+
*
|
|
689
|
+
* The reservation is appended before this function returns a promise. A later
|
|
690
|
+
* `close()` therefore cannot overtake an operation while it awaits an internal
|
|
691
|
+
* transaction/upsert lock before publishing work to the driver queue.
|
|
692
|
+
*/
|
|
693
|
+
function createInvocationBarrier() {
|
|
694
|
+
let tail = Promise.resolve();
|
|
695
|
+
let closePromise;
|
|
696
|
+
let state = "open";
|
|
697
|
+
const run = (work) => {
|
|
698
|
+
if (state !== "open") return Promise.reject(new DatabaseError("Secure SQLite adapter is closing or closed", {}));
|
|
699
|
+
const previous = tail;
|
|
700
|
+
let release = () => {};
|
|
701
|
+
const current = new Promise((resolveCurrent) => {
|
|
702
|
+
release = resolveCurrent;
|
|
703
|
+
});
|
|
704
|
+
tail = previous.then(() => current, () => current);
|
|
705
|
+
return previous.then(work, work).finally(release);
|
|
706
|
+
};
|
|
707
|
+
const close = (work) => {
|
|
708
|
+
if (state === "closed") return Promise.resolve();
|
|
709
|
+
if (closePromise) return closePromise;
|
|
710
|
+
state = "closing";
|
|
711
|
+
closePromise = tail.then(work, work).then(() => {
|
|
712
|
+
state = "closed";
|
|
713
|
+
}, (error) => {
|
|
714
|
+
state = "open";
|
|
715
|
+
closePromise = void 0;
|
|
716
|
+
throw error;
|
|
717
|
+
});
|
|
718
|
+
tail = closePromise.then(() => void 0, () => void 0);
|
|
719
|
+
return closePromise;
|
|
720
|
+
};
|
|
721
|
+
return {
|
|
722
|
+
run,
|
|
723
|
+
close
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
function delay(ms) {
|
|
727
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
728
|
+
}
|
|
729
|
+
function toSafeSqliteCount(value) {
|
|
730
|
+
const count = typeof value === "bigint" ? value : BigInt(String(value ?? 0));
|
|
731
|
+
if (count < 0n || count > BigInt(Number.MAX_SAFE_INTEGER)) throw new DatabaseError("SQLite count exceeds the safe integer range", {
|
|
732
|
+
value: String(value),
|
|
733
|
+
maximum: Number.MAX_SAFE_INTEGER
|
|
734
|
+
});
|
|
735
|
+
return Number(count);
|
|
736
|
+
}
|
|
737
|
+
function combineTransactionFailures(primary, secondary) {
|
|
738
|
+
return new AggregateError([primary, secondary], "Transaction callback and accepted transaction work both failed", { cause: primary });
|
|
739
|
+
}
|
|
740
|
+
var activeNativeTransferObservations = /* @__PURE__ */ new Set();
|
|
741
|
+
var trackedPromiseObservations = /* @__PURE__ */ new Set();
|
|
742
|
+
var nativePromiseResources = /* @__PURE__ */ new Map();
|
|
743
|
+
var nativePromiseTriggers = /* @__PURE__ */ new Map();
|
|
744
|
+
var nativePromiseIds = /* @__PURE__ */ new WeakMap();
|
|
745
|
+
var nativePromiseWatermarks = /* @__PURE__ */ new WeakMap();
|
|
746
|
+
var latestNativePromiseId = 0;
|
|
747
|
+
var nativePromiseTrackingUsers = 0;
|
|
748
|
+
var MAX_TRACKED_PROMISE_RESOURCES = 8192;
|
|
749
|
+
var nativePromiseHook = createHook({
|
|
750
|
+
init(asyncId, type, triggerAsyncId, resource) {
|
|
751
|
+
if (type === "PROMISE") {
|
|
752
|
+
latestNativePromiseId = Math.max(latestNativePromiseId, asyncId);
|
|
753
|
+
nativePromiseResources.set(asyncId, new WeakRef(resource));
|
|
754
|
+
nativePromiseTriggers.set(asyncId, triggerAsyncId);
|
|
755
|
+
nativePromiseIds.set(resource, asyncId);
|
|
756
|
+
while (nativePromiseResources.size > MAX_TRACKED_PROMISE_RESOURCES) {
|
|
757
|
+
const oldest = nativePromiseResources.keys().next().value;
|
|
758
|
+
if (oldest === void 0) break;
|
|
759
|
+
nativePromiseResources.delete(oldest);
|
|
760
|
+
nativePromiseTriggers.delete(oldest);
|
|
761
|
+
for (const observation of trackedPromiseObservations) if (!observation.observed && observation.nativeCorrelationIds.has(oldest)) observation.nativeCorrelationIncomplete = true;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
},
|
|
765
|
+
destroy(asyncId) {
|
|
766
|
+
nativePromiseResources.delete(asyncId);
|
|
767
|
+
nativePromiseTriggers.delete(asyncId);
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
var nativeTransferListenerInstalled = false;
|
|
771
|
+
function configuredUnhandledRejectionMode() {
|
|
772
|
+
return [...`${process.env.NODE_OPTIONS ?? ""} ${process.execArgv.join(" ")}`.matchAll(/--unhandled-rejections(?:=|\s+)(warn-with-error-code|none|strict|throw|warn)\b/g)].at(-1)?.[1] ?? "throw";
|
|
773
|
+
}
|
|
774
|
+
function promisesAreRelated(rejectedPromise, transferPromise) {
|
|
775
|
+
const rejectedId = nativePromiseIds.get(rejectedPromise);
|
|
776
|
+
const transferId = nativePromiseIds.get(transferPromise);
|
|
777
|
+
if (rejectedId === void 0 || transferId === void 0) return false;
|
|
778
|
+
if (rejectedId > (nativePromiseWatermarks.get(transferPromise) ?? -1)) return false;
|
|
779
|
+
if (rejectedId === transferId) return true;
|
|
780
|
+
const isAncestor = (ancestor, descendant) => {
|
|
781
|
+
let current = descendant;
|
|
782
|
+
while (nativePromiseTriggers.has(current)) {
|
|
783
|
+
current = nativePromiseTriggers.get(current) ?? 0;
|
|
784
|
+
if (current === ancestor) return true;
|
|
785
|
+
if (current === 0) break;
|
|
786
|
+
}
|
|
787
|
+
return false;
|
|
788
|
+
};
|
|
789
|
+
if (isAncestor(rejectedId, transferId) || isAncestor(transferId, rejectedId)) return true;
|
|
790
|
+
return rejectedId === transferId - 1 && nativePromiseTriggers.get(rejectedId) === nativePromiseTriggers.get(transferId);
|
|
791
|
+
}
|
|
792
|
+
var recordUnhandledNativeTransfer = (reason, rejectedPromise) => {
|
|
793
|
+
let matched = false;
|
|
794
|
+
for (const observation of activeNativeTransferObservations) for (const transferPromise of observation.nativeTransfers) if (promisesAreRelated(rejectedPromise, transferPromise)) {
|
|
795
|
+
observation.unhandledNativeTransfers.add(transferPromise);
|
|
796
|
+
observation.observed = false;
|
|
797
|
+
matched = true;
|
|
798
|
+
}
|
|
799
|
+
if (!matched && process.listenerCount("unhandledRejection") === 1) {
|
|
800
|
+
const mode = configuredUnhandledRejectionMode();
|
|
801
|
+
if (mode === "none" || mode === "strict") return;
|
|
802
|
+
if (mode === "warn") return;
|
|
803
|
+
if (mode === "warn-with-error-code") {
|
|
804
|
+
process.emitWarning(reason instanceof Error ? reason.stack ?? reason.message : String(reason), { type: "UnhandledPromiseRejectionWarning" });
|
|
805
|
+
process.exitCode = 1;
|
|
806
|
+
return;
|
|
807
|
+
}
|
|
808
|
+
const error = reason instanceof Error ? reason : new Error(`Unhandled promise rejection: ${String(reason)}`, { cause: reason });
|
|
809
|
+
queueMicrotask(() => {
|
|
810
|
+
if (process.hasUncaughtExceptionCaptureCallback()) throw error;
|
|
811
|
+
if (process.listenerCount("uncaughtException") > 0) {
|
|
812
|
+
const emitProcessEvent = process.emit;
|
|
813
|
+
emitProcessEvent.call(process, "uncaughtExceptionMonitor", error, "unhandledRejection");
|
|
814
|
+
emitProcessEvent.call(process, "uncaughtException", error, "unhandledRejection");
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
throw error;
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
function retainNativePromiseTracking(observation) {
|
|
822
|
+
if (observation.nativeTrackingActive) return;
|
|
823
|
+
observation.nativeTrackingActive = true;
|
|
824
|
+
trackedPromiseObservations.add(observation);
|
|
825
|
+
nativePromiseTrackingUsers += 1;
|
|
826
|
+
if (nativePromiseTrackingUsers === 1) nativePromiseHook.enable();
|
|
827
|
+
}
|
|
828
|
+
function releaseNativePromiseTracking(observation) {
|
|
829
|
+
if (!observation.nativeTrackingActive) return;
|
|
830
|
+
observation.nativeTrackingActive = false;
|
|
831
|
+
trackedPromiseObservations.delete(observation);
|
|
832
|
+
nativePromiseTrackingUsers -= 1;
|
|
833
|
+
if (nativePromiseTrackingUsers === 0) {
|
|
834
|
+
nativePromiseHook.disable();
|
|
835
|
+
nativePromiseResources.clear();
|
|
836
|
+
nativePromiseTriggers.clear();
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
function recordNativePromiseTransfer(observation) {
|
|
840
|
+
const executionId = executionAsyncId();
|
|
841
|
+
const adoptionId = nativePromiseTriggers.get(executionId) ?? executionId;
|
|
842
|
+
const adoptingPromise = nativePromiseResources.get(adoptionId)?.deref();
|
|
843
|
+
if (!adoptingPromise) return;
|
|
844
|
+
observation.nativeTransfers.add(adoptingPromise);
|
|
845
|
+
observation.observed = true;
|
|
846
|
+
nativePromiseWatermarks.set(adoptingPromise, latestNativePromiseId);
|
|
847
|
+
let correlationId = adoptionId;
|
|
848
|
+
observation.nativeCorrelationIds.add(correlationId);
|
|
849
|
+
while (nativePromiseTriggers.has(correlationId)) {
|
|
850
|
+
correlationId = nativePromiseTriggers.get(correlationId) ?? 0;
|
|
851
|
+
if (correlationId === 0) break;
|
|
852
|
+
observation.nativeCorrelationIds.add(correlationId);
|
|
853
|
+
}
|
|
854
|
+
activeNativeTransferObservations.add(observation);
|
|
855
|
+
if (!nativeTransferListenerInstalled) {
|
|
856
|
+
process.prependListener("unhandledRejection", recordUnhandledNativeTransfer);
|
|
857
|
+
nativeTransferListenerInstalled = true;
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
async function settleNativePromiseTransfers(observations) {
|
|
861
|
+
const observed = expandPromiseObservations(observations);
|
|
862
|
+
const transferred = observed.filter((observation) => observation.nativeTransfers.size > 0);
|
|
863
|
+
if (transferred.length === 0) {
|
|
864
|
+
for (const observation of observed) releaseNativePromiseTracking(observation);
|
|
865
|
+
return;
|
|
866
|
+
}
|
|
867
|
+
await new Promise((resolvePromise) => setImmediate(resolvePromise));
|
|
868
|
+
for (const observation of transferred) {
|
|
869
|
+
if (!observation.nativeCorrelationIncomplete && [...observation.nativeTransfers].every((promise) => !observation.unhandledNativeTransfers.has(promise))) observation.observed = true;
|
|
870
|
+
activeNativeTransferObservations.delete(observation);
|
|
871
|
+
}
|
|
872
|
+
for (const observation of observed) releaseNativePromiseTracking(observation);
|
|
873
|
+
if (nativeTransferListenerInstalled && activeNativeTransferObservations.size === 0) {
|
|
874
|
+
process.off("unhandledRejection", recordUnhandledNativeTransfer);
|
|
875
|
+
nativeTransferListenerInstalled = false;
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
function expandPromiseObservations(observations) {
|
|
879
|
+
const expanded = /* @__PURE__ */ new Set();
|
|
880
|
+
for (const observation of observations) {
|
|
881
|
+
expanded.add(observation);
|
|
882
|
+
for (const branch of observation.branches ?? []) expanded.add(branch);
|
|
883
|
+
}
|
|
884
|
+
return [...expanded];
|
|
885
|
+
}
|
|
886
|
+
function isNativePromiseAssimilation(onFulfilled, onRejected) {
|
|
887
|
+
const anonymousPromiseResolver = (handler) => handler.name === "" && handler.length === 1 && /\{\s*\[native code\]\s*\}/.test(Function.prototype.toString.call(handler));
|
|
888
|
+
return typeof onFulfilled === "function" && typeof onRejected === "function" && anonymousPromiseResolver(onFulfilled) && anonymousPromiseResolver(onRejected);
|
|
889
|
+
}
|
|
890
|
+
function looksLikeNativePromiseCapabilityExecutor(executor) {
|
|
891
|
+
return typeof executor === "function" && !types.isProxy(executor) && executor.name === "" && executor.length === 2 && /\{\s*\[native code\]\s*\}/.test(Function.prototype.toString.call(executor));
|
|
892
|
+
}
|
|
893
|
+
function isConsumedPromiseCapabilityExecutor(executor) {
|
|
894
|
+
try {
|
|
895
|
+
executor(() => void 0, () => void 0);
|
|
896
|
+
return false;
|
|
897
|
+
} catch (error) {
|
|
898
|
+
return error instanceof TypeError && error.message === "Promise executor has already been invoked with non-undefined arguments";
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* Returns a native Promise chain that records rejection handling per derived
|
|
903
|
+
* branch. A handler transfers responsibility from its parent to a branch-local
|
|
904
|
+
* observation, so catching one sibling cannot hide another detached failure.
|
|
905
|
+
*/
|
|
906
|
+
function withRejectionObservation(source, rootObservation) {
|
|
907
|
+
retainNativePromiseTracking(rootObservation);
|
|
908
|
+
const wrap = (promise, observation = rootObservation) => {
|
|
909
|
+
const createStandaloneObservation = () => ({
|
|
910
|
+
observed: false,
|
|
911
|
+
handlers: /* @__PURE__ */ new Set(),
|
|
912
|
+
nativeTransfers: /* @__PURE__ */ new Set(),
|
|
913
|
+
unhandledNativeTransfers: /* @__PURE__ */ new Set(),
|
|
914
|
+
nativeCorrelationIds: /* @__PURE__ */ new Set()
|
|
915
|
+
});
|
|
916
|
+
const suppressIntrinsicRecovery = (callback, targetObservation = observation) => {
|
|
917
|
+
targetObservation.suppressIntrinsicRecovery = (targetObservation.suppressIntrinsicRecovery ?? 0) + 1;
|
|
918
|
+
try {
|
|
919
|
+
return callback();
|
|
920
|
+
} finally {
|
|
921
|
+
targetObservation.suppressIntrinsicRecovery -= 1;
|
|
922
|
+
}
|
|
923
|
+
};
|
|
924
|
+
suppressIntrinsicRecovery(() => {
|
|
925
|
+
Promise.prototype.then.call(promise, void 0, () => void 0);
|
|
926
|
+
});
|
|
927
|
+
const intrinsicPromiseObservations = /* @__PURE__ */ new WeakMap();
|
|
928
|
+
const createPromiseSpeciesOwner = (parentObservation) => {
|
|
929
|
+
const speciesOwner = function transactionObservedPromiseBranch(executor) {
|
|
930
|
+
return new Promise(executor);
|
|
931
|
+
};
|
|
932
|
+
Object.setPrototypeOf(speciesOwner, Promise);
|
|
933
|
+
Object.defineProperty(speciesOwner, Symbol.species, {
|
|
934
|
+
configurable: true,
|
|
935
|
+
get: () => createIntrinsicDerivedPromise(parentObservation)
|
|
936
|
+
});
|
|
937
|
+
return speciesOwner;
|
|
938
|
+
};
|
|
939
|
+
const createIntrinsicDerivedPromise = (parentObservation) => {
|
|
940
|
+
class IntrinsicDerivedPromise extends Promise {
|
|
941
|
+
#observation;
|
|
942
|
+
static get [Symbol.species]() {
|
|
943
|
+
return Promise;
|
|
944
|
+
}
|
|
945
|
+
constructor(executor) {
|
|
946
|
+
const suppressed = (parentObservation.suppressIntrinsicRecovery ?? 0) > 0;
|
|
947
|
+
const candidateRecovery = !suppressed && looksLikeNativePromiseCapabilityExecutor(executor);
|
|
948
|
+
const branch = suppressed ? parentObservation : createStandaloneObservation();
|
|
949
|
+
let finishHandling = () => {};
|
|
950
|
+
const handling = new Promise((resolveHandling) => {
|
|
951
|
+
finishHandling = resolveHandling;
|
|
952
|
+
});
|
|
953
|
+
super((resolve, reject) => {
|
|
954
|
+
try {
|
|
955
|
+
executor((value) => {
|
|
956
|
+
if (candidateRecovery) new Promise((resolveValue) => resolveValue(value)).then(() => {
|
|
957
|
+
branch.observed = true;
|
|
958
|
+
finishHandling();
|
|
959
|
+
}, (error) => {
|
|
960
|
+
branch.error = error;
|
|
961
|
+
if (branch.propagateFailureToParent) branch.propagateFailureToParent.observed = true;
|
|
962
|
+
finishHandling();
|
|
963
|
+
});
|
|
964
|
+
else finishHandling();
|
|
965
|
+
resolve(value);
|
|
966
|
+
}, (reason) => {
|
|
967
|
+
if (candidateRecovery) {
|
|
968
|
+
branch.error = reason;
|
|
969
|
+
if (branch.propagateFailureToParent) branch.propagateFailureToParent.observed = true;
|
|
970
|
+
}
|
|
971
|
+
finishHandling();
|
|
972
|
+
reject(reason);
|
|
973
|
+
});
|
|
974
|
+
} catch (error) {
|
|
975
|
+
if (candidateRecovery) {
|
|
976
|
+
branch.error = error;
|
|
977
|
+
if (branch.propagateFailureToParent) branch.propagateFailureToParent.observed = true;
|
|
978
|
+
}
|
|
979
|
+
finishHandling();
|
|
980
|
+
throw error;
|
|
981
|
+
}
|
|
982
|
+
});
|
|
983
|
+
const tracksRecovery = candidateRecovery && isConsumedPromiseCapabilityExecutor(executor);
|
|
984
|
+
if (tracksRecovery) {
|
|
985
|
+
const branches = parentObservation.branches ?? /* @__PURE__ */ new Set();
|
|
986
|
+
parentObservation.branches = branches;
|
|
987
|
+
branch.branches = branches;
|
|
988
|
+
branches.add(branch);
|
|
989
|
+
parentObservation.observed = true;
|
|
990
|
+
}
|
|
991
|
+
this.#observation = branch;
|
|
992
|
+
intrinsicPromiseObservations.set(this, branch);
|
|
993
|
+
Promise.prototype.then.call(this, void 0, () => void 0);
|
|
994
|
+
if (tracksRecovery) {
|
|
995
|
+
branch.handlers.add(handling);
|
|
996
|
+
handling.finally(() => branch.handlers.delete(handling));
|
|
997
|
+
}
|
|
998
|
+
Object.defineProperty(this, "constructor", {
|
|
999
|
+
configurable: true,
|
|
1000
|
+
value: createPromiseSpeciesOwner(branch)
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
then(onFulfilled, onRejected) {
|
|
1004
|
+
const branch = this.#observation;
|
|
1005
|
+
const transfersToNativePromise = isNativePromiseAssimilation(onFulfilled, onRejected);
|
|
1006
|
+
const handleRejected = transfersToNativePromise && typeof onRejected === "function" ? (reason) => {
|
|
1007
|
+
recordNativePromiseTransfer(branch);
|
|
1008
|
+
return onRejected(reason);
|
|
1009
|
+
} : onRejected;
|
|
1010
|
+
const wasObserved = branch.observed;
|
|
1011
|
+
const derived = Promise.prototype.then.call(this, onFulfilled, handleRejected);
|
|
1012
|
+
if (transfersToNativePromise) {
|
|
1013
|
+
branch.observed = wasObserved;
|
|
1014
|
+
const child = intrinsicPromiseObservations.get(derived);
|
|
1015
|
+
if (child) child.propagateFailureToParent = branch;
|
|
1016
|
+
} else branch.observed = true;
|
|
1017
|
+
return derived;
|
|
1018
|
+
}
|
|
1019
|
+
catch(onRejected) {
|
|
1020
|
+
return this.then(void 0, onRejected);
|
|
1021
|
+
}
|
|
1022
|
+
finally(onFinally) {
|
|
1023
|
+
if (typeof onFinally !== "function") return this.then((value) => value, (reason) => {
|
|
1024
|
+
throw reason;
|
|
1025
|
+
});
|
|
1026
|
+
return this.then((value) => Promise.resolve(onFinally()).then(() => value), (reason) => Promise.resolve(onFinally()).then(() => {
|
|
1027
|
+
throw reason;
|
|
1028
|
+
}));
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
return IntrinsicDerivedPromise;
|
|
1032
|
+
};
|
|
1033
|
+
class ObservedPromise extends Promise {
|
|
1034
|
+
#observation;
|
|
1035
|
+
static get [Symbol.species]() {
|
|
1036
|
+
return createIntrinsicDerivedPromise(observation);
|
|
1037
|
+
}
|
|
1038
|
+
constructor(executor) {
|
|
1039
|
+
super((resolve, reject) => {
|
|
1040
|
+
if (executor) executor(resolve, reject);
|
|
1041
|
+
else Promise.prototype.then.call(promise, resolve, reject);
|
|
1042
|
+
});
|
|
1043
|
+
this.#observation = executor ? createStandaloneObservation() : observation;
|
|
1044
|
+
Object.defineProperty(this, "constructor", {
|
|
1045
|
+
configurable: true,
|
|
1046
|
+
value: createPromiseSpeciesOwner(this.#observation)
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
then(onFulfilled, onRejected) {
|
|
1050
|
+
const currentObservation = this.#observation;
|
|
1051
|
+
const transfersToNativePromise = isNativePromiseAssimilation(onFulfilled, onRejected);
|
|
1052
|
+
const handleRejected = transfersToNativePromise && typeof onRejected === "function" ? (reason) => {
|
|
1053
|
+
recordNativePromiseTransfer(currentObservation);
|
|
1054
|
+
return onRejected(reason);
|
|
1055
|
+
} : onRejected;
|
|
1056
|
+
const wasObserved = currentObservation.observed;
|
|
1057
|
+
const derived = Promise.prototype.then.call(this, onFulfilled, handleRejected);
|
|
1058
|
+
if (transfersToNativePromise) {
|
|
1059
|
+
currentObservation.observed = wasObserved;
|
|
1060
|
+
const child = intrinsicPromiseObservations.get(derived);
|
|
1061
|
+
if (child) child.propagateFailureToParent = currentObservation;
|
|
1062
|
+
} else currentObservation.observed = true;
|
|
1063
|
+
return derived;
|
|
1064
|
+
}
|
|
1065
|
+
catch(onRejected) {
|
|
1066
|
+
return this.then(void 0, onRejected);
|
|
1067
|
+
}
|
|
1068
|
+
finally(onFinally) {
|
|
1069
|
+
if (typeof onFinally !== "function") return this.then((value) => value, (reason) => {
|
|
1070
|
+
throw reason;
|
|
1071
|
+
});
|
|
1072
|
+
return this.then((value) => Promise.resolve(onFinally()).then(() => value), (reason) => Promise.resolve(onFinally()).then(() => {
|
|
1073
|
+
throw reason;
|
|
1074
|
+
}));
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
const observed = new ObservedPromise();
|
|
1078
|
+
suppressIntrinsicRecovery(() => Promise.prototype.then.call(observed, void 0, () => void 0));
|
|
1079
|
+
return observed;
|
|
1080
|
+
};
|
|
1081
|
+
return wrap(source);
|
|
1082
|
+
}
|
|
1083
|
+
function isRetriableSqliteTransactionError(error) {
|
|
1084
|
+
const formatted = formatDbError(error).toLowerCase();
|
|
1085
|
+
return formatted.includes("busy") || formatted.includes("locked") || formatted.includes("database is locked");
|
|
1086
|
+
}
|
|
1087
|
+
async function withNullAwareUpsertLock(lockKey, callback) {
|
|
1088
|
+
const previous = nullAwareUpsertLocks.get(lockKey) ?? Promise.resolve();
|
|
1089
|
+
let release = () => {};
|
|
1090
|
+
const current = new Promise((resolve) => {
|
|
1091
|
+
release = resolve;
|
|
1092
|
+
});
|
|
1093
|
+
const next = previous.catch(() => {}).then(() => current);
|
|
1094
|
+
nullAwareUpsertLocks.set(lockKey, next);
|
|
1095
|
+
await previous.catch(() => {});
|
|
1096
|
+
try {
|
|
1097
|
+
return await callback();
|
|
1098
|
+
} finally {
|
|
1099
|
+
release();
|
|
1100
|
+
if (nullAwareUpsertLocks.get(lockKey) === next) nullAwareUpsertLocks.delete(lockKey);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Generates a unique identifier for in-memory databases
|
|
1105
|
+
* @returns A unique database identifier string
|
|
1106
|
+
*/
|
|
1107
|
+
function generateDbId() {
|
|
1108
|
+
return `memory-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
1109
|
+
}
|
|
1110
|
+
/**
|
|
1111
|
+
* Creates a LibSQL client using the default client implementation
|
|
1112
|
+
* Supports in-memory databases and remote LibSQL URLs
|
|
1113
|
+
*
|
|
1114
|
+
* @param options - SQLite connection options
|
|
1115
|
+
* @returns Promise resolving to a LibSQL client instance
|
|
1116
|
+
*/
|
|
1117
|
+
async function createLibSQLClient(options) {
|
|
1118
|
+
const { url = ":memory:", authToken, encryptionKey } = options;
|
|
1119
|
+
let libsqlUrl = url;
|
|
1120
|
+
const remoteUrl = url.replace(/[\t\n\r]/g, "").trim();
|
|
1121
|
+
const hasRemoteScheme = /^(?:https?|libsql):/i.test(remoteUrl);
|
|
1122
|
+
if (hasRemoteScheme) {
|
|
1123
|
+
libsqlUrl = remoteUrl;
|
|
1124
|
+
let parsedRemoteUrl;
|
|
1125
|
+
try {
|
|
1126
|
+
parsedRemoteUrl = new URL(remoteUrl);
|
|
1127
|
+
} catch {
|
|
1128
|
+
const safeUrl = redactDatabaseUrl(remoteUrl);
|
|
1129
|
+
throw new DatabaseError(`Invalid remote database URL: ${safeUrl}`, { url: safeUrl });
|
|
1130
|
+
}
|
|
1131
|
+
if (!/^(?:https?|libsql):\/\//i.test(remoteUrl) || !parsedRemoteUrl.host) {
|
|
1132
|
+
const safeUrl = redactDatabaseUrl(remoteUrl);
|
|
1133
|
+
throw new DatabaseError(`Invalid remote database URL: ${safeUrl}`, { url: safeUrl });
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
if (url !== ":memory:" && !hasRemoteScheme && !url.startsWith("file:")) {
|
|
1137
|
+
const { resolve } = await import("node:path");
|
|
1138
|
+
libsqlUrl = `file://${resolve(url)}`;
|
|
1139
|
+
}
|
|
1140
|
+
try {
|
|
1141
|
+
const { createClient } = await import(
|
|
1142
|
+
/* @vite-ignore */
|
|
1143
|
+
"@libsql/client"
|
|
1144
|
+
);
|
|
1145
|
+
return createClient({
|
|
1146
|
+
url: libsqlUrl,
|
|
1147
|
+
authToken,
|
|
1148
|
+
encryptionKey
|
|
1149
|
+
});
|
|
1150
|
+
} catch (error) {
|
|
1151
|
+
const errorMessage = redactDatabaseUrl(error instanceof Error ? error.message : String(error));
|
|
1152
|
+
const safeUrl = redactDatabaseUrl(libsqlUrl);
|
|
1153
|
+
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}`, {
|
|
1154
|
+
url: safeUrl,
|
|
1155
|
+
originalError: errorMessage
|
|
1156
|
+
});
|
|
1157
|
+
throw new DatabaseError(`Failed to create LibSQL client: ${errorMessage}`, {
|
|
1158
|
+
url: safeUrl,
|
|
1159
|
+
originalError: errorMessage
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
function hasNativeSqliteCapabilities(options) {
|
|
1164
|
+
return Boolean(options.capabilities?.notifications || options.capabilities?.vector);
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Creates tables from provided schema definitions
|
|
1168
|
+
*
|
|
1169
|
+
* @param client - LibSQL client
|
|
1170
|
+
* @param schemas - Schema definitions to create
|
|
1171
|
+
*/
|
|
1172
|
+
async function createTablesFromSchemas(client, schemas) {
|
|
1173
|
+
for (const [tableName, schema] of Object.entries(schemas)) try {
|
|
1174
|
+
if ((await client.execute({
|
|
1175
|
+
sql: `SELECT name FROM sqlite_master WHERE type='table' AND name=?`,
|
|
1176
|
+
args: [tableName]
|
|
1177
|
+
})).rows.length > 0) {
|
|
1178
|
+
console.log(`[sqlite] Table ${tableName} already exists, skipping`);
|
|
1179
|
+
continue;
|
|
1180
|
+
}
|
|
1181
|
+
console.log(`[sqlite] Creating table ${tableName} from provided schema`);
|
|
1182
|
+
await client.execute(schema.ddl);
|
|
1183
|
+
if (schema.indexes && schema.indexes.length > 0) for (const indexSQL of schema.indexes) try {
|
|
1184
|
+
await client.execute(indexSQL);
|
|
1185
|
+
} catch (error) {
|
|
1186
|
+
console.warn(`[sqlite] Failed to create index for ${tableName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1187
|
+
}
|
|
1188
|
+
if (schema.triggers && schema.triggers.length > 0) for (const triggerSQL of schema.triggers) try {
|
|
1189
|
+
await client.execute(triggerSQL);
|
|
1190
|
+
} catch (error) {
|
|
1191
|
+
console.warn(`[sqlite] Failed to create trigger for ${tableName}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1192
|
+
}
|
|
1193
|
+
} catch (error) {
|
|
1194
|
+
console.error(`[sqlite] Error creating table ${tableName}:`, error instanceof Error ? error.message : String(error));
|
|
1195
|
+
console.error(`[sqlite] DDL was:`, schema.ddl?.substring(0, 200) + "...");
|
|
1196
|
+
throw new DatabaseError(`Failed to create table ${tableName} from schema: ${error instanceof Error ? error.message : String(error)}`, {
|
|
1197
|
+
tableName,
|
|
1198
|
+
schema,
|
|
1199
|
+
originalError: error instanceof Error ? error.message : String(error)
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Creates a SQLite database adapter
|
|
1205
|
+
*
|
|
1206
|
+
* @param options - SQLite connection options
|
|
1207
|
+
* @returns Database interface for SQLite
|
|
1208
|
+
*/
|
|
1209
|
+
async function getDatabase(options = {}) {
|
|
1210
|
+
validateDatabaseCacheOptions(options);
|
|
1211
|
+
const url = options.url || ":memory:";
|
|
1212
|
+
if (url === ":memory:" && !options.dbid && options.cache !== false) options.dbid = generateDbId();
|
|
1213
|
+
if (options.secureFile) {
|
|
1214
|
+
if (options.secureFile === true) throw new DatabaseError("Secure SQLite acquisition requires an explicit trusted-parent custody contract", { hint: "Use secureFile: { driver: 'node:sqlite', custody: 'trusted-parent' } after placing the database beneath a current-user-owned parent with no group/world write permission." });
|
|
1215
|
+
const secureOptions = options.secureFile;
|
|
1216
|
+
if (secureOptions.driver !== "node:sqlite") throw new DatabaseError(`Unsupported secure SQLite driver: ${String(secureOptions.driver)}`, { hint: "Use secureFile: { driver: 'node:sqlite', custody: 'trusted-parent' }." });
|
|
1217
|
+
if (secureOptions.custody !== "trusted-parent") throw new DatabaseError("Secure SQLite acquisition requires trusted-parent custody", { hint: "Set custody: 'trusted-parent' only when the application controls the database parent directory." });
|
|
1218
|
+
if (options.authToken || options.encryptionKey) throw new DatabaseError("Secure SQLite acquisition does not support LibSQL authentication or encryption options", { hint: "Remove authToken/encryptionKey for a local secure file, or disable secureFile to use LibSQL features." });
|
|
1219
|
+
if (hasNativeSqliteCapabilities(options)) throw new DatabaseError("Secure SQLite acquisition cannot be combined with native SQLite capabilities", { hint: "Use either secureFile custody or optional native capabilities for a connection, not both." });
|
|
1220
|
+
return getCachedSqliteDatabase("secure", options, () => createDatabase(options, url, () => createSecureSqliteClient(url, {
|
|
1221
|
+
custody: secureOptions.custody,
|
|
1222
|
+
root: secureOptions.root
|
|
1223
|
+
})));
|
|
1224
|
+
}
|
|
1225
|
+
if (hasNativeSqliteCapabilities(options)) return (await import("./sqlite-native-ByGwGEbS.js")).getNativeSqliteDatabase(options);
|
|
1226
|
+
return getCachedSqliteDatabase("libsql", options, () => createDatabase(options, url));
|
|
1227
|
+
}
|
|
1228
|
+
async function createDatabase(options, url, clientFactory = () => createLibSQLClient(options)) {
|
|
1229
|
+
return (async () => {
|
|
1230
|
+
const connectionLock = createTransactionLock("sqlite", options.transactionQueueTimeout);
|
|
1231
|
+
const client = await clientFactory();
|
|
1232
|
+
const executorContext = new AsyncLocalStorage();
|
|
1233
|
+
const transactionCallbackContext = new AsyncLocalStorage();
|
|
1234
|
+
const currentExecutor = () => executorContext.getStore() ?? client;
|
|
1235
|
+
const resolvedSchemas = resolveSchemas(options.schemas);
|
|
1236
|
+
if (resolvedSchemas && Object.keys(resolvedSchemas).length > 0) try {
|
|
1237
|
+
await createTablesFromSchemas(client, resolvedSchemas);
|
|
1238
|
+
} catch (error) {
|
|
1239
|
+
try {
|
|
1240
|
+
await client.close();
|
|
1241
|
+
} catch (cleanupError) {
|
|
1242
|
+
throw new AggregateError([error, cleanupError], "LibSQL schema initialization and cleanup failed");
|
|
1243
|
+
}
|
|
1244
|
+
throw error;
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* Serializes a value for SQLite storage
|
|
1248
|
+
* Converts objects and arrays to JSON strings
|
|
1249
|
+
* Converts Dates to ISO strings
|
|
1250
|
+
* Passes through primitives unchanged
|
|
1251
|
+
*/
|
|
1252
|
+
const serializeValue = (value) => {
|
|
1253
|
+
if (value === null || value === void 0) return value;
|
|
1254
|
+
if (value instanceof Date) return value.toISOString();
|
|
1255
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
1256
|
+
return value;
|
|
1257
|
+
};
|
|
1258
|
+
/**
|
|
1259
|
+
* Serializes all values in an object
|
|
1260
|
+
*/
|
|
1261
|
+
const serializeRecord = (record) => {
|
|
1262
|
+
const serialized = {};
|
|
1263
|
+
for (const [key, value] of Object.entries(record)) {
|
|
1264
|
+
if (value === void 0) continue;
|
|
1265
|
+
serialized[key] = serializeValue(value);
|
|
1266
|
+
}
|
|
1267
|
+
return serialized;
|
|
1268
|
+
};
|
|
1269
|
+
const hasNullConflictValue = (conflictColumns, data) => conflictColumns.some((col) => data[col] === null);
|
|
1270
|
+
const buildNullAwareUpsertLockKey = (table, conflictColumns) => {
|
|
1271
|
+
const material = JSON.stringify({
|
|
1272
|
+
url,
|
|
1273
|
+
table: table.toLowerCase(),
|
|
1274
|
+
conflicts: [...new Set(conflictColumns.map((column) => column.toLowerCase()))].sort()
|
|
1275
|
+
});
|
|
1276
|
+
return `sqlite-null-aware:${createHash("sha256").update(material).digest("hex")}`;
|
|
1277
|
+
};
|
|
1278
|
+
const validateUpsertConflictColumns = (table, conflictColumns, serializedData) => {
|
|
1279
|
+
const missingColumns = conflictColumns.filter((col) => !(col in serializedData));
|
|
1280
|
+
if (missingColumns.length > 0) throw new DatabaseError("Conflict columns missing from data", {
|
|
1281
|
+
table,
|
|
1282
|
+
conflictColumns,
|
|
1283
|
+
missingColumns,
|
|
1284
|
+
availableColumns: Object.keys(serializedData),
|
|
1285
|
+
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."
|
|
1286
|
+
});
|
|
1287
|
+
};
|
|
1288
|
+
const executeStandardUpsert = async (table, conflictColumns, serializedData) => {
|
|
1289
|
+
const keys = Object.keys(serializedData);
|
|
1290
|
+
const values = Object.values(serializedData);
|
|
1291
|
+
const placeholders = keys.map(() => "?").join(", ");
|
|
1292
|
+
const updateSet = keys.map((key) => `${key} = excluded.${key}`).join(", ");
|
|
1293
|
+
const conflict = conflictColumns.join(", ");
|
|
1294
|
+
const sql = `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders}) ON CONFLICT(${conflict}) DO UPDATE SET ${updateSet}`;
|
|
1295
|
+
return {
|
|
1296
|
+
operation: "upsert",
|
|
1297
|
+
affected: (await currentExecutor().execute({
|
|
1298
|
+
sql,
|
|
1299
|
+
args: values
|
|
1300
|
+
})).rowsAffected
|
|
1301
|
+
};
|
|
1302
|
+
};
|
|
1303
|
+
const executeNullAwareUpsertAttempt = async (executor, table, conflictColumns, serializedData) => {
|
|
1304
|
+
const keys = Object.keys(serializedData);
|
|
1305
|
+
const values = Object.values(serializedData);
|
|
1306
|
+
const updateSet = keys.map((key) => `${key} = ?`).join(", ");
|
|
1307
|
+
const whereClause = conflictColumns.map((col) => `${col} IS ?`).join(" AND ");
|
|
1308
|
+
const whereValues = conflictColumns.map((col) => serializedData[col]);
|
|
1309
|
+
const updateSql = `UPDATE ${table} SET ${updateSet} WHERE ${whereClause}`;
|
|
1310
|
+
const updateResult = await executor.execute({
|
|
1311
|
+
sql: updateSql,
|
|
1312
|
+
args: [...values, ...whereValues]
|
|
1313
|
+
});
|
|
1314
|
+
if ((updateResult.rowsAffected ?? 0) > 0) return {
|
|
1315
|
+
operation: "upsert",
|
|
1316
|
+
affected: updateResult.rowsAffected
|
|
1317
|
+
};
|
|
1318
|
+
const placeholders = keys.map(() => "?").join(", ");
|
|
1319
|
+
const insertSql = `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders})`;
|
|
1320
|
+
return {
|
|
1321
|
+
operation: "upsert",
|
|
1322
|
+
affected: (await executor.execute({
|
|
1323
|
+
sql: insertSql,
|
|
1324
|
+
args: values
|
|
1325
|
+
})).rowsAffected
|
|
1326
|
+
};
|
|
1327
|
+
};
|
|
1328
|
+
const executeNullAwareUpsertWithRetry = async (table, conflictColumns, serializedData, lockKey) => {
|
|
1329
|
+
if (url === ":memory:") return withNullAwareUpsertLock(lockKey, () => executeNullAwareUpsertAttempt(client, table, conflictColumns, serializedData));
|
|
1330
|
+
let lastError;
|
|
1331
|
+
for (let attempt = 0; attempt < NULL_AWARE_UPSERT_MAX_ATTEMPTS; attempt++) {
|
|
1332
|
+
let transaction;
|
|
1333
|
+
const releaseConnection = await connectionLock.acquire();
|
|
1334
|
+
try {
|
|
1335
|
+
return await withNullAwareUpsertLock(lockKey, async () => {
|
|
1336
|
+
transaction = await client.transaction("write");
|
|
1337
|
+
const result = await executeNullAwareUpsertAttempt(transaction, table, conflictColumns, serializedData);
|
|
1338
|
+
await transaction.commit();
|
|
1339
|
+
return result;
|
|
1340
|
+
});
|
|
1341
|
+
} catch (error) {
|
|
1342
|
+
lastError = error;
|
|
1343
|
+
if (transaction && !transaction.closed) try {
|
|
1344
|
+
await transaction.rollback();
|
|
1345
|
+
} catch (rollbackError) {
|
|
1346
|
+
lastError = combineTransactionFailures(lastError, rollbackError);
|
|
1347
|
+
}
|
|
1348
|
+
} finally {
|
|
1349
|
+
transaction?.close();
|
|
1350
|
+
releaseConnection();
|
|
1351
|
+
}
|
|
1352
|
+
if (lastError && attempt < NULL_AWARE_UPSERT_MAX_ATTEMPTS - 1 && isRetriableSqliteTransactionError(lastError)) {
|
|
1353
|
+
await delay(NULL_AWARE_UPSERT_BASE_DELAY_MS * 2 ** attempt);
|
|
1354
|
+
continue;
|
|
1355
|
+
}
|
|
1356
|
+
break;
|
|
1357
|
+
}
|
|
1358
|
+
throw lastError;
|
|
1359
|
+
};
|
|
1360
|
+
/**
|
|
1361
|
+
* Rejects hostile table/column identifiers before an upsert builds SQL.
|
|
1362
|
+
*
|
|
1363
|
+
* Called from the public entry points rather than from `executeUpsert`, so
|
|
1364
|
+
* the failure reaches the caller naming the bad identifier instead of being
|
|
1365
|
+
* flattened into the wrapper's generic "Failed to upsert record into table".
|
|
1366
|
+
*
|
|
1367
|
+
* Validates the keys of the already-serialized snapshot — the exact array
|
|
1368
|
+
* the executors interpolate — not a fresh enumeration of the caller's
|
|
1369
|
+
* `data`. Enumerating `data` again could disagree with the snapshot if it is
|
|
1370
|
+
* a Proxy whose `ownKeys` trap returns different keys on successive reads.
|
|
1371
|
+
*/
|
|
1372
|
+
const validateUpsertIdentifiers = (table, conflictColumns, serializedData) => {
|
|
1373
|
+
validateTableName(table);
|
|
1374
|
+
validateColumnNames([...conflictColumns, ...Object.keys(serializedData)]);
|
|
1375
|
+
};
|
|
1376
|
+
const executeUpsert = async (table, conflictColumns, serializedData, options, acquireTransaction) => {
|
|
1377
|
+
validateUpsertConflictColumns(table, conflictColumns, serializedData);
|
|
1378
|
+
if (options?.nullsDistinct || !hasNullConflictValue(conflictColumns, serializedData)) return executeStandardUpsert(table, conflictColumns, serializedData);
|
|
1379
|
+
if (!acquireTransaction) {
|
|
1380
|
+
const executor = currentExecutor();
|
|
1381
|
+
return withNullAwareUpsertLock(buildNullAwareUpsertLockKey(table, conflictColumns), () => executeNullAwareUpsertAttempt(executor, table, conflictColumns, serializedData));
|
|
1382
|
+
}
|
|
1383
|
+
return executeNullAwareUpsertWithRetry(table, conflictColumns, serializedData, buildNullAwareUpsertLockKey(table, conflictColumns));
|
|
1384
|
+
};
|
|
1385
|
+
/**
|
|
1386
|
+
* Inserts one or more records into a table
|
|
1387
|
+
*
|
|
1388
|
+
* @param table - Table name
|
|
1389
|
+
* @param data - Single record or array of records to insert
|
|
1390
|
+
* @returns Promise resolving to operation result
|
|
1391
|
+
* @throws Error if the insert operation fails
|
|
1392
|
+
*/
|
|
1393
|
+
const insert = async (table, data) => {
|
|
1394
|
+
validateTableName(table);
|
|
1395
|
+
let sql;
|
|
1396
|
+
let values;
|
|
1397
|
+
if (Array.isArray(data)) {
|
|
1398
|
+
if (data.length === 0) return {
|
|
1399
|
+
operation: "insert",
|
|
1400
|
+
affected: 0
|
|
1401
|
+
};
|
|
1402
|
+
const serializedRecords = data.map((record) => serializeRecord(record));
|
|
1403
|
+
const keys = resolveInsertColumns(table, serializedRecords);
|
|
1404
|
+
validateColumnNames(keys);
|
|
1405
|
+
const placeholders = serializedRecords.map(() => `(${keys.map(() => "?").join(", ")})`).join(", ");
|
|
1406
|
+
sql = `INSERT INTO ${table} (${keys.join(", ")}) VALUES ${placeholders}`;
|
|
1407
|
+
values = serializedRecords.flatMap((record) => keys.map((key) => record[key]));
|
|
1408
|
+
} else {
|
|
1409
|
+
const serializedData = serializeRecord(data);
|
|
1410
|
+
const keys = Object.keys(serializedData);
|
|
1411
|
+
validateColumnNames(keys);
|
|
1412
|
+
const placeholders = keys.map(() => "?").join(", ");
|
|
1413
|
+
sql = `INSERT INTO ${table} (${keys.join(", ")}) VALUES (${placeholders})`;
|
|
1414
|
+
values = Object.values(serializedData);
|
|
1415
|
+
}
|
|
1416
|
+
try {
|
|
1417
|
+
return {
|
|
1418
|
+
operation: "insert",
|
|
1419
|
+
affected: (await currentExecutor().execute({
|
|
1420
|
+
sql,
|
|
1421
|
+
args: values
|
|
1422
|
+
})).rowsAffected
|
|
1423
|
+
};
|
|
1424
|
+
} catch (e) {
|
|
1425
|
+
throw new DatabaseError("Failed to insert records into table", {
|
|
1426
|
+
table,
|
|
1427
|
+
sql,
|
|
1428
|
+
values,
|
|
1429
|
+
originalError: formatDbError(e)
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
};
|
|
1433
|
+
/**
|
|
1434
|
+
* Retrieves a single record matching the where criteria
|
|
1435
|
+
*
|
|
1436
|
+
* @param table - Table name
|
|
1437
|
+
* @param where - Criteria to match records
|
|
1438
|
+
* @returns Promise resolving to matching record or null if not found
|
|
1439
|
+
* @throws Error if the query fails
|
|
1440
|
+
*/
|
|
1441
|
+
const get = async (table, where) => {
|
|
1442
|
+
validateTableName(table);
|
|
1443
|
+
const { sql: whereClause, values } = buildWhere(where, 1, "sqlite");
|
|
1444
|
+
if (!whereClause) throw new DatabaseError("GET requires at least one WHERE condition to prevent returning an arbitrary record", { table });
|
|
1445
|
+
const sql = `SELECT * FROM ${table} ${whereClause}`;
|
|
1446
|
+
try {
|
|
1447
|
+
return (await currentExecutor().execute({
|
|
1448
|
+
sql,
|
|
1449
|
+
args: values
|
|
1450
|
+
})).rows[0] || null;
|
|
1451
|
+
} catch (e) {
|
|
1452
|
+
throw new DatabaseError("Failed to retrieve record from table", {
|
|
1453
|
+
table,
|
|
1454
|
+
sql,
|
|
1455
|
+
values,
|
|
1456
|
+
originalError: formatDbError(e)
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
};
|
|
1460
|
+
/**
|
|
1461
|
+
* Retrieves multiple records matching the where criteria
|
|
1462
|
+
*
|
|
1463
|
+
* @param table - Table name
|
|
1464
|
+
* @param where - Criteria to match records
|
|
1465
|
+
* @returns Promise resolving to array of matching records
|
|
1466
|
+
* @throws Error if the query fails
|
|
1467
|
+
*/
|
|
1468
|
+
const list = async (table, where) => {
|
|
1469
|
+
validateTableName(table);
|
|
1470
|
+
const { sql: whereClause, values } = buildWhere(where, 1, "sqlite");
|
|
1471
|
+
const sql = `SELECT * FROM ${table} ${whereClause}`;
|
|
1472
|
+
try {
|
|
1473
|
+
return (await currentExecutor().execute({
|
|
1474
|
+
sql,
|
|
1475
|
+
args: values
|
|
1476
|
+
})).rows;
|
|
1477
|
+
} catch (e) {
|
|
1478
|
+
throw new DatabaseError("Failed to list records from table", {
|
|
1479
|
+
table,
|
|
1480
|
+
sql,
|
|
1481
|
+
values,
|
|
1482
|
+
originalError: formatDbError(e)
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
};
|
|
1486
|
+
/**
|
|
1487
|
+
* Updates records matching the where criteria
|
|
1488
|
+
*
|
|
1489
|
+
* @param table - Table name
|
|
1490
|
+
* @param where - Criteria to match records to update
|
|
1491
|
+
* @param data - New data to set
|
|
1492
|
+
* @returns Promise resolving to operation result
|
|
1493
|
+
* @throws Error if the update operation fails
|
|
1494
|
+
*/
|
|
1495
|
+
const update = async (table, where, data) => {
|
|
1496
|
+
validateTableName(table);
|
|
1497
|
+
const serializedData = serializeRecord(data);
|
|
1498
|
+
const keys = Object.keys(serializedData);
|
|
1499
|
+
validateColumnNames(keys);
|
|
1500
|
+
const values = Object.values(serializedData);
|
|
1501
|
+
const setClause = keys.map((key, i) => `${key} = $${i + 1}`).join(", ");
|
|
1502
|
+
const { sql: whereClause, values: whereValues } = buildWhere(where, values.length + 1, "sqlite");
|
|
1503
|
+
if (!whereClause) throw new DatabaseError("UPDATE requires at least one WHERE condition to prevent accidental update of all records", { table });
|
|
1504
|
+
const sql = `UPDATE ${table} SET ${setClause} ${whereClause}`;
|
|
1505
|
+
try {
|
|
1506
|
+
return {
|
|
1507
|
+
operation: "update",
|
|
1508
|
+
affected: (await currentExecutor().execute({
|
|
1509
|
+
sql,
|
|
1510
|
+
args: [...values, ...whereValues]
|
|
1511
|
+
})).rowsAffected
|
|
1512
|
+
};
|
|
1513
|
+
} catch (e) {
|
|
1514
|
+
throw new DatabaseError("Failed to update records in table", {
|
|
1515
|
+
table,
|
|
1516
|
+
sql,
|
|
1517
|
+
values: [...values, ...whereValues],
|
|
1518
|
+
originalError: formatDbError(e)
|
|
1519
|
+
});
|
|
1520
|
+
}
|
|
1521
|
+
};
|
|
1522
|
+
/**
|
|
1523
|
+
* Inserts a record or updates it if it already exists (UPSERT)
|
|
1524
|
+
*
|
|
1525
|
+
* @param table - Table name
|
|
1526
|
+
* @param conflictColumns - Columns that define the uniqueness constraint
|
|
1527
|
+
* @param data - Data to insert or update
|
|
1528
|
+
* @returns Promise resolving to operation result
|
|
1529
|
+
* @throws Error if the upsert operation fails
|
|
1530
|
+
*/
|
|
1531
|
+
const upsert = async (table, conflictColumns, data, options) => {
|
|
1532
|
+
const serializedData = serializeRecord(data);
|
|
1533
|
+
const conflictCols = [...conflictColumns];
|
|
1534
|
+
validateUpsertIdentifiers(table, conflictCols, serializedData);
|
|
1535
|
+
try {
|
|
1536
|
+
return await executeUpsert(table, conflictCols, serializedData, options, true);
|
|
1537
|
+
} catch (e) {
|
|
1538
|
+
if (e instanceof DatabaseError && e.message === "Conflict columns missing from data") throw e;
|
|
1539
|
+
throw new DatabaseError("Failed to upsert record into table", {
|
|
1540
|
+
table,
|
|
1541
|
+
values: data,
|
|
1542
|
+
conflictColumns,
|
|
1543
|
+
originalError: formatDbError(e)
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
};
|
|
1547
|
+
let savepointSequence = 0;
|
|
1548
|
+
const upsertInCurrentTransaction = async (table, conflictColumns, data, options) => {
|
|
1549
|
+
const serializedData = serializeRecord(data);
|
|
1550
|
+
const conflictCols = [...conflictColumns];
|
|
1551
|
+
validateUpsertIdentifiers(table, conflictCols, serializedData);
|
|
1552
|
+
try {
|
|
1553
|
+
return await executeUpsert(table, conflictCols, serializedData, options, false);
|
|
1554
|
+
} catch (e) {
|
|
1555
|
+
if (e instanceof DatabaseError && e.message === "Conflict columns missing from data") throw e;
|
|
1556
|
+
throw new DatabaseError("Failed to upsert record into table", {
|
|
1557
|
+
table,
|
|
1558
|
+
values: data,
|
|
1559
|
+
conflictColumns,
|
|
1560
|
+
originalError: formatDbError(e)
|
|
1561
|
+
});
|
|
1562
|
+
}
|
|
1563
|
+
};
|
|
1564
|
+
/**
|
|
1565
|
+
* Gets a record matching the where criteria or inserts it if not found
|
|
1566
|
+
*
|
|
1567
|
+
* @param table - Table name
|
|
1568
|
+
* @param where - Criteria to match existing record
|
|
1569
|
+
* @param data - Data to insert if no record found
|
|
1570
|
+
* @returns Promise resolving to the record (either retrieved or newly inserted)
|
|
1571
|
+
* @throws Error if the operation fails or if the record cannot be retrieved after insert
|
|
1572
|
+
*/
|
|
1573
|
+
const getOrInsert = async (table, where, data) => {
|
|
1574
|
+
const result = await get(table, where);
|
|
1575
|
+
if (result) return result;
|
|
1576
|
+
await insert(table, data);
|
|
1577
|
+
const inserted = await get(table, where);
|
|
1578
|
+
if (!inserted) throw new DatabaseError("Failed to insert and retrieve record", {
|
|
1579
|
+
table,
|
|
1580
|
+
where,
|
|
1581
|
+
data
|
|
1582
|
+
});
|
|
1583
|
+
return inserted;
|
|
1584
|
+
};
|
|
1585
|
+
/**
|
|
1586
|
+
* Deletes records from a table matching the where criteria
|
|
1587
|
+
*
|
|
1588
|
+
* @param table - Table name
|
|
1589
|
+
* @param where - Criteria to match records for deletion
|
|
1590
|
+
* @returns Promise resolving to operation result with count of deleted rows
|
|
1591
|
+
* @throws Error if the delete operation fails
|
|
1592
|
+
*/
|
|
1593
|
+
const deleteRecords = async (table, where) => {
|
|
1594
|
+
validateTableName(table);
|
|
1595
|
+
if (Object.keys(where).length === 0) throw new DatabaseError("DELETE requires at least one WHERE condition to prevent accidental deletion of all records", { table });
|
|
1596
|
+
const { sql: whereClause, values } = buildWhere(where, 1, "sqlite");
|
|
1597
|
+
try {
|
|
1598
|
+
await currentExecutor().execute({
|
|
1599
|
+
sql: `DELETE FROM ${table} ${whereClause}`,
|
|
1600
|
+
args: values
|
|
1601
|
+
});
|
|
1602
|
+
return {
|
|
1603
|
+
operation: "delete",
|
|
1604
|
+
affected: 1
|
|
1605
|
+
};
|
|
1606
|
+
} catch (e) {
|
|
1607
|
+
throw new DatabaseError("Failed to delete records from table", {
|
|
1608
|
+
table,
|
|
1609
|
+
where,
|
|
1610
|
+
originalError: e
|
|
1611
|
+
});
|
|
1612
|
+
}
|
|
1613
|
+
};
|
|
1614
|
+
/**
|
|
1615
|
+
* Counts records in a table matching the where criteria
|
|
1616
|
+
*
|
|
1617
|
+
* @param table - Table name
|
|
1618
|
+
* @param where - Criteria to match records (optional, counts all if omitted)
|
|
1619
|
+
* @returns Promise resolving to count of matching records
|
|
1620
|
+
* @throws Error if the count operation fails
|
|
1621
|
+
*/
|
|
1622
|
+
const count = async (table, where) => {
|
|
1623
|
+
validateTableName(table);
|
|
1624
|
+
try {
|
|
1625
|
+
if (!where || Object.keys(where).length === 0) return toSafeSqliteCount((await currentExecutor().execute({
|
|
1626
|
+
sql: `SELECT COUNT(*) as count FROM ${table}`,
|
|
1627
|
+
args: []
|
|
1628
|
+
})).rows[0]?.count);
|
|
1629
|
+
const { sql: whereClause, values } = buildWhere(where, 1, "sqlite");
|
|
1630
|
+
return toSafeSqliteCount((await currentExecutor().execute({
|
|
1631
|
+
sql: `SELECT COUNT(*) as count FROM ${table} ${whereClause}`,
|
|
1632
|
+
args: values
|
|
1633
|
+
})).rows[0]?.count);
|
|
1634
|
+
} catch (e) {
|
|
1635
|
+
if (e instanceof DatabaseError) throw e;
|
|
1636
|
+
throw new DatabaseError("Failed to count records in table", {
|
|
1637
|
+
table,
|
|
1638
|
+
where,
|
|
1639
|
+
originalError: e
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
};
|
|
1643
|
+
/**
|
|
1644
|
+
* Checks if a table exists in the database
|
|
1645
|
+
*
|
|
1646
|
+
* @param tableName - Name of the table to check
|
|
1647
|
+
* @returns Promise resolving to boolean indicating if the table exists
|
|
1648
|
+
*/
|
|
1649
|
+
const tableExists = async (tableName) => {
|
|
1650
|
+
return !!await pluck`SELECT name FROM sqlite_master WHERE type='table' AND name=${tableName}`;
|
|
1651
|
+
};
|
|
1652
|
+
/**
|
|
1653
|
+
* Synchronizes database schema with provided SQL DDL
|
|
1654
|
+
* Creates tables if they don't exist and adds missing columns
|
|
1655
|
+
*
|
|
1656
|
+
* @param schema - SQL schema definition with CREATE TABLE statements
|
|
1657
|
+
* @returns Promise that resolves when schema is synchronized
|
|
1658
|
+
*/
|
|
1659
|
+
const syncSchema = async (schema) => {
|
|
1660
|
+
console.log("[sqlite.syncSchema] Starting schema sync for dbid:", options.dbid);
|
|
1661
|
+
console.log("[sqlite.syncSchema] Schema length:", schema.length, "chars");
|
|
1662
|
+
const reservedKeywords = /* @__PURE__ */ new Set([
|
|
1663
|
+
"references",
|
|
1664
|
+
"order",
|
|
1665
|
+
"group",
|
|
1666
|
+
"table",
|
|
1667
|
+
"index",
|
|
1668
|
+
"select",
|
|
1669
|
+
"from",
|
|
1670
|
+
"where",
|
|
1671
|
+
"join",
|
|
1672
|
+
"left",
|
|
1673
|
+
"right",
|
|
1674
|
+
"inner",
|
|
1675
|
+
"outer",
|
|
1676
|
+
"on",
|
|
1677
|
+
"as",
|
|
1678
|
+
"and",
|
|
1679
|
+
"or",
|
|
1680
|
+
"not",
|
|
1681
|
+
"like",
|
|
1682
|
+
"in",
|
|
1683
|
+
"between",
|
|
1684
|
+
"case",
|
|
1685
|
+
"when",
|
|
1686
|
+
"then",
|
|
1687
|
+
"else",
|
|
1688
|
+
"end",
|
|
1689
|
+
"union",
|
|
1690
|
+
"distinct",
|
|
1691
|
+
"having",
|
|
1692
|
+
"limit",
|
|
1693
|
+
"offset",
|
|
1694
|
+
"default",
|
|
1695
|
+
"check",
|
|
1696
|
+
"unique",
|
|
1697
|
+
"primary",
|
|
1698
|
+
"foreign",
|
|
1699
|
+
"key",
|
|
1700
|
+
"constraint",
|
|
1701
|
+
"cascade"
|
|
1702
|
+
]);
|
|
1703
|
+
let normalizedSchema = schema.replace(/\s+/g, " ").replace(/DEFAULT CAST\(([^)]+)\s+AS\s+\w+\)/gi, "DEFAULT $1").trim();
|
|
1704
|
+
normalizedSchema = normalizedSchema.replace(/"(\w+)"/g, (match, identifier) => {
|
|
1705
|
+
return reservedKeywords.has(identifier.toLowerCase()) ? match : identifier;
|
|
1706
|
+
});
|
|
1707
|
+
console.log("[sqlite.syncSchema] Normalized schema:", normalizedSchema);
|
|
1708
|
+
const commands = normalizedSchema.split(";").map((cmd) => cmd.trim()).filter((cmd) => cmd.length > 0);
|
|
1709
|
+
console.log("[sqlite.syncSchema] Found", commands.length, "commands to process");
|
|
1710
|
+
for (const command of commands) try {
|
|
1711
|
+
console.log("[sqlite.syncSchema] Executing:", `${command.substring(0, 50)}...`);
|
|
1712
|
+
await currentExecutor().execute(command);
|
|
1713
|
+
console.log("[sqlite.syncSchema] Successfully executed command");
|
|
1714
|
+
} catch (error) {
|
|
1715
|
+
console.error("[sqlite.syncSchema] Failed to execute command:", command);
|
|
1716
|
+
console.error("[sqlite.syncSchema] Error:", error);
|
|
1717
|
+
throw error;
|
|
1718
|
+
}
|
|
1719
|
+
};
|
|
1720
|
+
const createNestedTransaction = (transactionClient) => {
|
|
1721
|
+
const scopeContext = new AsyncLocalStorage();
|
|
1722
|
+
const rootScope = {
|
|
1723
|
+
accepting: true,
|
|
1724
|
+
tail: Promise.resolve(),
|
|
1725
|
+
children: /* @__PURE__ */ new Set()
|
|
1726
|
+
};
|
|
1727
|
+
const enqueueInScope = (scope, operation) => {
|
|
1728
|
+
if (!scope.accepting) return Promise.reject(new DatabaseError("Nested transaction scope is ending", {}));
|
|
1729
|
+
const current = scope.tail.then(operation);
|
|
1730
|
+
scope.tail = current.then(() => void 0, () => void 0);
|
|
1731
|
+
return current;
|
|
1732
|
+
};
|
|
1733
|
+
const assertBoundScope = (scope) => {
|
|
1734
|
+
const ambientScope = scopeContext.getStore();
|
|
1735
|
+
if (ambientScope && ambientScope !== scope) throw new DatabaseError("A parent transaction handle cannot run inside a nested transaction callback", { hint: "Use the nested callback transaction handle, or wait for the nested transaction to settle before using its parent handle." });
|
|
1736
|
+
};
|
|
1737
|
+
const runBoundOperation = (scope, operation) => {
|
|
1738
|
+
try {
|
|
1739
|
+
assertBoundScope(scope);
|
|
1740
|
+
return enqueueInScope(scope, operation);
|
|
1741
|
+
} catch (error) {
|
|
1742
|
+
return Promise.reject(error);
|
|
1743
|
+
}
|
|
1744
|
+
};
|
|
1745
|
+
const drainNestedScope = async (scope) => {
|
|
1746
|
+
await scope.tail;
|
|
1747
|
+
while (expandPromiseObservations(scope.children).some((child) => child.handlers.size > 0)) await Promise.all(expandPromiseObservations(scope.children).flatMap((child) => [...child.handlers]));
|
|
1748
|
+
await settleNativePromiseTransfers(scope.children);
|
|
1749
|
+
const failures = expandPromiseObservations(scope.children).filter((child) => !child.observed && "error" in child).map((child) => child.error);
|
|
1750
|
+
if (failures.length === 1) throw failures[0];
|
|
1751
|
+
if (failures.length > 1) throw new AggregateError(failures, "Accepted nested transactions failed");
|
|
1752
|
+
};
|
|
1753
|
+
const runScope = async (scope, callback) => {
|
|
1754
|
+
savepointSequence += 1;
|
|
1755
|
+
const name = `hv_sp_${savepointSequence}`;
|
|
1756
|
+
await transactionClient.execute({
|
|
1757
|
+
sql: `SAVEPOINT ${name}`,
|
|
1758
|
+
args: []
|
|
1759
|
+
});
|
|
1760
|
+
const callbackScope = createTransactionScope(transactionClient, nestedTransaction, (operation) => runBoundOperation(scope, operation), (nestedCallback) => {
|
|
1761
|
+
try {
|
|
1762
|
+
assertBoundScope(scope);
|
|
1763
|
+
return startNested(scope, nestedCallback);
|
|
1764
|
+
} catch (error) {
|
|
1765
|
+
return Promise.reject(error);
|
|
1766
|
+
}
|
|
1767
|
+
});
|
|
1768
|
+
const drainScope = async () => {
|
|
1769
|
+
await callbackScope.sealAndDrain();
|
|
1770
|
+
await drainNestedScope(scope);
|
|
1771
|
+
};
|
|
1772
|
+
try {
|
|
1773
|
+
const result = await callback(callbackScope.database);
|
|
1774
|
+
scope.accepting = false;
|
|
1775
|
+
await drainScope();
|
|
1776
|
+
await transactionClient.execute({
|
|
1777
|
+
sql: `RELEASE SAVEPOINT ${name}`,
|
|
1778
|
+
args: []
|
|
1779
|
+
});
|
|
1780
|
+
return result;
|
|
1781
|
+
} catch (error) {
|
|
1782
|
+
scope.accepting = false;
|
|
1783
|
+
let failure = error;
|
|
1784
|
+
try {
|
|
1785
|
+
await drainScope();
|
|
1786
|
+
} catch (drainError) {
|
|
1787
|
+
if (drainError !== error) failure = combineTransactionFailures(error, drainError);
|
|
1788
|
+
}
|
|
1789
|
+
try {
|
|
1790
|
+
await transactionClient.execute({
|
|
1791
|
+
sql: `ROLLBACK TO SAVEPOINT ${name}`,
|
|
1792
|
+
args: []
|
|
1793
|
+
});
|
|
1794
|
+
await transactionClient.execute({
|
|
1795
|
+
sql: `RELEASE SAVEPOINT ${name}`,
|
|
1796
|
+
args: []
|
|
1797
|
+
});
|
|
1798
|
+
} catch (teardownError) {
|
|
1799
|
+
if (teardownError !== failure) failure = combineTransactionFailures(failure, teardownError);
|
|
1800
|
+
}
|
|
1801
|
+
throw failure;
|
|
1802
|
+
}
|
|
1803
|
+
};
|
|
1804
|
+
const startNested = (parentScope, callback) => {
|
|
1805
|
+
if (!parentScope.accepting) return Promise.reject(new DatabaseError("Nested transaction scope is ending", {}));
|
|
1806
|
+
const scope = {
|
|
1807
|
+
accepting: true,
|
|
1808
|
+
tail: Promise.resolve(),
|
|
1809
|
+
children: /* @__PURE__ */ new Set()
|
|
1810
|
+
};
|
|
1811
|
+
const child = {
|
|
1812
|
+
observed: false,
|
|
1813
|
+
handlers: /* @__PURE__ */ new Set(),
|
|
1814
|
+
nativeTransfers: /* @__PURE__ */ new Set(),
|
|
1815
|
+
unhandledNativeTransfers: /* @__PURE__ */ new Set(),
|
|
1816
|
+
nativeCorrelationIds: /* @__PURE__ */ new Set()
|
|
1817
|
+
};
|
|
1818
|
+
parentScope.children.add(child);
|
|
1819
|
+
const current = enqueueInScope(parentScope, () => scopeContext.run(scope, () => runScope(scope, callback)));
|
|
1820
|
+
current.catch((error) => {
|
|
1821
|
+
child.error = error;
|
|
1822
|
+
});
|
|
1823
|
+
return withRejectionObservation(current, child);
|
|
1824
|
+
};
|
|
1825
|
+
const nestedTransaction = ((callback) => startNested(scopeContext.getStore() ?? rootScope, callback));
|
|
1826
|
+
nestedTransaction.drain = () => {
|
|
1827
|
+
rootScope.accepting = false;
|
|
1828
|
+
return drainNestedScope(rootScope);
|
|
1829
|
+
};
|
|
1830
|
+
nestedTransaction.runOperation = (operation) => runBoundOperation(rootScope, operation);
|
|
1831
|
+
nestedTransaction.runNested = (callback) => {
|
|
1832
|
+
try {
|
|
1833
|
+
assertBoundScope(rootScope);
|
|
1834
|
+
return startNested(rootScope, callback);
|
|
1835
|
+
} catch (error) {
|
|
1836
|
+
return Promise.reject(error);
|
|
1837
|
+
}
|
|
1838
|
+
};
|
|
1839
|
+
return nestedTransaction;
|
|
1840
|
+
};
|
|
1841
|
+
/**
|
|
1842
|
+
* Executes a callback within a database transaction
|
|
1843
|
+
* Automatically commits on success or rolls back on error
|
|
1844
|
+
*
|
|
1845
|
+
* @param callback - Function to execute within transaction
|
|
1846
|
+
* @returns Promise resolving to callback result
|
|
1847
|
+
*/
|
|
1848
|
+
const transaction = async (callback, preAcquiredLock) => {
|
|
1849
|
+
const releaseConnection = await (preAcquiredLock ?? connectionLock.acquire());
|
|
1850
|
+
try {
|
|
1851
|
+
if (client.transactionReservation !== "exclusive") {
|
|
1852
|
+
let nestedTransaction;
|
|
1853
|
+
let transactionScope;
|
|
1854
|
+
try {
|
|
1855
|
+
await client.execute({
|
|
1856
|
+
sql: "BEGIN TRANSACTION",
|
|
1857
|
+
args: []
|
|
1858
|
+
});
|
|
1859
|
+
nestedTransaction = createNestedTransaction(client);
|
|
1860
|
+
transactionScope = createTransactionScope(client, nestedTransaction);
|
|
1861
|
+
const activeScope = transactionScope;
|
|
1862
|
+
const result = await transactionCallbackContext.run(true, () => callback(activeScope.database));
|
|
1863
|
+
await transactionScope.sealAndDrain();
|
|
1864
|
+
await nestedTransaction.drain();
|
|
1865
|
+
await client.execute({
|
|
1866
|
+
sql: "COMMIT",
|
|
1867
|
+
args: []
|
|
1868
|
+
});
|
|
1869
|
+
return result;
|
|
1870
|
+
} catch (error) {
|
|
1871
|
+
let failure = error;
|
|
1872
|
+
try {
|
|
1873
|
+
await transactionScope?.sealAndDrain();
|
|
1874
|
+
} catch (drainError) {
|
|
1875
|
+
if (drainError !== error) failure = combineTransactionFailures(failure, drainError);
|
|
1876
|
+
}
|
|
1877
|
+
try {
|
|
1878
|
+
await nestedTransaction?.drain();
|
|
1879
|
+
} catch (drainError) {
|
|
1880
|
+
if (drainError !== failure) failure = combineTransactionFailures(failure, drainError);
|
|
1881
|
+
}
|
|
1882
|
+
try {
|
|
1883
|
+
await client.execute({
|
|
1884
|
+
sql: "ROLLBACK",
|
|
1885
|
+
args: []
|
|
1886
|
+
});
|
|
1887
|
+
} catch {}
|
|
1888
|
+
throw failure;
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
const transactionClient = await client.transaction("write");
|
|
1892
|
+
let nestedTransaction;
|
|
1893
|
+
let transactionScope;
|
|
1894
|
+
try {
|
|
1895
|
+
nestedTransaction = createNestedTransaction(transactionClient);
|
|
1896
|
+
transactionScope = createTransactionScope(transactionClient, nestedTransaction);
|
|
1897
|
+
const activeScope = transactionScope;
|
|
1898
|
+
const result = await transactionCallbackContext.run(true, () => callback(activeScope.database));
|
|
1899
|
+
await transactionScope.sealAndDrain();
|
|
1900
|
+
await nestedTransaction.drain();
|
|
1901
|
+
await transactionClient.commit();
|
|
1902
|
+
return result;
|
|
1903
|
+
} catch (error) {
|
|
1904
|
+
let failure = error;
|
|
1905
|
+
try {
|
|
1906
|
+
await transactionScope?.sealAndDrain();
|
|
1907
|
+
} catch (drainError) {
|
|
1908
|
+
if (drainError !== error) failure = combineTransactionFailures(failure, drainError);
|
|
1909
|
+
}
|
|
1910
|
+
try {
|
|
1911
|
+
await nestedTransaction?.drain();
|
|
1912
|
+
} catch (drainError) {
|
|
1913
|
+
if (drainError !== failure) failure = combineTransactionFailures(failure, drainError);
|
|
1914
|
+
}
|
|
1915
|
+
if (!transactionClient.closed) try {
|
|
1916
|
+
await transactionClient.rollback();
|
|
1917
|
+
} catch (rollbackError) {
|
|
1918
|
+
failure = combineTransactionFailures(failure, rollbackError);
|
|
1919
|
+
}
|
|
1920
|
+
throw failure;
|
|
1921
|
+
} finally {
|
|
1922
|
+
transactionClient.close();
|
|
1923
|
+
}
|
|
1924
|
+
} finally {
|
|
1925
|
+
releaseConnection();
|
|
1926
|
+
}
|
|
1927
|
+
};
|
|
1928
|
+
/**
|
|
1929
|
+
* Builds the transaction-scoped interface handed to a transaction callback.
|
|
1930
|
+
*
|
|
1931
|
+
* The only difference from the top-level interface is `transaction`: this
|
|
1932
|
+
* one re-enters the transaction already in progress under a savepoint
|
|
1933
|
+
* instead of issuing a second BEGIN. SQLite shares one connection, so the
|
|
1934
|
+
* old behaviour of re-exposing the top-level `transaction` sent a nested
|
|
1935
|
+
* call into `BEGIN` on a connection that was already in a transaction; that
|
|
1936
|
+
* throws, and the nested call's own ROLLBACK then discarded the *enclosing*
|
|
1937
|
+
* transaction's work while later writes silently committed in autocommit.
|
|
1938
|
+
*/
|
|
1939
|
+
const createTransactionScope = (transactionClient, nestedTransaction, runOperation = nestedTransaction.runOperation, runNested = nestedTransaction.runNested) => {
|
|
1940
|
+
const isSecureTransaction = client.transactionReservation === "exclusive";
|
|
1941
|
+
const scopedExecutor = isSecureTransaction ? { execute: (statement) => {
|
|
1942
|
+
return transactionClient.execute(prepareManagedStatement(statement));
|
|
1943
|
+
} } : transactionClient;
|
|
1944
|
+
let accepting = true;
|
|
1945
|
+
const pending = /* @__PURE__ */ new Set();
|
|
1946
|
+
const operations = /* @__PURE__ */ new Set();
|
|
1947
|
+
let drainPromise;
|
|
1948
|
+
const bind = (fn, trackFailure = true) => ((...args) => {
|
|
1949
|
+
if (!accepting) return Promise.reject(new DatabaseError("Transaction scope is ending or ended", {}));
|
|
1950
|
+
const run = () => Promise.resolve(executorContext.run(scopedExecutor, () => fn(...args)));
|
|
1951
|
+
const operation = isSecureTransaction ? runOperation(run) : run();
|
|
1952
|
+
const tracked = {
|
|
1953
|
+
observed: false,
|
|
1954
|
+
handlers: /* @__PURE__ */ new Set(),
|
|
1955
|
+
nativeTransfers: /* @__PURE__ */ new Set(),
|
|
1956
|
+
unhandledNativeTransfers: /* @__PURE__ */ new Set(),
|
|
1957
|
+
nativeCorrelationIds: /* @__PURE__ */ new Set()
|
|
1958
|
+
};
|
|
1959
|
+
operations.add(tracked);
|
|
1960
|
+
pending.add(operation);
|
|
1961
|
+
operation.then(() => pending.delete(operation), (error) => {
|
|
1962
|
+
pending.delete(operation);
|
|
1963
|
+
if (trackFailure) tracked.error = error;
|
|
1964
|
+
});
|
|
1965
|
+
return withRejectionObservation(operation, tracked);
|
|
1966
|
+
});
|
|
1967
|
+
const scopedInsert = bind(insert);
|
|
1968
|
+
const scopedGet = bind(get);
|
|
1969
|
+
const scopedList = bind(list);
|
|
1970
|
+
const scopedUpdate = bind(update);
|
|
1971
|
+
const scopedUpsert = bind(upsertInCurrentTransaction);
|
|
1972
|
+
const scopedGetOrInsert = bind(getOrInsert);
|
|
1973
|
+
const scopedDelete = bind(deleteRecords);
|
|
1974
|
+
const scopedCount = bind(count);
|
|
1975
|
+
const scopedMany = bind(many);
|
|
1976
|
+
const scopedSingle = bind(single);
|
|
1977
|
+
const scopedPluck = bind(pluck);
|
|
1978
|
+
const scopedExecute = bind(execute);
|
|
1979
|
+
const scopedQuery = bind(query);
|
|
1980
|
+
const scopedTableExists = bind(tableExists);
|
|
1981
|
+
const scopedSyncSchema = bind(syncSchema);
|
|
1982
|
+
const scopedTable = (tableName) => {
|
|
1983
|
+
if (!accepting) {
|
|
1984
|
+
const reject = () => Promise.reject(new DatabaseError("Transaction scope is ending or ended", {}));
|
|
1985
|
+
return {
|
|
1986
|
+
insert: reject,
|
|
1987
|
+
get: reject,
|
|
1988
|
+
list: reject
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
return {
|
|
1992
|
+
insert: (data) => scopedInsert(tableName, data),
|
|
1993
|
+
get: (where) => scopedGet(tableName, where),
|
|
1994
|
+
list: (where) => scopedList(tableName, where)
|
|
1995
|
+
};
|
|
1996
|
+
};
|
|
1997
|
+
const scopedTransaction = ((callback) => {
|
|
1998
|
+
if (!accepting) return Promise.reject(new DatabaseError("Transaction scope is ending or ended", {}));
|
|
1999
|
+
return executorContext.run(scopedExecutor, () => runNested(callback));
|
|
2000
|
+
});
|
|
2001
|
+
const scopedClient = {
|
|
2002
|
+
transactionReservation: "exclusive",
|
|
2003
|
+
execute: bind((statement) => scopedExecutor.execute(statement)),
|
|
2004
|
+
transaction: () => Promise.reject(new DatabaseError("Use the transaction scope transaction() method instead of client.transaction()", {})),
|
|
2005
|
+
close: () => Promise.reject(new DatabaseError("Transaction-scoped SQLite clients cannot be closed directly", {}))
|
|
2006
|
+
};
|
|
2007
|
+
return {
|
|
2008
|
+
database: {
|
|
2009
|
+
url,
|
|
2010
|
+
client: client.transactionReservation === "exclusive" ? scopedClient : transactionClient,
|
|
2011
|
+
insert: scopedInsert,
|
|
2012
|
+
get: scopedGet,
|
|
2013
|
+
list: scopedList,
|
|
2014
|
+
update: scopedUpdate,
|
|
2015
|
+
upsert: scopedUpsert,
|
|
2016
|
+
getOrInsert: scopedGetOrInsert,
|
|
2017
|
+
delete: scopedDelete,
|
|
2018
|
+
count: scopedCount,
|
|
2019
|
+
table: scopedTable,
|
|
2020
|
+
many: scopedMany,
|
|
2021
|
+
single: scopedSingle,
|
|
2022
|
+
pluck: scopedPluck,
|
|
2023
|
+
execute: scopedExecute,
|
|
2024
|
+
query: scopedQuery,
|
|
2025
|
+
oo: scopedMany,
|
|
2026
|
+
oO: scopedSingle,
|
|
2027
|
+
ox: scopedPluck,
|
|
2028
|
+
xx: scopedExecute,
|
|
2029
|
+
tableExists: scopedTableExists,
|
|
2030
|
+
syncSchema: scopedSyncSchema,
|
|
2031
|
+
transaction: scopedTransaction
|
|
2032
|
+
},
|
|
2033
|
+
sealAndDrain: async () => {
|
|
2034
|
+
accepting = false;
|
|
2035
|
+
drainPromise ??= (async () => {
|
|
2036
|
+
while (pending.size > 0) await Promise.allSettled([...pending]);
|
|
2037
|
+
while (expandPromiseObservations(operations).some((entry) => entry.handlers.size > 0)) await Promise.all(expandPromiseObservations(operations).flatMap((entry) => [...entry.handlers]));
|
|
2038
|
+
await settleNativePromiseTransfers(operations);
|
|
2039
|
+
const failures = expandPromiseObservations(operations).filter((operation) => !operation.observed && "error" in operation).map((operation) => operation.error);
|
|
2040
|
+
if (failures.length === 1) throw failures[0];
|
|
2041
|
+
if (failures.length > 1) throw new AggregateError(failures, "Accepted transaction operations failed");
|
|
2042
|
+
})();
|
|
2043
|
+
return drainPromise;
|
|
2044
|
+
}
|
|
2045
|
+
};
|
|
2046
|
+
};
|
|
2047
|
+
/**
|
|
2048
|
+
* Begins a new transaction and returns a handle for manual control
|
|
2049
|
+
*
|
|
2050
|
+
* Unlike transaction(), this gives you explicit control over commit/rollback.
|
|
2051
|
+
* Ideal for test isolation where you want to rollback after each test.
|
|
2052
|
+
*
|
|
2053
|
+
* @returns Promise resolving to a TransactionHandle
|
|
2054
|
+
*/
|
|
2055
|
+
const beginTransaction = async (preAcquiredLock) => {
|
|
2056
|
+
const releaseConnection = await (preAcquiredLock ?? connectionLock.acquire());
|
|
2057
|
+
let transactionClient;
|
|
2058
|
+
try {
|
|
2059
|
+
if (client.transactionReservation === "exclusive") transactionClient = await client.transaction("write");
|
|
2060
|
+
else await client.execute({
|
|
2061
|
+
sql: "BEGIN TRANSACTION",
|
|
2062
|
+
args: []
|
|
2063
|
+
});
|
|
2064
|
+
} catch (error) {
|
|
2065
|
+
releaseConnection();
|
|
2066
|
+
throw error;
|
|
2067
|
+
}
|
|
2068
|
+
let active = true;
|
|
2069
|
+
let nestedTransaction;
|
|
2070
|
+
let transactionScope;
|
|
2071
|
+
const end = async (command) => {
|
|
2072
|
+
if (!active) throw new DatabaseError("Transaction already ended", {});
|
|
2073
|
+
active = false;
|
|
2074
|
+
try {
|
|
2075
|
+
let drainFailure;
|
|
2076
|
+
try {
|
|
2077
|
+
await transactionScope?.sealAndDrain();
|
|
2078
|
+
} catch (error) {
|
|
2079
|
+
drainFailure = error;
|
|
2080
|
+
}
|
|
2081
|
+
try {
|
|
2082
|
+
await nestedTransaction?.drain();
|
|
2083
|
+
} catch (error) {
|
|
2084
|
+
drainFailure = drainFailure ? combineTransactionFailures(drainFailure, error) : error;
|
|
2085
|
+
}
|
|
2086
|
+
if (drainFailure) throw drainFailure;
|
|
2087
|
+
if (transactionClient) if (command === "COMMIT") await transactionClient.commit();
|
|
2088
|
+
else await transactionClient.rollback();
|
|
2089
|
+
else await client.execute({
|
|
2090
|
+
sql: command,
|
|
2091
|
+
args: []
|
|
2092
|
+
});
|
|
2093
|
+
} catch (error) {
|
|
2094
|
+
let failure = error;
|
|
2095
|
+
if (transactionClient && !transactionClient.closed) try {
|
|
2096
|
+
await transactionClient.rollback();
|
|
2097
|
+
} catch (rollbackError) {
|
|
2098
|
+
failure = combineTransactionFailures(failure, rollbackError);
|
|
2099
|
+
}
|
|
2100
|
+
else if (!transactionClient) try {
|
|
2101
|
+
await client.execute({
|
|
2102
|
+
sql: "ROLLBACK",
|
|
2103
|
+
args: []
|
|
2104
|
+
});
|
|
2105
|
+
} catch (rollbackError) {
|
|
2106
|
+
failure = combineTransactionFailures(failure, rollbackError);
|
|
2107
|
+
}
|
|
2108
|
+
throw failure;
|
|
2109
|
+
} finally {
|
|
2110
|
+
transactionClient?.close();
|
|
2111
|
+
releaseConnection();
|
|
2112
|
+
}
|
|
2113
|
+
};
|
|
2114
|
+
const commit = () => end("COMMIT");
|
|
2115
|
+
const rollback = () => end("ROLLBACK");
|
|
2116
|
+
const isActive = () => active;
|
|
2117
|
+
const executor = transactionClient ?? client;
|
|
2118
|
+
nestedTransaction = createNestedTransaction(executor);
|
|
2119
|
+
transactionScope = createTransactionScope(executor, nestedTransaction);
|
|
2120
|
+
return {
|
|
2121
|
+
...transactionScope.database,
|
|
2122
|
+
commit,
|
|
2123
|
+
rollback,
|
|
2124
|
+
isActive
|
|
2125
|
+
};
|
|
2126
|
+
};
|
|
2127
|
+
/**
|
|
2128
|
+
* Creates a table-specific interface for simplified table operations
|
|
2129
|
+
*
|
|
2130
|
+
* @param tableName - Table name
|
|
2131
|
+
* @returns TableInterface for the specified table
|
|
2132
|
+
*/
|
|
2133
|
+
const table = (tableName) => ({
|
|
2134
|
+
insert: (data) => insert(tableName, data),
|
|
2135
|
+
get: (where) => get(tableName, where),
|
|
2136
|
+
list: (where) => list(tableName, where)
|
|
2137
|
+
});
|
|
2138
|
+
/**
|
|
2139
|
+
* Parses a tagged template literal into a SQL query and values
|
|
2140
|
+
*
|
|
2141
|
+
* @param strings - Template strings
|
|
2142
|
+
* @param vars - Variables to interpolate into the query
|
|
2143
|
+
* @returns Object with SQL query and values array
|
|
2144
|
+
*/
|
|
2145
|
+
const parseTemplate = (strings, ...vars) => {
|
|
2146
|
+
let sql = strings[0];
|
|
2147
|
+
const values = [];
|
|
2148
|
+
for (let i = 0; i < vars.length; i++) {
|
|
2149
|
+
values.push(vars[i]);
|
|
2150
|
+
sql += `?${strings[i + 1]}`;
|
|
2151
|
+
}
|
|
2152
|
+
return {
|
|
2153
|
+
sql,
|
|
2154
|
+
values
|
|
2155
|
+
};
|
|
2156
|
+
};
|
|
2157
|
+
/**
|
|
2158
|
+
* Executes a SQL query using template literals and returns a single value
|
|
2159
|
+
*
|
|
2160
|
+
* @param strings - Template strings
|
|
2161
|
+
* @param vars - Variables to interpolate into the query
|
|
2162
|
+
* @returns Promise resolving to a single value (first column of first row)
|
|
2163
|
+
* @throws Error if the query fails
|
|
2164
|
+
*/
|
|
2165
|
+
const pluck = async (strings, ...vars) => {
|
|
2166
|
+
const { sql, values } = parseTemplate(strings, ...vars);
|
|
2167
|
+
try {
|
|
2168
|
+
const result = await currentExecutor().execute({
|
|
2169
|
+
sql,
|
|
2170
|
+
args: values
|
|
2171
|
+
});
|
|
2172
|
+
return result.rows[0]?.[Object.keys(result.rows[0])[0]] ?? null;
|
|
2173
|
+
} catch (e) {
|
|
2174
|
+
throw new DatabaseError("Failed to execute pluck query", {
|
|
2175
|
+
sql,
|
|
2176
|
+
values,
|
|
2177
|
+
originalError: formatDbError(e)
|
|
2178
|
+
});
|
|
2179
|
+
}
|
|
2180
|
+
};
|
|
2181
|
+
/**
|
|
2182
|
+
* Executes a SQL query using template literals and returns a single row
|
|
2183
|
+
*
|
|
2184
|
+
* @param strings - Template strings
|
|
2185
|
+
* @param vars - Variables to interpolate into the query
|
|
2186
|
+
* @returns Promise resolving to a single result record or null
|
|
2187
|
+
* @throws Error if the query fails
|
|
2188
|
+
*/
|
|
2189
|
+
const single = async (strings, ...vars) => {
|
|
2190
|
+
const { sql, values } = parseTemplate(strings, ...vars);
|
|
2191
|
+
try {
|
|
2192
|
+
return (await currentExecutor().execute({
|
|
2193
|
+
sql,
|
|
2194
|
+
args: values
|
|
2195
|
+
})).rows[0] || null;
|
|
2196
|
+
} catch (e) {
|
|
2197
|
+
throw new DatabaseError("Failed to execute single query", {
|
|
2198
|
+
sql,
|
|
2199
|
+
values,
|
|
2200
|
+
originalError: formatDbError(e)
|
|
2201
|
+
});
|
|
2202
|
+
}
|
|
2203
|
+
};
|
|
2204
|
+
/**
|
|
2205
|
+
* Executes a SQL query using template literals and returns multiple rows
|
|
2206
|
+
*
|
|
2207
|
+
* @param strings - Template strings
|
|
2208
|
+
* @param vars - Variables to interpolate into the query
|
|
2209
|
+
* @returns Promise resolving to array of result records
|
|
2210
|
+
* @throws Error if the query fails
|
|
2211
|
+
*/
|
|
2212
|
+
const many = async (strings, ...vars) => {
|
|
2213
|
+
const { sql, values } = parseTemplate(strings, ...vars);
|
|
2214
|
+
try {
|
|
2215
|
+
return (await currentExecutor().execute({
|
|
2216
|
+
sql,
|
|
2217
|
+
args: values
|
|
2218
|
+
})).rows;
|
|
2219
|
+
} catch (e) {
|
|
2220
|
+
throw new DatabaseError("Failed to execute many query", {
|
|
2221
|
+
sql,
|
|
2222
|
+
values,
|
|
2223
|
+
originalError: formatDbError(e)
|
|
2224
|
+
});
|
|
2225
|
+
}
|
|
2226
|
+
};
|
|
2227
|
+
/**
|
|
2228
|
+
* Executes a SQL query using template literals without returning results
|
|
2229
|
+
*
|
|
2230
|
+
* @param strings - Template strings
|
|
2231
|
+
* @param vars - Variables to interpolate into the query
|
|
2232
|
+
* @returns Promise that resolves when the query completes
|
|
2233
|
+
* @throws Error if the query fails
|
|
2234
|
+
*/
|
|
2235
|
+
const execute = async (strings, ...vars) => {
|
|
2236
|
+
const { sql, values } = parseTemplate(strings, ...vars);
|
|
2237
|
+
try {
|
|
2238
|
+
await currentExecutor().execute({
|
|
2239
|
+
sql,
|
|
2240
|
+
args: values
|
|
2241
|
+
});
|
|
2242
|
+
} catch (e) {
|
|
2243
|
+
throw new DatabaseError("Failed to execute query", {
|
|
2244
|
+
sql,
|
|
2245
|
+
values,
|
|
2246
|
+
originalError: formatDbError(e)
|
|
2247
|
+
});
|
|
2248
|
+
}
|
|
2249
|
+
};
|
|
2250
|
+
/**
|
|
2251
|
+
* Executes a raw SQL query with parameterized values
|
|
2252
|
+
*
|
|
2253
|
+
* @param str - SQL query string
|
|
2254
|
+
* @param values - Variables to use as parameters
|
|
2255
|
+
* @returns Promise resolving to query result with rows and metadata
|
|
2256
|
+
* @throws Error if the query fails
|
|
2257
|
+
*/
|
|
2258
|
+
const query = async (str, ...values) => {
|
|
2259
|
+
const sql = str;
|
|
2260
|
+
const args = Array.isArray(values[0]) ? values[0] : values;
|
|
2261
|
+
try {
|
|
2262
|
+
const result = await currentExecutor().execute({
|
|
2263
|
+
sql,
|
|
2264
|
+
args
|
|
2265
|
+
});
|
|
2266
|
+
return {
|
|
2267
|
+
command: sql.split(" ")[0].toUpperCase(),
|
|
2268
|
+
rowCount: result.rowsAffected ?? result.rows.length,
|
|
2269
|
+
oid: null,
|
|
2270
|
+
fields: Object.keys(result.rows[0] || {}).map((name) => ({
|
|
2271
|
+
name,
|
|
2272
|
+
tableID: 0,
|
|
2273
|
+
columnID: 0,
|
|
2274
|
+
dataTypeID: 0,
|
|
2275
|
+
dataTypeSize: -1,
|
|
2276
|
+
dataTypeModifier: -1,
|
|
2277
|
+
format: "text"
|
|
2278
|
+
})),
|
|
2279
|
+
rows: result.rows
|
|
2280
|
+
};
|
|
2281
|
+
} catch (e) {
|
|
2282
|
+
throw wrapDatabaseError("Failed to execute raw query", e, {
|
|
2283
|
+
sql,
|
|
2284
|
+
args
|
|
2285
|
+
});
|
|
2286
|
+
}
|
|
2287
|
+
};
|
|
2288
|
+
const oo = many;
|
|
2289
|
+
const oO = single;
|
|
2290
|
+
const ox = pluck;
|
|
2291
|
+
const xx = execute;
|
|
2292
|
+
/**
|
|
2293
|
+
* Initialize database schemas from JSON manifest
|
|
2294
|
+
* Supports dependency resolution and schema overrides
|
|
2295
|
+
*
|
|
2296
|
+
* @param options - Schema initialization options
|
|
2297
|
+
* @returns Promise that resolves when schemas are initialized
|
|
2298
|
+
*/
|
|
2299
|
+
const initializeSchemas = async (options) => {
|
|
2300
|
+
const schemaManager = new DatabaseSchemaManager();
|
|
2301
|
+
const currentDb = {
|
|
2302
|
+
url,
|
|
2303
|
+
client,
|
|
2304
|
+
query,
|
|
2305
|
+
insert,
|
|
2306
|
+
update,
|
|
2307
|
+
upsert,
|
|
2308
|
+
get,
|
|
2309
|
+
list,
|
|
2310
|
+
getOrInsert,
|
|
2311
|
+
delete: deleteRecords,
|
|
2312
|
+
count,
|
|
2313
|
+
table,
|
|
2314
|
+
tableExists,
|
|
2315
|
+
many,
|
|
2316
|
+
single,
|
|
2317
|
+
pluck,
|
|
2318
|
+
execute,
|
|
2319
|
+
oo,
|
|
2320
|
+
oO,
|
|
2321
|
+
ox,
|
|
2322
|
+
xx,
|
|
2323
|
+
syncSchema,
|
|
2324
|
+
transaction
|
|
2325
|
+
};
|
|
2326
|
+
await schemaManager.initializeSchemas(currentDb, options);
|
|
2327
|
+
};
|
|
2328
|
+
/**
|
|
2329
|
+
* Retrieves the schema information for a table
|
|
2330
|
+
*
|
|
2331
|
+
* @param table - Table name
|
|
2332
|
+
* @returns Promise resolving to table schema info or null if table doesn't exist
|
|
2333
|
+
* @throws Error if the query fails
|
|
2334
|
+
*/
|
|
2335
|
+
const getTableSchema = async (table) => {
|
|
2336
|
+
validateTableName(table);
|
|
2337
|
+
try {
|
|
2338
|
+
if (!await tableExists(table)) return null;
|
|
2339
|
+
const columnRows = await many`SELECT * FROM pragma_table_info(${table})`;
|
|
2340
|
+
const columns = {};
|
|
2341
|
+
for (const row of columnRows) {
|
|
2342
|
+
const colName = row.name;
|
|
2343
|
+
columns[colName] = {
|
|
2344
|
+
type: row.type,
|
|
2345
|
+
primaryKey: row.pk === 1,
|
|
2346
|
+
notNull: row.notnull === 1,
|
|
2347
|
+
defaultValue: row.dflt_value
|
|
2348
|
+
};
|
|
2349
|
+
}
|
|
2350
|
+
const indexRows = await many`
|
|
2351
|
+
SELECT name, sql FROM sqlite_master
|
|
2352
|
+
WHERE type = 'index'
|
|
2353
|
+
AND tbl_name = ${table}
|
|
2354
|
+
AND name NOT LIKE 'sqlite_%'
|
|
2355
|
+
`;
|
|
2356
|
+
const indexes = [];
|
|
2357
|
+
for (const row of indexRows) {
|
|
2358
|
+
const indexName = row.name;
|
|
2359
|
+
const indexInfoRows = await many`SELECT * FROM pragma_index_info(${indexName})`;
|
|
2360
|
+
const indexColumns = [];
|
|
2361
|
+
for (const infoRow of indexInfoRows) indexColumns.push(infoRow.name);
|
|
2362
|
+
const indexListRow = await single`SELECT * FROM pragma_index_list(${table}) WHERE name = ${indexName}`;
|
|
2363
|
+
indexes.push({
|
|
2364
|
+
name: indexName,
|
|
2365
|
+
columns: indexColumns,
|
|
2366
|
+
unique: indexListRow?.unique === 1
|
|
2367
|
+
});
|
|
2368
|
+
}
|
|
2369
|
+
const fkRows = await many`SELECT * FROM pragma_foreign_key_list(${table})`;
|
|
2370
|
+
const foreignKeys = [];
|
|
2371
|
+
for (const fkRow of fkRows) foreignKeys.push({
|
|
2372
|
+
column: fkRow.from,
|
|
2373
|
+
referencesTable: fkRow.table,
|
|
2374
|
+
referencesColumn: fkRow.to,
|
|
2375
|
+
onDelete: fkRow.on_delete,
|
|
2376
|
+
onUpdate: fkRow.on_update
|
|
2377
|
+
});
|
|
2378
|
+
return {
|
|
2379
|
+
tableName: table,
|
|
2380
|
+
columns,
|
|
2381
|
+
indexes,
|
|
2382
|
+
foreignKeys
|
|
2383
|
+
};
|
|
2384
|
+
} catch (e) {
|
|
2385
|
+
throw new DatabaseError("Failed to retrieve table schema", {
|
|
2386
|
+
table,
|
|
2387
|
+
originalError: formatDbError(e)
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2390
|
+
};
|
|
2391
|
+
/**
|
|
2392
|
+
* ALTER TABLE operations for schema evolution
|
|
2393
|
+
*/
|
|
2394
|
+
const alterTable = {
|
|
2395
|
+
/**
|
|
2396
|
+
* Adds a new column to an existing table
|
|
2397
|
+
*
|
|
2398
|
+
* @param table - Table name
|
|
2399
|
+
* @param column - Column definition with name
|
|
2400
|
+
* @returns Promise that resolves when column is added
|
|
2401
|
+
* @throws Error if the alter operation fails
|
|
2402
|
+
*/
|
|
2403
|
+
addColumn: async (table, column) => {
|
|
2404
|
+
validateTableName(table);
|
|
2405
|
+
validateColumnName(column.name);
|
|
2406
|
+
let sql;
|
|
2407
|
+
try {
|
|
2408
|
+
sql = generateAddColumnStatement(table, column, "sqlite");
|
|
2409
|
+
await currentExecutor().execute({
|
|
2410
|
+
sql,
|
|
2411
|
+
args: []
|
|
2412
|
+
});
|
|
2413
|
+
} catch (e) {
|
|
2414
|
+
throw wrapDatabaseError("Failed to add column to table", e, {
|
|
2415
|
+
table,
|
|
2416
|
+
column: column.name,
|
|
2417
|
+
...sql ? { sql } : {}
|
|
2418
|
+
});
|
|
2419
|
+
}
|
|
2420
|
+
},
|
|
2421
|
+
/**
|
|
2422
|
+
* Adds a new index to an existing table
|
|
2423
|
+
*
|
|
2424
|
+
* @param table - Table name
|
|
2425
|
+
* @param index - Index definition
|
|
2426
|
+
* @returns Promise that resolves when index is created
|
|
2427
|
+
* @throws Error if the create index operation fails
|
|
2428
|
+
*/
|
|
2429
|
+
addIndex: async (table, index) => {
|
|
2430
|
+
validateTableName(table);
|
|
2431
|
+
validateIndexName(index.name);
|
|
2432
|
+
for (const col of index.columns) validateColumnName(col);
|
|
2433
|
+
let sql;
|
|
2434
|
+
try {
|
|
2435
|
+
sql = generateCreateIndexStatement(table, index);
|
|
2436
|
+
await currentExecutor().execute({
|
|
2437
|
+
sql,
|
|
2438
|
+
args: []
|
|
2439
|
+
});
|
|
2440
|
+
} catch (e) {
|
|
2441
|
+
throw wrapDatabaseError("Failed to create index on table", e, {
|
|
2442
|
+
table,
|
|
2443
|
+
index: index.name,
|
|
2444
|
+
...sql ? { sql } : {}
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
};
|
|
2449
|
+
let closed = false;
|
|
2450
|
+
const rawClose = async () => {
|
|
2451
|
+
if (closed) return;
|
|
2452
|
+
await client.close();
|
|
2453
|
+
closed = true;
|
|
2454
|
+
};
|
|
2455
|
+
const db = {
|
|
2456
|
+
url,
|
|
2457
|
+
client,
|
|
2458
|
+
query,
|
|
2459
|
+
insert,
|
|
2460
|
+
update,
|
|
2461
|
+
upsert,
|
|
2462
|
+
get,
|
|
2463
|
+
list,
|
|
2464
|
+
getOrInsert,
|
|
2465
|
+
delete: deleteRecords,
|
|
2466
|
+
count,
|
|
2467
|
+
table,
|
|
2468
|
+
tableExists,
|
|
2469
|
+
many,
|
|
2470
|
+
single,
|
|
2471
|
+
pluck,
|
|
2472
|
+
execute,
|
|
2473
|
+
oo,
|
|
2474
|
+
oO,
|
|
2475
|
+
ox,
|
|
2476
|
+
xx,
|
|
2477
|
+
syncSchema,
|
|
2478
|
+
initializeSchemas,
|
|
2479
|
+
transaction,
|
|
2480
|
+
beginTransaction,
|
|
2481
|
+
getTableSchema,
|
|
2482
|
+
alterTable,
|
|
2483
|
+
close: rawClose
|
|
2484
|
+
};
|
|
2485
|
+
if (client.transactionReservation !== "exclusive") return db;
|
|
2486
|
+
const barrier = createInvocationBarrier();
|
|
2487
|
+
const managedRootExecutor = { execute: (statement) => {
|
|
2488
|
+
return client.execute(prepareManagedStatement(statement));
|
|
2489
|
+
} };
|
|
2490
|
+
const rootCallbackError = (operation) => operation === "close" ? new DatabaseError("Cannot close the SQLite database from inside its active transaction callback", { hint: "Finish the transaction callback before closing the database." }) : new DatabaseError("Cannot use the root SQLite database from inside its active transaction callback", { hint: "Use the transaction-scoped database passed to the callback." });
|
|
2491
|
+
const reservedClose = () => {
|
|
2492
|
+
if (transactionCallbackContext.getStore() === true) return Promise.reject(rootCallbackError("close"));
|
|
2493
|
+
return barrier.close(async () => {
|
|
2494
|
+
if (connectionLock.held) (await connectionLock.acquire())();
|
|
2495
|
+
await rawClose();
|
|
2496
|
+
});
|
|
2497
|
+
};
|
|
2498
|
+
const reserveManaged = (fn) => ((...args) => {
|
|
2499
|
+
if (transactionCallbackContext.getStore() === true) return Promise.reject(rootCallbackError("use"));
|
|
2500
|
+
const timeoutMs = options.transactionQueueTimeout ?? 3e4;
|
|
2501
|
+
let expired = false;
|
|
2502
|
+
let timer;
|
|
2503
|
+
const deadline = new Promise((_, reject) => {
|
|
2504
|
+
timer = setTimeout(() => {
|
|
2505
|
+
expired = true;
|
|
2506
|
+
reject(new DatabaseError(`Timed out after ${timeoutMs}ms waiting for the sqlite connection's current operation to finish`, {
|
|
2507
|
+
adapter: "sqlite",
|
|
2508
|
+
timeoutMs
|
|
2509
|
+
}));
|
|
2510
|
+
}, timeoutMs);
|
|
2511
|
+
});
|
|
2512
|
+
const queued = barrier.run(async () => {
|
|
2513
|
+
if (expired) return deadline;
|
|
2514
|
+
if (timer) {
|
|
2515
|
+
clearTimeout(timer);
|
|
2516
|
+
timer = void 0;
|
|
2517
|
+
}
|
|
2518
|
+
if (connectionLock.held) (await connectionLock.acquire())();
|
|
2519
|
+
return executorContext.run(managedRootExecutor, () => fn(...args));
|
|
2520
|
+
});
|
|
2521
|
+
return Promise.race([queued, deadline]).finally(() => {
|
|
2522
|
+
if (timer) clearTimeout(timer);
|
|
2523
|
+
});
|
|
2524
|
+
});
|
|
2525
|
+
const reservedTable = (tableName) => {
|
|
2526
|
+
const scoped = table(tableName);
|
|
2527
|
+
return {
|
|
2528
|
+
insert: reserveManaged(scoped.insert),
|
|
2529
|
+
get: reserveManaged(scoped.get),
|
|
2530
|
+
list: reserveManaged(scoped.list)
|
|
2531
|
+
};
|
|
2532
|
+
};
|
|
2533
|
+
const reservedAlterTable = {
|
|
2534
|
+
addColumn: reserveManaged(alterTable.addColumn),
|
|
2535
|
+
addIndex: reserveManaged(alterTable.addIndex)
|
|
2536
|
+
};
|
|
2537
|
+
const reserveTransactionEntry = (work) => {
|
|
2538
|
+
if (transactionCallbackContext.getStore() === true) return Promise.reject(rootCallbackError("use"));
|
|
2539
|
+
const timeoutMs = options.transactionQueueTimeout ?? 3e4;
|
|
2540
|
+
let expired = false;
|
|
2541
|
+
let timer;
|
|
2542
|
+
const deadline = new Promise((_, reject) => {
|
|
2543
|
+
timer = setTimeout(() => {
|
|
2544
|
+
expired = true;
|
|
2545
|
+
reject(new DatabaseError(`Timed out after ${timeoutMs}ms waiting for the sqlite connection's current transaction to finish`, {
|
|
2546
|
+
adapter: "sqlite",
|
|
2547
|
+
timeoutMs
|
|
2548
|
+
}));
|
|
2549
|
+
}, timeoutMs);
|
|
2550
|
+
});
|
|
2551
|
+
const queued = barrier.run(async () => {
|
|
2552
|
+
if (expired) return deadline;
|
|
2553
|
+
const acquisition = connectionLock.acquire();
|
|
2554
|
+
try {
|
|
2555
|
+
const release = await Promise.race([acquisition, deadline]);
|
|
2556
|
+
if (expired) {
|
|
2557
|
+
release();
|
|
2558
|
+
return deadline;
|
|
2559
|
+
}
|
|
2560
|
+
if (timer) {
|
|
2561
|
+
clearTimeout(timer);
|
|
2562
|
+
timer = void 0;
|
|
2563
|
+
}
|
|
2564
|
+
return work(Promise.resolve(release));
|
|
2565
|
+
} catch (error) {
|
|
2566
|
+
if (expired) acquisition.then((release) => release(), () => {});
|
|
2567
|
+
throw error;
|
|
2568
|
+
}
|
|
2569
|
+
});
|
|
2570
|
+
return Promise.race([queued, deadline]).finally(() => {
|
|
2571
|
+
if (timer) clearTimeout(timer);
|
|
2572
|
+
});
|
|
2573
|
+
};
|
|
2574
|
+
const reservedTransaction = (callback) => reserveTransactionEntry((preAcquiredLock) => transaction(callback, preAcquiredLock));
|
|
2575
|
+
const reservedBeginTransaction = () => reserveTransactionEntry((preAcquiredLock) => beginTransaction(preAcquiredLock));
|
|
2576
|
+
const reservedClient = {
|
|
2577
|
+
transactionReservation: "exclusive",
|
|
2578
|
+
execute: reserveManaged(async (statement) => {
|
|
2579
|
+
return client.execute(prepareManagedStatement(statement));
|
|
2580
|
+
}),
|
|
2581
|
+
transaction: () => Promise.reject(new DatabaseError("Use database.transaction() or database.beginTransaction() instead of client.transaction()", {})),
|
|
2582
|
+
close: reservedClose
|
|
2583
|
+
};
|
|
2584
|
+
return {
|
|
2585
|
+
...db,
|
|
2586
|
+
client: reservedClient,
|
|
2587
|
+
query: reserveManaged(query),
|
|
2588
|
+
insert: reserveManaged(insert),
|
|
2589
|
+
update: reserveManaged(update),
|
|
2590
|
+
upsert: reserveManaged(upsert),
|
|
2591
|
+
get: reserveManaged(get),
|
|
2592
|
+
list: reserveManaged(list),
|
|
2593
|
+
getOrInsert: reserveManaged(getOrInsert),
|
|
2594
|
+
delete: reserveManaged(deleteRecords),
|
|
2595
|
+
count: reserveManaged(count),
|
|
2596
|
+
table: reservedTable,
|
|
2597
|
+
tableExists: reserveManaged(tableExists),
|
|
2598
|
+
many: reserveManaged(many),
|
|
2599
|
+
single: reserveManaged(single),
|
|
2600
|
+
pluck: reserveManaged(pluck),
|
|
2601
|
+
execute: reserveManaged(execute),
|
|
2602
|
+
oo: reserveManaged(oo),
|
|
2603
|
+
oO: reserveManaged(oO),
|
|
2604
|
+
ox: reserveManaged(ox),
|
|
2605
|
+
xx: reserveManaged(xx),
|
|
2606
|
+
syncSchema: reserveManaged(syncSchema),
|
|
2607
|
+
initializeSchemas: reserveManaged(initializeSchemas),
|
|
2608
|
+
transaction: reservedTransaction,
|
|
2609
|
+
beginTransaction: reservedBeginTransaction,
|
|
2610
|
+
getTableSchema: reserveManaged(getTableSchema),
|
|
2611
|
+
alterTable: reservedAlterTable,
|
|
2612
|
+
close: reservedClose
|
|
2613
|
+
};
|
|
2614
|
+
})();
|
|
2615
|
+
}
|
|
2616
|
+
//#endregion
|
|
2617
|
+
export { getDatabase };
|
|
2618
|
+
|
|
2619
|
+
//# sourceMappingURL=sqlite-Cz2NMiDs.js.map
|