@velarscript-labs/sqlite 0.3.3 → 0.3.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/CHANGELOG.md +6 -0
- package/README.md +6 -0
- package/THIRD_PARTY_NOTICES.md +35 -0
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/index.veli.json +73 -73
- package/dist/velar-library.json +10 -6
- package/package.json +13 -4
- package/src/generated/worker-source.vel +3 -0
- package/src/index.vel +44 -242
package/src/index.vel
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/// Per-connection bounds. Every operation is serialized through one Worker;
|
|
2
2
|
/// queueCapacity bounds admitted operations waiting for that connection.
|
|
3
3
|
import {DatabaseExecutor, DatabaseStatement, trustedSql} from "@velarscript-labs/database"
|
|
4
|
+
import {sqliteWorkerSource, sqliteWorkerSourceFormatVersion} from "./generated/worker-source.vel"
|
|
5
|
+
|
|
6
|
+
assert sqliteWorkerSourceFormatVersion == 1 else "SQLite Worker source module has an unsupported format"
|
|
4
7
|
|
|
5
8
|
export enum SqliteJournalMode:
|
|
6
9
|
delete
|
|
@@ -80,235 +83,6 @@ extern js`
|
|
|
80
83
|
};
|
|
81
84
|
}
|
|
82
85
|
|
|
83
|
-
function workerMain() {
|
|
84
|
-
const { Buffer } = require("node:buffer");
|
|
85
|
-
const { parentPort, workerData } = require("node:worker_threads");
|
|
86
|
-
const { DatabaseSync, constants } = require("node:sqlite");
|
|
87
|
-
const { parseStmt } = require("sqlite3-parser");
|
|
88
|
-
let database = null;
|
|
89
|
-
let statements = null;
|
|
90
|
-
let closed = false;
|
|
91
|
-
|
|
92
|
-
function failure(message, code, operation, retryable = false) {
|
|
93
|
-
const error = new Error(message);
|
|
94
|
-
error.code = code;
|
|
95
|
-
error.operation = operation;
|
|
96
|
-
error.retryable = retryable;
|
|
97
|
-
return error;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function errorData(error, operation) {
|
|
101
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
102
|
-
const rawCode = error && typeof error === "object" ? error.errstr ?? error.code ?? null : null;
|
|
103
|
-
const code = rawCode == null ? null : String(rawCode);
|
|
104
|
-
const upper = code == null ? "" : code.toUpperCase();
|
|
105
|
-
return {
|
|
106
|
-
message,
|
|
107
|
-
sqliteCode: code,
|
|
108
|
-
operation: error && typeof error.operation === "string" ? error.operation : operation,
|
|
109
|
-
retryable: error && error.retryable === true || upper.includes("BUSY") || upper.includes("LOCKED"),
|
|
110
|
-
};
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function parameter(value, budget) {
|
|
114
|
-
if (value === null) return null;
|
|
115
|
-
if (typeof value === "boolean") return value ? 1 : 0;
|
|
116
|
-
if (typeof value === "string") {
|
|
117
|
-
budget.value += Buffer.byteLength(value, "utf8");
|
|
118
|
-
return value;
|
|
119
|
-
}
|
|
120
|
-
if (typeof value === "number") {
|
|
121
|
-
if (!Number.isFinite(value)) throw failure("SQLite number parameters must be finite", "SQLITE_INPUT", "params");
|
|
122
|
-
if (Number.isInteger(value) && !Number.isSafeInteger(value)) throw failure("SQLite integer parameters must be safe integers", "SQLITE_INPUT", "params");
|
|
123
|
-
budget.value += 8;
|
|
124
|
-
return value;
|
|
125
|
-
}
|
|
126
|
-
if (value instanceof Uint8Array) {
|
|
127
|
-
budget.value += value.byteLength;
|
|
128
|
-
return value;
|
|
129
|
-
}
|
|
130
|
-
throw failure("SQLite parameters support only null, bool, string, finite number, and Bytes", "SQLITE_INPUT", "params");
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function parameters(value) {
|
|
134
|
-
if (!Array.isArray(value) || value.length > 999) throw failure("SQLite parameters must be a List with at most 999 values", "SQLITE_INPUT", "params");
|
|
135
|
-
const budget = { value: 0 };
|
|
136
|
-
const output = value.map(item => parameter(item, budget));
|
|
137
|
-
if (budget.value > workerData.maxResultBytes) throw failure("SQLite parameters exceed the connection byte limit", "SQLITE_INPUT", "params");
|
|
138
|
-
return output;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function statementParts(rawFragments, rawParameters, operation) {
|
|
142
|
-
if (!Array.isArray(rawFragments) || rawFragments.length !== rawParameters.length + 1 || rawFragments.length > 1000) {
|
|
143
|
-
throw failure("SQLite statements require exactly one more SQL fragment than bound parameters", "SQLITE_INPUT", operation);
|
|
144
|
-
}
|
|
145
|
-
let sourceBytes = 0;
|
|
146
|
-
const fragments = rawFragments.map(fragment => {
|
|
147
|
-
if (typeof fragment !== "string" || fragment.includes("\0") || fragment.includes("?")) {
|
|
148
|
-
throw failure("SQLite SQL fragments must be text without NUL or raw parameter placeholders", "SQLITE_INPUT", operation);
|
|
149
|
-
}
|
|
150
|
-
sourceBytes += Buffer.byteLength(fragment, "utf8");
|
|
151
|
-
return fragment;
|
|
152
|
-
});
|
|
153
|
-
if (sourceBytes === 0 || sourceBytes > 1024 * 1024) throw failure("SQLite SQL must be non-empty text no longer than 1 MiB", "SQLITE_INPUT", operation);
|
|
154
|
-
const bound = parameters(rawParameters);
|
|
155
|
-
const source = fragments.join("?");
|
|
156
|
-
const parsed = parseStmt(source);
|
|
157
|
-
if (parsed.status !== "ok") {
|
|
158
|
-
const message = parsed.errors.length === 0 ? "invalid statement" : parsed.errors[0].message;
|
|
159
|
-
throw failure("SQLite requires exactly one valid statement: " + message, "SQLITE_INPUT", operation);
|
|
160
|
-
}
|
|
161
|
-
return { fragments, bound };
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function count(value, label) {
|
|
165
|
-
const number = typeof value === "bigint" ? Number(value) : value;
|
|
166
|
-
if (!Number.isSafeInteger(number) || number < 0) throw failure(label + " is outside VelarScript's safe integer range", "SQLITE_INTEGER_RANGE", "result");
|
|
167
|
-
return number;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function resultValue(value, budget, field) {
|
|
171
|
-
if (value === null) return null;
|
|
172
|
-
if (typeof value === "string") {
|
|
173
|
-
budget.value += Buffer.byteLength(value, "utf8");
|
|
174
|
-
return value;
|
|
175
|
-
}
|
|
176
|
-
if (typeof value === "number") {
|
|
177
|
-
if (!Number.isFinite(value)) throw failure("SQLite returned a non-finite number in " + field, "SQLITE_RESULT", "result");
|
|
178
|
-
budget.value += 8;
|
|
179
|
-
return value;
|
|
180
|
-
}
|
|
181
|
-
if (typeof value === "bigint") {
|
|
182
|
-
const number = Number(value);
|
|
183
|
-
if (!Number.isSafeInteger(number)) throw failure("SQLite returned an integer outside VelarScript's safe range in " + field, "SQLITE_INTEGER_RANGE", "result");
|
|
184
|
-
budget.value += 8;
|
|
185
|
-
return number;
|
|
186
|
-
}
|
|
187
|
-
if (value instanceof Uint8Array) {
|
|
188
|
-
budget.value += value.byteLength;
|
|
189
|
-
return value;
|
|
190
|
-
}
|
|
191
|
-
throw failure("SQLite returned an unsupported value in " + field, "SQLITE_RESULT", "result");
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function resultRow(value, budget) {
|
|
195
|
-
if (value === null || typeof value !== "object" || Array.isArray(value)) throw failure("SQLite returned a non-record row", "SQLITE_RESULT", "result");
|
|
196
|
-
const output = Object.create(null);
|
|
197
|
-
for (const key of Object.keys(value)) {
|
|
198
|
-
budget.value += Buffer.byteLength(key, "utf8");
|
|
199
|
-
output[key] = resultValue(value[key], budget, key);
|
|
200
|
-
if (budget.value > workerData.maxResultBytes) throw failure("SQLite result exceeds the connection byte limit", "SQLITE_RESULT_LIMIT", "result");
|
|
201
|
-
}
|
|
202
|
-
return output;
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
function execute(message) {
|
|
206
|
-
const statement = statementParts(message.fragments, message.params, "execute");
|
|
207
|
-
const result = statements.run(statement.fragments, ...statement.bound);
|
|
208
|
-
return count(result.changes, "SQLite affected row count");
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
function one(message) {
|
|
212
|
-
const statement = statementParts(message.fragments, message.params, "one");
|
|
213
|
-
const row = statements.get(statement.fragments, ...statement.bound);
|
|
214
|
-
if (row === undefined) return null;
|
|
215
|
-
return resultRow(row, { value: 0 });
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
function all(message) {
|
|
219
|
-
const statement = statementParts(message.fragments, message.params, "all");
|
|
220
|
-
const rows = [];
|
|
221
|
-
const budget = { value: 0 };
|
|
222
|
-
for (const row of statements.iterate(statement.fragments, ...statement.bound)) {
|
|
223
|
-
if (rows.length >= workerData.maxRows) throw failure("SQLite result exceeds the connection row limit", "SQLITE_ROW_LIMIT", "all");
|
|
224
|
-
rows.push(resultRow(row, budget));
|
|
225
|
-
}
|
|
226
|
-
return rows;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
function dispatch(message) {
|
|
230
|
-
if (closed) throw failure("SQLite connection is closed", "SQLITE_CLOSED", message.operation);
|
|
231
|
-
if (message.operation === "execute") return execute(message);
|
|
232
|
-
if (message.operation === "one") return one(message);
|
|
233
|
-
if (message.operation === "all") return all(message);
|
|
234
|
-
if (message.operation === "begin") {
|
|
235
|
-
if (database.isTransaction) throw failure("SQLite transaction is already active", "SQLITE_TRANSACTION", "begin", true);
|
|
236
|
-
database.exec("BEGIN IMMEDIATE");
|
|
237
|
-
return null;
|
|
238
|
-
}
|
|
239
|
-
if (message.operation === "commit") {
|
|
240
|
-
if (!database.isTransaction) throw failure("SQLite transaction is not active", "SQLITE_TRANSACTION", "commit");
|
|
241
|
-
database.exec("COMMIT");
|
|
242
|
-
return null;
|
|
243
|
-
}
|
|
244
|
-
if (message.operation === "rollback") {
|
|
245
|
-
if (database.isTransaction) database.exec("ROLLBACK");
|
|
246
|
-
return null;
|
|
247
|
-
}
|
|
248
|
-
if (message.operation === "close") {
|
|
249
|
-
try {
|
|
250
|
-
if (database.isTransaction) database.exec("ROLLBACK");
|
|
251
|
-
} finally {
|
|
252
|
-
database.close();
|
|
253
|
-
closed = true;
|
|
254
|
-
}
|
|
255
|
-
return null;
|
|
256
|
-
}
|
|
257
|
-
throw failure("Unknown SQLite operation", "SQLITE_PROTOCOL", String(message.operation));
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
try {
|
|
261
|
-
database = new DatabaseSync(workerData.path, {
|
|
262
|
-
timeout: workerData.busyTimeoutMilliseconds,
|
|
263
|
-
readOnly: workerData.readOnly,
|
|
264
|
-
enableForeignKeyConstraints: true,
|
|
265
|
-
enableDoubleQuotedStringLiterals: false,
|
|
266
|
-
allowExtension: false,
|
|
267
|
-
readBigInts: true,
|
|
268
|
-
allowBareNamedParameters: false,
|
|
269
|
-
allowUnknownNamedParameters: false,
|
|
270
|
-
defensive: true,
|
|
271
|
-
limits: {
|
|
272
|
-
length: workerData.maxResultBytes,
|
|
273
|
-
sqlLength: 1024 * 1024,
|
|
274
|
-
column: 2000,
|
|
275
|
-
exprDepth: 1000,
|
|
276
|
-
compoundSelect: 500,
|
|
277
|
-
vdbeOp: 250000,
|
|
278
|
-
functionArg: 1000,
|
|
279
|
-
attach: 0,
|
|
280
|
-
likePatternLength: 50000,
|
|
281
|
-
variableNumber: 999,
|
|
282
|
-
triggerDepth: 100,
|
|
283
|
-
},
|
|
284
|
-
});
|
|
285
|
-
database.enableDefensive(true);
|
|
286
|
-
database.enableLoadExtension(false);
|
|
287
|
-
database.setAuthorizer((actionCode, arg1, arg2) => {
|
|
288
|
-
if (actionCode === constants.SQLITE_ATTACH || actionCode === constants.SQLITE_DETACH) return constants.SQLITE_DENY;
|
|
289
|
-
if (actionCode === constants.SQLITE_FUNCTION && typeof arg2 === "string" && arg2.toLowerCase() === "load_extension") return constants.SQLITE_DENY;
|
|
290
|
-
return constants.SQLITE_OK;
|
|
291
|
-
});
|
|
292
|
-
statements = database.createTagStore(workerData.statementCacheCapacity);
|
|
293
|
-
if (workerData.journalMode !== null) database.exec("PRAGMA journal_mode = " + workerData.journalMode.toUpperCase());
|
|
294
|
-
parentPort.postMessage({ kind: "ready" });
|
|
295
|
-
} catch (error) {
|
|
296
|
-
parentPort.postMessage({ kind: "ready", error: errorData(error, "open") });
|
|
297
|
-
parentPort.close();
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
parentPort.on("message", message => {
|
|
302
|
-
try {
|
|
303
|
-
const value = dispatch(message);
|
|
304
|
-
parentPort.postMessage({ kind: "response", id: message.id, value });
|
|
305
|
-
if (message.operation === "close") parentPort.close();
|
|
306
|
-
} catch (error) {
|
|
307
|
-
parentPort.postMessage({ kind: "response", id: message.id, error: errorData(error, message.operation) });
|
|
308
|
-
}
|
|
309
|
-
});
|
|
310
|
-
}
|
|
311
|
-
|
|
312
86
|
function nativeError(value) {
|
|
313
87
|
return new NativeSqliteError(value.message, value.sqliteCode, value.operation, value.retryable);
|
|
314
88
|
}
|
|
@@ -323,11 +97,18 @@ extern js`
|
|
|
323
97
|
state.pending.clear();
|
|
324
98
|
}
|
|
325
99
|
|
|
100
|
+
function failWorker(state, error) {
|
|
101
|
+
if (state.workerFailure !== null) return;
|
|
102
|
+
state.workerFailure = error;
|
|
103
|
+
state.closed = true;
|
|
104
|
+
failPending(state, error);
|
|
105
|
+
}
|
|
106
|
+
|
|
326
107
|
function request(state, operation, fragments = [""], params = []) {
|
|
327
108
|
if (state.closed) return Promise.reject(new NativeSqliteError("SQLite connection is closed", "SQLITE_CLOSED", operation));
|
|
328
109
|
const id = state.nextId++;
|
|
329
110
|
return new Promise((resolve, reject) => {
|
|
330
|
-
state.pending.set(id, { resolve, reject });
|
|
111
|
+
state.pending.set(id, { resolve, reject, operation });
|
|
331
112
|
try {
|
|
332
113
|
state.worker.postMessage({ id, operation, fragments, params });
|
|
333
114
|
} catch (error) {
|
|
@@ -430,9 +211,16 @@ extern js`
|
|
|
430
211
|
if (transactionContext.getStore() === state) return Promise.reject(new NativeSqliteConcurrencyError("SQLite connection cannot close inside its transaction callback"));
|
|
431
212
|
state.closing = true;
|
|
432
213
|
state.closePromise = state.tail.then(async () => {
|
|
214
|
+
let closeError = null;
|
|
433
215
|
try {
|
|
434
216
|
if (!state.closed) await request(state, "close");
|
|
217
|
+
} catch (error) {
|
|
218
|
+
closeError = error;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
435
221
|
await state.exited;
|
|
222
|
+
if (closeError !== null) throw closeError;
|
|
223
|
+
if (state.workerFailure !== null) throw state.workerFailure;
|
|
436
224
|
} finally {
|
|
437
225
|
state.closed = true;
|
|
438
226
|
}
|
|
@@ -441,11 +229,13 @@ extern js`
|
|
|
441
229
|
}
|
|
442
230
|
}
|
|
443
231
|
|
|
444
|
-
export async function openNativeSqlite(path, options = {}) {
|
|
232
|
+
export async function openNativeSqlite(path, options = {}, workerSource = "") {
|
|
445
233
|
if (typeof path !== "string" || path.trim() === "" || path.length > 4096 || path.includes("\0")) throw new TypeError("SQLite path must be non-empty bounded text");
|
|
234
|
+
if (typeof workerSource !== "string") throw new TypeError("SQLite Worker source must be text");
|
|
235
|
+
if (workerSource.length === 0 || workerSource.length > 4 * 1024 * 1024) throw new TypeError("SQLite Worker source must be non-empty bounded text");
|
|
446
236
|
const checked = checkedOptions(options);
|
|
447
|
-
const
|
|
448
|
-
const worker = new Worker(
|
|
237
|
+
const workerUrl = new URL("data:text/javascript;charset=utf-8," + encodeURIComponent(workerSource));
|
|
238
|
+
const worker = new Worker(workerUrl, { type: "module", workerData: { path, ...checked } });
|
|
449
239
|
let readyResolve;
|
|
450
240
|
let readyReject;
|
|
451
241
|
let exitResolve;
|
|
@@ -463,10 +253,14 @@ extern js`
|
|
|
463
253
|
tail: Promise.resolve(),
|
|
464
254
|
closing: false,
|
|
465
255
|
closed: false,
|
|
256
|
+
closeAcknowledged: false,
|
|
257
|
+
workerFailure: null,
|
|
258
|
+
exitCode: null,
|
|
466
259
|
closePromise: null,
|
|
467
260
|
};
|
|
468
261
|
worker.on("message", message => {
|
|
469
262
|
if (message.kind === "ready") {
|
|
263
|
+
if (state.readyResolve === null) return;
|
|
470
264
|
const resolve = state.readyResolve;
|
|
471
265
|
const reject = state.readyReject;
|
|
472
266
|
state.readyResolve = null;
|
|
@@ -479,21 +273,29 @@ extern js`
|
|
|
479
273
|
const pending = state.pending.get(message.id);
|
|
480
274
|
if (!pending) return;
|
|
481
275
|
state.pending.delete(message.id);
|
|
276
|
+
if (pending.operation === "close") state.closeAcknowledged = true;
|
|
482
277
|
if (message.error) pending.reject(nativeError(message.error));
|
|
483
278
|
else pending.resolve(message.value);
|
|
484
279
|
});
|
|
485
280
|
worker.on("error", error => {
|
|
486
|
-
state.
|
|
487
|
-
failPending(state, new NativeSqliteError(error.message, "SQLITE_WORKER", "worker"));
|
|
281
|
+
failWorker(state, new NativeSqliteError(error.message, "SQLITE_WORKER", "worker"));
|
|
488
282
|
});
|
|
489
283
|
worker.on("exit", code => {
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
284
|
+
state.exitCode = code;
|
|
285
|
+
const expected = code === 0 && state.closeAcknowledged && state.workerFailure === null;
|
|
286
|
+
if (!expected) {
|
|
287
|
+
failWorker(state, new NativeSqliteError("SQLite Worker exited unexpectedly with code " + code, "SQLITE_WORKER", "worker"));
|
|
494
288
|
}
|
|
289
|
+
state.closed = true;
|
|
290
|
+
exitResolve(code);
|
|
495
291
|
});
|
|
496
|
-
|
|
292
|
+
try {
|
|
293
|
+
await ready;
|
|
294
|
+
} catch (error) {
|
|
295
|
+
if (state.exitCode === null) await worker.terminate();
|
|
296
|
+
await exited;
|
|
297
|
+
throw error;
|
|
298
|
+
}
|
|
497
299
|
return new NativeSqliteConnection(TOKEN, state);
|
|
498
300
|
}
|
|
499
301
|
`:
|
|
@@ -521,7 +323,7 @@ extern js`
|
|
|
521
323
|
async def transaction(operation: (NativeSqliteTransaction) -> Promise<null>) -> null
|
|
522
324
|
async def close() -> null
|
|
523
325
|
|
|
524
|
-
export async def openNativeSqlite(path: string, options: SqliteOptions = {}) -> NativeSqliteConnection
|
|
326
|
+
export async def openNativeSqlite(path: string, options: SqliteOptions = {}, workerSource: string = "") -> NativeSqliteConnection
|
|
525
327
|
|
|
526
328
|
/// A checked SQLite failure. sqliteCode preserves the driver result code when
|
|
527
329
|
/// Node exposes one; retryable is true for busy, locked, and queue pressure.
|
|
@@ -686,7 +488,7 @@ export class SqliteConnection:
|
|
|
686
488
|
@dispose: await self.close()
|
|
687
489
|
|
|
688
490
|
export async def openSqlite(path: string, options: SqliteOptions = {}) -> SqliteConnection:
|
|
689
|
-
try: return SqliteConnection(await openNativeSqlite(path, options))
|
|
491
|
+
try: return SqliteConnection(await openNativeSqlite(path, options, sqliteWorkerSource))
|
|
690
492
|
catch error:
|
|
691
493
|
if error is NativeSqliteError: throw sqliteFailure(error)
|
|
692
494
|
throw SqliteError(error.message)
|