@delali/sirannon-db 0.1.6 → 0.1.7
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/README.md +134 -19
- package/dist/backup-scheduler/index.d.ts +18 -3
- package/dist/backup-scheduler/index.mjs +2 -2
- package/dist/{change-tracker-CFTQ9TSn.d.ts → change-tracker-CbmaMO-N.d.ts} +12 -2
- package/dist/chunk-4IGMIJQK.mjs +318 -0
- package/dist/chunk-BNUTBHHH.mjs +22 -0
- package/dist/chunk-CJLYFDP5.mjs +26 -0
- package/dist/chunk-CW6S3WL5.mjs +222 -0
- package/dist/chunk-DJLX6CAE.mjs +20 -0
- package/dist/chunk-DVWQD3GF.mjs +49 -0
- package/dist/chunk-GEZUUIKV.mjs +268 -0
- package/dist/{chunk-UVMVN3OT.mjs → chunk-H5AB6NIR.mjs} +1 -1
- package/dist/{chunk-UTO3ZAFS.mjs → chunk-HHRMRFFR.mjs} +148 -28
- package/dist/chunk-TKGHYWQ6.mjs +35 -0
- package/dist/chunk-VLTICJOD.mjs +470 -0
- package/dist/{chunk-O7BHI3CF.mjs → chunk-YPYVQJ4C.mjs} +15 -1
- package/dist/client/index.d.ts +107 -15
- package/dist/client/index.mjs +348 -37
- package/dist/core/index.d.ts +70 -13
- package/dist/core/index.mjs +635 -192
- package/dist/core/writer-worker.d.ts +2 -0
- package/dist/core/writer-worker.mjs +107 -0
- package/dist/{database-BVY1GqE7.d.ts → database-DuGp0Rtr.d.ts} +51 -19
- package/dist/driver/better-sqlite3.d.ts +1 -1
- package/dist/driver/better-sqlite3.mjs +44 -6
- package/dist/driver/bun.mjs +35 -6
- package/dist/driver/expo.mjs +2 -2
- package/dist/driver/node.d.ts +1 -1
- package/dist/driver/node.mjs +48 -5
- package/dist/driver/wa-sqlite.d.ts +109 -0
- package/dist/driver/wa-sqlite.mjs +30 -2
- package/dist/{errors-C00ed08Q.d.ts → errors-5Nf5ZAEC.d.ts} +19 -1
- package/dist/file-migrations/index.d.ts +2 -3
- package/dist/file-migrations/index.mjs +1 -1
- package/dist/replication/coordinator/etcd.mjs +2 -2
- package/dist/replication/index.d.ts +7 -8
- package/dist/replication/index.mjs +79 -78
- package/dist/server/index.d.ts +111 -28
- package/dist/server/index.mjs +1016 -384
- package/dist/{sirannon-Cd-lK6T0.d.ts → sirannon-4SspRvP5.d.ts} +3 -3
- package/dist/transport/grpc.d.ts +3 -4
- package/dist/transport/grpc.mjs +2 -2
- package/dist/{types-Lc7ywFx7.d.ts → types-BsjobKbl.d.ts} +2 -2
- package/dist/{types-BeozgNPr.d.ts → types-D4p4UyDK.d.ts} +1 -1
- package/dist/types-D_hQW1hr.d.ts +494 -0
- package/package.json +3 -23
- package/dist/chunk-3MCMONVP.mjs +0 -115
- package/dist/chunk-74UN4DIE.mjs +0 -14
- package/dist/chunk-FB2U2Q3Y.mjs +0 -21
- package/dist/chunk-GS7T5YMI.mjs +0 -51
- package/dist/chunk-PXKAKK2V.mjs +0 -124
- package/dist/index-CLdNrcPz.d.ts +0 -16
- package/dist/types-BFSsG77t.d.ts +0 -29
- package/dist/types-D-74JiXb.d.ts +0 -265
package/dist/core/index.mjs
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
import { SubscriptionBuilderImpl, ChangeTracker, SubscriptionManager, startPolling } from '../chunk-
|
|
2
|
-
export { ChangeTracker } from '../chunk-
|
|
3
|
-
export { defineDriver } from '../chunk-
|
|
4
|
-
import {
|
|
5
|
-
export {
|
|
6
|
-
import { Transaction
|
|
7
|
-
export { Transaction
|
|
8
|
-
import '../chunk-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
import { runBulkLoad, SubscriptionBuilderImpl, ChangeTracker, SubscriptionManager, startPolling } from '../chunk-HHRMRFFR.mjs';
|
|
2
|
+
export { ChangeTracker, isBulkLoadDurability, runBulkLoad } from '../chunk-HHRMRFFR.mjs';
|
|
3
|
+
export { defineDriver } from '../chunk-BNUTBHHH.mjs';
|
|
4
|
+
import { DEFAULT_SYNCHRONOUS } from '../chunk-CJLYFDP5.mjs';
|
|
5
|
+
export { DEFAULT_SYNCHRONOUS, isSynchronousLevel, synchronousPragmaValue } from '../chunk-CJLYFDP5.mjs';
|
|
6
|
+
import { Transaction } from '../chunk-TKGHYWQ6.mjs';
|
|
7
|
+
export { Transaction } from '../chunk-TKGHYWQ6.mjs';
|
|
8
|
+
import { query, queryForWire, queryOne, executeBatch, executeBatchSummary, execute, bindParams, executeGroup } from '../chunk-CW6S3WL5.mjs';
|
|
9
|
+
export { execute, executeBatch, query, queryOne } from '../chunk-CW6S3WL5.mjs';
|
|
10
|
+
import { MIGRATIONS_TABLE } from '../chunk-GEZUUIKV.mjs';
|
|
11
|
+
import { SirannonError, ConnectionPoolError, MigrationError, ReadOnlyError, MaxDatabasesError, DatabaseAlreadyExistsError, DatabaseNotFoundError, WriteOverloadError, ExtensionError } from '../chunk-YPYVQJ4C.mjs';
|
|
12
|
+
export { BackupError, CDCError, ConnectionPoolError, DatabaseAlreadyExistsError, DatabaseNotFoundError, ExtensionError, ForbiddenSqlError, HookDeniedError, MaxDatabasesError, MigrationError, QueryError, ReadOnlyError, SirannonError, TransactionError, WriteOverloadError } from '../chunk-YPYVQJ4C.mjs';
|
|
11
13
|
|
|
12
14
|
// src/core/connection-pool.ts
|
|
13
15
|
async function closeAllSilently(connections) {
|
|
@@ -29,12 +31,22 @@ var ConnectionPool = class _ConnectionPool {
|
|
|
29
31
|
this.readers = readers;
|
|
30
32
|
}
|
|
31
33
|
static async create(options) {
|
|
32
|
-
const { driver, path, readOnly = false, readPoolSize = 4, walMode = true } = options;
|
|
34
|
+
const { driver, path, readOnly = false, readPoolSize = 4, walMode = true, synchronous } = options;
|
|
33
35
|
let writer = null;
|
|
34
36
|
const readers = [];
|
|
35
37
|
try {
|
|
36
38
|
if (!readOnly) {
|
|
37
|
-
|
|
39
|
+
if (options.useWriterWorker) {
|
|
40
|
+
if (!driver.startWriterHost) {
|
|
41
|
+
throw new SirannonError(
|
|
42
|
+
"writerWorker is enabled but the driver does not carry a worker entry; use a driver that supports it or disable writerWorker",
|
|
43
|
+
"WRITER_WORKER_UNSUPPORTED"
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
writer = await driver.startWriterHost(path, { walMode, synchronous }, options.workerHostOptions);
|
|
47
|
+
} else {
|
|
48
|
+
writer = await driver.open(path, { walMode, synchronous });
|
|
49
|
+
}
|
|
38
50
|
}
|
|
39
51
|
const poolSize = driver.capabilities.multipleConnections ? Math.max(readPoolSize, 1) : 0;
|
|
40
52
|
for (let i = 0; i < poolSize; i++) {
|
|
@@ -120,6 +132,48 @@ async function applyDdlSideEffects(tracker, writerConn, sql) {
|
|
|
120
132
|
await tracker.pruneDroppedTables(writerConn, [dropped]);
|
|
121
133
|
}
|
|
122
134
|
}
|
|
135
|
+
function applyDdlSideEffectsIfRelevant(tracker, writerConn, sql) {
|
|
136
|
+
if (!tracker || !isCdcRelevantDdl(sql)) {
|
|
137
|
+
return Promise.resolve();
|
|
138
|
+
}
|
|
139
|
+
return applyDdlSideEffects(tracker, writerConn, sql);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/core/database-backup.ts
|
|
143
|
+
var DatabaseBackupController = class {
|
|
144
|
+
constructor(runExclusive, acquireWriter, engine) {
|
|
145
|
+
this.runExclusive = runExclusive;
|
|
146
|
+
this.acquireWriter = acquireWriter;
|
|
147
|
+
this.engine = engine;
|
|
148
|
+
}
|
|
149
|
+
cancellers = [];
|
|
150
|
+
require() {
|
|
151
|
+
if (!this.engine) {
|
|
152
|
+
throw new SirannonError(
|
|
153
|
+
"Backups need a driver that can write files; this driver does not provide one",
|
|
154
|
+
"BACKUP_UNSUPPORTED"
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return this.engine;
|
|
158
|
+
}
|
|
159
|
+
backup(destPath) {
|
|
160
|
+
const engine = this.require();
|
|
161
|
+
return this.runExclusive(() => engine.backup(this.acquireWriter(), destPath));
|
|
162
|
+
}
|
|
163
|
+
schedule(options) {
|
|
164
|
+
const engine = this.require();
|
|
165
|
+
this.cancellers.push(engine.schedule(this.acquireWriter(), options, this.runExclusive));
|
|
166
|
+
}
|
|
167
|
+
cancelAll() {
|
|
168
|
+
for (const cancel of this.cancellers) {
|
|
169
|
+
try {
|
|
170
|
+
cancel();
|
|
171
|
+
} catch {
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
this.cancellers.length = 0;
|
|
175
|
+
}
|
|
176
|
+
};
|
|
123
177
|
|
|
124
178
|
// src/core/cdc/cdc-aware-transaction.ts
|
|
125
179
|
var CdcAwareTransaction = class extends Transaction {
|
|
@@ -148,9 +202,169 @@ var CdcAwareTransaction = class extends Transaction {
|
|
|
148
202
|
}
|
|
149
203
|
};
|
|
150
204
|
|
|
205
|
+
// src/core/database-cdc.ts
|
|
206
|
+
var DatabaseCdcController = class {
|
|
207
|
+
constructor(runExclusive, acquireWriter, pollInterval, retention) {
|
|
208
|
+
this.runExclusive = runExclusive;
|
|
209
|
+
this.acquireWriter = acquireWriter;
|
|
210
|
+
this.pollInterval = pollInterval;
|
|
211
|
+
this.retention = retention;
|
|
212
|
+
}
|
|
213
|
+
tracker = null;
|
|
214
|
+
subscriptions = null;
|
|
215
|
+
stopPolling = null;
|
|
216
|
+
get changeTracker() {
|
|
217
|
+
return this.tracker;
|
|
218
|
+
}
|
|
219
|
+
async watch(table) {
|
|
220
|
+
const { tracker } = this.ensure();
|
|
221
|
+
await this.runExclusive(() => tracker.watch(this.acquireWriter(), table));
|
|
222
|
+
this.ensurePolling();
|
|
223
|
+
}
|
|
224
|
+
async unwatch(table) {
|
|
225
|
+
const tracker = this.tracker;
|
|
226
|
+
if (!tracker) return;
|
|
227
|
+
await this.runExclusive(() => tracker.unwatch(this.acquireWriter(), table));
|
|
228
|
+
if (tracker.watchedTables.size === 0) {
|
|
229
|
+
this.stop();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
on(table) {
|
|
233
|
+
const { subscriptions } = this.ensure();
|
|
234
|
+
return new SubscriptionBuilderImpl(table, subscriptions);
|
|
235
|
+
}
|
|
236
|
+
async runTransaction(writer, fn) {
|
|
237
|
+
const tracker = this.tracker;
|
|
238
|
+
if (!tracker) {
|
|
239
|
+
return Transaction.run(writer, fn);
|
|
240
|
+
}
|
|
241
|
+
const state = { sawDdl: false, droppedTables: [] };
|
|
242
|
+
const result = await writer.transaction((txConn) => fn(new CdcAwareTransaction(txConn, tracker, state)));
|
|
243
|
+
if (state.sawDdl && state.droppedTables.length > 0) {
|
|
244
|
+
await tracker.pruneDroppedTables(writer, state.droppedTables);
|
|
245
|
+
}
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
248
|
+
stop() {
|
|
249
|
+
if (this.stopPolling) {
|
|
250
|
+
this.stopPolling();
|
|
251
|
+
this.stopPolling = null;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
ensure() {
|
|
255
|
+
const tracker = this.tracker ?? new ChangeTracker({ retention: this.retention });
|
|
256
|
+
const subscriptions = this.subscriptions ?? new SubscriptionManager();
|
|
257
|
+
this.tracker = tracker;
|
|
258
|
+
this.subscriptions = subscriptions;
|
|
259
|
+
return { tracker, subscriptions };
|
|
260
|
+
}
|
|
261
|
+
ensurePolling() {
|
|
262
|
+
if (this.stopPolling) return;
|
|
263
|
+
if (!this.tracker || !this.subscriptions) return;
|
|
264
|
+
this.stopPolling = startPolling(
|
|
265
|
+
this.acquireWriter(),
|
|
266
|
+
this.tracker,
|
|
267
|
+
this.subscriptions,
|
|
268
|
+
this.pollInterval,
|
|
269
|
+
void 0,
|
|
270
|
+
this.runExclusive
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// src/core/hooks/query-hooks.ts
|
|
276
|
+
function fireBeforeQueryHooks(parentHooks, localHooks, databaseId, sql, params, options) {
|
|
277
|
+
const hasParent = parentHooks?.has("beforeQuery");
|
|
278
|
+
const hasLocal = localHooks.has("beforeQuery");
|
|
279
|
+
if (!hasParent && !hasLocal) return;
|
|
280
|
+
const ctx = {
|
|
281
|
+
databaseId,
|
|
282
|
+
sql,
|
|
283
|
+
params,
|
|
284
|
+
writeConcern: options?.writeConcern,
|
|
285
|
+
readConcern: options?.readConcern
|
|
286
|
+
};
|
|
287
|
+
parentHooks?.invokeSync("beforeQuery", ctx);
|
|
288
|
+
localHooks.invokeSync("beforeQuery", ctx);
|
|
289
|
+
}
|
|
290
|
+
function fireAfterQueryHooks(parentHooks, localHooks, databaseId, sql, params, durationMs) {
|
|
291
|
+
const hasParent = parentHooks?.has("afterQuery");
|
|
292
|
+
const hasLocal = localHooks.has("afterQuery");
|
|
293
|
+
if (!hasParent && !hasLocal) return;
|
|
294
|
+
const ctx = { databaseId, sql, params, durationMs };
|
|
295
|
+
try {
|
|
296
|
+
parentHooks?.invokeSync("afterQuery", ctx);
|
|
297
|
+
} catch {
|
|
298
|
+
}
|
|
299
|
+
try {
|
|
300
|
+
localHooks.invokeSync("afterQuery", ctx);
|
|
301
|
+
} catch {
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// src/core/database-observability.ts
|
|
306
|
+
var DatabaseObserver = class {
|
|
307
|
+
constructor(databaseId, localHooks, parentHooks, metrics) {
|
|
308
|
+
this.databaseId = databaseId;
|
|
309
|
+
this.localHooks = localHooks;
|
|
310
|
+
this.parentHooks = parentHooks;
|
|
311
|
+
this.metrics = metrics;
|
|
312
|
+
}
|
|
313
|
+
/** False lets a caller skip the timing calls on a path where nothing would read them. */
|
|
314
|
+
get observesQueries() {
|
|
315
|
+
return this.metrics !== null || this.localHooks.has("beforeQuery") || this.localHooks.has("afterQuery") || (this.parentHooks?.has("beforeQuery") ?? false) || (this.parentHooks?.has("afterQuery") ?? false);
|
|
316
|
+
}
|
|
317
|
+
async withQueryHooks(sql, params, options, op) {
|
|
318
|
+
this.fireBefore(sql, params, options);
|
|
319
|
+
const start = performance.now();
|
|
320
|
+
try {
|
|
321
|
+
return await op();
|
|
322
|
+
} finally {
|
|
323
|
+
this.fireAfter(sql, params, performance.now() - start);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
track(sql, op) {
|
|
327
|
+
if (!this.metrics) return op();
|
|
328
|
+
return this.metrics.trackQuery(op, { databaseId: this.databaseId, sql });
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Reports each statement separately, since a hook filtering on SQL has no other
|
|
332
|
+
* way to see them. They share the transaction's duration because the group runs
|
|
333
|
+
* them back to back under one commit and no statement has a duration of its own,
|
|
334
|
+
* which is already what a grouped {@link Database.execute} reports.
|
|
335
|
+
*/
|
|
336
|
+
async withTransactionHooks(statements, op) {
|
|
337
|
+
for (const statement of statements) this.fireBefore(statement.sql, statement.params);
|
|
338
|
+
const start = performance.now();
|
|
339
|
+
try {
|
|
340
|
+
return await this.trackEach(statements, op);
|
|
341
|
+
} finally {
|
|
342
|
+
const durationMs = performance.now() - start;
|
|
343
|
+
for (const statement of statements) this.fireAfter(statement.sql, statement.params, durationMs);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
trackEach(statements, op) {
|
|
347
|
+
const metrics = this.metrics;
|
|
348
|
+
if (!metrics) return op();
|
|
349
|
+
let run = op;
|
|
350
|
+
for (const statement of statements) {
|
|
351
|
+
const inner = run;
|
|
352
|
+
const sql = statement.sql;
|
|
353
|
+
run = () => metrics.trackQuery(inner, { databaseId: this.databaseId, sql });
|
|
354
|
+
}
|
|
355
|
+
return run();
|
|
356
|
+
}
|
|
357
|
+
fireBefore(sql, params, options) {
|
|
358
|
+
fireBeforeQueryHooks(this.parentHooks, this.localHooks, this.databaseId, sql, params, options);
|
|
359
|
+
}
|
|
360
|
+
fireAfter(sql, params, durationMs) {
|
|
361
|
+
fireAfterQueryHooks(this.parentHooks, this.localHooks, this.databaseId, sql, params, durationMs);
|
|
362
|
+
}
|
|
363
|
+
};
|
|
364
|
+
|
|
151
365
|
// src/core/extension-loader.ts
|
|
152
366
|
async function loadExtension(driver, writer, extensionPath) {
|
|
153
|
-
if (!driver.capabilities.extensions) {
|
|
367
|
+
if (!driver.capabilities.extensions || !driver.resolveExtensionPath) {
|
|
154
368
|
throw new ExtensionError(extensionPath, "Extensions are not supported by the current driver");
|
|
155
369
|
}
|
|
156
370
|
if (!extensionPath || extensionPath.includes("\0")) {
|
|
@@ -165,8 +379,7 @@ async function loadExtension(driver, writer, extensionPath) {
|
|
|
165
379
|
if (segments.includes("..")) {
|
|
166
380
|
throw new ExtensionError(extensionPath, "Extension path must not contain directory traversal segments");
|
|
167
381
|
}
|
|
168
|
-
const
|
|
169
|
-
const resolved = resolve(extensionPath);
|
|
382
|
+
const resolved = driver.resolveExtensionPath(extensionPath);
|
|
170
383
|
try {
|
|
171
384
|
const escaped = resolved.replace(/'/g, "''");
|
|
172
385
|
await writer.exec(`SELECT load_extension('${escaped}')`);
|
|
@@ -175,6 +388,137 @@ async function loadExtension(driver, writer, extensionPath) {
|
|
|
175
388
|
}
|
|
176
389
|
}
|
|
177
390
|
|
|
391
|
+
// src/core/group-committer.ts
|
|
392
|
+
var DEFAULT_MAX_GROUP_SIZE = 1e3;
|
|
393
|
+
var DML_PREFIX_RE = /^\s*(?:INSERT|UPDATE|DELETE|REPLACE)\b/i;
|
|
394
|
+
function isGroupable(sql) {
|
|
395
|
+
return DML_PREFIX_RE.test(sql);
|
|
396
|
+
}
|
|
397
|
+
function canGroupTransaction(statements) {
|
|
398
|
+
for (const statement of statements) {
|
|
399
|
+
if (!isGroupable(statement.sql)) return false;
|
|
400
|
+
}
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
var GroupCommitter = class {
|
|
404
|
+
constructor(writerLock, hooks, maxGroup = DEFAULT_MAX_GROUP_SIZE) {
|
|
405
|
+
this.writerLock = writerLock;
|
|
406
|
+
this.hooks = hooks;
|
|
407
|
+
this.maxGroup = maxGroup;
|
|
408
|
+
}
|
|
409
|
+
pending = [];
|
|
410
|
+
running = false;
|
|
411
|
+
scheduled = false;
|
|
412
|
+
loopPromise = Promise.resolve();
|
|
413
|
+
submit(sql, params) {
|
|
414
|
+
return new Promise((resolve, reject) => {
|
|
415
|
+
this.pending.push({
|
|
416
|
+
statements: [{ sql, params }],
|
|
417
|
+
resolve: (values) => resolve(values[0]),
|
|
418
|
+
reject
|
|
419
|
+
});
|
|
420
|
+
this.schedule();
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
submitTransaction(statements) {
|
|
424
|
+
return new Promise((resolve, reject) => {
|
|
425
|
+
this.pending.push({ statements, resolve, reject });
|
|
426
|
+
this.schedule();
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
async drain() {
|
|
430
|
+
while (this.running || this.pending.length > 0) {
|
|
431
|
+
if (!this.running) this.schedule();
|
|
432
|
+
await this.loopPromise;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
runUngrouped(sql, params) {
|
|
436
|
+
return this.writerLock.run(() => this.runSingle({ sql, params }));
|
|
437
|
+
}
|
|
438
|
+
schedule() {
|
|
439
|
+
if (this.running || this.scheduled) return;
|
|
440
|
+
this.scheduled = true;
|
|
441
|
+
queueMicrotask(() => {
|
|
442
|
+
this.scheduled = false;
|
|
443
|
+
this.writerLock.detached(() => this.runLoop());
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
runLoop() {
|
|
447
|
+
if (this.running) return;
|
|
448
|
+
this.running = true;
|
|
449
|
+
this.loopPromise = (async () => {
|
|
450
|
+
try {
|
|
451
|
+
while (this.pending.length > 0) {
|
|
452
|
+
await this.flush(this.takeBatch());
|
|
453
|
+
}
|
|
454
|
+
} finally {
|
|
455
|
+
this.running = false;
|
|
456
|
+
if (this.pending.length > 0) this.schedule();
|
|
457
|
+
}
|
|
458
|
+
})();
|
|
459
|
+
}
|
|
460
|
+
takeBatch() {
|
|
461
|
+
const pending = this.pending;
|
|
462
|
+
if (!canGroupTransaction(pending[0].statements)) return pending.splice(0, 1);
|
|
463
|
+
let n = 1;
|
|
464
|
+
let statements = pending[0].statements.length;
|
|
465
|
+
while (n < pending.length && canGroupTransaction(pending[n].statements)) {
|
|
466
|
+
const size = pending[n].statements.length;
|
|
467
|
+
if (statements + size > this.maxGroup) break;
|
|
468
|
+
statements += size;
|
|
469
|
+
n++;
|
|
470
|
+
}
|
|
471
|
+
return pending.splice(0, n);
|
|
472
|
+
}
|
|
473
|
+
async flush(batch) {
|
|
474
|
+
const lone = batch[0];
|
|
475
|
+
if (batch.length === 1 && lone.statements.length === 1) {
|
|
476
|
+
try {
|
|
477
|
+
lone.resolve([await this.writerLock.run(() => this.runSingle(lone.statements[0]))]);
|
|
478
|
+
} catch (err) {
|
|
479
|
+
lone.reject(err);
|
|
480
|
+
}
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
let outcomes;
|
|
484
|
+
try {
|
|
485
|
+
outcomes = await this.writerLock.run(() => this.runGroup(batch));
|
|
486
|
+
} catch (err) {
|
|
487
|
+
for (const job of batch) job.reject(err);
|
|
488
|
+
return;
|
|
489
|
+
}
|
|
490
|
+
for (let i = 0; i < batch.length; i++) {
|
|
491
|
+
const outcome = outcomes[i];
|
|
492
|
+
if (outcome.ok) batch[i].resolve(outcome.values);
|
|
493
|
+
else batch[i].reject(outcome.error);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
async runSingle(statement) {
|
|
497
|
+
const writer = this.hooks.acquireWriter();
|
|
498
|
+
const result = await execute(writer, statement.sql, statement.params);
|
|
499
|
+
await this.hooks.afterCommit(writer, statement.sql);
|
|
500
|
+
return result;
|
|
501
|
+
}
|
|
502
|
+
async runGroup(batch) {
|
|
503
|
+
const writer = this.hooks.acquireWriter();
|
|
504
|
+
const units = batch.map((job) => ({
|
|
505
|
+
statements: job.statements.map((statement) => ({ sql: statement.sql, params: bindParams(statement.params) }))
|
|
506
|
+
}));
|
|
507
|
+
if (writer.runGroup) {
|
|
508
|
+
const raw = await writer.runGroup(units);
|
|
509
|
+
return raw.map(
|
|
510
|
+
(outcome) => outcome.ok ? { ok: true, values: outcome.results } : { ok: false, error: toError(outcome.error) }
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
return executeGroup(writer, units);
|
|
514
|
+
}
|
|
515
|
+
};
|
|
516
|
+
function toError(error) {
|
|
517
|
+
const err = error.code ? new SirannonError(error.message, error.code) : new Error(error.message);
|
|
518
|
+
if (error.name) err.name = error.name;
|
|
519
|
+
return err;
|
|
520
|
+
}
|
|
521
|
+
|
|
178
522
|
// src/core/hooks/registry.ts
|
|
179
523
|
var HOOK_CONFIG_MAP = {
|
|
180
524
|
onBeforeQuery: "beforeQuery",
|
|
@@ -259,7 +603,7 @@ var HookRegistry = class {
|
|
|
259
603
|
|
|
260
604
|
// src/core/migrations/runner.ts
|
|
261
605
|
var CREATE_TRACKING_TABLE = `
|
|
262
|
-
CREATE TABLE IF NOT EXISTS
|
|
606
|
+
CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
|
|
263
607
|
version INTEGER PRIMARY KEY,
|
|
264
608
|
name TEXT NOT NULL,
|
|
265
609
|
applied_at REAL NOT NULL DEFAULT (unixepoch('subsec'))
|
|
@@ -276,7 +620,7 @@ var MigrationRunner = class _MigrationRunner {
|
|
|
276
620
|
}
|
|
277
621
|
const appliedEntries = [];
|
|
278
622
|
await conn.transaction(async (txConn) => {
|
|
279
|
-
const insertStmt = await txConn.prepare(
|
|
623
|
+
const insertStmt = await txConn.prepare(`INSERT INTO ${MIGRATIONS_TABLE} (version, name) VALUES (?, ?)`);
|
|
280
624
|
for (const migration of pending) {
|
|
281
625
|
try {
|
|
282
626
|
if (typeof migration.up === "string") {
|
|
@@ -310,7 +654,7 @@ var MigrationRunner = class _MigrationRunner {
|
|
|
310
654
|
);
|
|
311
655
|
}
|
|
312
656
|
await conn.exec(CREATE_TRACKING_TABLE);
|
|
313
|
-
const selectStmt = await conn.prepare(
|
|
657
|
+
const selectStmt = await conn.prepare(`SELECT version, name FROM ${MIGRATIONS_TABLE} ORDER BY version DESC`);
|
|
314
658
|
const appliedRows = await selectStmt.all();
|
|
315
659
|
if (appliedRows.length === 0) {
|
|
316
660
|
return { rolledBack: [] };
|
|
@@ -337,7 +681,7 @@ var MigrationRunner = class _MigrationRunner {
|
|
|
337
681
|
}
|
|
338
682
|
const rolledBackEntries = [];
|
|
339
683
|
await conn.transaction(async (txConn) => {
|
|
340
|
-
const deleteStmt = await txConn.prepare(
|
|
684
|
+
const deleteStmt = await txConn.prepare(`DELETE FROM ${MIGRATIONS_TABLE} WHERE version = ?`);
|
|
341
685
|
for (const entry of rollbackSet) {
|
|
342
686
|
const migration = downByVersion.get(entry.version);
|
|
343
687
|
if (!migration) {
|
|
@@ -408,12 +752,90 @@ var MigrationRunner = class _MigrationRunner {
|
|
|
408
752
|
return [...migrations].sort((a, b) => a.version - b.version);
|
|
409
753
|
}
|
|
410
754
|
static async getAppliedVersions(conn) {
|
|
411
|
-
const stmt = await conn.prepare(
|
|
755
|
+
const stmt = await conn.prepare(`SELECT version FROM ${MIGRATIONS_TABLE} ORDER BY version`);
|
|
412
756
|
const rows = await stmt.all();
|
|
413
757
|
return new Set(rows.map((r) => r.version));
|
|
414
758
|
}
|
|
415
759
|
};
|
|
416
760
|
|
|
761
|
+
// src/core/worker/config.ts
|
|
762
|
+
var DEFAULT_MAX_PENDING_WRITES = 1024;
|
|
763
|
+
var DEFAULT_RETRY_AFTER_MS = 1e3;
|
|
764
|
+
function requireCount(value, name, min) {
|
|
765
|
+
if (!Number.isInteger(value) || value < min) {
|
|
766
|
+
throw new SirannonError(`writerWorker.${name} must be an integer >= ${min}`, "INVALID_WRITER_WORKER");
|
|
767
|
+
}
|
|
768
|
+
return value;
|
|
769
|
+
}
|
|
770
|
+
function resolveWriterWorkerConfig(option) {
|
|
771
|
+
if (!option) {
|
|
772
|
+
return { enabled: false, maxPendingWrites: 0, retryAfterMs: DEFAULT_RETRY_AFTER_MS, host: {} };
|
|
773
|
+
}
|
|
774
|
+
const opts = option === true ? {} : option;
|
|
775
|
+
const host = {};
|
|
776
|
+
if (opts.writeTimeoutMs !== void 0) host.writeTimeoutMs = requireCount(opts.writeTimeoutMs, "writeTimeoutMs", 0);
|
|
777
|
+
if (opts.maxRestarts !== void 0) host.maxRestarts = requireCount(opts.maxRestarts, "maxRestarts", 0);
|
|
778
|
+
const maxPendingWrites = opts.maxPendingWrites === void 0 ? DEFAULT_MAX_PENDING_WRITES : requireCount(opts.maxPendingWrites, "maxPendingWrites", 1);
|
|
779
|
+
return { enabled: true, maxPendingWrites, retryAfterMs: DEFAULT_RETRY_AFTER_MS, host };
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// src/core/worker/gate.ts
|
|
783
|
+
var WriteGate = class {
|
|
784
|
+
constructor(limit, retryAfterMs) {
|
|
785
|
+
this.limit = limit;
|
|
786
|
+
this.retryAfterMs = retryAfterMs;
|
|
787
|
+
}
|
|
788
|
+
inFlight = 0;
|
|
789
|
+
run(op) {
|
|
790
|
+
if (this.limit > 0 && this.inFlight >= this.limit) {
|
|
791
|
+
return Promise.reject(new WriteOverloadError(this.limit, this.retryAfterMs));
|
|
792
|
+
}
|
|
793
|
+
this.inFlight++;
|
|
794
|
+
return op().finally(() => {
|
|
795
|
+
this.inFlight--;
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
|
|
800
|
+
// src/core/writer-lock.ts
|
|
801
|
+
var UNTRACKED = {
|
|
802
|
+
run: (operation) => operation(),
|
|
803
|
+
isActive: () => false,
|
|
804
|
+
exit: (operation) => operation()
|
|
805
|
+
};
|
|
806
|
+
var WriterLock = class {
|
|
807
|
+
tail = Promise.resolve();
|
|
808
|
+
context;
|
|
809
|
+
constructor(context) {
|
|
810
|
+
this.context = context ?? UNTRACKED;
|
|
811
|
+
}
|
|
812
|
+
run(operation) {
|
|
813
|
+
if (this.context.isActive()) {
|
|
814
|
+
return operation();
|
|
815
|
+
}
|
|
816
|
+
const enter = () => this.context.run(operation);
|
|
817
|
+
const ticket = this.tail.then(enter, enter);
|
|
818
|
+
this.tail = ticket.then(swallow, swallow);
|
|
819
|
+
return ticket;
|
|
820
|
+
}
|
|
821
|
+
isHeld() {
|
|
822
|
+
return this.context.isActive();
|
|
823
|
+
}
|
|
824
|
+
async settle() {
|
|
825
|
+
await this.run(() => Promise.resolve());
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* Work scheduled from inside a held operation inherits it, so shared
|
|
829
|
+
* machinery must detach or it runs every caller's work inside one caller's
|
|
830
|
+
* transaction.
|
|
831
|
+
*/
|
|
832
|
+
detached(operation) {
|
|
833
|
+
return this.context.exit(operation);
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
function swallow() {
|
|
837
|
+
}
|
|
838
|
+
|
|
417
839
|
// src/core/database.ts
|
|
418
840
|
var Database = class _Database {
|
|
419
841
|
id;
|
|
@@ -421,184 +843,252 @@ var Database = class _Database {
|
|
|
421
843
|
readOnly;
|
|
422
844
|
pool;
|
|
423
845
|
driver;
|
|
846
|
+
synchronous;
|
|
847
|
+
walMode;
|
|
848
|
+
writerLock;
|
|
849
|
+
writeGate;
|
|
850
|
+
groupCommitter;
|
|
424
851
|
closeListeners = [];
|
|
425
852
|
_closed = false;
|
|
426
|
-
|
|
427
|
-
subscriptionManager = null;
|
|
428
|
-
stopCdcPolling = null;
|
|
429
|
-
cdcPollInterval;
|
|
430
|
-
cdcRetention;
|
|
853
|
+
cdc;
|
|
431
854
|
hookRegistry = new HookRegistry();
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
backupScheduler = new BackupScheduler(this.backupManager);
|
|
436
|
-
scheduledBackupCancellers = [];
|
|
437
|
-
constructor(id, path, pool, driver, options, internals) {
|
|
855
|
+
observer;
|
|
856
|
+
backups;
|
|
857
|
+
constructor(id, path, pool, driver, writeGate, options, internals) {
|
|
438
858
|
this.id = id;
|
|
439
859
|
this.path = path;
|
|
440
860
|
this.pool = pool;
|
|
441
861
|
this.driver = driver;
|
|
862
|
+
this.writeGate = writeGate;
|
|
863
|
+
this.writerLock = new WriterLock(driver.createWriterContext?.());
|
|
864
|
+
this.backups = new DatabaseBackupController(
|
|
865
|
+
(op) => this.writerLock.run(op),
|
|
866
|
+
() => this.pool.acquireWriter(),
|
|
867
|
+
driver.createBackupEngine?.()
|
|
868
|
+
);
|
|
442
869
|
this.readOnly = options?.readOnly ?? false;
|
|
443
|
-
this.
|
|
444
|
-
this.
|
|
445
|
-
this.
|
|
446
|
-
|
|
870
|
+
this.synchronous = options?.synchronous ?? DEFAULT_SYNCHRONOUS;
|
|
871
|
+
this.walMode = options?.walMode ?? true;
|
|
872
|
+
this.observer = new DatabaseObserver(
|
|
873
|
+
id,
|
|
874
|
+
this.hookRegistry,
|
|
875
|
+
internals?.parentHooks ?? null,
|
|
876
|
+
internals?.metrics ?? null
|
|
877
|
+
);
|
|
878
|
+
this.cdc = new DatabaseCdcController(
|
|
879
|
+
(op) => this.writerLock.run(op),
|
|
880
|
+
() => this.pool.acquireWriter(),
|
|
881
|
+
options?.cdcPollInterval ?? 50,
|
|
882
|
+
options?.cdcRetention ?? 36e5
|
|
883
|
+
);
|
|
884
|
+
this.groupCommitter = new GroupCommitter(this.writerLock, {
|
|
885
|
+
acquireWriter: () => this.pool.acquireWriter(),
|
|
886
|
+
afterCommit: (writer, sql) => applyDdlSideEffectsIfRelevant(this.cdc.changeTracker, writer, sql)
|
|
887
|
+
});
|
|
447
888
|
}
|
|
448
889
|
static async create(id, path, driver, options, internals) {
|
|
890
|
+
const writerWorker = resolveWriterWorkerConfig(options?.writerWorker);
|
|
891
|
+
const readOnly = options?.readOnly ?? false;
|
|
892
|
+
if (writerWorker.enabled && !readOnly && !driver.startWriterHost) {
|
|
893
|
+
throw new SirannonError(
|
|
894
|
+
`writerWorker is enabled for database '${id}' but the driver does not carry a worker entry; use a driver that supports worker offload or disable writerWorker`,
|
|
895
|
+
"WRITER_WORKER_UNSUPPORTED"
|
|
896
|
+
);
|
|
897
|
+
}
|
|
449
898
|
const pool = await ConnectionPool.create({
|
|
450
899
|
driver,
|
|
451
900
|
path,
|
|
452
901
|
readOnly: options?.readOnly,
|
|
453
902
|
readPoolSize: options?.readPoolSize ?? 4,
|
|
454
|
-
walMode: options?.walMode ?? true
|
|
903
|
+
walMode: options?.walMode ?? true,
|
|
904
|
+
synchronous: options?.synchronous,
|
|
905
|
+
useWriterWorker: writerWorker.enabled && !readOnly,
|
|
906
|
+
workerHostOptions: writerWorker.host
|
|
455
907
|
});
|
|
456
|
-
|
|
908
|
+
const writeGate = new WriteGate(writerWorker.enabled ? writerWorker.maxPendingWrites : 0, writerWorker.retryAfterMs);
|
|
909
|
+
return new _Database(id, path, pool, driver, writeGate, options, internals);
|
|
457
910
|
}
|
|
458
911
|
async query(sql, params, options) {
|
|
459
912
|
this.ensureOpen();
|
|
460
|
-
this.
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
913
|
+
return this.observer.withQueryHooks(
|
|
914
|
+
sql,
|
|
915
|
+
params,
|
|
916
|
+
options,
|
|
917
|
+
() => this.runRead(sql, (conn) => query(conn, sql, params))
|
|
918
|
+
);
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Returns query rows already encoded for the wire (safe-range integers as
|
|
922
|
+
* plain numbers, larger integers and BLOBs as tagged envelopes) in a single
|
|
923
|
+
* pass. The server response path uses this so a read walks its values once
|
|
924
|
+
* rather than narrowing on the driver and re-scanning to tag.
|
|
925
|
+
*/
|
|
926
|
+
async queryForWire(sql, params, options) {
|
|
927
|
+
this.ensureOpen();
|
|
928
|
+
return this.observer.withQueryHooks(
|
|
929
|
+
sql,
|
|
930
|
+
params,
|
|
931
|
+
options,
|
|
932
|
+
() => this.runRead(sql, (conn) => queryForWire(conn, sql, params))
|
|
933
|
+
);
|
|
474
934
|
}
|
|
475
935
|
async queryOne(sql, params, options) {
|
|
476
936
|
this.ensureOpen();
|
|
477
|
-
this.
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
937
|
+
return this.observer.withQueryHooks(
|
|
938
|
+
sql,
|
|
939
|
+
params,
|
|
940
|
+
options,
|
|
941
|
+
() => this.runRead(sql, (conn) => queryOne(conn, sql, params))
|
|
942
|
+
);
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* On a single-connection driver the reader is the writer, so serialise the
|
|
946
|
+
* read through the writer lock to avoid a bulk load's uncommitted rows.
|
|
947
|
+
*/
|
|
948
|
+
runRead(sql, op) {
|
|
949
|
+
if (this.pool.readerCount === 0) {
|
|
950
|
+
return this.writerLock.run(() => this.observer.track(sql, () => op(this.pool.acquireWriter())));
|
|
490
951
|
}
|
|
952
|
+
return this.observer.track(sql, () => op(this.pool.acquireReader()));
|
|
491
953
|
}
|
|
492
954
|
async execute(sql, params, options) {
|
|
493
955
|
this.ensureOpen();
|
|
494
956
|
if (this.readOnly) throw new ReadOnlyError(this.id);
|
|
495
|
-
this.
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
this.fireAfterQueryHooks(sql, params, performance.now() - start);
|
|
507
|
-
}
|
|
957
|
+
return this.observer.withQueryHooks(
|
|
958
|
+
sql,
|
|
959
|
+
params,
|
|
960
|
+
options,
|
|
961
|
+
() => this.writeGate.run(
|
|
962
|
+
() => this.observer.track(
|
|
963
|
+
sql,
|
|
964
|
+
() => this.writerLock.isHeld() ? this.groupCommitter.runUngrouped(sql, params) : this.groupCommitter.submit(sql, params)
|
|
965
|
+
)
|
|
966
|
+
)
|
|
967
|
+
);
|
|
508
968
|
}
|
|
509
969
|
async executeBatch(sql, paramsBatch, options) {
|
|
510
970
|
this.ensureOpen();
|
|
511
971
|
if (this.readOnly) throw new ReadOnlyError(this.id);
|
|
512
|
-
this.
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
972
|
+
return this.observer.withQueryHooks(
|
|
973
|
+
sql,
|
|
974
|
+
void 0,
|
|
975
|
+
options,
|
|
976
|
+
() => this.writeGate.run(
|
|
977
|
+
() => this.writerLock.run(
|
|
978
|
+
() => this.runInTransaction(this.pool.acquireWriter(), sql, (txConn) => executeBatch(txConn, sql, paramsBatch))
|
|
979
|
+
)
|
|
980
|
+
)
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
async runInTransaction(writer, sql, run) {
|
|
984
|
+
const result = await this.observer.track(sql, () => writer.transaction(run));
|
|
985
|
+
await applyDdlSideEffectsIfRelevant(this.cdc.changeTracker, writer, sql);
|
|
986
|
+
return result;
|
|
526
987
|
}
|
|
527
|
-
|
|
988
|
+
/**
|
|
989
|
+
* Load rows with relaxed writer durability. The load holds the writer lock
|
|
990
|
+
* for its whole duration, so no other write commits under the relaxed level
|
|
991
|
+
* and no two loads race on the shared `synchronous` setting; the configured
|
|
992
|
+
* level is restored before this resolves, whether the load succeeds or
|
|
993
|
+
* fails. The load runs in one transaction, so one commit and one durability
|
|
994
|
+
* barrier cover the whole batch. Rows are summed rather than returned
|
|
995
|
+
* per-row to bound memory on large loads. Like `execute` and `transaction`,
|
|
996
|
+
* this writes only to the local database; under replication the server routes
|
|
997
|
+
* loads through the engine, not through this method.
|
|
998
|
+
*/
|
|
999
|
+
async bulkLoad(sql, paramsBatch, options) {
|
|
528
1000
|
this.ensureOpen();
|
|
529
1001
|
if (this.readOnly) throw new ReadOnlyError(this.id);
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
1002
|
+
return this.observer.withQueryHooks(
|
|
1003
|
+
sql,
|
|
1004
|
+
void 0,
|
|
1005
|
+
void 0,
|
|
1006
|
+
() => this.writeGate.run(
|
|
1007
|
+
() => this.writerLock.run(() => {
|
|
1008
|
+
const writer = this.pool.acquireWriter();
|
|
1009
|
+
return runBulkLoad({
|
|
1010
|
+
writer,
|
|
1011
|
+
configuredSynchronous: this.synchronous,
|
|
1012
|
+
walMode: this.walMode,
|
|
1013
|
+
durability: options?.durability,
|
|
1014
|
+
checkpoint: options?.checkpoint ?? true,
|
|
1015
|
+
loadRows: () => this.runInTransaction(writer, sql, (txConn) => executeBatchSummary(txConn, sql, paramsBatch))
|
|
1016
|
+
});
|
|
1017
|
+
})
|
|
1018
|
+
)
|
|
1019
|
+
);
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Takes the statements up front rather than a callback, since a group cannot wait on an
|
|
1023
|
+
* arbitrary caller-supplied callback without delaying every transaction beside it.
|
|
1024
|
+
*/
|
|
1025
|
+
async executeTransaction(statements) {
|
|
1026
|
+
this.ensureOpen();
|
|
1027
|
+
if (this.readOnly) throw new ReadOnlyError(this.id);
|
|
1028
|
+
if (statements.length === 0) return [];
|
|
1029
|
+
const owned = statements.map((statement) => ({ sql: statement.sql, params: statement.params }));
|
|
1030
|
+
const run = canGroupTransaction(owned) ? () => this.writeGate.run(() => this.groupCommitter.submitTransaction(owned)) : () => this.runStatementsAlone(owned);
|
|
1031
|
+
if (!this.observer.observesQueries) return run();
|
|
1032
|
+
return this.observer.withTransactionHooks(owned, run);
|
|
1033
|
+
}
|
|
1034
|
+
runStatementsAlone(statements) {
|
|
1035
|
+
return this.transaction(async (tx) => {
|
|
1036
|
+
const results = new Array(statements.length);
|
|
1037
|
+
for (let i = 0; i < statements.length; i++) {
|
|
1038
|
+
results[i] = await tx.execute(statements[i].sql, statements[i].params);
|
|
1039
|
+
}
|
|
1040
|
+
return results;
|
|
539
1041
|
});
|
|
540
|
-
if (state.sawDdl && state.droppedTables.length > 0) {
|
|
541
|
-
await tracker.pruneDroppedTables(writer, state.droppedTables);
|
|
542
|
-
}
|
|
543
|
-
return result;
|
|
544
1042
|
}
|
|
545
|
-
async
|
|
546
|
-
|
|
547
|
-
if (
|
|
548
|
-
|
|
549
|
-
await applyDdlSideEffects(tracker, writer, sql);
|
|
1043
|
+
async transaction(fn) {
|
|
1044
|
+
this.ensureOpen();
|
|
1045
|
+
if (this.readOnly) throw new ReadOnlyError(this.id);
|
|
1046
|
+
return this.writeGate.run(() => this.writerLock.run(() => this.cdc.runTransaction(this.pool.acquireWriter(), fn)));
|
|
550
1047
|
}
|
|
551
1048
|
async watch(table) {
|
|
552
1049
|
this.ensureOpen();
|
|
553
1050
|
if (this.readOnly) {
|
|
554
1051
|
throw new ReadOnlyError(this.id);
|
|
555
1052
|
}
|
|
556
|
-
this.
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
1053
|
+
await this.cdc.watch(table);
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Runs a CDC maintenance write (change-log pruning) on the shared writer
|
|
1057
|
+
* under the writer lock. Serialising it with application writes keeps it
|
|
1058
|
+
* from becoming a second writer that contends for SQLite's single write
|
|
1059
|
+
* lock and stalls the event loop on `busy_timeout`.
|
|
1060
|
+
*/
|
|
1061
|
+
async runCdcMaintenance(op) {
|
|
1062
|
+
if (this._closed) return;
|
|
1063
|
+
await this.writerLock.run(() => op(this.pool.acquireWriter()));
|
|
560
1064
|
}
|
|
561
1065
|
async unwatch(table) {
|
|
562
1066
|
this.ensureOpen();
|
|
563
|
-
|
|
564
|
-
const writer = this.pool.acquireWriter();
|
|
565
|
-
await this.changeTracker.unwatch(writer, table);
|
|
566
|
-
if (this.changeTracker.watchedTables.size === 0) {
|
|
567
|
-
this.stopCdcPollingLoop();
|
|
568
|
-
}
|
|
1067
|
+
await this.cdc.unwatch(table);
|
|
569
1068
|
}
|
|
570
1069
|
on(table) {
|
|
571
1070
|
this.ensureOpen();
|
|
572
|
-
this.
|
|
573
|
-
const manager = this.subscriptionManager;
|
|
574
|
-
if (!manager) throw new Error("subscriptionManager not initialized");
|
|
575
|
-
return new SubscriptionBuilderImpl(table, manager);
|
|
1071
|
+
return this.cdc.on(table);
|
|
576
1072
|
}
|
|
577
1073
|
async migrate(migrations) {
|
|
578
1074
|
this.ensureOpen();
|
|
579
|
-
|
|
580
|
-
return MigrationRunner.run(writer, migrations);
|
|
1075
|
+
return this.writerLock.run(() => MigrationRunner.run(this.pool.acquireWriter(), migrations));
|
|
581
1076
|
}
|
|
582
1077
|
async rollback(migrations, version) {
|
|
583
1078
|
this.ensureOpen();
|
|
584
|
-
|
|
585
|
-
return MigrationRunner.rollback(writer, migrations, version);
|
|
1079
|
+
return this.writerLock.run(() => MigrationRunner.rollback(this.pool.acquireWriter(), migrations, version));
|
|
586
1080
|
}
|
|
587
1081
|
async backup(destPath) {
|
|
588
1082
|
this.ensureOpen();
|
|
589
|
-
|
|
590
|
-
await this.backupManager.backup(writer, destPath);
|
|
1083
|
+
await this.backups.backup(destPath);
|
|
591
1084
|
}
|
|
592
1085
|
scheduleBackup(options) {
|
|
593
1086
|
this.ensureOpen();
|
|
594
|
-
|
|
595
|
-
const cancel = this.backupScheduler.schedule(writer, options);
|
|
596
|
-
this.scheduledBackupCancellers.push(cancel);
|
|
1087
|
+
this.backups.schedule(options);
|
|
597
1088
|
}
|
|
598
1089
|
async loadExtension(extensionPath) {
|
|
599
1090
|
this.ensureOpen();
|
|
600
|
-
|
|
601
|
-
await loadExtension(this.driver, writer, extensionPath);
|
|
1091
|
+
await this.writerLock.run(() => loadExtension(this.driver, this.pool.acquireWriter(), extensionPath));
|
|
602
1092
|
}
|
|
603
1093
|
onBeforeQuery(hook) {
|
|
604
1094
|
this.hookRegistry.register("beforeQuery", hook);
|
|
@@ -613,16 +1103,12 @@ var Database = class _Database {
|
|
|
613
1103
|
async close() {
|
|
614
1104
|
if (this._closed) return;
|
|
615
1105
|
this._closed = true;
|
|
616
|
-
this.
|
|
617
|
-
|
|
618
|
-
try {
|
|
619
|
-
cancel();
|
|
620
|
-
} catch {
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
this.scheduledBackupCancellers.length = 0;
|
|
1106
|
+
this.cdc.stop();
|
|
1107
|
+
this.backups.cancelAll();
|
|
624
1108
|
let poolError;
|
|
625
1109
|
try {
|
|
1110
|
+
await this.groupCommitter.drain();
|
|
1111
|
+
await this.writerLock.settle();
|
|
626
1112
|
await this.pool.close();
|
|
627
1113
|
} catch (err) {
|
|
628
1114
|
poolError = err;
|
|
@@ -648,54 +1134,6 @@ var Database = class _Database {
|
|
|
648
1134
|
throw new SirannonError(`Database '${this.id}' is closed`, "DATABASE_CLOSED");
|
|
649
1135
|
}
|
|
650
1136
|
}
|
|
651
|
-
ensureCdc() {
|
|
652
|
-
if (!this.changeTracker) {
|
|
653
|
-
this.changeTracker = new ChangeTracker({ retention: this.cdcRetention });
|
|
654
|
-
}
|
|
655
|
-
if (!this.subscriptionManager) {
|
|
656
|
-
this.subscriptionManager = new SubscriptionManager();
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
ensureCdcPolling() {
|
|
660
|
-
if (this.stopCdcPolling) return;
|
|
661
|
-
if (!this.changeTracker || !this.subscriptionManager) return;
|
|
662
|
-
const writer = this.pool.acquireWriter();
|
|
663
|
-
this.stopCdcPolling = startPolling(writer, this.changeTracker, this.subscriptionManager, this.cdcPollInterval);
|
|
664
|
-
}
|
|
665
|
-
stopCdcPollingLoop() {
|
|
666
|
-
if (this.stopCdcPolling) {
|
|
667
|
-
this.stopCdcPolling();
|
|
668
|
-
this.stopCdcPolling = null;
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
fireBeforeQueryHooks(sql, params, options) {
|
|
672
|
-
const hasParent = this.parentHooks?.has("beforeQuery");
|
|
673
|
-
const hasLocal = this.hookRegistry.has("beforeQuery");
|
|
674
|
-
if (!hasParent && !hasLocal) return;
|
|
675
|
-
const ctx = {
|
|
676
|
-
databaseId: this.id,
|
|
677
|
-
sql,
|
|
678
|
-
params,
|
|
679
|
-
writeConcern: options?.writeConcern,
|
|
680
|
-
readConcern: options?.readConcern
|
|
681
|
-
};
|
|
682
|
-
this.parentHooks?.invokeSync("beforeQuery", ctx);
|
|
683
|
-
this.hookRegistry.invokeSync("beforeQuery", ctx);
|
|
684
|
-
}
|
|
685
|
-
fireAfterQueryHooks(sql, params, durationMs) {
|
|
686
|
-
const hasParent = this.parentHooks?.has("afterQuery");
|
|
687
|
-
const hasLocal = this.hookRegistry.has("afterQuery");
|
|
688
|
-
if (!hasParent && !hasLocal) return;
|
|
689
|
-
const ctx = { databaseId: this.id, sql, params, durationMs };
|
|
690
|
-
try {
|
|
691
|
-
this.parentHooks?.invokeSync("afterQuery", ctx);
|
|
692
|
-
} catch {
|
|
693
|
-
}
|
|
694
|
-
try {
|
|
695
|
-
this.hookRegistry.invokeSync("afterQuery", ctx);
|
|
696
|
-
} catch {
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
1137
|
};
|
|
700
1138
|
|
|
701
1139
|
// src/core/lifecycle/manager.ts
|
|
@@ -950,7 +1388,7 @@ var Sirannon = class {
|
|
|
950
1388
|
if (this.hookRegistry.has("beforeConnect")) {
|
|
951
1389
|
this.hookRegistry.invokeSync("beforeConnect", { databaseId: id, path });
|
|
952
1390
|
}
|
|
953
|
-
db = await Database.create(id, path, this._driver, options, {
|
|
1391
|
+
db = await Database.create(id, path, this._driver, this.withRegistryDefaults(options), {
|
|
954
1392
|
parentHooks: this.hookRegistry,
|
|
955
1393
|
metrics: this.metricsCollector ?? void 0
|
|
956
1394
|
});
|
|
@@ -1000,6 +1438,11 @@ var Sirannon = class {
|
|
|
1000
1438
|
});
|
|
1001
1439
|
return db;
|
|
1002
1440
|
}
|
|
1441
|
+
withRegistryDefaults(options) {
|
|
1442
|
+
const fallback = this.options.writerWorker;
|
|
1443
|
+
if (fallback === void 0 || options?.writerWorker !== void 0) return options;
|
|
1444
|
+
return { ...options, writerWorker: fallback };
|
|
1445
|
+
}
|
|
1003
1446
|
async close(id) {
|
|
1004
1447
|
this.ensureRunning();
|
|
1005
1448
|
const db = this.dbs.get(id);
|