@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/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 source = "(" + workerMain.toString() + ")()";
448
- const worker = new Worker(source, { eval: true, workerData: { path, ...checked } });
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.closed = true;
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
- exitResolve(code);
491
- if (code !== 0 && !state.closed) {
492
- state.closed = true;
493
- failPending(state, new NativeSqliteError("SQLite Worker exited with code " + code, "SQLITE_WORKER", "worker"));
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
- await ready;
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)