@delali/sirannon-db 0.1.7 → 0.2.0
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 +117 -969
- package/dist/backup-scheduler/index.d.ts +1 -1
- package/dist/backup-scheduler/index.mjs +2 -2
- package/dist/baseline-Br77Fnhb.d.ts +6 -0
- package/dist/{change-tracker-CbmaMO-N.d.ts → change-tracker-DKRVUC3l.d.ts} +5 -8
- package/dist/{chunk-DJLX6CAE.mjs → chunk-4ISB7XMA.mjs} +2 -1
- package/dist/chunk-5NOIGN5Y.mjs +18 -0
- package/dist/chunk-67M7KAH6.mjs +90 -0
- package/dist/{chunk-GEZUUIKV.mjs → chunk-D7LAYTKN.mjs} +5 -2
- package/dist/{chunk-4IGMIJQK.mjs → chunk-EBJXPQQO.mjs} +59 -20
- package/dist/{chunk-BNUTBHHH.mjs → chunk-FHWTZFI4.mjs} +1 -1
- package/dist/chunk-GVCNMPOS.mjs +130 -0
- package/dist/chunk-H237TXZW.mjs +1407 -0
- package/dist/{chunk-HHRMRFFR.mjs → chunk-H6PIVVDN.mjs} +353 -142
- package/dist/chunk-HR5CWTLC.mjs +661 -0
- package/dist/chunk-JU64Y7HM.mjs +299 -0
- package/dist/{chunk-CW6S3WL5.mjs → chunk-JZGINXTN.mjs} +7 -5
- package/dist/{chunk-CJLYFDP5.mjs → chunk-LFZ37BSX.mjs} +1 -1
- package/dist/chunk-LNY2VVHE.mjs +18 -0
- package/dist/chunk-NVQS53NT.mjs +665 -0
- package/dist/{chunk-H5AB6NIR.mjs → chunk-O7SLN3GI.mjs} +2 -15
- package/dist/chunk-OQVZBEBY.mjs +283 -0
- package/dist/{chunk-VLTICJOD.mjs → chunk-PIKHN33N.mjs} +1 -1
- package/dist/chunk-TJF5GZSV.mjs +187 -0
- package/dist/chunk-TUD5CJ76.mjs +126 -0
- package/dist/{chunk-YPYVQJ4C.mjs → chunk-UC3SCMIN.mjs} +9 -1
- package/dist/client/index.d.ts +216 -321
- package/dist/client/index.mjs +1192 -1342
- package/dist/client/topology.d.ts +61 -0
- package/dist/client/topology.mjs +682 -0
- package/dist/client-base-CLWmH5Ln.d.ts +234 -0
- package/dist/codegen/cli.d.ts +2 -0
- package/dist/codegen/cli.mjs +11 -0
- package/dist/codegen/index.d.ts +30 -0
- package/dist/codegen/index.mjs +5 -0
- package/dist/core/index.d.ts +37 -16
- package/dist/core/index.mjs +2322 -298
- package/dist/core/writer-worker.mjs +28 -6
- package/dist/{database-DuGp0Rtr.d.ts → database-BY0L5Q2n.d.ts} +59 -14
- package/dist/driver/better-sqlite3.d.ts +1 -1
- package/dist/driver/better-sqlite3.mjs +6 -6
- package/dist/driver/node.d.ts +1 -1
- package/dist/driver/node.mjs +6 -6
- package/dist/driver/wa-sqlite.d.ts +1 -136
- package/dist/{errors-5Nf5ZAEC.d.ts → errors-Bw5MdNCu.d.ts} +5 -1
- package/dist/file-migrations/index.d.ts +7 -5
- package/dist/file-migrations/index.mjs +42 -37
- package/dist/operation-registry-9DcvxcE5.d.ts +45 -0
- package/dist/primary-wins-DPAm2AKG.d.ts +61 -0
- package/dist/protocol-6KrSq2Hy.d.ts +66 -0
- package/dist/react/index.d.ts +20 -0
- package/dist/react/index.mjs +137 -0
- package/dist/replication/coordinator/etcd.d.ts +5 -3
- package/dist/replication/coordinator/etcd.mjs +554 -462
- package/dist/replication/index.d.ts +51 -105
- package/dist/replication/index.mjs +1415 -2061
- package/dist/server/index.d.ts +111 -87
- package/dist/server/index.mjs +1917 -489
- package/dist/server-options-1JHu8pid.d.ts +191 -0
- package/dist/{sirannon-4SspRvP5.d.ts → sirannon-DaQSyhbJ.d.ts} +7 -2
- package/dist/transport/grpc.d.ts +7 -4
- package/dist/transport/grpc.mjs +191 -97
- package/dist/transport/memory.d.ts +5 -134
- package/dist/transport/memory.mjs +3 -1
- package/dist/{types-BsjobKbl.d.ts → types-B7gmEsZW.d.ts} +8 -61
- package/dist/{types-BEu1I_9_.d.ts → types-BsVabqSI.d.ts} +1 -1
- package/dist/types-CL6piSnD.d.ts +43 -0
- package/dist/types-C_D8IhpO.d.ts +60 -0
- package/dist/{types-D_hQW1hr.d.ts → types-zhnRXrsb.d.ts} +55 -165
- package/package.json +44 -6
- package/dist/chunk-TKGHYWQ6.mjs +0 -35
- package/dist/types-D4p4UyDK.d.ts +0 -26
package/dist/core/index.mjs
CHANGED
|
@@ -1,15 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
export {
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
import { MIGRATIONS_TABLE } from '../chunk-
|
|
11
|
-
import
|
|
12
|
-
export {
|
|
1
|
+
import { Transaction, LWWResolver, REMOTE_ORIGIN_NODE_ID, BatchReader, BatchApplier, recordLocalColumnVersions, HLC, loadPersistedHlc, isWellFormedHlc, HLC_CLOCK_META_KEY } from '../chunk-HR5CWTLC.mjs';
|
|
2
|
+
export { Transaction } from '../chunk-HR5CWTLC.mjs';
|
|
3
|
+
import { executeBatch, executeBatchSummary, execute, bindParams, executeGroup, query, queryForWire, queryOne } from '../chunk-JZGINXTN.mjs';
|
|
4
|
+
export { execute, executeBatch, query, queryOne } from '../chunk-JZGINXTN.mjs';
|
|
5
|
+
import { tokenizeSql, unsupported, findClauses, readSource, readSortTerms, readWindow, readSelectItems, parameterName } from '../chunk-JU64Y7HM.mjs';
|
|
6
|
+
import { reconcileMigrationChecksums, migrationContentChecksum, runBulkLoad, ensureCdcEpoch, SubscriptionBuilderImpl, ChangeTracker, SubscriptionManager, startPolling, randomHex, TransactionGrouper, PrimedSubscription } from '../chunk-H6PIVVDN.mjs';
|
|
7
|
+
export { ChangeTracker, isBulkLoadDurability, runBulkLoad } from '../chunk-H6PIVVDN.mjs';
|
|
8
|
+
import { ensureMigrationsTable, prepareUpdateMigrationChecksum, prepareInsertMigration, selectAppliedMigrationsNewestFirst, prepareDeleteMigration, selectAppliedMigrations, tableInfoRows, selectTableSql, selectTableExists, selectMetaValue, upsertMetaValue, SEQ_STRING_RE, replaceMigrationHistory, highestMigrationVersion, selectCountOutboundChanges, ensureMetaTable, ensureChangesTable, ensureBatchApplyTables, PkResolver, selectMaxChangeSeq, selectMaxAppliedSourceSeq, insertMetaValueIfAbsent, selectMaxChangeHlc, updateUnstampedChangeStampsSql, UPSERT_META_VALUE_SQL, setForeignKeysEnabled, IDENTIFIER_RE, validateDdlSafety, ensureStagedChangesTable, upsertStagedChangeSql, selectFirstStagedTransactionEnd, deleteStagedChangesUpToSeq, selectMaxStagedSeq, deleteAllStagedChanges, deleteMetaValue, assertSafeIdentifier, selectStagedChangesInRange } from '../chunk-NVQS53NT.mjs';
|
|
9
|
+
import { ReplicationError } from '../chunk-5NOIGN5Y.mjs';
|
|
10
|
+
import { MIGRATIONS_TABLE, LIVE_PROBE_TABLE_PREFIX, META_TABLE, CHANGES_TABLE, assertSqlAllowed, INTERNAL_TABLE_PREFIX, encodeTaggedValues, decodeTaggedValues } from '../chunk-D7LAYTKN.mjs';
|
|
11
|
+
import '../chunk-GVCNMPOS.mjs';
|
|
12
|
+
export { defineDriver } from '../chunk-FHWTZFI4.mjs';
|
|
13
|
+
import { DEFAULT_SYNCHRONOUS } from '../chunk-LFZ37BSX.mjs';
|
|
14
|
+
export { DEFAULT_SYNCHRONOUS, isSynchronousLevel, synchronousPragmaValue } from '../chunk-LFZ37BSX.mjs';
|
|
15
|
+
import { LAZY_DOWN_SQL, parseMigrationFilename } from '../chunk-LNY2VVHE.mjs';
|
|
16
|
+
export { MIGRATION_FILENAME_PATTERN, parseMigrationFilename } from '../chunk-LNY2VVHE.mjs';
|
|
17
|
+
import { resolveEffectiveBaseline, planPendingMigrations, SQLITE_USER_VERSION_MAX, applyBaselineOption } from '../chunk-67M7KAH6.mjs';
|
|
18
|
+
import { SirannonError, ConnectionPoolError, MigrationError, ReadOnlyError, MaxDatabasesError, DatabaseAlreadyExistsError, DatabaseNotFoundError, CDCError, ExtensionError, WriteOverloadError } from '../chunk-UC3SCMIN.mjs';
|
|
19
|
+
export { BackupError, CDCError, ConnectionPoolError, DatabaseAlreadyExistsError, DatabaseNotFoundError, ExtensionError, ForbiddenSqlError, HookDeniedError, MaxDatabasesError, MigrationError, QueryError, ReadOnlyError, RequestDeniedError, SirannonError, TransactionError, WriteOverloadError } from '../chunk-UC3SCMIN.mjs';
|
|
13
20
|
|
|
14
21
|
// src/core/connection-pool.ts
|
|
15
22
|
async function closeAllSilently(connections) {
|
|
@@ -202,25 +209,170 @@ var CdcAwareTransaction = class extends Transaction {
|
|
|
202
209
|
}
|
|
203
210
|
};
|
|
204
211
|
|
|
212
|
+
// src/core/cdc/read-position.ts
|
|
213
|
+
var TOKEN_VERSION = "1";
|
|
214
|
+
var HEX_RE = /^[0-9a-f]+$/;
|
|
215
|
+
function encodeReadPosition(position) {
|
|
216
|
+
if (!HEX_RE.test(position.epoch)) {
|
|
217
|
+
throw new CDCError(`Cannot issue a read position for epoch '${position.epoch}': an epoch is lower-case hex`);
|
|
218
|
+
}
|
|
219
|
+
if (position.seq < 0n) {
|
|
220
|
+
throw new CDCError("Cannot issue a read position for a negative change-log sequence");
|
|
221
|
+
}
|
|
222
|
+
return toHex(`${TOKEN_VERSION}:${position.epoch}:${position.seq.toString()}`);
|
|
223
|
+
}
|
|
224
|
+
async function readAtPosition(conn, epoch, read) {
|
|
225
|
+
return conn.transaction(async (txConn) => {
|
|
226
|
+
const value = await read(txConn);
|
|
227
|
+
const seq = await selectTableExists(txConn, CHANGES_TABLE) ? await selectMaxChangeSeq(txConn) : 0n;
|
|
228
|
+
return { value, position: encodeReadPosition({ epoch, seq }), seq };
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
function toHex(value) {
|
|
232
|
+
let hex = "";
|
|
233
|
+
for (let i = 0; i < value.length; i++) {
|
|
234
|
+
hex += value.charCodeAt(i).toString(16).padStart(2, "0");
|
|
235
|
+
}
|
|
236
|
+
return hex;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/core/sync/stamper.ts
|
|
240
|
+
var NODE_ID_META_KEY = "node_id";
|
|
241
|
+
var SyncStamper = class _SyncStamper {
|
|
242
|
+
constructor(nodeId, hlc, changesTable) {
|
|
243
|
+
this.nodeId = nodeId;
|
|
244
|
+
this.hlc = hlc;
|
|
245
|
+
this.changesTable = changesTable;
|
|
246
|
+
}
|
|
247
|
+
static async init(conn, changesTable = CHANGES_TABLE) {
|
|
248
|
+
await ensureMetaTable(conn);
|
|
249
|
+
await insertMetaValueIfAbsent(conn, NODE_ID_META_KEY, randomHex(16));
|
|
250
|
+
const nodeId = await selectMetaValue(conn, NODE_ID_META_KEY);
|
|
251
|
+
if (nodeId === null || nodeId.length === 0) {
|
|
252
|
+
throw new CDCError("Failed to read the persisted node identity");
|
|
253
|
+
}
|
|
254
|
+
const hlc = new HLC(nodeId);
|
|
255
|
+
const persisted = await loadPersistedHlc(conn);
|
|
256
|
+
if (persisted !== null) {
|
|
257
|
+
hlc.receive(persisted);
|
|
258
|
+
}
|
|
259
|
+
const persistedMax = await selectMaxChangeHlc(conn, changesTable);
|
|
260
|
+
if (persistedMax !== null && isWellFormedHlc(persistedMax)) {
|
|
261
|
+
hlc.receive(persistedMax);
|
|
262
|
+
}
|
|
263
|
+
return new _SyncStamper(nodeId, hlc, changesTable);
|
|
264
|
+
}
|
|
265
|
+
stampStatements(options) {
|
|
266
|
+
const hlcValue = this.hlc.now();
|
|
267
|
+
const txId = randomHex(16);
|
|
268
|
+
const statements = [
|
|
269
|
+
{
|
|
270
|
+
sql: updateUnstampedChangeStampsSql(this.changesTable),
|
|
271
|
+
params: [this.nodeId, txId, hlcValue],
|
|
272
|
+
trusted: true
|
|
273
|
+
}
|
|
274
|
+
];
|
|
275
|
+
if (options?.persistClock !== false) {
|
|
276
|
+
statements.push({
|
|
277
|
+
sql: UPSERT_META_VALUE_SQL,
|
|
278
|
+
params: [HLC_CLOCK_META_KEY, hlcValue],
|
|
279
|
+
trusted: true
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return statements;
|
|
283
|
+
}
|
|
284
|
+
async applyStamps(txConn) {
|
|
285
|
+
for (const statement of this.stampStatements()) {
|
|
286
|
+
const stmt = await txConn.prepare(statement.sql);
|
|
287
|
+
await stmt.run(...statement.params);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
|
|
205
292
|
// src/core/database-cdc.ts
|
|
206
293
|
var DatabaseCdcController = class {
|
|
207
|
-
constructor(runExclusive, acquireWriter, pollInterval, retention) {
|
|
294
|
+
constructor(runExclusive, acquireWriter, pollInterval, retention, openSnapshotConnection) {
|
|
208
295
|
this.runExclusive = runExclusive;
|
|
209
296
|
this.acquireWriter = acquireWriter;
|
|
210
297
|
this.pollInterval = pollInterval;
|
|
211
298
|
this.retention = retention;
|
|
299
|
+
this.openSnapshotConnection = openSnapshotConnection;
|
|
212
300
|
}
|
|
213
301
|
tracker = null;
|
|
214
|
-
|
|
302
|
+
subscriptionManager = null;
|
|
303
|
+
liveConnRequest = null;
|
|
215
304
|
stopPolling = null;
|
|
305
|
+
stamper = null;
|
|
306
|
+
epochRequest = null;
|
|
307
|
+
ensureEpoch() {
|
|
308
|
+
this.epochRequest ??= this.runExclusive(() => ensureCdcEpoch(this.acquireWriter())).catch((err) => {
|
|
309
|
+
this.epochRequest = null;
|
|
310
|
+
throw err;
|
|
311
|
+
});
|
|
312
|
+
return this.epochRequest;
|
|
313
|
+
}
|
|
314
|
+
async readAtPositionWith(read) {
|
|
315
|
+
const epoch = await this.ensureEpoch();
|
|
316
|
+
const open = this.openSnapshotConnection;
|
|
317
|
+
if (open === null) {
|
|
318
|
+
return this.runExclusive(() => readAtPosition(this.acquireWriter(), epoch, read));
|
|
319
|
+
}
|
|
320
|
+
const conn = await open();
|
|
321
|
+
try {
|
|
322
|
+
return await readAtPosition(conn, epoch, read);
|
|
323
|
+
} finally {
|
|
324
|
+
await conn.close().catch(() => {
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
async liveConnection() {
|
|
329
|
+
const open = this.openSnapshotConnection;
|
|
330
|
+
if (open === null) {
|
|
331
|
+
return { conn: this.acquireWriter(), run: this.runExclusive };
|
|
332
|
+
}
|
|
333
|
+
this.liveConnRequest ??= open().catch((err) => {
|
|
334
|
+
this.liveConnRequest = null;
|
|
335
|
+
throw err;
|
|
336
|
+
});
|
|
337
|
+
return { conn: await this.liveConnRequest, run: (op) => op() };
|
|
338
|
+
}
|
|
339
|
+
async closeLiveConnection() {
|
|
340
|
+
const request = this.liveConnRequest;
|
|
341
|
+
if (request === null) return;
|
|
342
|
+
this.liveConnRequest = null;
|
|
343
|
+
await request.then((conn) => conn.close()).catch(() => {
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
get subscriptions() {
|
|
347
|
+
return this.subscriptionManager;
|
|
348
|
+
}
|
|
216
349
|
get changeTracker() {
|
|
217
350
|
return this.tracker;
|
|
218
351
|
}
|
|
219
352
|
async watch(table) {
|
|
220
353
|
const { tracker } = this.ensure();
|
|
221
|
-
await this.runExclusive(() =>
|
|
354
|
+
await this.runExclusive(async () => {
|
|
355
|
+
await tracker.watch(this.acquireWriter(), table);
|
|
356
|
+
await this.ensureStamper();
|
|
357
|
+
});
|
|
222
358
|
this.ensurePolling();
|
|
223
359
|
}
|
|
360
|
+
async ensureStamper() {
|
|
361
|
+
this.stamper ??= await SyncStamper.init(this.acquireWriter());
|
|
362
|
+
return this.stamper;
|
|
363
|
+
}
|
|
364
|
+
async ensureStamping() {
|
|
365
|
+
if (this.stamper) return;
|
|
366
|
+
await this.runExclusive(() => this.ensureStamper());
|
|
367
|
+
}
|
|
368
|
+
stampStatements(options) {
|
|
369
|
+
return this.stamper ? this.stamper.stampStatements(options) : null;
|
|
370
|
+
}
|
|
371
|
+
async applyStamps(txConn) {
|
|
372
|
+
if (this.stamper) {
|
|
373
|
+
await this.stamper.applyStamps(txConn);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
224
376
|
async unwatch(table) {
|
|
225
377
|
const tracker = this.tracker;
|
|
226
378
|
if (!tracker) return;
|
|
@@ -239,7 +391,11 @@ var DatabaseCdcController = class {
|
|
|
239
391
|
return Transaction.run(writer, fn);
|
|
240
392
|
}
|
|
241
393
|
const state = { sawDdl: false, droppedTables: [] };
|
|
242
|
-
const result = await writer.transaction((txConn) =>
|
|
394
|
+
const result = await writer.transaction(async (txConn) => {
|
|
395
|
+
const value = await fn(new CdcAwareTransaction(txConn, tracker, state));
|
|
396
|
+
await this.applyStamps(txConn);
|
|
397
|
+
return value;
|
|
398
|
+
});
|
|
243
399
|
if (state.sawDdl && state.droppedTables.length > 0) {
|
|
244
400
|
await tracker.pruneDroppedTables(writer, state.droppedTables);
|
|
245
401
|
}
|
|
@@ -253,18 +409,18 @@ var DatabaseCdcController = class {
|
|
|
253
409
|
}
|
|
254
410
|
ensure() {
|
|
255
411
|
const tracker = this.tracker ?? new ChangeTracker({ retention: this.retention });
|
|
256
|
-
const subscriptions = this.
|
|
412
|
+
const subscriptions = this.subscriptionManager ?? new SubscriptionManager();
|
|
257
413
|
this.tracker = tracker;
|
|
258
|
-
this.
|
|
414
|
+
this.subscriptionManager = subscriptions;
|
|
259
415
|
return { tracker, subscriptions };
|
|
260
416
|
}
|
|
261
417
|
ensurePolling() {
|
|
262
418
|
if (this.stopPolling) return;
|
|
263
|
-
if (!this.tracker || !this.
|
|
419
|
+
if (!this.tracker || !this.subscriptionManager) return;
|
|
264
420
|
this.stopPolling = startPolling(
|
|
265
421
|
this.acquireWriter(),
|
|
266
422
|
this.tracker,
|
|
267
|
-
this.
|
|
423
|
+
this.subscriptionManager,
|
|
268
424
|
this.pollInterval,
|
|
269
425
|
void 0,
|
|
270
426
|
this.runExclusive
|
|
@@ -310,7 +466,6 @@ var DatabaseObserver = class {
|
|
|
310
466
|
this.parentHooks = parentHooks;
|
|
311
467
|
this.metrics = metrics;
|
|
312
468
|
}
|
|
313
|
-
/** False lets a caller skip the timing calls on a path where nothing would read them. */
|
|
314
469
|
get observesQueries() {
|
|
315
470
|
return this.metrics !== null || this.localHooks.has("beforeQuery") || this.localHooks.has("afterQuery") || (this.parentHooks?.has("beforeQuery") ?? false) || (this.parentHooks?.has("afterQuery") ?? false);
|
|
316
471
|
}
|
|
@@ -327,12 +482,6 @@ var DatabaseObserver = class {
|
|
|
327
482
|
if (!this.metrics) return op();
|
|
328
483
|
return this.metrics.trackQuery(op, { databaseId: this.databaseId, sql });
|
|
329
484
|
}
|
|
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
485
|
async withTransactionHooks(statements, op) {
|
|
337
486
|
for (const statement of statements) this.fireBefore(statement.sql, statement.params);
|
|
338
487
|
const start = performance.now();
|
|
@@ -362,32 +511,497 @@ var DatabaseObserver = class {
|
|
|
362
511
|
}
|
|
363
512
|
};
|
|
364
513
|
|
|
365
|
-
// src/core/
|
|
366
|
-
async function
|
|
367
|
-
|
|
368
|
-
|
|
514
|
+
// src/core/migrations/schema-version.ts
|
|
515
|
+
async function readSchemaVersion(conn) {
|
|
516
|
+
const stmt = await conn.prepare("PRAGMA user_version");
|
|
517
|
+
const row = await stmt.get();
|
|
518
|
+
return row === void 0 ? 0 : Number(row.user_version);
|
|
519
|
+
}
|
|
520
|
+
async function writeSchemaVersion(conn, version) {
|
|
521
|
+
if (!Number.isSafeInteger(version) || version < 0 || version > SQLITE_USER_VERSION_MAX) {
|
|
522
|
+
throw new SirannonError(
|
|
523
|
+
`Schema version is outside the PRAGMA user_version range: ${version}`,
|
|
524
|
+
"INTERNAL_SCHEMA_ERROR"
|
|
525
|
+
);
|
|
369
526
|
}
|
|
370
|
-
|
|
371
|
-
|
|
527
|
+
await conn.exec(`PRAGMA user_version = ${version}`);
|
|
528
|
+
}
|
|
529
|
+
async function mirrorSchemaVersion(conn, version) {
|
|
530
|
+
if (version > SQLITE_USER_VERSION_MAX) return;
|
|
531
|
+
await writeSchemaVersion(conn, version);
|
|
532
|
+
}
|
|
533
|
+
async function syncSchemaVersion(conn, expected) {
|
|
534
|
+
if (expected > SQLITE_USER_VERSION_MAX) return;
|
|
535
|
+
const current = await readSchemaVersion(conn);
|
|
536
|
+
if (current !== expected) {
|
|
537
|
+
await writeSchemaVersion(conn, expected);
|
|
372
538
|
}
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
// src/core/sync/snapshot-apply.ts
|
|
542
|
+
var SNAPSHOT_STATE_META_KEY = "device_sync_snapshot_state";
|
|
543
|
+
var SNAPSHOT_STATE_LOADING = "loading";
|
|
544
|
+
function assertTableNames(tables) {
|
|
545
|
+
for (const table of tables) {
|
|
546
|
+
if (!IDENTIFIER_RE.test(table)) {
|
|
547
|
+
throw new ReplicationError(`Invalid table name: ${table}`);
|
|
376
548
|
}
|
|
377
549
|
}
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
550
|
+
}
|
|
551
|
+
async function snapshotLoadPending(conn) {
|
|
552
|
+
if (!await selectTableExists(conn, META_TABLE)) return false;
|
|
553
|
+
return await selectMetaValue(conn, SNAPSHOT_STATE_META_KEY) !== null;
|
|
554
|
+
}
|
|
555
|
+
async function beginSnapshotLoad(conn, tables, tracker) {
|
|
556
|
+
assertTableNames(tables);
|
|
557
|
+
await ensureMetaTable(conn);
|
|
558
|
+
await upsertMetaValue(conn, SNAPSHOT_STATE_META_KEY, SNAPSHOT_STATE_LOADING);
|
|
559
|
+
await setForeignKeysEnabled(conn, false);
|
|
560
|
+
await ensureStagedChangesTable(conn);
|
|
561
|
+
await conn.transaction(async (tx) => {
|
|
562
|
+
await deleteAllStagedChanges(tx);
|
|
563
|
+
for (const table of tables) {
|
|
564
|
+
await tracker?.unwatch(tx, table);
|
|
565
|
+
}
|
|
566
|
+
const reversed = [...tables].reverse();
|
|
567
|
+
for (const table of reversed) {
|
|
568
|
+
await tx.exec(`DROP TABLE IF EXISTS "${table}"`);
|
|
569
|
+
}
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
async function applySnapshotSchema(conn, schema) {
|
|
573
|
+
for (const ddl of schema) {
|
|
574
|
+
if (!validateDdlSafety(ddl)) {
|
|
575
|
+
throw new ReplicationError(`Snapshot schema statement failed safety validation: ${ddl.slice(0, 120)}`);
|
|
576
|
+
}
|
|
381
577
|
}
|
|
382
|
-
const
|
|
578
|
+
for (const ddl of schema) {
|
|
579
|
+
const rewritten = ddl.replace(/^\s*CREATE\s+TABLE\s+(?!IF\s+NOT\s+EXISTS)/i, "CREATE TABLE IF NOT EXISTS ").replace(/^\s*CREATE\s+(UNIQUE\s+)?INDEX\s+(?!IF\s+NOT\s+EXISTS)/i, "CREATE $1INDEX IF NOT EXISTS ");
|
|
580
|
+
await conn.exec(rewritten);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
async function loadSnapshotPage(conn, table, rows) {
|
|
584
|
+
if (!IDENTIFIER_RE.test(table)) {
|
|
585
|
+
throw new ReplicationError(`Invalid table name: ${table}`);
|
|
586
|
+
}
|
|
587
|
+
if (rows.length === 0) return;
|
|
588
|
+
const columns = Object.keys(rows[0]);
|
|
589
|
+
if (columns.length === 0) {
|
|
590
|
+
throw new ReplicationError(`Snapshot page for table '${table}' carries rows without columns`);
|
|
591
|
+
}
|
|
592
|
+
for (const column of columns) {
|
|
593
|
+
if (!IDENTIFIER_RE.test(column) && column !== "rowid") {
|
|
594
|
+
throw new ReplicationError(`Invalid column name in snapshot page for table '${table}': ${column}`);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
const columnSet = new Set(columns);
|
|
598
|
+
for (const row of rows) {
|
|
599
|
+
const keys = Object.keys(row);
|
|
600
|
+
if (keys.length !== columns.length || keys.some((key) => !columnSet.has(key))) {
|
|
601
|
+
throw new ReplicationError(`Snapshot page rows for table '${table}' do not share one column set`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
const columnList = columns.map((column) => `"${column}"`).join(", ");
|
|
605
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
606
|
+
await conn.transaction(async (tx) => {
|
|
607
|
+
const insert = await tx.prepare(`INSERT INTO "${table}" (${columnList}) VALUES (${placeholders})`);
|
|
608
|
+
for (const row of rows) {
|
|
609
|
+
await insert.run(...columns.map((column) => row[column]));
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
async function endSnapshotLoad(conn, tables, tracker) {
|
|
614
|
+
assertTableNames(tables);
|
|
615
|
+
for (const table of tables) {
|
|
616
|
+
await tracker?.watch(conn, table);
|
|
617
|
+
}
|
|
618
|
+
await setForeignKeysEnabled(conn, true);
|
|
619
|
+
await deleteMetaValue(conn, SNAPSHOT_STATE_META_KEY);
|
|
620
|
+
}
|
|
621
|
+
async function abortSnapshotLoad(conn) {
|
|
622
|
+
await setForeignKeysEnabled(conn, true);
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// src/core/sync/staged-pull.ts
|
|
626
|
+
var STAGED_APPLY_BATCH_ROWS = 500;
|
|
627
|
+
async function stagePulledChanges(conn, events) {
|
|
628
|
+
if (events.length === 0) return null;
|
|
629
|
+
await ensureStagedChangesTable(conn);
|
|
630
|
+
let maxSeq2 = null;
|
|
631
|
+
await conn.transaction(async (tx) => {
|
|
632
|
+
const stmt = await tx.prepare(upsertStagedChangeSql());
|
|
633
|
+
for (const event of events) {
|
|
634
|
+
await stmt.run(
|
|
635
|
+
event.seq.toString(),
|
|
636
|
+
event.table,
|
|
637
|
+
event.type,
|
|
638
|
+
event.rowId ?? "",
|
|
639
|
+
event.timestamp,
|
|
640
|
+
event.oldRow === void 0 ? null : JSON.stringify(encodeTaggedValues(event.oldRow)),
|
|
641
|
+
event.type === "delete" ? null : JSON.stringify(encodeTaggedValues(event.row)),
|
|
642
|
+
event.origin ?? "",
|
|
643
|
+
event.txId ?? "",
|
|
644
|
+
event.hlc ?? "",
|
|
645
|
+
event.txEnd === true ? 1 : 0
|
|
646
|
+
);
|
|
647
|
+
if (maxSeq2 === null || event.seq > maxSeq2) {
|
|
648
|
+
maxSeq2 = event.seq;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
return maxSeq2;
|
|
653
|
+
}
|
|
654
|
+
async function applyStagedTransactions(conn, applier, options) {
|
|
655
|
+
await ensureStagedChangesTable(conn);
|
|
656
|
+
let appliedThrough = null;
|
|
657
|
+
while (true) {
|
|
658
|
+
const head = await selectFirstStagedTransactionEnd(conn);
|
|
659
|
+
if (head === null) break;
|
|
660
|
+
const endSeq = head.seq;
|
|
661
|
+
const result = await applier.applyStagedGroup({
|
|
662
|
+
source: {
|
|
663
|
+
nextBatch: (afterSeq) => readStagedEntries(conn, afterSeq, endSeq)
|
|
664
|
+
},
|
|
665
|
+
resolver: options.resolver,
|
|
666
|
+
withinTx: options.withinTx === void 0 ? void 0 : (tx) => options.withinTx?.(tx, endSeq) ?? Promise.resolve()
|
|
667
|
+
});
|
|
668
|
+
if (result.maxHlc !== "") {
|
|
669
|
+
await applier.mergeHlc(result.maxHlc);
|
|
670
|
+
}
|
|
671
|
+
if (options.onChange) {
|
|
672
|
+
await emitStagedEvents(conn, endSeq, options.onChange);
|
|
673
|
+
}
|
|
674
|
+
await deleteStagedChangesUpToSeq(conn, endSeq);
|
|
675
|
+
appliedThrough = endSeq;
|
|
676
|
+
}
|
|
677
|
+
return appliedThrough;
|
|
678
|
+
}
|
|
679
|
+
async function recoverStagedPull(conn, applier, appliedFloor, options) {
|
|
680
|
+
await ensureStagedChangesTable(conn);
|
|
681
|
+
if (appliedFloor !== null) {
|
|
682
|
+
await deleteStagedChangesUpToSeq(conn, appliedFloor);
|
|
683
|
+
}
|
|
684
|
+
let appliedThrough = null;
|
|
685
|
+
let applyError = null;
|
|
383
686
|
try {
|
|
384
|
-
|
|
385
|
-
await writer.exec(`SELECT load_extension('${escaped}')`);
|
|
687
|
+
appliedThrough = await applyStagedTransactions(conn, applier, options);
|
|
386
688
|
} catch (err) {
|
|
387
|
-
|
|
689
|
+
applyError = err;
|
|
690
|
+
}
|
|
691
|
+
const stagedMax = await selectMaxStagedSeq(conn);
|
|
692
|
+
const appliedSeq = maxSeq(appliedFloor, appliedThrough);
|
|
693
|
+
return { resumeSeq: maxSeq(appliedSeq, stagedMax), appliedSeq, applyError };
|
|
694
|
+
}
|
|
695
|
+
function maxSeq(a, b) {
|
|
696
|
+
if (a === null) return b;
|
|
697
|
+
if (b === null) return a;
|
|
698
|
+
return a > b ? a : b;
|
|
699
|
+
}
|
|
700
|
+
async function readStagedEntries(conn, afterSeq, upToSeq) {
|
|
701
|
+
const rows = await selectStagedChangesInRange(conn, afterSeq, upToSeq, STAGED_APPLY_BATCH_ROWS);
|
|
702
|
+
return rows.map((row) => ({ seq: BigInt(row.seq), change: rowToReplicationChange(row) }));
|
|
703
|
+
}
|
|
704
|
+
function rowToReplicationChange(row) {
|
|
705
|
+
return {
|
|
706
|
+
table: row.table_name,
|
|
707
|
+
operation: row.operation,
|
|
708
|
+
rowId: row.row_id,
|
|
709
|
+
primaryKey: {},
|
|
710
|
+
hlc: row.hlc,
|
|
711
|
+
txId: row.tx_id,
|
|
712
|
+
nodeId: row.node_id,
|
|
713
|
+
newData: row.new_data === null ? null : decodeTaggedValues(JSON.parse(row.new_data)),
|
|
714
|
+
oldData: row.old_data === null ? null : decodeTaggedValues(JSON.parse(row.old_data))
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
function rowToChangeEvent(row) {
|
|
718
|
+
return {
|
|
719
|
+
type: row.operation,
|
|
720
|
+
table: row.table_name,
|
|
721
|
+
row: row.new_data === null ? {} : decodeTaggedValues(JSON.parse(row.new_data)),
|
|
722
|
+
oldRow: row.old_data === null ? void 0 : decodeTaggedValues(JSON.parse(row.old_data)),
|
|
723
|
+
seq: BigInt(row.seq),
|
|
724
|
+
timestamp: row.changed_at,
|
|
725
|
+
rowId: row.row_id,
|
|
726
|
+
...row.node_id !== "" ? { origin: row.node_id } : {},
|
|
727
|
+
...row.hlc !== "" ? { hlc: row.hlc } : {},
|
|
728
|
+
...row.tx_id !== "" ? { txId: row.tx_id } : {},
|
|
729
|
+
...row.tx_end === 1 ? { txEnd: true } : {}
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
async function emitStagedEvents(conn, upToSeq, onChange) {
|
|
733
|
+
let cursor = 0n;
|
|
734
|
+
while (true) {
|
|
735
|
+
const rows = await selectStagedChangesInRange(conn, cursor, upToSeq, STAGED_APPLY_BATCH_ROWS);
|
|
736
|
+
if (rows.length === 0) return;
|
|
737
|
+
for (const row of rows) {
|
|
738
|
+
cursor = BigInt(row.seq);
|
|
739
|
+
try {
|
|
740
|
+
onChange(rowToChangeEvent(row));
|
|
741
|
+
} catch {
|
|
742
|
+
}
|
|
743
|
+
}
|
|
388
744
|
}
|
|
389
745
|
}
|
|
390
746
|
|
|
747
|
+
// src/core/database-sync.ts
|
|
748
|
+
var PUSHED_SEQ_META_KEY = "device_sync_pushed_seq";
|
|
749
|
+
var PULL_SEQ_META_KEY = "device_sync_pull_seq";
|
|
750
|
+
var PULL_EPOCH_META_KEY = "device_sync_pull_epoch";
|
|
751
|
+
var RESYNC_REQUIRED_META_KEY = "device_sync_resync_required";
|
|
752
|
+
var COLUMN_VERSIONS_SEQ_META_KEY = "device_sync_column_versions_seq";
|
|
753
|
+
var DatabaseSyncController = class {
|
|
754
|
+
constructor(runExclusive, acquireWriter, cdc) {
|
|
755
|
+
this.runExclusive = runExclusive;
|
|
756
|
+
this.acquireWriter = acquireWriter;
|
|
757
|
+
this.cdc = cdc;
|
|
758
|
+
}
|
|
759
|
+
pkResolver = null;
|
|
760
|
+
tablesReady = false;
|
|
761
|
+
metaReady = false;
|
|
762
|
+
outboxReader = null;
|
|
763
|
+
localPruneBoundary = null;
|
|
764
|
+
snapshotGate = false;
|
|
765
|
+
defaultResolver = new LWWResolver();
|
|
766
|
+
applyChanges(batch, resolver) {
|
|
767
|
+
return this.runExclusive(async () => {
|
|
768
|
+
const applier = await this.ensureApplier();
|
|
769
|
+
const result = await applier.applyBatch(batch, resolver ?? this.defaultResolver);
|
|
770
|
+
if (result.droppedTables.length > 0) {
|
|
771
|
+
await this.cdc.changeTracker?.pruneDroppedTables(this.acquireWriter(), result.droppedTables);
|
|
772
|
+
}
|
|
773
|
+
return result;
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
devicePort() {
|
|
777
|
+
return {
|
|
778
|
+
identity: () => this.runExclusive(async () => ({ nodeId: (await this.cdc.ensureStamper()).nodeId })),
|
|
779
|
+
applyPulledTransaction: (changes, pullSeq, resolver) => this.applyPulledTransaction(changes, pullSeq, resolver),
|
|
780
|
+
stagePulledChanges: (events) => this.stagePulled(events),
|
|
781
|
+
applyStagedPull: (resolver, onChange) => this.applyStagedPull(resolver, onChange),
|
|
782
|
+
recoverStagedPull: (resolver, onChange) => this.recoverStagedPullState(resolver, onChange),
|
|
783
|
+
readOutboxBatch: (afterSeq, limit) => this.readOutboxBatch(afterSeq, limit),
|
|
784
|
+
countOutboxPending: (afterSeq) => this.countOutboxPending(afterSeq),
|
|
785
|
+
getPushCursor: () => this.getMetaSeq(PUSHED_SEQ_META_KEY).then((seq) => seq ?? 0n),
|
|
786
|
+
setPushCursor: (seq) => this.setMetaSeq(PUSHED_SEQ_META_KEY, seq),
|
|
787
|
+
getPullState: () => this.getPullState(),
|
|
788
|
+
getResyncRequired: () => this.getResyncRequired(),
|
|
789
|
+
setResyncRequired: (required) => this.setResyncRequired(required),
|
|
790
|
+
setPullState: (seq, epoch) => this.setPullState(seq, epoch),
|
|
791
|
+
protectUnpushedChanges: (pushedSeq) => {
|
|
792
|
+
this.localPruneBoundary = pushedSeq;
|
|
793
|
+
this.applyLocalPruneBoundary();
|
|
794
|
+
},
|
|
795
|
+
snapshotLoadPending: () => this.runExclusive(() => snapshotLoadPending(this.acquireWriter())),
|
|
796
|
+
beginSnapshotLoad: (tables) => this.beginSnapshotLoad(tables),
|
|
797
|
+
applySnapshotSchema: (schema) => this.runExclusive(() => applySnapshotSchema(this.acquireWriter(), schema)),
|
|
798
|
+
loadSnapshotPage: (table, rows) => this.runExclusive(() => loadSnapshotPage(this.acquireWriter(), table, rows)),
|
|
799
|
+
replaceMigrationHistory: (rows) => this.replaceMigrationHistory(rows),
|
|
800
|
+
endSnapshotLoad: (tables) => this.endSnapshotLoad(tables),
|
|
801
|
+
abortSnapshotLoad: () => this.runExclusive(() => abortSnapshotLoad(this.acquireWriter()))
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
get snapshotLoadBlocked() {
|
|
805
|
+
return this.snapshotGate;
|
|
806
|
+
}
|
|
807
|
+
seedSnapshotGate() {
|
|
808
|
+
this.snapshotGate = true;
|
|
809
|
+
}
|
|
810
|
+
applyPulledTransaction(changes, pullSeq, resolver) {
|
|
811
|
+
return this.runExclusive(async () => {
|
|
812
|
+
if (changes.length === 0) {
|
|
813
|
+
return { applied: 0, skipped: 0, conflicts: 0, droppedTables: [] };
|
|
814
|
+
}
|
|
815
|
+
await this.ensureMeta();
|
|
816
|
+
const applier = await this.ensureApplier();
|
|
817
|
+
let maxHlc = "";
|
|
818
|
+
let sourceNodeId = REMOTE_ORIGIN_NODE_ID;
|
|
819
|
+
for (const change of changes) {
|
|
820
|
+
if (change.hlc > maxHlc) maxHlc = change.hlc;
|
|
821
|
+
if (sourceNodeId === REMOTE_ORIGIN_NODE_ID && change.nodeId !== "") sourceNodeId = change.nodeId;
|
|
822
|
+
}
|
|
823
|
+
const result = await applier.applyGroup({
|
|
824
|
+
sourceNodeId,
|
|
825
|
+
txId: changes[0].txId,
|
|
826
|
+
changes,
|
|
827
|
+
resolver: resolver ?? this.defaultResolver,
|
|
828
|
+
withinTx: (tx) => upsertMetaValue(tx, PULL_SEQ_META_KEY, pullSeq.toString())
|
|
829
|
+
});
|
|
830
|
+
if (result.droppedTables.length > 0) {
|
|
831
|
+
await this.cdc.changeTracker?.pruneDroppedTables(this.acquireWriter(), result.droppedTables);
|
|
832
|
+
}
|
|
833
|
+
if (maxHlc !== "") {
|
|
834
|
+
await applier.mergeHlc(maxHlc);
|
|
835
|
+
}
|
|
836
|
+
return result;
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
stagePulled(events) {
|
|
840
|
+
return this.runExclusive(async () => {
|
|
841
|
+
await this.ensureMeta();
|
|
842
|
+
return stagePulledChanges(this.acquireWriter(), events);
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
applyStagedPull(resolver, onChange) {
|
|
846
|
+
return this.runExclusive(async () => {
|
|
847
|
+
await this.ensureMeta();
|
|
848
|
+
const applier = await this.ensureApplier();
|
|
849
|
+
return applyStagedTransactions(this.acquireWriter(), applier, this.stagedApplyOptions(resolver, onChange));
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
recoverStagedPullState(resolver, onChange) {
|
|
853
|
+
return this.runExclusive(async () => {
|
|
854
|
+
const writer = await this.ensureMeta();
|
|
855
|
+
const applier = await this.ensureApplier();
|
|
856
|
+
const recorded = await selectMetaValue(writer, PULL_SEQ_META_KEY);
|
|
857
|
+
const appliedFloor = recorded !== null && SEQ_STRING_RE.test(recorded) ? BigInt(recorded) : null;
|
|
858
|
+
return recoverStagedPull(writer, applier, appliedFloor, this.stagedApplyOptions(resolver, onChange));
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
stagedApplyOptions(resolver, onChange) {
|
|
862
|
+
return {
|
|
863
|
+
resolver: resolver ?? this.defaultResolver,
|
|
864
|
+
withinTx: (tx, appliedThroughSeq) => upsertMetaValue(tx, PULL_SEQ_META_KEY, appliedThroughSeq.toString()),
|
|
865
|
+
onChange
|
|
866
|
+
};
|
|
867
|
+
}
|
|
868
|
+
replaceMigrationHistory(rows) {
|
|
869
|
+
return this.runExclusive(async () => {
|
|
870
|
+
const writer = this.acquireWriter();
|
|
871
|
+
await replaceMigrationHistory(writer, rows);
|
|
872
|
+
await mirrorSchemaVersion(writer, highestMigrationVersion(rows));
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
async beginSnapshotLoad(tables) {
|
|
876
|
+
this.snapshotGate = true;
|
|
877
|
+
try {
|
|
878
|
+
await this.runExclusive(() => beginSnapshotLoad(this.acquireWriter(), tables, this.cdc.changeTracker));
|
|
879
|
+
} catch (err) {
|
|
880
|
+
this.snapshotGate = await this.runExclusive(() => snapshotLoadPending(this.acquireWriter()));
|
|
881
|
+
throw err;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
async endSnapshotLoad(tables) {
|
|
885
|
+
await this.runExclusive(() => endSnapshotLoad(this.acquireWriter(), tables, this.cdc.changeTracker));
|
|
886
|
+
this.snapshotGate = false;
|
|
887
|
+
}
|
|
888
|
+
applyLocalPruneBoundary() {
|
|
889
|
+
if (this.localPruneBoundary !== null) {
|
|
890
|
+
this.cdc.changeTracker?.setPruneBoundary(this.localPruneBoundary);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
readOutboxBatch(afterSeq, limit) {
|
|
894
|
+
return this.runExclusive(async () => {
|
|
895
|
+
this.applyLocalPruneBoundary();
|
|
896
|
+
const reader = await this.ensureOutboxReader();
|
|
897
|
+
return reader.readBatch(afterSeq, limit);
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
countOutboxPending(afterSeq) {
|
|
901
|
+
return this.runExclusive(async () => {
|
|
902
|
+
this.applyLocalPruneBoundary();
|
|
903
|
+
const writer = this.acquireWriter();
|
|
904
|
+
await this.ensureSyncTables(writer);
|
|
905
|
+
const stamper = await this.cdc.ensureStamper();
|
|
906
|
+
return selectCountOutboundChanges(writer, CHANGES_TABLE, afterSeq, stamper.nodeId);
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
getResyncRequired() {
|
|
910
|
+
return this.runExclusive(async () => {
|
|
911
|
+
const writer = await this.ensureMeta();
|
|
912
|
+
return await selectMetaValue(writer, RESYNC_REQUIRED_META_KEY) === "1";
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
setResyncRequired(required) {
|
|
916
|
+
return this.runExclusive(async () => {
|
|
917
|
+
const writer = await this.ensureMeta();
|
|
918
|
+
await upsertMetaValue(writer, RESYNC_REQUIRED_META_KEY, required ? "1" : "0");
|
|
919
|
+
});
|
|
920
|
+
}
|
|
921
|
+
getPullState() {
|
|
922
|
+
return this.runExclusive(async () => {
|
|
923
|
+
const writer = await this.ensureMeta();
|
|
924
|
+
const seq = await selectMetaValue(writer, PULL_SEQ_META_KEY);
|
|
925
|
+
if (seq === null || !SEQ_STRING_RE.test(seq)) return null;
|
|
926
|
+
const epoch = await selectMetaValue(writer, PULL_EPOCH_META_KEY);
|
|
927
|
+
return { seq: BigInt(seq), epoch: epoch ?? void 0 };
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
setPullState(seq, epoch) {
|
|
931
|
+
return this.runExclusive(async () => {
|
|
932
|
+
const writer = await this.ensureMeta();
|
|
933
|
+
await upsertMetaValue(writer, PULL_SEQ_META_KEY, seq.toString());
|
|
934
|
+
if (epoch !== void 0) {
|
|
935
|
+
await upsertMetaValue(writer, PULL_EPOCH_META_KEY, epoch);
|
|
936
|
+
}
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
getMetaSeq(key) {
|
|
940
|
+
return this.runExclusive(async () => {
|
|
941
|
+
const writer = await this.ensureMeta();
|
|
942
|
+
const value = await selectMetaValue(writer, key);
|
|
943
|
+
if (value === null || !SEQ_STRING_RE.test(value)) return null;
|
|
944
|
+
return BigInt(value);
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
setMetaSeq(key, seq) {
|
|
948
|
+
return this.runExclusive(async () => {
|
|
949
|
+
const writer = await this.ensureMeta();
|
|
950
|
+
await upsertMetaValue(writer, key, seq.toString());
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
async ensureMeta() {
|
|
954
|
+
const writer = this.acquireWriter();
|
|
955
|
+
if (!this.metaReady) {
|
|
956
|
+
await ensureMetaTable(writer);
|
|
957
|
+
this.metaReady = true;
|
|
958
|
+
}
|
|
959
|
+
return writer;
|
|
960
|
+
}
|
|
961
|
+
async ensureSyncTables(writer) {
|
|
962
|
+
if (!this.tablesReady) {
|
|
963
|
+
await ensureChangesTable(writer, CHANGES_TABLE);
|
|
964
|
+
await ensureBatchApplyTables(writer);
|
|
965
|
+
this.tablesReady = true;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
async ensureOutboxReader() {
|
|
969
|
+
const writer = this.acquireWriter();
|
|
970
|
+
await this.ensureSyncTables(writer);
|
|
971
|
+
const stamper = await this.cdc.ensureStamper();
|
|
972
|
+
this.pkResolver ??= new PkResolver(writer);
|
|
973
|
+
this.outboxReader ??= new BatchReader(writer, stamper.nodeId, CHANGES_TABLE, this.pkResolver, true);
|
|
974
|
+
return this.outboxReader;
|
|
975
|
+
}
|
|
976
|
+
async ensureApplier() {
|
|
977
|
+
const writer = this.acquireWriter();
|
|
978
|
+
await this.ensureSyncTables(writer);
|
|
979
|
+
const stamper = await this.cdc.ensureStamper();
|
|
980
|
+
this.pkResolver ??= new PkResolver(writer);
|
|
981
|
+
return new BatchApplier(
|
|
982
|
+
writer,
|
|
983
|
+
stamper.nodeId,
|
|
984
|
+
stamper.hlc,
|
|
985
|
+
this.pkResolver,
|
|
986
|
+
(fromNodeId) => this.lastAppliedSeq(fromNodeId),
|
|
987
|
+
this.cdc.changeTracker ?? void 0,
|
|
988
|
+
CHANGES_TABLE,
|
|
989
|
+
(tx) => this.catchUpLocalColumnVersions(tx, stamper.nodeId)
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
async catchUpLocalColumnVersions(tx, localNodeId) {
|
|
993
|
+
const recorded = await selectMetaValue(tx, COLUMN_VERSIONS_SEQ_META_KEY);
|
|
994
|
+
const afterSeq = recorded !== null && SEQ_STRING_RE.test(recorded) ? BigInt(recorded) : 0n;
|
|
995
|
+
const latestSeq = await selectMaxChangeSeq(tx, CHANGES_TABLE);
|
|
996
|
+
if (latestSeq <= afterSeq) return;
|
|
997
|
+
await recordLocalColumnVersions(tx, CHANGES_TABLE, localNodeId, afterSeq);
|
|
998
|
+
await upsertMetaValue(tx, COLUMN_VERSIONS_SEQ_META_KEY, latestSeq.toString());
|
|
999
|
+
}
|
|
1000
|
+
lastAppliedSeq(fromNodeId) {
|
|
1001
|
+
return selectMaxAppliedSourceSeq(this.acquireWriter(), fromNodeId);
|
|
1002
|
+
}
|
|
1003
|
+
};
|
|
1004
|
+
|
|
391
1005
|
// src/core/group-committer.ts
|
|
392
1006
|
var DEFAULT_MAX_GROUP_SIZE = 1e3;
|
|
393
1007
|
var DML_PREFIX_RE = /^\s*(?:INSERT|UPDATE|DELETE|REPLACE)\b/i;
|
|
@@ -472,7 +1086,7 @@ var GroupCommitter = class {
|
|
|
472
1086
|
}
|
|
473
1087
|
async flush(batch) {
|
|
474
1088
|
const lone = batch[0];
|
|
475
|
-
if (batch.length === 1 && lone.statements.length === 1) {
|
|
1089
|
+
if (batch.length === 1 && lone.statements.length === 1 && !this.needsStamp(lone.statements[0].sql)) {
|
|
476
1090
|
try {
|
|
477
1091
|
lone.resolve([await this.writerLock.run(() => this.runSingle(lone.statements[0]))]);
|
|
478
1092
|
} catch (err) {
|
|
@@ -489,21 +1103,52 @@ var GroupCommitter = class {
|
|
|
489
1103
|
}
|
|
490
1104
|
for (let i = 0; i < batch.length; i++) {
|
|
491
1105
|
const outcome = outcomes[i];
|
|
492
|
-
if (outcome.ok) batch[i].resolve(outcome.values);
|
|
1106
|
+
if (outcome.ok) batch[i].resolve(outcome.values.slice(0, batch[i].statements.length));
|
|
493
1107
|
else batch[i].reject(outcome.error);
|
|
494
1108
|
}
|
|
495
1109
|
}
|
|
1110
|
+
needsStamp(sql) {
|
|
1111
|
+
if (!isGroupable(sql)) return false;
|
|
1112
|
+
const stamps = this.hooks.stampStatements();
|
|
1113
|
+
return stamps !== null && stamps.length > 0;
|
|
1114
|
+
}
|
|
496
1115
|
async runSingle(statement) {
|
|
497
1116
|
const writer = this.hooks.acquireWriter();
|
|
498
|
-
const
|
|
1117
|
+
const stamps = isGroupable(statement.sql) ? this.hooks.stampStatements() : null;
|
|
1118
|
+
const result = stamps === null || stamps.length === 0 ? await execute(writer, statement.sql, statement.params) : await this.runStampedAlone(writer, statement, stamps);
|
|
499
1119
|
await this.hooks.afterCommit(writer, statement.sql);
|
|
500
1120
|
return result;
|
|
501
1121
|
}
|
|
1122
|
+
async runStampedAlone(writer, statement, stamps) {
|
|
1123
|
+
await writer.exec("SAVEPOINT sirannon_stamp");
|
|
1124
|
+
try {
|
|
1125
|
+
const result = await execute(writer, statement.sql, statement.params);
|
|
1126
|
+
for (const stamp of stamps) {
|
|
1127
|
+
await execute(writer, stamp.sql, stamp.params, stamp.trusted === true);
|
|
1128
|
+
}
|
|
1129
|
+
await writer.exec("RELEASE sirannon_stamp");
|
|
1130
|
+
return result;
|
|
1131
|
+
} catch (err) {
|
|
1132
|
+
try {
|
|
1133
|
+
await writer.exec("ROLLBACK TO sirannon_stamp");
|
|
1134
|
+
await writer.exec("RELEASE sirannon_stamp");
|
|
1135
|
+
} catch {
|
|
1136
|
+
}
|
|
1137
|
+
throw err;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
502
1140
|
async runGroup(batch) {
|
|
503
1141
|
const writer = this.hooks.acquireWriter();
|
|
504
|
-
const units = batch.map((job) =>
|
|
505
|
-
|
|
506
|
-
|
|
1142
|
+
const units = batch.map((job, index) => {
|
|
1143
|
+
const stamps = this.hooks.stampStatements({ persistClock: index === batch.length - 1 }) ?? [];
|
|
1144
|
+
return {
|
|
1145
|
+
statements: [...job.statements, ...stamps].map((statement) => ({
|
|
1146
|
+
sql: statement.sql,
|
|
1147
|
+
params: bindParams(statement.params),
|
|
1148
|
+
...statement.trusted === true ? { trusted: true } : {}
|
|
1149
|
+
}))
|
|
1150
|
+
};
|
|
1151
|
+
});
|
|
507
1152
|
if (writer.runGroup) {
|
|
508
1153
|
const raw = await writer.runGroup(units);
|
|
509
1154
|
return raw.map(
|
|
@@ -589,38 +1234,1331 @@ var HookRegistry = class {
|
|
|
589
1234
|
this.hooks.set(event, [hook]);
|
|
590
1235
|
}
|
|
591
1236
|
}
|
|
592
|
-
loadConfig(config) {
|
|
593
|
-
for (const [configKey, event] of Object.entries(HOOK_CONFIG_MAP)) {
|
|
594
|
-
const value = config[configKey];
|
|
595
|
-
if (!value) continue;
|
|
596
|
-
const hooks = Array.isArray(value) ? value : [value];
|
|
597
|
-
for (const hook of hooks) {
|
|
598
|
-
this.addHook(event, hook);
|
|
599
|
-
}
|
|
1237
|
+
loadConfig(config) {
|
|
1238
|
+
for (const [configKey, event] of Object.entries(HOOK_CONFIG_MAP)) {
|
|
1239
|
+
const value = config[configKey];
|
|
1240
|
+
if (!value) continue;
|
|
1241
|
+
const hooks = Array.isArray(value) ? value : [value];
|
|
1242
|
+
for (const hook of hooks) {
|
|
1243
|
+
this.addHook(event, hook);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
};
|
|
1248
|
+
|
|
1249
|
+
// src/core/worker/config.ts
|
|
1250
|
+
var DEFAULT_MAX_PENDING_WRITES = 1024;
|
|
1251
|
+
var DEFAULT_RETRY_AFTER_MS = 1e3;
|
|
1252
|
+
function requireCount(value, name, min) {
|
|
1253
|
+
if (!Number.isInteger(value) || value < min) {
|
|
1254
|
+
throw new SirannonError(`writerWorker.${name} must be an integer >= ${min}`, "INVALID_WRITER_WORKER");
|
|
1255
|
+
}
|
|
1256
|
+
return value;
|
|
1257
|
+
}
|
|
1258
|
+
function resolveWriterWorkerConfig(option) {
|
|
1259
|
+
if (!option) {
|
|
1260
|
+
return { enabled: false, maxPendingWrites: 0, retryAfterMs: DEFAULT_RETRY_AFTER_MS, host: {} };
|
|
1261
|
+
}
|
|
1262
|
+
const opts = option === true ? {} : option;
|
|
1263
|
+
const host = {};
|
|
1264
|
+
if (opts.writeTimeoutMs !== void 0) host.writeTimeoutMs = requireCount(opts.writeTimeoutMs, "writeTimeoutMs", 0);
|
|
1265
|
+
if (opts.maxRestarts !== void 0) host.maxRestarts = requireCount(opts.maxRestarts, "maxRestarts", 0);
|
|
1266
|
+
const maxPendingWrites = opts.maxPendingWrites === void 0 ? DEFAULT_MAX_PENDING_WRITES : requireCount(opts.maxPendingWrites, "maxPendingWrites", 1);
|
|
1267
|
+
return { enabled: true, maxPendingWrites, retryAfterMs: DEFAULT_RETRY_AFTER_MS, host };
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// src/core/worker/gate.ts
|
|
1271
|
+
var WriteGate = class {
|
|
1272
|
+
constructor(limit, retryAfterMs) {
|
|
1273
|
+
this.limit = limit;
|
|
1274
|
+
this.retryAfterMs = retryAfterMs;
|
|
1275
|
+
}
|
|
1276
|
+
inFlight = 0;
|
|
1277
|
+
run(op) {
|
|
1278
|
+
if (this.limit > 0 && this.inFlight >= this.limit) {
|
|
1279
|
+
return Promise.reject(new WriteOverloadError(this.limit, this.retryAfterMs));
|
|
1280
|
+
}
|
|
1281
|
+
this.inFlight++;
|
|
1282
|
+
return op().finally(() => {
|
|
1283
|
+
this.inFlight--;
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
};
|
|
1287
|
+
|
|
1288
|
+
// src/core/writer-lock.ts
|
|
1289
|
+
var UNTRACKED = {
|
|
1290
|
+
run: (operation) => operation(),
|
|
1291
|
+
isActive: () => false,
|
|
1292
|
+
exit: (operation) => operation()
|
|
1293
|
+
};
|
|
1294
|
+
var WriterLock = class {
|
|
1295
|
+
tail = Promise.resolve();
|
|
1296
|
+
context;
|
|
1297
|
+
constructor(context) {
|
|
1298
|
+
this.context = context ?? UNTRACKED;
|
|
1299
|
+
}
|
|
1300
|
+
run(operation) {
|
|
1301
|
+
if (this.context.isActive()) {
|
|
1302
|
+
return operation();
|
|
1303
|
+
}
|
|
1304
|
+
const enter = () => this.context.run(operation);
|
|
1305
|
+
const ticket = this.tail.then(enter, enter);
|
|
1306
|
+
this.tail = ticket.then(swallow, swallow);
|
|
1307
|
+
return ticket;
|
|
1308
|
+
}
|
|
1309
|
+
isHeld() {
|
|
1310
|
+
return this.context.isActive();
|
|
1311
|
+
}
|
|
1312
|
+
async settle() {
|
|
1313
|
+
await this.run(() => Promise.resolve());
|
|
1314
|
+
}
|
|
1315
|
+
detached(operation) {
|
|
1316
|
+
return this.context.exit(operation);
|
|
1317
|
+
}
|
|
1318
|
+
};
|
|
1319
|
+
function swallow() {
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// src/core/database-create.ts
|
|
1323
|
+
async function createDatabaseRuntime(id, path, driver, options, internals) {
|
|
1324
|
+
const writerWorker = resolveWriterWorkerConfig(options?.writerWorker);
|
|
1325
|
+
const readOnly = options?.readOnly ?? false;
|
|
1326
|
+
if (writerWorker.enabled && !readOnly && !driver.startWriterHost) {
|
|
1327
|
+
throw new SirannonError(
|
|
1328
|
+
`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`,
|
|
1329
|
+
"WRITER_WORKER_UNSUPPORTED"
|
|
1330
|
+
);
|
|
1331
|
+
}
|
|
1332
|
+
const pool = await ConnectionPool.create({
|
|
1333
|
+
driver,
|
|
1334
|
+
path,
|
|
1335
|
+
readOnly: options?.readOnly,
|
|
1336
|
+
readPoolSize: options?.readPoolSize ?? 4,
|
|
1337
|
+
walMode: options?.walMode ?? true,
|
|
1338
|
+
synchronous: options?.synchronous,
|
|
1339
|
+
useWriterWorker: writerWorker.enabled && !readOnly,
|
|
1340
|
+
workerHostOptions: writerWorker.host
|
|
1341
|
+
});
|
|
1342
|
+
const writeGate = new WriteGate(writerWorker.enabled ? writerWorker.maxPendingWrites : 0, writerWorker.retryAfterMs);
|
|
1343
|
+
const writerLock = new WriterLock(driver.createWriterContext?.());
|
|
1344
|
+
const hookRegistry = new HookRegistry();
|
|
1345
|
+
const observer = new DatabaseObserver(id, hookRegistry, internals?.parentHooks ?? null, internals?.metrics ?? null);
|
|
1346
|
+
const backups = new DatabaseBackupController(
|
|
1347
|
+
(op) => writerLock.run(op),
|
|
1348
|
+
() => pool.acquireWriter(),
|
|
1349
|
+
driver.createBackupEngine?.()
|
|
1350
|
+
);
|
|
1351
|
+
const canOpenSnapshotConnection = driver.capabilities.multipleConnections && path !== ":memory:";
|
|
1352
|
+
const cdc = new DatabaseCdcController(
|
|
1353
|
+
(op) => writerLock.run(op),
|
|
1354
|
+
() => pool.acquireWriter(),
|
|
1355
|
+
options?.cdcPollInterval ?? 50,
|
|
1356
|
+
options?.cdcRetention ?? 36e5,
|
|
1357
|
+
canOpenSnapshotConnection ? () => driver.open(path, { readonly: true, walMode: false }) : null
|
|
1358
|
+
);
|
|
1359
|
+
const sync = new DatabaseSyncController(
|
|
1360
|
+
(op) => writeGate.run(() => writerLock.run(op)),
|
|
1361
|
+
() => pool.acquireWriter(),
|
|
1362
|
+
cdc
|
|
1363
|
+
);
|
|
1364
|
+
if (!readOnly && await snapshotLoadPending(pool.acquireWriter())) {
|
|
1365
|
+
sync.seedSnapshotGate();
|
|
1366
|
+
}
|
|
1367
|
+
const groupCommitter = new GroupCommitter(writerLock, {
|
|
1368
|
+
acquireWriter: () => pool.acquireWriter(),
|
|
1369
|
+
afterCommit: (writer, sql) => applyDdlSideEffectsIfRelevant(cdc.changeTracker, writer, sql),
|
|
1370
|
+
stampStatements: (stampOptions) => cdc.stampStatements(stampOptions)
|
|
1371
|
+
});
|
|
1372
|
+
return { pool, writeGate, writerLock, hookRegistry, observer, backups, cdc, sync, groupCommitter };
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
// src/core/database-reads.ts
|
|
1376
|
+
function readRows(deps, sql, params, options) {
|
|
1377
|
+
return deps.observer.withQueryHooks(
|
|
1378
|
+
sql,
|
|
1379
|
+
params,
|
|
1380
|
+
options,
|
|
1381
|
+
() => onReadConnection(deps, sql, (conn) => query(conn, sql, params))
|
|
1382
|
+
);
|
|
1383
|
+
}
|
|
1384
|
+
function readWireRows(deps, sql, params, options) {
|
|
1385
|
+
return deps.observer.withQueryHooks(
|
|
1386
|
+
sql,
|
|
1387
|
+
params,
|
|
1388
|
+
options,
|
|
1389
|
+
() => onReadConnection(deps, sql, (conn) => queryForWire(conn, sql, params))
|
|
1390
|
+
);
|
|
1391
|
+
}
|
|
1392
|
+
function readOneRow(deps, sql, params, options) {
|
|
1393
|
+
return deps.observer.withQueryHooks(
|
|
1394
|
+
sql,
|
|
1395
|
+
params,
|
|
1396
|
+
options,
|
|
1397
|
+
() => onReadConnection(deps, sql, (conn) => queryOne(conn, sql, params))
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
function onReadConnection(deps, sql, op) {
|
|
1401
|
+
if (deps.pool.readerCount === 0) {
|
|
1402
|
+
return deps.writerLock.run(() => deps.observer.track(sql, () => op(deps.pool.acquireWriter())));
|
|
1403
|
+
}
|
|
1404
|
+
return deps.observer.track(sql, () => op(deps.pool.acquireReader()));
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// src/core/extension-loader.ts
|
|
1408
|
+
async function loadExtension(driver, writer, extensionPath) {
|
|
1409
|
+
if (!driver.capabilities.extensions || !driver.resolveExtensionPath) {
|
|
1410
|
+
throw new ExtensionError(extensionPath, "Extensions are not supported by the current driver");
|
|
1411
|
+
}
|
|
1412
|
+
if (!extensionPath || extensionPath.includes("\0")) {
|
|
1413
|
+
throw new ExtensionError(extensionPath || "", "Extension path is empty or contains null bytes");
|
|
1414
|
+
}
|
|
1415
|
+
for (let i = 0; i < extensionPath.length; i++) {
|
|
1416
|
+
if (extensionPath.charCodeAt(i) <= 31) {
|
|
1417
|
+
throw new ExtensionError(extensionPath, "Extension path contains control characters");
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
const segments = extensionPath.split(/[/\\]/);
|
|
1421
|
+
if (segments.includes("..")) {
|
|
1422
|
+
throw new ExtensionError(extensionPath, "Extension path must not contain directory traversal segments");
|
|
1423
|
+
}
|
|
1424
|
+
const resolved = driver.resolveExtensionPath(extensionPath);
|
|
1425
|
+
try {
|
|
1426
|
+
const escaped = resolved.replace(/'/g, "''");
|
|
1427
|
+
await writer.exec(`SELECT load_extension('${escaped}')`);
|
|
1428
|
+
} catch (err) {
|
|
1429
|
+
throw new ExtensionError(extensionPath, err instanceof Error ? err.message : String(err));
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// src/core/live/column-collations.ts
|
|
1434
|
+
var TABLE_CONSTRAINT_STARTS = /* @__PURE__ */ new Set(["constraint", "primary", "unique", "check", "foreign"]);
|
|
1435
|
+
function parseColumnCollations(ddl) {
|
|
1436
|
+
const collations = /* @__PURE__ */ new Map();
|
|
1437
|
+
const tokens = tokenizeSql(ddl);
|
|
1438
|
+
const open = tokens.findIndex((token) => token.kind === "punct" && token.value === "(" && token.depth === 0);
|
|
1439
|
+
if (open === -1) return collations;
|
|
1440
|
+
let definition = [];
|
|
1441
|
+
for (let i = open + 1; i < tokens.length; i++) {
|
|
1442
|
+
const token = tokens[i];
|
|
1443
|
+
if (token.depth === 0) break;
|
|
1444
|
+
if (token.depth === 1 && token.kind === "punct" && token.value === ",") {
|
|
1445
|
+
readDefinition(definition, collations);
|
|
1446
|
+
definition = [];
|
|
1447
|
+
continue;
|
|
1448
|
+
}
|
|
1449
|
+
definition.push(token);
|
|
1450
|
+
}
|
|
1451
|
+
readDefinition(definition, collations);
|
|
1452
|
+
return collations;
|
|
1453
|
+
}
|
|
1454
|
+
function readDefinition(definition, collations) {
|
|
1455
|
+
const name = definition[0];
|
|
1456
|
+
if (name === void 0 || name.kind !== "word") return;
|
|
1457
|
+
if (!name.quoted && TABLE_CONSTRAINT_STARTS.has(name.lower)) return;
|
|
1458
|
+
for (let i = 1; i < definition.length - 1; i++) {
|
|
1459
|
+
const token = definition[i];
|
|
1460
|
+
if (token.kind === "word" && !token.quoted && token.lower === "collate") {
|
|
1461
|
+
collations.set(name.value, definition[i + 1].value);
|
|
1462
|
+
return;
|
|
1463
|
+
}
|
|
1464
|
+
}
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// src/core/live/sqlite-order.ts
|
|
1468
|
+
var TYPE_NULL = 0;
|
|
1469
|
+
var TYPE_NUMBER = 1;
|
|
1470
|
+
var TYPE_TEXT = 2;
|
|
1471
|
+
var TYPE_BLOB = 3;
|
|
1472
|
+
var encoder = new TextEncoder();
|
|
1473
|
+
function resolveCollation(name) {
|
|
1474
|
+
if (name === null) return "binary";
|
|
1475
|
+
const lower = name.toLowerCase();
|
|
1476
|
+
if (lower === "binary" || lower === "nocase" || lower === "rtrim") return lower;
|
|
1477
|
+
throw new CDCError(
|
|
1478
|
+
`Cannot keep a live query ordered by the '${name}' collation: order is maintained for BINARY, NOCASE, and RTRIM`
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
function toSortValue(value, collation) {
|
|
1482
|
+
if (value === null || value === void 0) return { type: TYPE_NULL };
|
|
1483
|
+
if (typeof value === "number" || typeof value === "bigint") return { type: TYPE_NUMBER, numeric: value };
|
|
1484
|
+
if (typeof value === "boolean") return { type: TYPE_NUMBER, numeric: value ? 1 : 0 };
|
|
1485
|
+
if (typeof value === "string") return { type: TYPE_TEXT, bytes: encoder.encode(applyCollation(value, collation)) };
|
|
1486
|
+
if (value instanceof Uint8Array) return { type: TYPE_BLOB, bytes: value };
|
|
1487
|
+
if (ArrayBuffer.isView(value)) {
|
|
1488
|
+
const view = value;
|
|
1489
|
+
return { type: TYPE_BLOB, bytes: new Uint8Array(view.buffer, view.byteOffset, view.byteLength) };
|
|
1490
|
+
}
|
|
1491
|
+
return { type: TYPE_TEXT, bytes: encoder.encode(applyCollation(String(value), collation)) };
|
|
1492
|
+
}
|
|
1493
|
+
function applyCollation(text, collation) {
|
|
1494
|
+
if (collation === "nocase") return upperAscii(text);
|
|
1495
|
+
if (collation === "rtrim") return trimTrailingSpaces(text);
|
|
1496
|
+
return text;
|
|
1497
|
+
}
|
|
1498
|
+
function upperAscii(text) {
|
|
1499
|
+
let result = "";
|
|
1500
|
+
for (let i = 0; i < text.length; i++) {
|
|
1501
|
+
const code = text.charCodeAt(i);
|
|
1502
|
+
result += code >= 97 && code <= 122 ? String.fromCharCode(code - 32) : text[i];
|
|
1503
|
+
}
|
|
1504
|
+
return result;
|
|
1505
|
+
}
|
|
1506
|
+
function trimTrailingSpaces(text) {
|
|
1507
|
+
let end = text.length;
|
|
1508
|
+
while (end > 0 && text.charCodeAt(end - 1) === 32) end--;
|
|
1509
|
+
return text.slice(0, end);
|
|
1510
|
+
}
|
|
1511
|
+
function compareSortValues(left, right) {
|
|
1512
|
+
if (left.type !== right.type) return left.type < right.type ? -1 : 1;
|
|
1513
|
+
if (left.type === TYPE_NULL) return 0;
|
|
1514
|
+
if (left.type === TYPE_NUMBER) return compareNumeric(left.numeric, right.numeric);
|
|
1515
|
+
return compareBytes(left.bytes, right.bytes);
|
|
1516
|
+
}
|
|
1517
|
+
function compareNumeric(left, right) {
|
|
1518
|
+
if (typeof left === "number" && typeof right === "number") {
|
|
1519
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
1520
|
+
}
|
|
1521
|
+
if (typeof left === "bigint" && typeof right === "bigint") {
|
|
1522
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
1523
|
+
}
|
|
1524
|
+
const big = typeof left === "bigint" ? left : right;
|
|
1525
|
+
const real = typeof left === "bigint" ? right : left;
|
|
1526
|
+
const sign = typeof left === "bigint" ? 1 : -1;
|
|
1527
|
+
if (!Number.isFinite(real)) return real > 0 ? -sign : sign;
|
|
1528
|
+
const whole = Math.floor(real);
|
|
1529
|
+
const wholeAsBig = BigInt(whole);
|
|
1530
|
+
if (big !== wholeAsBig) return big < wholeAsBig ? -sign : sign;
|
|
1531
|
+
return real > whole ? -sign : 0;
|
|
1532
|
+
}
|
|
1533
|
+
function compareBytes(left, right) {
|
|
1534
|
+
const shared = Math.min(left.length, right.length);
|
|
1535
|
+
for (let i = 0; i < shared; i++) {
|
|
1536
|
+
if (left[i] !== right[i]) return left[i] < right[i] ? -1 : 1;
|
|
1537
|
+
}
|
|
1538
|
+
return left.length === right.length ? 0 : left.length < right.length ? -1 : 1;
|
|
1539
|
+
}
|
|
1540
|
+
function buildSortKeyPlan(terms) {
|
|
1541
|
+
const collations = terms.map((term) => resolveCollation(term.collation));
|
|
1542
|
+
const descending = terms.map((term) => term.direction === "desc");
|
|
1543
|
+
const nullsLast = terms.map((term) => term.nulls === "last");
|
|
1544
|
+
return {
|
|
1545
|
+
collations,
|
|
1546
|
+
compare(left, right) {
|
|
1547
|
+
for (let i = 0; i < terms.length; i++) {
|
|
1548
|
+
const a = left[i];
|
|
1549
|
+
const b = right[i];
|
|
1550
|
+
const aNull = a.type === TYPE_NULL;
|
|
1551
|
+
const bNull = b.type === TYPE_NULL;
|
|
1552
|
+
if (aNull !== bNull) return (aNull ? 1 : -1) * (nullsLast[i] ? 1 : -1);
|
|
1553
|
+
if (aNull) continue;
|
|
1554
|
+
const order = compareSortValues(a, b);
|
|
1555
|
+
if (order !== 0) return descending[i] ? -order : order;
|
|
1556
|
+
}
|
|
1557
|
+
return 0;
|
|
1558
|
+
}
|
|
1559
|
+
};
|
|
1560
|
+
}
|
|
1561
|
+
function lowerBoundIndex(rows, key, plan) {
|
|
1562
|
+
let low = 0;
|
|
1563
|
+
let high = rows.length;
|
|
1564
|
+
while (low < high) {
|
|
1565
|
+
const middle = low + high >>> 1;
|
|
1566
|
+
if (plan.compare(rows[middle].sort, key) < 0) low = middle + 1;
|
|
1567
|
+
else high = middle;
|
|
1568
|
+
}
|
|
1569
|
+
return low;
|
|
1570
|
+
}
|
|
1571
|
+
function placementIndex(rows, key, plan) {
|
|
1572
|
+
let low = 0;
|
|
1573
|
+
let high = rows.length;
|
|
1574
|
+
while (low < high) {
|
|
1575
|
+
const middle = low + high >>> 1;
|
|
1576
|
+
if (plan.compare(rows[middle].sort, key) <= 0) low = middle + 1;
|
|
1577
|
+
else high = middle;
|
|
1578
|
+
}
|
|
1579
|
+
return low;
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
// src/core/live/live-result.ts
|
|
1583
|
+
var LiveResult = class {
|
|
1584
|
+
constructor(plan) {
|
|
1585
|
+
this.plan = plan;
|
|
1586
|
+
}
|
|
1587
|
+
rows = [];
|
|
1588
|
+
index = /* @__PURE__ */ new Map();
|
|
1589
|
+
windowTruncated = false;
|
|
1590
|
+
get size() {
|
|
1591
|
+
return this.rows.length;
|
|
1592
|
+
}
|
|
1593
|
+
snapshot() {
|
|
1594
|
+
return this.rows.map((held) => held.row);
|
|
1595
|
+
}
|
|
1596
|
+
reset(rows) {
|
|
1597
|
+
this.rows = rows;
|
|
1598
|
+
this.index = new Map(rows.map((held) => [held.key, held]));
|
|
1599
|
+
this.windowTruncated = this.plan.limit !== null && rows.length >= this.plan.limit;
|
|
1600
|
+
}
|
|
1601
|
+
apply(changes) {
|
|
1602
|
+
const draft = new Draft(this.plan, this.rows.slice(), new Map(this.index), this.windowTruncated);
|
|
1603
|
+
for (const change of changes) {
|
|
1604
|
+
if (!draft.apply(change)) return null;
|
|
1605
|
+
}
|
|
1606
|
+
if (!draft.windowIsWhole()) return null;
|
|
1607
|
+
this.rows = draft.rows;
|
|
1608
|
+
this.index = draft.index;
|
|
1609
|
+
this.windowTruncated = draft.truncated;
|
|
1610
|
+
return draft.ops;
|
|
1611
|
+
}
|
|
1612
|
+
};
|
|
1613
|
+
var Draft = class {
|
|
1614
|
+
constructor(plan, rows, index, truncated) {
|
|
1615
|
+
this.plan = plan;
|
|
1616
|
+
this.rows = rows;
|
|
1617
|
+
this.index = index;
|
|
1618
|
+
this.truncated = truncated;
|
|
1619
|
+
}
|
|
1620
|
+
ops = [];
|
|
1621
|
+
apply(change) {
|
|
1622
|
+
const held = this.index.get(change.key);
|
|
1623
|
+
if (change.after === null) {
|
|
1624
|
+
if (change.before === null) return true;
|
|
1625
|
+
return held === void 0 ? this.plan.offset === 0 : this.remove(held);
|
|
1626
|
+
}
|
|
1627
|
+
const entering = { key: change.key, row: change.after.row, sort: change.after.sort };
|
|
1628
|
+
if (held !== void 0) {
|
|
1629
|
+
if (this.plan.sortPlan.compare(held.sort, entering.sort) === 0) return this.replace(held, entering);
|
|
1630
|
+
if (!this.remove(held)) return false;
|
|
1631
|
+
} else if (change.before !== null && this.plan.offset > 0) {
|
|
1632
|
+
return false;
|
|
1633
|
+
}
|
|
1634
|
+
return this.insert(entering);
|
|
1635
|
+
}
|
|
1636
|
+
windowIsWhole() {
|
|
1637
|
+
if (this.plan.limit === null || !this.truncated) return true;
|
|
1638
|
+
return this.rows.length >= this.plan.limit;
|
|
1639
|
+
}
|
|
1640
|
+
replace(held, replacement) {
|
|
1641
|
+
const at = this.locate(held);
|
|
1642
|
+
if (at === -1) return false;
|
|
1643
|
+
this.rows[at] = replacement;
|
|
1644
|
+
this.index.set(replacement.key, replacement);
|
|
1645
|
+
this.ops.push({ op: "update", index: at, row: replacement.row });
|
|
1646
|
+
return true;
|
|
1647
|
+
}
|
|
1648
|
+
remove(held) {
|
|
1649
|
+
if (this.plan.offset > 0) return false;
|
|
1650
|
+
const at = this.locate(held);
|
|
1651
|
+
if (at === -1) return false;
|
|
1652
|
+
this.rows.splice(at, 1);
|
|
1653
|
+
this.index.delete(held.key);
|
|
1654
|
+
this.ops.push({ op: "delete", index: at });
|
|
1655
|
+
return true;
|
|
1656
|
+
}
|
|
1657
|
+
insert(held) {
|
|
1658
|
+
if (this.plan.offset > 0) {
|
|
1659
|
+
if (this.rows.length === 0) return false;
|
|
1660
|
+
if (this.plan.sortPlan.compare(held.sort, this.rows[0].sort) <= 0) return false;
|
|
1661
|
+
}
|
|
1662
|
+
const at = placementIndex(this.rows, held.sort, this.plan.sortPlan);
|
|
1663
|
+
if (this.plan.limit !== null && at >= this.plan.limit) {
|
|
1664
|
+
this.truncated = true;
|
|
1665
|
+
return true;
|
|
1666
|
+
}
|
|
1667
|
+
this.rows.splice(at, 0, held);
|
|
1668
|
+
this.index.set(held.key, held);
|
|
1669
|
+
this.ops.push({ op: "insert", index: at, row: held.row });
|
|
1670
|
+
if (this.plan.limit !== null && this.rows.length > this.plan.limit) {
|
|
1671
|
+
const dropped = this.rows.pop();
|
|
1672
|
+
if (dropped !== void 0) this.index.delete(dropped.key);
|
|
1673
|
+
this.ops.push({ op: "delete", index: this.rows.length });
|
|
1674
|
+
this.truncated = true;
|
|
1675
|
+
}
|
|
1676
|
+
return true;
|
|
1677
|
+
}
|
|
1678
|
+
locate(held) {
|
|
1679
|
+
let at = lowerBoundIndex(this.rows, held.sort, this.plan.sortPlan);
|
|
1680
|
+
while (at < this.rows.length && this.rows[at].key !== held.key) {
|
|
1681
|
+
if (this.plan.sortPlan.compare(this.rows[at].sort, held.sort) !== 0) return -1;
|
|
1682
|
+
at++;
|
|
1683
|
+
}
|
|
1684
|
+
return at < this.rows.length ? at : -1;
|
|
1685
|
+
}
|
|
1686
|
+
};
|
|
1687
|
+
|
|
1688
|
+
// src/core/live/row-keys.ts
|
|
1689
|
+
var HEX = "0123456789abcdef";
|
|
1690
|
+
function rowidKey(value) {
|
|
1691
|
+
return `rowid:${String(value)}`;
|
|
1692
|
+
}
|
|
1693
|
+
function encodeRowKey(values) {
|
|
1694
|
+
let key = "";
|
|
1695
|
+
for (const value of values) {
|
|
1696
|
+
const part = encodePart(value);
|
|
1697
|
+
key += `${part.length}:${part}`;
|
|
1698
|
+
}
|
|
1699
|
+
return key;
|
|
1700
|
+
}
|
|
1701
|
+
function encodePart(value) {
|
|
1702
|
+
if (value === null || value === void 0) return "z";
|
|
1703
|
+
if (typeof value === "bigint") return `n${value.toString()}`;
|
|
1704
|
+
if (typeof value === "number") return `n${Number.isInteger(value) ? value.toFixed(0) : value.toString()}`;
|
|
1705
|
+
if (typeof value === "boolean") return `n${value ? "1" : "0"}`;
|
|
1706
|
+
if (typeof value === "string") return `s${value}`;
|
|
1707
|
+
if (value instanceof Uint8Array) return `b${toHex2(value)}`;
|
|
1708
|
+
if (ArrayBuffer.isView(value)) {
|
|
1709
|
+
const view = value;
|
|
1710
|
+
return `b${toHex2(new Uint8Array(view.buffer, view.byteOffset, view.byteLength))}`;
|
|
1711
|
+
}
|
|
1712
|
+
return `s${String(value)}`;
|
|
1713
|
+
}
|
|
1714
|
+
function toHex2(bytes) {
|
|
1715
|
+
let hex = "";
|
|
1716
|
+
for (const byte of bytes) {
|
|
1717
|
+
hex += HEX[byte >> 4] + HEX[byte & 15];
|
|
1718
|
+
}
|
|
1719
|
+
return hex;
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
// src/core/live/live-query.ts
|
|
1723
|
+
var MaintainedLiveQuery = class _MaintainedLiveQuery {
|
|
1724
|
+
constructor(source) {
|
|
1725
|
+
this.source = source;
|
|
1726
|
+
this.result = new LiveResult(source.plan);
|
|
1727
|
+
}
|
|
1728
|
+
state = { status: "pending" };
|
|
1729
|
+
listeners = /* @__PURE__ */ new Set();
|
|
1730
|
+
result;
|
|
1731
|
+
batches = [];
|
|
1732
|
+
pending = [];
|
|
1733
|
+
pendingBytes = 0;
|
|
1734
|
+
pendingOverflowed = false;
|
|
1735
|
+
stopDelivery = null;
|
|
1736
|
+
appliedThroughSeq = -1n;
|
|
1737
|
+
reading = false;
|
|
1738
|
+
readAgain = false;
|
|
1739
|
+
draining = false;
|
|
1740
|
+
closed = false;
|
|
1741
|
+
static async open(source) {
|
|
1742
|
+
const query2 = new _MaintainedLiveQuery(source);
|
|
1743
|
+
try {
|
|
1744
|
+
await query2.refresh(false);
|
|
1745
|
+
} catch (err) {
|
|
1746
|
+
await query2.close();
|
|
1747
|
+
throw err;
|
|
1748
|
+
}
|
|
1749
|
+
if (query2.state.status === "error") {
|
|
1750
|
+
const failure = query2.state.error;
|
|
1751
|
+
await query2.close();
|
|
1752
|
+
throw failure;
|
|
1753
|
+
}
|
|
1754
|
+
return query2;
|
|
1755
|
+
}
|
|
1756
|
+
getState() {
|
|
1757
|
+
return this.state;
|
|
1758
|
+
}
|
|
1759
|
+
subscribe(listener) {
|
|
1760
|
+
this.listeners.add(listener);
|
|
1761
|
+
return () => {
|
|
1762
|
+
this.listeners.delete(listener);
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
async close() {
|
|
1766
|
+
if (this.closed) return;
|
|
1767
|
+
this.closed = true;
|
|
1768
|
+
this.stopDelivery?.();
|
|
1769
|
+
this.stopDelivery = null;
|
|
1770
|
+
this.listeners.clear();
|
|
1771
|
+
this.batches.length = 0;
|
|
1772
|
+
this.pending = [];
|
|
1773
|
+
await this.source.release();
|
|
1774
|
+
}
|
|
1775
|
+
receive(event) {
|
|
1776
|
+
if (this.closed) return;
|
|
1777
|
+
if (event.seq <= this.appliedThroughSeq) return;
|
|
1778
|
+
if (!this.pendingOverflowed) {
|
|
1779
|
+
this.pending.push(event);
|
|
1780
|
+
this.pendingBytes += approximateBytes(event);
|
|
1781
|
+
if (this.pending.length > this.source.maxTransactionChanges || this.pendingBytes > this.source.maxTransactionBytes) {
|
|
1782
|
+
this.pendingOverflowed = true;
|
|
1783
|
+
this.pending = [];
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
if (event.txEnd !== true) return;
|
|
1787
|
+
const overflowed = this.pendingOverflowed;
|
|
1788
|
+
const batch = this.pending;
|
|
1789
|
+
this.pending = [];
|
|
1790
|
+
this.pendingBytes = 0;
|
|
1791
|
+
this.pendingOverflowed = false;
|
|
1792
|
+
this.batches.push(overflowed ? [] : batch);
|
|
1793
|
+
void this.drain();
|
|
1794
|
+
}
|
|
1795
|
+
deliveryLost() {
|
|
1796
|
+
if (this.closed) return;
|
|
1797
|
+
this.stopDelivery = null;
|
|
1798
|
+
this.batches.length = 0;
|
|
1799
|
+
this.pending = [];
|
|
1800
|
+
this.pendingBytes = 0;
|
|
1801
|
+
this.pendingOverflowed = false;
|
|
1802
|
+
void this.reread();
|
|
1803
|
+
}
|
|
1804
|
+
async drain() {
|
|
1805
|
+
if (this.draining || this.reading || this.closed) return;
|
|
1806
|
+
this.draining = true;
|
|
1807
|
+
try {
|
|
1808
|
+
while (this.batches.length > 0 && !this.closed && !this.reading) {
|
|
1809
|
+
const batch = this.batches.shift();
|
|
1810
|
+
if (batch === void 0) break;
|
|
1811
|
+
await this.applyBatch(batch);
|
|
1812
|
+
}
|
|
1813
|
+
} finally {
|
|
1814
|
+
this.draining = false;
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
async applyBatch(batch) {
|
|
1818
|
+
if (batch.length === 0) {
|
|
1819
|
+
await this.reread();
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
const fresh = batch.filter((event) => event.seq > this.appliedThroughSeq);
|
|
1823
|
+
if (fresh.length === 0) return;
|
|
1824
|
+
let changes;
|
|
1825
|
+
try {
|
|
1826
|
+
changes = await this.evaluate(fresh);
|
|
1827
|
+
} catch (err) {
|
|
1828
|
+
this.fail(err);
|
|
1829
|
+
return;
|
|
1830
|
+
}
|
|
1831
|
+
if (this.closed) return;
|
|
1832
|
+
const highest = fresh[fresh.length - 1].seq;
|
|
1833
|
+
if (changes.length === 0) {
|
|
1834
|
+
this.appliedThroughSeq = highest;
|
|
1835
|
+
return;
|
|
1836
|
+
}
|
|
1837
|
+
const ops = changes.length > this.result.size ? null : this.result.apply(changes);
|
|
1838
|
+
if (ops === null) {
|
|
1839
|
+
await this.reread();
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
this.appliedThroughSeq = highest;
|
|
1843
|
+
this.publish({ status: "ready", rows: this.result.snapshot(), revalidating: false }, { kind: "ops", ops });
|
|
1844
|
+
}
|
|
1845
|
+
async evaluate(events) {
|
|
1846
|
+
const candidates = [];
|
|
1847
|
+
for (let i = 0; i < events.length; i++) {
|
|
1848
|
+
const event = events[i];
|
|
1849
|
+
if (event.type !== "insert" && event.oldRow !== void 0) {
|
|
1850
|
+
candidates.push({ slot: i * 2, payload: event.oldRow });
|
|
1851
|
+
}
|
|
1852
|
+
if (event.type !== "delete") {
|
|
1853
|
+
candidates.push({ slot: i * 2 + 1, payload: event.row });
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
const matches = await this.source.probe.evaluate(candidates);
|
|
1857
|
+
const changes = [];
|
|
1858
|
+
for (let i = 0; i < events.length; i++) {
|
|
1859
|
+
const event = events[i];
|
|
1860
|
+
const before = matches.get(i * 2) ?? null;
|
|
1861
|
+
const after = matches.get(i * 2 + 1) ?? null;
|
|
1862
|
+
if (before === null && after === null) continue;
|
|
1863
|
+
const beforeKey = event.oldRow === void 0 ? null : this.keyOf(event, event.oldRow);
|
|
1864
|
+
const afterKey = event.type === "delete" ? null : this.keyOf(event, event.row);
|
|
1865
|
+
if (before !== null && after !== null && beforeKey !== null && afterKey !== null && beforeKey !== afterKey) {
|
|
1866
|
+
changes.push({ key: beforeKey, before, after: null });
|
|
1867
|
+
changes.push({ key: afterKey, before: null, after });
|
|
1868
|
+
continue;
|
|
1869
|
+
}
|
|
1870
|
+
const key = after !== null ? afterKey ?? beforeKey : beforeKey;
|
|
1871
|
+
if (key === null) continue;
|
|
1872
|
+
changes.push({ key, before, after });
|
|
1873
|
+
}
|
|
1874
|
+
return changes;
|
|
1875
|
+
}
|
|
1876
|
+
keyOf(event, payload) {
|
|
1877
|
+
if (this.source.plan.usesRowid) return event.rowId === void 0 ? null : rowidKey(event.rowId);
|
|
1878
|
+
return encodeRowKey(this.source.plan.keyColumns.map((column) => payload[column]));
|
|
1879
|
+
}
|
|
1880
|
+
async reread() {
|
|
1881
|
+
if (this.reading) {
|
|
1882
|
+
this.readAgain = true;
|
|
1883
|
+
return;
|
|
1884
|
+
}
|
|
1885
|
+
if (this.state.status === "ready" && !this.state.revalidating) {
|
|
1886
|
+
this.publish({ status: "ready", rows: this.state.rows, revalidating: true }, { kind: "revalidating" });
|
|
1887
|
+
}
|
|
1888
|
+
await this.refresh(true);
|
|
1889
|
+
}
|
|
1890
|
+
async refresh(spread) {
|
|
1891
|
+
this.reading = true;
|
|
1892
|
+
try {
|
|
1893
|
+
do {
|
|
1894
|
+
this.readAgain = false;
|
|
1895
|
+
if (spread && this.source.rereadJitterMs > 0) {
|
|
1896
|
+
await this.source.wait(Math.floor(Math.random() * this.source.rereadJitterMs));
|
|
1897
|
+
}
|
|
1898
|
+
if (this.closed) return;
|
|
1899
|
+
const refreshed = await this.source.read();
|
|
1900
|
+
if (this.closed) return;
|
|
1901
|
+
this.result.reset(refreshed.rows);
|
|
1902
|
+
this.appliedThroughSeq = refreshed.seq;
|
|
1903
|
+
if (this.stopDelivery === null) {
|
|
1904
|
+
const stop = await this.source.start(
|
|
1905
|
+
{ onEvent: (event) => this.receive(event), onLost: () => this.deliveryLost() },
|
|
1906
|
+
refreshed.seq
|
|
1907
|
+
);
|
|
1908
|
+
if (this.closed) {
|
|
1909
|
+
stop();
|
|
1910
|
+
return;
|
|
1911
|
+
}
|
|
1912
|
+
this.stopDelivery = stop;
|
|
1913
|
+
}
|
|
1914
|
+
} while (this.readAgain && !this.closed);
|
|
1915
|
+
this.publish({ status: "ready", rows: this.result.snapshot(), revalidating: false }, { kind: "rows" });
|
|
1916
|
+
} catch (err) {
|
|
1917
|
+
this.fail(err);
|
|
1918
|
+
} finally {
|
|
1919
|
+
this.reading = false;
|
|
1920
|
+
}
|
|
1921
|
+
if (!this.closed && this.batches.length > 0) void this.drain();
|
|
1922
|
+
}
|
|
1923
|
+
fail(err) {
|
|
1924
|
+
if (this.closed) return;
|
|
1925
|
+
this.publish({ status: "error", error: err instanceof Error ? err : new Error(String(err)) }, { kind: "error" });
|
|
1926
|
+
}
|
|
1927
|
+
publish(next, update) {
|
|
1928
|
+
this.state = next;
|
|
1929
|
+
for (const listener of [...this.listeners]) {
|
|
1930
|
+
try {
|
|
1931
|
+
listener(update);
|
|
1932
|
+
} catch {
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
};
|
|
1937
|
+
function approximateBytes(event) {
|
|
1938
|
+
return approximateRowBytes(event.row) + (event.oldRow === void 0 ? 0 : approximateRowBytes(event.oldRow));
|
|
1939
|
+
}
|
|
1940
|
+
function approximateRowBytes(row) {
|
|
1941
|
+
let bytes = 0;
|
|
1942
|
+
for (const key of Object.keys(row)) {
|
|
1943
|
+
const value = row[key];
|
|
1944
|
+
bytes += key.length + 8;
|
|
1945
|
+
if (typeof value === "string") bytes += value.length;
|
|
1946
|
+
else if (value instanceof Uint8Array) bytes += value.byteLength;
|
|
1947
|
+
}
|
|
1948
|
+
return bytes;
|
|
1949
|
+
}
|
|
1950
|
+
|
|
1951
|
+
// src/core/live/primed-delivery.ts
|
|
1952
|
+
var PrimedLiveDelivery = class {
|
|
1953
|
+
constructor(cdc, conn, run, table) {
|
|
1954
|
+
this.cdc = cdc;
|
|
1955
|
+
this.conn = conn;
|
|
1956
|
+
this.run = run;
|
|
1957
|
+
this.table = table;
|
|
1958
|
+
}
|
|
1959
|
+
async start(handlers, sinceSeq) {
|
|
1960
|
+
const tracker = this.cdc.changeTracker;
|
|
1961
|
+
const manager = this.cdc.subscriptions;
|
|
1962
|
+
if (tracker === null || manager === null) {
|
|
1963
|
+
throw new CDCError(`Cannot deliver changes for '${this.table}': the table is not watched`);
|
|
1964
|
+
}
|
|
1965
|
+
const grouper = new TransactionGrouper((event) => {
|
|
1966
|
+
handlers.onEvent(event);
|
|
1967
|
+
return true;
|
|
1968
|
+
});
|
|
1969
|
+
let stopped = false;
|
|
1970
|
+
let removeBatchEnd = () => {
|
|
1971
|
+
};
|
|
1972
|
+
let subscription = null;
|
|
1973
|
+
const stop = () => {
|
|
1974
|
+
if (stopped) return;
|
|
1975
|
+
stopped = true;
|
|
1976
|
+
removeBatchEnd();
|
|
1977
|
+
subscription?.unsubscribe();
|
|
1978
|
+
};
|
|
1979
|
+
const primed = new PrimedSubscription(
|
|
1980
|
+
sinceSeq,
|
|
1981
|
+
(event) => {
|
|
1982
|
+
grouper.receive(event);
|
|
1983
|
+
return true;
|
|
1984
|
+
},
|
|
1985
|
+
() => {
|
|
1986
|
+
stop();
|
|
1987
|
+
handlers.onLost();
|
|
1988
|
+
}
|
|
1989
|
+
);
|
|
1990
|
+
const boundary = tracker.cursor;
|
|
1991
|
+
const boundaryEndsTransaction = tracker.pollEndedAtTxBoundary;
|
|
1992
|
+
subscription = manager.subscribe(this.table, void 0, (event) => primed.onLiveEvent(event));
|
|
1993
|
+
try {
|
|
1994
|
+
await this.run(() => primed.replayTables(tracker, this.conn, [this.table], void 0, boundary));
|
|
1995
|
+
} catch (err) {
|
|
1996
|
+
stop();
|
|
1997
|
+
throw err;
|
|
1998
|
+
}
|
|
1999
|
+
if (stopped) return stop;
|
|
2000
|
+
grouper.flush(boundaryEndsTransaction);
|
|
2001
|
+
primed.goLive();
|
|
2002
|
+
removeBatchEnd = manager.addBatchEndListener((atTxBoundary) => {
|
|
2003
|
+
grouper.flush(atTxBoundary);
|
|
2004
|
+
});
|
|
2005
|
+
return stop;
|
|
2006
|
+
}
|
|
2007
|
+
};
|
|
2008
|
+
|
|
2009
|
+
// src/core/system-catalog/live-probe-table.ts
|
|
2010
|
+
var SAFE_DECLARED_TYPE = /^[A-Za-z0-9_ ()',.+-]*$/;
|
|
2011
|
+
async function ensureLiveProbeTable(conn, probeTable, columns) {
|
|
2012
|
+
assertSafeIdentifier(probeTable);
|
|
2013
|
+
if (columns.length === 0) {
|
|
2014
|
+
throw new SirannonError(`Cannot stage rows for '${probeTable}': the table declares no columns`, "CDC_ERROR");
|
|
2015
|
+
}
|
|
2016
|
+
const definitions = columns.map((column) => {
|
|
2017
|
+
assertSafeIdentifier(column.name);
|
|
2018
|
+
if (!SAFE_DECLARED_TYPE.test(column.type)) {
|
|
2019
|
+
throw new SirannonError(`Unsafe column type for internal schema: ${column.type}`, "INTERNAL_SCHEMA_ERROR");
|
|
2020
|
+
}
|
|
2021
|
+
if (column.collation !== null) assertSafeIdentifier(column.collation);
|
|
2022
|
+
const type = column.type.trim().length === 0 ? "" : ` ${column.type.trim()}`;
|
|
2023
|
+
const collate = column.collation === null ? "" : ` COLLATE "${column.collation}"`;
|
|
2024
|
+
return `"${column.name}"${type}${collate}`;
|
|
2025
|
+
});
|
|
2026
|
+
await conn.exec(`CREATE TEMP TABLE IF NOT EXISTS "${probeTable}" (${definitions.join(", ")})`);
|
|
2027
|
+
}
|
|
2028
|
+
async function dropLiveProbeTable(conn, probeTable) {
|
|
2029
|
+
assertSafeIdentifier(probeTable);
|
|
2030
|
+
await conn.exec(`DROP TABLE IF EXISTS temp."${probeTable}"`);
|
|
2031
|
+
}
|
|
2032
|
+
async function prepareInsertLiveProbeRow(conn, probeTable, columns) {
|
|
2033
|
+
assertSafeIdentifier(probeTable);
|
|
2034
|
+
const names = columns.map((column) => {
|
|
2035
|
+
assertSafeIdentifier(column.name);
|
|
2036
|
+
return `"${column.name}"`;
|
|
2037
|
+
});
|
|
2038
|
+
const placeholders = new Array(columns.length + 1).fill("?").join(", ");
|
|
2039
|
+
return conn.prepare(`INSERT INTO temp."${probeTable}" (rowid, ${names.join(", ")}) VALUES (${placeholders})`);
|
|
2040
|
+
}
|
|
2041
|
+
async function prepareDeleteLiveProbeRows(conn, probeTable) {
|
|
2042
|
+
assertSafeIdentifier(probeTable);
|
|
2043
|
+
return conn.prepare(`DELETE FROM temp."${probeTable}"`);
|
|
2044
|
+
}
|
|
2045
|
+
function selectLiveProbeMatchesSql(probeTable, keyColumn, projection, where, sourceName) {
|
|
2046
|
+
assertSafeIdentifier(probeTable);
|
|
2047
|
+
assertSafeIdentifier(keyColumn);
|
|
2048
|
+
const filter = where === null ? "" : `
|
|
2049
|
+
WHERE (${where}
|
|
2050
|
+
)`;
|
|
2051
|
+
return `SELECT rowid AS "${keyColumn}"
|
|
2052
|
+
, ${projection}
|
|
2053
|
+
FROM temp."${probeTable}" AS "${sourceName}"${filter}`;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
// src/core/live/query-plan.ts
|
|
2057
|
+
var KEY_COLUMN = "_sirannon_k";
|
|
2058
|
+
var SORT_COLUMN_PREFIX = "_sirannon_s";
|
|
2059
|
+
var IDENTIFIER_RE2 = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
2060
|
+
function buildLivePlan(shape, table, params) {
|
|
2061
|
+
const sourceName = shape.alias ?? shape.table;
|
|
2062
|
+
assertIdentifier(shape, sourceName, "the table or its alias");
|
|
2063
|
+
for (const column of table.columns) {
|
|
2064
|
+
if (column.name.toLowerCase().startsWith(INTERNAL_TABLE_PREFIX)) {
|
|
2065
|
+
throw new CDCError(
|
|
2066
|
+
`Cannot open a live query for '${shape.sql}': column '${column.name}' uses the '${INTERNAL_TABLE_PREFIX}' prefix Sirannon reserves`
|
|
2067
|
+
);
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
const usesRowid = table.pkColumns.length === 0;
|
|
2071
|
+
const keyColumns = usesRowid ? ["rowid"] : table.pkColumns;
|
|
2072
|
+
const keyColumnAliases = keyColumns.map((_, index) => `${KEY_COLUMN}${index}`);
|
|
2073
|
+
const sortColumns = shape.sortTerms.map((_, index) => `${SORT_COLUMN_PREFIX}${index}`);
|
|
2074
|
+
const sortExpressions = shape.sortTerms.map((term) => resolveSortExpression(shape, term));
|
|
2075
|
+
const collations = shape.sortTerms.map(
|
|
2076
|
+
(term, index) => effectiveCollation(term, sortExpressions[index], sourceName, table.collations)
|
|
2077
|
+
);
|
|
2078
|
+
const sortPlan = buildSortKeyPlan(shape.sortTerms.map((term, index) => ({ ...term, collation: collations[index] })));
|
|
2079
|
+
const projection = [
|
|
2080
|
+
shape.selectList,
|
|
2081
|
+
...sortExpressions.map((expression, index) => `${expression} AS "${sortColumns[index]}"`)
|
|
2082
|
+
].join("\n, ");
|
|
2083
|
+
const keyProjection = keyColumns.map((column, index) => `"${sourceName}"."${column}" AS "${keyColumnAliases[index]}"`).join(", ");
|
|
2084
|
+
const from = shape.alias === null ? `"${shape.table}"` : `"${shape.table}" AS "${shape.alias}"`;
|
|
2085
|
+
const where = shape.where === null ? "" : `
|
|
2086
|
+
WHERE (${shape.where}
|
|
2087
|
+
)`;
|
|
2088
|
+
const tail = shape.tail.length === 0 ? "" : `
|
|
2089
|
+
${shape.tail}`;
|
|
2090
|
+
return {
|
|
2091
|
+
shape,
|
|
2092
|
+
sourceName,
|
|
2093
|
+
probeColumns: table.columns.map((column) => ({
|
|
2094
|
+
name: column.name,
|
|
2095
|
+
type: column.type,
|
|
2096
|
+
collation: table.collations.get(column.name) ?? null
|
|
2097
|
+
})),
|
|
2098
|
+
keyColumns,
|
|
2099
|
+
usesRowid,
|
|
2100
|
+
sortPlan,
|
|
2101
|
+
sortColumns,
|
|
2102
|
+
keyColumnAliases,
|
|
2103
|
+
readSql: `SELECT ${projection}
|
|
2104
|
+
, ${keyProjection}
|
|
2105
|
+
FROM ${from}${where}${tail}`,
|
|
2106
|
+
probeSql: (probeTable) => selectLiveProbeMatchesSql(probeTable, KEY_COLUMN, projection, shape.where, sourceName),
|
|
2107
|
+
probeParams: (bound) => sliceProbeParams(shape, bound),
|
|
2108
|
+
limit: shape.limit === null ? null : resolveBound(shape, shape.limit, params, "LIMIT"),
|
|
2109
|
+
offset: shape.offset === null ? 0 : resolveBound(shape, shape.offset, params, "OFFSET")
|
|
2110
|
+
};
|
|
2111
|
+
}
|
|
2112
|
+
function sliceProbeParams(shape, params) {
|
|
2113
|
+
if (params === void 0) return [];
|
|
2114
|
+
if (!Array.isArray(params)) return [params];
|
|
2115
|
+
return params.slice(0, shape.selectParameters + shape.whereParameters);
|
|
2116
|
+
}
|
|
2117
|
+
function probeNamedParams(probeSql, params) {
|
|
2118
|
+
if (params === void 0 || Array.isArray(params)) return params;
|
|
2119
|
+
const used = /* @__PURE__ */ new Set();
|
|
2120
|
+
for (const token of tokenizeSql(probeSql)) {
|
|
2121
|
+
const name = parameterName(token);
|
|
2122
|
+
if (name !== null) used.add(name);
|
|
2123
|
+
}
|
|
2124
|
+
const picked = {};
|
|
2125
|
+
for (const [key, value] of Object.entries(params)) {
|
|
2126
|
+
if (used.has(key)) picked[key] = value;
|
|
2127
|
+
}
|
|
2128
|
+
return picked;
|
|
2129
|
+
}
|
|
2130
|
+
function resolveSortExpression(shape, term) {
|
|
2131
|
+
const tokens = tokenizeSql(term.expression);
|
|
2132
|
+
if (tokens.length === 1 && tokens[0].kind === "number") {
|
|
2133
|
+
return resolveOrdinal(shape, Number(tokens[0].value));
|
|
2134
|
+
}
|
|
2135
|
+
if (tokens.length === 1 && tokens[0].kind === "word") {
|
|
2136
|
+
const item = shape.selectItems.find((candidate) => candidate.alias === tokens[0].value);
|
|
2137
|
+
if (item !== void 0) return item.expression;
|
|
2138
|
+
}
|
|
2139
|
+
return term.expression;
|
|
2140
|
+
}
|
|
2141
|
+
function resolveOrdinal(shape, ordinal) {
|
|
2142
|
+
if (shape.selectItems.some((item2) => item2.star)) {
|
|
2143
|
+
throw new CDCError(
|
|
2144
|
+
`Cannot open a live query for '${shape.sql}': ORDER BY ${ordinal} cannot be resolved through '*', so name the column instead`
|
|
2145
|
+
);
|
|
2146
|
+
}
|
|
2147
|
+
const item = shape.selectItems[ordinal - 1];
|
|
2148
|
+
if (item === void 0) {
|
|
2149
|
+
throw new CDCError(
|
|
2150
|
+
`Cannot open a live query for '${shape.sql}': ORDER BY ${ordinal} names no column of the select list`
|
|
2151
|
+
);
|
|
2152
|
+
}
|
|
2153
|
+
return item.expression;
|
|
2154
|
+
}
|
|
2155
|
+
function effectiveCollation(term, expression, sourceName, collations) {
|
|
2156
|
+
if (term.collation !== null) return term.collation;
|
|
2157
|
+
const column = bareColumnReference(expression, sourceName);
|
|
2158
|
+
return column === null ? null : collations.get(column) ?? null;
|
|
2159
|
+
}
|
|
2160
|
+
function bareColumnReference(expression, sourceName) {
|
|
2161
|
+
const tokens = tokenizeSql(expression);
|
|
2162
|
+
if (tokens.length === 1 && tokens[0].kind === "word") return tokens[0].value;
|
|
2163
|
+
if (tokens.length === 3 && tokens[0].kind === "word" && tokens[1].value === "." && tokens[2].kind === "word") {
|
|
2164
|
+
return tokens[0].value === sourceName ? tokens[2].value : null;
|
|
2165
|
+
}
|
|
2166
|
+
return null;
|
|
2167
|
+
}
|
|
2168
|
+
function resolveBound(shape, bound, params, label) {
|
|
2169
|
+
if (bound.kind === "literal") return bound.literal;
|
|
2170
|
+
const value = boundParameterValue(bound, params, shape);
|
|
2171
|
+
if (typeof value === "bigint") return assertWholeBound(shape, Number(value), label);
|
|
2172
|
+
if (typeof value === "number") return assertWholeBound(shape, value, label);
|
|
2173
|
+
throw new CDCError(`Cannot open a live query for '${shape.sql}': ${label} is bound to a value that is not a number`);
|
|
2174
|
+
}
|
|
2175
|
+
function boundParameterValue(bound, params, shape) {
|
|
2176
|
+
if (params === void 0) {
|
|
2177
|
+
throw new CDCError(`Cannot open a live query for '${shape.sql}': ${bound.parameter} has no bound value`);
|
|
2178
|
+
}
|
|
2179
|
+
if (!Array.isArray(params)) return params[bound.parameter.slice(1)];
|
|
2180
|
+
const numbered = bound.parameter.length > 1 ? Number(bound.parameter.slice(1)) : 0;
|
|
2181
|
+
const ordinal = numbered > 0 ? numbered : placeholderOrdinal(shape.sql, bound.position);
|
|
2182
|
+
return params[ordinal - 1];
|
|
2183
|
+
}
|
|
2184
|
+
function placeholderOrdinal(sql, position) {
|
|
2185
|
+
let ordinal = 0;
|
|
2186
|
+
for (const token of tokenizeSql(sql)) {
|
|
2187
|
+
if (token.kind !== "param") continue;
|
|
2188
|
+
ordinal++;
|
|
2189
|
+
if (token.start === position) return ordinal;
|
|
2190
|
+
}
|
|
2191
|
+
return ordinal;
|
|
2192
|
+
}
|
|
2193
|
+
function assertWholeBound(shape, value, label) {
|
|
2194
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
2195
|
+
throw new CDCError(
|
|
2196
|
+
`Cannot open a live query for '${shape.sql}': ${label} is bound to ${value}, and it takes a non-negative whole number`
|
|
2197
|
+
);
|
|
2198
|
+
}
|
|
2199
|
+
return value;
|
|
2200
|
+
}
|
|
2201
|
+
function assertIdentifier(shape, name, label) {
|
|
2202
|
+
if (!IDENTIFIER_RE2.test(name)) {
|
|
2203
|
+
throw new CDCError(
|
|
2204
|
+
`Cannot open a live query for '${shape.sql}': ${label} must contain only letters, digits, and underscores, and start with a letter or underscore`
|
|
2205
|
+
);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
// src/core/live/row-probe.ts
|
|
2210
|
+
var RowProbe = class _RowProbe {
|
|
2211
|
+
constructor(conn, probeTable, plan, run, insert, clear, match, matchParams) {
|
|
2212
|
+
this.conn = conn;
|
|
2213
|
+
this.probeTable = probeTable;
|
|
2214
|
+
this.plan = plan;
|
|
2215
|
+
this.run = run;
|
|
2216
|
+
this.insert = insert;
|
|
2217
|
+
this.clear = clear;
|
|
2218
|
+
this.match = match;
|
|
2219
|
+
this.matchParams = matchParams;
|
|
2220
|
+
}
|
|
2221
|
+
closed = false;
|
|
2222
|
+
static async open(conn, probeTable, plan, params, run) {
|
|
2223
|
+
const sql = plan.probeSql(probeTable);
|
|
2224
|
+
try {
|
|
2225
|
+
return await run(async () => {
|
|
2226
|
+
await ensureLiveProbeTable(conn, probeTable, plan.probeColumns);
|
|
2227
|
+
const insert = await prepareInsertLiveProbeRow(conn, probeTable, plan.probeColumns);
|
|
2228
|
+
const clear = await prepareDeleteLiveProbeRows(conn, probeTable);
|
|
2229
|
+
const match = await conn.prepare(sql);
|
|
2230
|
+
const named = probeNamedParams(sql, params);
|
|
2231
|
+
const matchParams = named === void 0 || Array.isArray(named) ? plan.probeParams(params) : [named];
|
|
2232
|
+
return new _RowProbe(conn, probeTable, plan, run, insert, clear, match, matchParams);
|
|
2233
|
+
});
|
|
2234
|
+
} catch (err) {
|
|
2235
|
+
await run(() => dropLiveProbeTable(conn, probeTable)).catch(() => {
|
|
2236
|
+
});
|
|
2237
|
+
throw err;
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
async evaluate(candidates) {
|
|
2241
|
+
const matches = /* @__PURE__ */ new Map();
|
|
2242
|
+
if (candidates.length === 0 || this.closed) return matches;
|
|
2243
|
+
await this.run(async () => {
|
|
2244
|
+
try {
|
|
2245
|
+
for (const candidate of candidates) {
|
|
2246
|
+
const values = this.plan.probeColumns.map((column) => candidate.payload[column.name] ?? null);
|
|
2247
|
+
await this.insert.run(candidate.slot, ...values);
|
|
2248
|
+
}
|
|
2249
|
+
const rows = await this.match.all(...this.matchParams);
|
|
2250
|
+
for (const row of rows) {
|
|
2251
|
+
matches.set(Number(row[KEY_COLUMN]), this.take(row));
|
|
2252
|
+
}
|
|
2253
|
+
} finally {
|
|
2254
|
+
await this.clear.run();
|
|
2255
|
+
}
|
|
2256
|
+
});
|
|
2257
|
+
return matches;
|
|
2258
|
+
}
|
|
2259
|
+
take(row) {
|
|
2260
|
+
const sort = this.plan.sortColumns.map(
|
|
2261
|
+
(column, index) => toSortValue(row[column], this.plan.sortPlan.collations[index])
|
|
2262
|
+
);
|
|
2263
|
+
delete row[KEY_COLUMN];
|
|
2264
|
+
for (const column of this.plan.sortColumns) delete row[column];
|
|
2265
|
+
return { row, sort };
|
|
2266
|
+
}
|
|
2267
|
+
async close() {
|
|
2268
|
+
if (this.closed) return;
|
|
2269
|
+
this.closed = true;
|
|
2270
|
+
try {
|
|
2271
|
+
await this.run(() => dropLiveProbeTable(this.conn, this.probeTable));
|
|
2272
|
+
} catch {
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
};
|
|
2276
|
+
|
|
2277
|
+
// src/core/live/statement-shape.ts
|
|
2278
|
+
var UNSUPPORTED_WORDS = /* @__PURE__ */ new Map([
|
|
2279
|
+
["union", "a compound SELECT"],
|
|
2280
|
+
["intersect", "a compound SELECT"],
|
|
2281
|
+
["except", "a compound SELECT"],
|
|
2282
|
+
["group", "GROUP BY"],
|
|
2283
|
+
["having", "HAVING"],
|
|
2284
|
+
["window", "a window clause"],
|
|
2285
|
+
["join", "a join"],
|
|
2286
|
+
["natural", "a join"],
|
|
2287
|
+
["cross", "a join"],
|
|
2288
|
+
["inner", "a join"],
|
|
2289
|
+
["outer", "a join"],
|
|
2290
|
+
["left", "a join"],
|
|
2291
|
+
["right", "a join"],
|
|
2292
|
+
["full", "a join"],
|
|
2293
|
+
["with", "a common table expression"],
|
|
2294
|
+
["values", "a VALUES clause"],
|
|
2295
|
+
["distinct", "DISTINCT"]
|
|
2296
|
+
]);
|
|
2297
|
+
var CLOCK_KEYWORDS = /* @__PURE__ */ new Set(["current_date", "current_time", "current_timestamp"]);
|
|
2298
|
+
var VOLATILE_FUNCTIONS = /* @__PURE__ */ new Set(["random", "randomblob", "changes", "last_insert_rowid", "total_changes"]);
|
|
2299
|
+
var CLOCK_FUNCTIONS = /* @__PURE__ */ new Set(["date", "time", "datetime", "julianday", "unixepoch", "strftime", "timediff"]);
|
|
2300
|
+
var CLOCK_MODIFIER = /\b(now|localtime)\b/i;
|
|
2301
|
+
var AGGREGATE_FUNCTIONS = /* @__PURE__ */ new Set([
|
|
2302
|
+
"avg",
|
|
2303
|
+
"count",
|
|
2304
|
+
"group_concat",
|
|
2305
|
+
"json_group_array",
|
|
2306
|
+
"json_group_object",
|
|
2307
|
+
"max",
|
|
2308
|
+
"min",
|
|
2309
|
+
"string_agg",
|
|
2310
|
+
"sum",
|
|
2311
|
+
"total"
|
|
2312
|
+
]);
|
|
2313
|
+
function analyseStatement(sql) {
|
|
2314
|
+
assertSqlAllowed(sql);
|
|
2315
|
+
const tokens = tokenizeSql(sql);
|
|
2316
|
+
if (tokens.length === 0 || tokens[0].lower !== "select" || tokens[0].quoted) {
|
|
2317
|
+
throw unsupported(sql, "a live query reads with a single SELECT statement");
|
|
2318
|
+
}
|
|
2319
|
+
const body = withoutTrailingSemicolon(tokens);
|
|
2320
|
+
assertSupportedWords(sql, body);
|
|
2321
|
+
assertDeterministic(sql, body);
|
|
2322
|
+
const clauses = findClauses(sql, body);
|
|
2323
|
+
const source = readSource(sql, body, clauses);
|
|
2324
|
+
const sortTerms = readSortTerms(sql, body, clauses);
|
|
2325
|
+
const window = readWindow(sql, body, clauses);
|
|
2326
|
+
if (window.limit !== null && sortTerms.length === 0) {
|
|
2327
|
+
throw unsupported(sql, "a live query with LIMIT orders its rows, otherwise the window has no defined membership");
|
|
2328
|
+
}
|
|
2329
|
+
const usesPositionalParameters = body.some((token) => token.kind === "param" && token.value.startsWith("?"));
|
|
2330
|
+
assertSortTermsBindByName(sql, sortTerms, usesPositionalParameters);
|
|
2331
|
+
return {
|
|
2332
|
+
sql,
|
|
2333
|
+
table: source.table,
|
|
2334
|
+
alias: source.alias,
|
|
2335
|
+
selectList: sql.slice(clauses.selectStart, clauses.fromKeyword).trim(),
|
|
2336
|
+
selectItems: readSelectItems(sql, body, clauses),
|
|
2337
|
+
where: readWhere(sql, clauses),
|
|
2338
|
+
sortTerms,
|
|
2339
|
+
limit: window.limit,
|
|
2340
|
+
offset: window.offset,
|
|
2341
|
+
tail: sql.slice(clauses.tailStart, clauses.statementEnd).trim(),
|
|
2342
|
+
selectParameters: countParameters(body, clauses.selectStart, clauses.fromKeyword),
|
|
2343
|
+
whereParameters: clauses.whereStart === null || clauses.whereEnd === null ? 0 : countParameters(body, clauses.whereStart, clauses.whereEnd),
|
|
2344
|
+
usesPositionalParameters
|
|
2345
|
+
};
|
|
2346
|
+
}
|
|
2347
|
+
function countParameters(body, start, end) {
|
|
2348
|
+
return body.filter((token) => token.kind === "param" && token.start >= start && token.end <= end).length;
|
|
2349
|
+
}
|
|
2350
|
+
function readWhere(sql, clauses) {
|
|
2351
|
+
if (clauses.whereStart === null || clauses.whereEnd === null) return null;
|
|
2352
|
+
return sql.slice(clauses.whereStart, clauses.whereEnd).trim();
|
|
2353
|
+
}
|
|
2354
|
+
function assertSortTermsBindByName(sql, sortTerms, positional) {
|
|
2355
|
+
if (!positional) return;
|
|
2356
|
+
for (const term of sortTerms) {
|
|
2357
|
+
if (tokenizeSql(term.expression).some((token) => token.kind === "param")) {
|
|
2358
|
+
throw unsupported(
|
|
2359
|
+
sql,
|
|
2360
|
+
"a live query binds an ORDER BY parameter by name, because it evaluates the term a second time to place a changed row"
|
|
2361
|
+
);
|
|
2362
|
+
}
|
|
2363
|
+
}
|
|
2364
|
+
}
|
|
2365
|
+
function assertSupportedWords(sql, body) {
|
|
2366
|
+
for (let i = 0; i < body.length; i++) {
|
|
2367
|
+
const token = body[i];
|
|
2368
|
+
if (token.kind !== "word" || token.quoted) continue;
|
|
2369
|
+
if (i > 0 && token.lower === "select") {
|
|
2370
|
+
throw unsupported(sql, "a subquery in a live query is not maintainable");
|
|
2371
|
+
}
|
|
2372
|
+
if (token.lower === "over" && body[i + 1]?.value === "(") {
|
|
2373
|
+
throw unsupported(sql, "a window function in a live query is not maintainable");
|
|
2374
|
+
}
|
|
2375
|
+
if (AGGREGATE_FUNCTIONS.has(token.lower) && body[i + 1]?.value === "(") {
|
|
2376
|
+
throw unsupported(sql, `the aggregate ${token.value} in a live query is not maintainable`);
|
|
2377
|
+
}
|
|
2378
|
+
const reason = UNSUPPORTED_WORDS.get(token.lower);
|
|
2379
|
+
if (reason !== void 0 && token.depth === 0) {
|
|
2380
|
+
throw unsupported(sql, `${reason} in a live query is not maintainable`);
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
if (body.some((token) => token.kind === "punct" && token.value === ";")) {
|
|
2384
|
+
throw unsupported(sql, "a live query reads with one statement");
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
2387
|
+
function assertDeterministic(sql, body) {
|
|
2388
|
+
for (let i = 0; i < body.length; i++) {
|
|
2389
|
+
const token = body[i];
|
|
2390
|
+
if (token.kind !== "word" || token.quoted) continue;
|
|
2391
|
+
if (CLOCK_KEYWORDS.has(token.lower)) {
|
|
2392
|
+
throw volatileRead(sql, token.value);
|
|
2393
|
+
}
|
|
2394
|
+
const open = body[i + 1];
|
|
2395
|
+
if (open === void 0 || open.kind !== "punct" || open.value !== "(") continue;
|
|
2396
|
+
if (VOLATILE_FUNCTIONS.has(token.lower)) {
|
|
2397
|
+
throw volatileRead(sql, `${token.value}()`);
|
|
2398
|
+
}
|
|
2399
|
+
if (CLOCK_FUNCTIONS.has(token.lower) && readsTheClock(token.lower, callArguments(body, i + 1))) {
|
|
2400
|
+
throw volatileRead(sql, `${token.value}()`);
|
|
600
2401
|
}
|
|
601
2402
|
}
|
|
602
|
-
}
|
|
2403
|
+
}
|
|
2404
|
+
function callArguments(body, openIndex) {
|
|
2405
|
+
const outer = body[openIndex].depth;
|
|
2406
|
+
const args = [];
|
|
2407
|
+
let current = [];
|
|
2408
|
+
for (let i = openIndex + 1; i < body.length; i++) {
|
|
2409
|
+
const token = body[i];
|
|
2410
|
+
if (token.kind === "punct" && token.value === ")" && token.depth === outer) break;
|
|
2411
|
+
if (token.kind === "punct" && token.value === "," && token.depth === outer + 1) {
|
|
2412
|
+
args.push(current);
|
|
2413
|
+
current = [];
|
|
2414
|
+
continue;
|
|
2415
|
+
}
|
|
2416
|
+
current.push(token);
|
|
2417
|
+
}
|
|
2418
|
+
if (current.length > 0) args.push(current);
|
|
2419
|
+
return args;
|
|
2420
|
+
}
|
|
2421
|
+
function readsTheClock(name, args) {
|
|
2422
|
+
if (args.length === 0) return true;
|
|
2423
|
+
if (name === "strftime" && args.length === 1) return true;
|
|
2424
|
+
return args.some((argument) => argument.some((token) => token.kind === "string" && CLOCK_MODIFIER.test(token.value)));
|
|
2425
|
+
}
|
|
2426
|
+
function volatileRead(sql, reference) {
|
|
2427
|
+
return unsupported(
|
|
2428
|
+
sql,
|
|
2429
|
+
`${reference} returns a different value on each evaluation, and a live query only re-evaluates a row a change event touches, so its rows would go stale without any change to report`
|
|
2430
|
+
);
|
|
2431
|
+
}
|
|
2432
|
+
function withoutTrailingSemicolon(tokens) {
|
|
2433
|
+
const last = tokens[tokens.length - 1];
|
|
2434
|
+
if (last !== void 0 && last.kind === "punct" && last.value === ";") return tokens.slice(0, -1);
|
|
2435
|
+
return tokens;
|
|
2436
|
+
}
|
|
2437
|
+
|
|
2438
|
+
// src/core/live/database-live.ts
|
|
2439
|
+
var DEFAULT_REREAD_JITTER_MS = 25;
|
|
2440
|
+
var DEFAULT_MAX_TRANSACTION_CHANGES = 1e4;
|
|
2441
|
+
var MAX_TRANSACTION_BYTES = 16 * 1048576;
|
|
2442
|
+
var nextProbeTable = 0;
|
|
2443
|
+
async function openLiveQuery(deps, sql, params, options) {
|
|
2444
|
+
const shape = analyseStatement(sql);
|
|
2445
|
+
await deps.watch(shape.table);
|
|
2446
|
+
const { conn, run } = await deps.cdc.liveConnection();
|
|
2447
|
+
const info = await run(() => tableInfoRows(conn, shape.table));
|
|
2448
|
+
if (info.length === 0) {
|
|
2449
|
+
throw new CDCError(`Cannot open a live query for '${sql}': table '${shape.table}' has no columns`);
|
|
2450
|
+
}
|
|
2451
|
+
const ddl = await run(() => selectTableSql(conn, shape.table));
|
|
2452
|
+
const plan = buildLivePlan(
|
|
2453
|
+
shape,
|
|
2454
|
+
{
|
|
2455
|
+
columns: info.map((column) => ({ name: column.name, type: column.type })),
|
|
2456
|
+
collations: ddl === null ? /* @__PURE__ */ new Map() : parseColumnCollations(ddl),
|
|
2457
|
+
pkColumns: info.filter((column) => column.pk > 0).sort((left, right) => left.pk - right.pk).map((column) => column.name)
|
|
2458
|
+
},
|
|
2459
|
+
params
|
|
2460
|
+
);
|
|
2461
|
+
const probeTable = `${LIVE_PROBE_TABLE_PREFIX}${nextProbeTable++}`;
|
|
2462
|
+
const probe = await RowProbe.open(conn, probeTable, plan, params, run);
|
|
2463
|
+
try {
|
|
2464
|
+
return await MaintainedLiveQuery.open(buildSource(deps, conn, run, plan, probe, params, options));
|
|
2465
|
+
} catch (err) {
|
|
2466
|
+
await probe.close().catch(() => {
|
|
2467
|
+
});
|
|
2468
|
+
throw err;
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
function buildSource(deps, conn, run, plan, probe, params, options) {
|
|
2472
|
+
const delivery = new PrimedLiveDelivery(deps.cdc, conn, run, plan.shape.table);
|
|
2473
|
+
return {
|
|
2474
|
+
plan,
|
|
2475
|
+
probe,
|
|
2476
|
+
rereadJitterMs: options?.rereadJitterMs ?? DEFAULT_REREAD_JITTER_MS,
|
|
2477
|
+
maxTransactionChanges: options?.maxTransactionChanges ?? DEFAULT_MAX_TRANSACTION_CHANGES,
|
|
2478
|
+
maxTransactionBytes: MAX_TRANSACTION_BYTES,
|
|
2479
|
+
wait: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
2480
|
+
read: () => readAtPosition2(deps.cdc, plan, params),
|
|
2481
|
+
start: (handlers, sinceSeq) => delivery.start(handlers, sinceSeq),
|
|
2482
|
+
release: () => probe.close()
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
async function readAtPosition2(cdc, plan, params) {
|
|
2486
|
+
const captured = await cdc.readAtPositionWith(async (conn) => {
|
|
2487
|
+
const stmt = await conn.prepare(plan.readSql);
|
|
2488
|
+
return stmt.all(...bindRead(params));
|
|
2489
|
+
});
|
|
2490
|
+
return { rows: captured.value.map((row) => toHeldRow(plan, row)), seq: captured.seq };
|
|
2491
|
+
}
|
|
2492
|
+
function bindRead(params) {
|
|
2493
|
+
if (params === void 0) return [];
|
|
2494
|
+
return Array.isArray(params) ? params : [params];
|
|
2495
|
+
}
|
|
2496
|
+
function toHeldRow(plan, row) {
|
|
2497
|
+
const key = plan.usesRowid ? rowidKey(row[plan.keyColumnAliases[0]]) : encodeRowKey(plan.keyColumnAliases.map((alias) => row[alias]));
|
|
2498
|
+
const sort = plan.sortColumns.map((column, index) => toSortValue(row[column], plan.sortPlan.collations[index]));
|
|
2499
|
+
for (const alias of plan.keyColumnAliases) delete row[alias];
|
|
2500
|
+
for (const column of plan.sortColumns) delete row[column];
|
|
2501
|
+
return { key, row, sort };
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
// src/core/migrations/concurrency.ts
|
|
2505
|
+
var BUSY_CODES = /* @__PURE__ */ new Set(["SQLITE_BUSY", "SQLITE_BUSY_SNAPSHOT", "SQLITE_BUSY_RECOVERY", "SQLITE_BUSY_TIMEOUT"]);
|
|
2506
|
+
function isConcurrentMigrationConflict(err) {
|
|
2507
|
+
if (!(err instanceof Error)) return false;
|
|
2508
|
+
const code = err.code;
|
|
2509
|
+
if (typeof code === "string" && BUSY_CODES.has(code)) return true;
|
|
2510
|
+
const message = err.message;
|
|
2511
|
+
if (/SQLITE_BUSY|database is locked/i.test(message)) return true;
|
|
2512
|
+
return message.includes(`UNIQUE constraint failed: ${MIGRATIONS_TABLE}.version`);
|
|
2513
|
+
}
|
|
2514
|
+
|
|
2515
|
+
// src/core/migrations/types.ts
|
|
2516
|
+
var MIGRATION_NAME_RE = /^\w+$/;
|
|
603
2517
|
|
|
604
2518
|
// src/core/migrations/runner.ts
|
|
605
|
-
var CREATE_TRACKING_TABLE = `
|
|
606
|
-
CREATE TABLE IF NOT EXISTS ${MIGRATIONS_TABLE} (
|
|
607
|
-
version INTEGER PRIMARY KEY,
|
|
608
|
-
name TEXT NOT NULL,
|
|
609
|
-
applied_at REAL NOT NULL DEFAULT (unixepoch('subsec'))
|
|
610
|
-
)
|
|
611
|
-
`;
|
|
612
2519
|
var MigrationRunner = class _MigrationRunner {
|
|
613
2520
|
static async run(conn, migrations) {
|
|
614
|
-
await conn.exec(CREATE_TRACKING_TABLE);
|
|
615
2521
|
const validated = _MigrationRunner.validateMigrations(migrations);
|
|
616
|
-
const
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
2522
|
+
const effectiveBaseline = resolveEffectiveBaseline(validated);
|
|
2523
|
+
return _MigrationRunner.retryOnConcurrentConflict(
|
|
2524
|
+
() => _MigrationRunner.applyPending(conn, validated, effectiveBaseline)
|
|
2525
|
+
);
|
|
2526
|
+
}
|
|
2527
|
+
static async retryOnConcurrentConflict(operation) {
|
|
2528
|
+
try {
|
|
2529
|
+
return await operation();
|
|
2530
|
+
} catch (err) {
|
|
2531
|
+
if (!isConcurrentMigrationConflict(err)) throw err;
|
|
2532
|
+
}
|
|
2533
|
+
try {
|
|
2534
|
+
return await operation();
|
|
2535
|
+
} catch (err) {
|
|
2536
|
+
if (!isConcurrentMigrationConflict(err)) throw err;
|
|
2537
|
+
throw new MigrationError(
|
|
2538
|
+
"Another process is writing to the migration history and the busy timeout elapsed; retry once it finishes",
|
|
2539
|
+
0,
|
|
2540
|
+
"MIGRATION_CONCURRENT"
|
|
2541
|
+
);
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
static async applyPending(conn, validated, effectiveBaseline) {
|
|
2545
|
+
return conn.transaction(async (txConn) => {
|
|
2546
|
+
await ensureMigrationsTable(txConn);
|
|
2547
|
+
const applied = await _MigrationRunner.getAppliedRows(txConn);
|
|
2548
|
+
const backfills = reconcileMigrationChecksums(validated, applied);
|
|
2549
|
+
if (backfills.length > 0) {
|
|
2550
|
+
const updateStmt = await prepareUpdateMigrationChecksum(txConn);
|
|
2551
|
+
for (const backfill of backfills) {
|
|
2552
|
+
await updateStmt.run(backfill.checksum, backfill.version);
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
const pending = planPendingMigrations(validated, effectiveBaseline, new Set(applied.keys()));
|
|
2556
|
+
if (pending.length === 0) {
|
|
2557
|
+
await syncSchemaVersion(txConn, _MigrationRunner.highestVersion(applied.keys()));
|
|
2558
|
+
return { applied: [], skipped: validated.length };
|
|
2559
|
+
}
|
|
2560
|
+
const appliedEntries = [];
|
|
2561
|
+
const insertStmt = await prepareInsertMigration(txConn);
|
|
624
2562
|
for (const migration of pending) {
|
|
625
2563
|
try {
|
|
626
2564
|
if (typeof migration.up === "string") {
|
|
@@ -631,19 +2569,27 @@ var MigrationRunner = class _MigrationRunner {
|
|
|
631
2569
|
}
|
|
632
2570
|
} catch (err) {
|
|
633
2571
|
if (err instanceof MigrationError) throw err;
|
|
2572
|
+
if (isConcurrentMigrationConflict(err)) throw err;
|
|
634
2573
|
throw new MigrationError(
|
|
635
2574
|
`Migration ${migration.version}_${migration.name} failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
636
2575
|
migration.version
|
|
637
2576
|
);
|
|
638
2577
|
}
|
|
639
|
-
await insertStmt.run(migration.version, migration.name);
|
|
2578
|
+
await insertStmt.run(migration.version, migration.name, migrationContentChecksum(migration));
|
|
640
2579
|
appliedEntries.push({ version: migration.version, name: migration.name });
|
|
641
2580
|
}
|
|
2581
|
+
await mirrorSchemaVersion(
|
|
2582
|
+
txConn,
|
|
2583
|
+
Math.max(
|
|
2584
|
+
_MigrationRunner.highestVersion(applied.keys()),
|
|
2585
|
+
_MigrationRunner.highestVersion(appliedEntries.map((e) => e.version))
|
|
2586
|
+
)
|
|
2587
|
+
);
|
|
2588
|
+
return {
|
|
2589
|
+
applied: appliedEntries,
|
|
2590
|
+
skipped: validated.length - pending.length
|
|
2591
|
+
};
|
|
642
2592
|
});
|
|
643
|
-
return {
|
|
644
|
-
applied: appliedEntries,
|
|
645
|
-
skipped: validated.length - pending.length
|
|
646
|
-
};
|
|
647
2593
|
}
|
|
648
2594
|
static async rollback(conn, migrations, version) {
|
|
649
2595
|
if (version !== void 0 && (!Number.isSafeInteger(version) || version < 0)) {
|
|
@@ -653,76 +2599,92 @@ var MigrationRunner = class _MigrationRunner {
|
|
|
653
2599
|
"MIGRATION_VALIDATION_ERROR"
|
|
654
2600
|
);
|
|
655
2601
|
}
|
|
656
|
-
await conn.exec(CREATE_TRACKING_TABLE);
|
|
657
|
-
const selectStmt = await conn.prepare(`SELECT version, name FROM ${MIGRATIONS_TABLE} ORDER BY version DESC`);
|
|
658
|
-
const appliedRows = await selectStmt.all();
|
|
659
|
-
if (appliedRows.length === 0) {
|
|
660
|
-
return { rolledBack: [] };
|
|
661
|
-
}
|
|
662
|
-
let rollbackSet;
|
|
663
|
-
if (version === void 0) {
|
|
664
|
-
rollbackSet = [appliedRows[0]];
|
|
665
|
-
} else {
|
|
666
|
-
rollbackSet = appliedRows.filter((row) => row.version > version);
|
|
667
|
-
}
|
|
668
|
-
if (rollbackSet.length === 0) {
|
|
669
|
-
return { rolledBack: [] };
|
|
670
|
-
}
|
|
671
2602
|
_MigrationRunner.validateMigrations(migrations);
|
|
672
|
-
const rollbackVersions = rollbackSet.map((r) => r.version);
|
|
673
2603
|
const inputByVersion = new Map(migrations.map((m) => [m.version, m]));
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
if (
|
|
688
|
-
|
|
689
|
-
|
|
2604
|
+
return _MigrationRunner.retryOnConcurrentConflict(
|
|
2605
|
+
() => conn.transaction(async (txConn) => {
|
|
2606
|
+
await ensureMigrationsTable(txConn);
|
|
2607
|
+
const appliedRows = await selectAppliedMigrationsNewestFirst(txConn);
|
|
2608
|
+
if (appliedRows.length === 0) {
|
|
2609
|
+
return { rolledBack: [] };
|
|
2610
|
+
}
|
|
2611
|
+
let rollbackSet;
|
|
2612
|
+
if (version === void 0) {
|
|
2613
|
+
rollbackSet = [appliedRows[0]];
|
|
2614
|
+
} else {
|
|
2615
|
+
rollbackSet = appliedRows.filter((row) => row.version > version);
|
|
2616
|
+
}
|
|
2617
|
+
if (rollbackSet.length === 0) {
|
|
2618
|
+
return { rolledBack: [] };
|
|
2619
|
+
}
|
|
2620
|
+
const downByVersion = /* @__PURE__ */ new Map();
|
|
2621
|
+
for (const entry of rollbackSet) {
|
|
2622
|
+
downByVersion.set(
|
|
690
2623
|
entry.version,
|
|
691
|
-
|
|
2624
|
+
_MigrationRunner.resolveDownMigration(inputByVersion.get(entry.version), entry.version)
|
|
692
2625
|
);
|
|
693
2626
|
}
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
2627
|
+
const rolledBackEntries = [];
|
|
2628
|
+
const deleteStmt = await prepareDeleteMigration(txConn);
|
|
2629
|
+
for (const entry of rollbackSet) {
|
|
2630
|
+
const migration = downByVersion.get(entry.version);
|
|
2631
|
+
if (!migration) {
|
|
2632
|
+
throw new MigrationError(
|
|
2633
|
+
`No down migration found for version ${entry.version}`,
|
|
2634
|
+
entry.version,
|
|
2635
|
+
"MIGRATION_ROLLBACK_ERROR"
|
|
2636
|
+
);
|
|
700
2637
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
2638
|
+
try {
|
|
2639
|
+
if (typeof migration.down === "string") {
|
|
2640
|
+
await txConn.exec(migration.down);
|
|
2641
|
+
} else {
|
|
2642
|
+
const result = migration.down?.(new Transaction(txConn));
|
|
2643
|
+
if (result instanceof Promise) await result;
|
|
2644
|
+
}
|
|
2645
|
+
} catch (err) {
|
|
2646
|
+
if (err instanceof MigrationError) throw err;
|
|
2647
|
+
if (isConcurrentMigrationConflict(err)) throw err;
|
|
2648
|
+
throw new MigrationError(
|
|
2649
|
+
`Rollback of migration ${entry.version}_${entry.name} failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
2650
|
+
entry.version,
|
|
2651
|
+
"MIGRATION_ROLLBACK_ERROR"
|
|
2652
|
+
);
|
|
2653
|
+
}
|
|
2654
|
+
await deleteStmt.run(entry.version);
|
|
2655
|
+
rolledBackEntries.push({ version: entry.version, name: entry.name });
|
|
708
2656
|
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
2657
|
+
const rolledBackVersions = new Set(rollbackSet.map((r) => r.version));
|
|
2658
|
+
const remaining = appliedRows.find((row) => !rolledBackVersions.has(row.version));
|
|
2659
|
+
await mirrorSchemaVersion(txConn, remaining === void 0 ? 0 : remaining.version);
|
|
2660
|
+
return { rolledBack: rolledBackEntries };
|
|
2661
|
+
})
|
|
2662
|
+
);
|
|
2663
|
+
}
|
|
2664
|
+
static resolveDownMigration(migration, version) {
|
|
2665
|
+
if (!migration) {
|
|
2666
|
+
throw new MigrationError(`Migration version ${version} has no down migration`, version, "MIGRATION_NO_DOWN");
|
|
2667
|
+
}
|
|
2668
|
+
if (migration.down !== void 0) {
|
|
2669
|
+
return migration;
|
|
2670
|
+
}
|
|
2671
|
+
const readDown = migration[LAZY_DOWN_SQL];
|
|
2672
|
+
if (readDown === void 0) {
|
|
2673
|
+
throw new MigrationError(`Migration version ${version} has no down migration`, version, "MIGRATION_NO_DOWN");
|
|
2674
|
+
}
|
|
2675
|
+
return { version: migration.version, name: migration.name, up: migration.up, down: readDown() };
|
|
714
2676
|
}
|
|
715
2677
|
static validateMigrations(migrations) {
|
|
716
2678
|
const seenVersions = /* @__PURE__ */ new Map();
|
|
717
2679
|
for (const m of migrations) {
|
|
718
|
-
if (!Number.isSafeInteger(m.version) || m.version <= 0) {
|
|
2680
|
+
if (!Number.isSafeInteger(m.version) || m.version <= 0 || m.version > SQLITE_USER_VERSION_MAX) {
|
|
719
2681
|
throw new MigrationError(
|
|
720
|
-
`Invalid migration version: ${m.version}`,
|
|
2682
|
+
`Invalid migration version: ${m.version}. Versions must be integers from 1 to ${SQLITE_USER_VERSION_MAX} so they mirror to PRAGMA user_version`,
|
|
721
2683
|
typeof m.version === "number" && Number.isFinite(m.version) ? m.version : 0,
|
|
722
2684
|
"MIGRATION_VALIDATION_ERROR"
|
|
723
2685
|
);
|
|
724
2686
|
}
|
|
725
|
-
if (
|
|
2687
|
+
if (!MIGRATION_NAME_RE.test(m.name)) {
|
|
726
2688
|
throw new MigrationError(`Invalid migration name: '${m.name}'`, m.version, "MIGRATION_VALIDATION_ERROR");
|
|
727
2689
|
}
|
|
728
2690
|
const existing = seenVersions.get(m.version);
|
|
@@ -751,89 +2713,37 @@ var MigrationRunner = class _MigrationRunner {
|
|
|
751
2713
|
}
|
|
752
2714
|
return [...migrations].sort((a, b) => a.version - b.version);
|
|
753
2715
|
}
|
|
754
|
-
static
|
|
755
|
-
|
|
756
|
-
const
|
|
757
|
-
|
|
2716
|
+
static highestVersion(versions) {
|
|
2717
|
+
let highest = 0;
|
|
2718
|
+
for (const version of versions) {
|
|
2719
|
+
if (version > highest) highest = version;
|
|
2720
|
+
}
|
|
2721
|
+
return highest;
|
|
2722
|
+
}
|
|
2723
|
+
static async getAppliedRows(conn) {
|
|
2724
|
+
const rows = await selectAppliedMigrations(conn);
|
|
2725
|
+
return new Map(rows.map((r) => [r.version, { name: r.name, checksum: r.checksum }]));
|
|
758
2726
|
}
|
|
759
2727
|
};
|
|
760
2728
|
|
|
761
|
-
// src/core/
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
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;
|
|
2729
|
+
// src/core/database-migrations.ts
|
|
2730
|
+
async function refreshTriggersAfterSchemaChange(tracker, conn) {
|
|
2731
|
+
if (!tracker || tracker.watchedTables.size === 0) return;
|
|
2732
|
+
await tracker.refreshAllTriggersUsingConnection(conn);
|
|
769
2733
|
}
|
|
770
|
-
function
|
|
771
|
-
|
|
772
|
-
|
|
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 };
|
|
2734
|
+
async function migrateWithTriggerRefresh(conn, tracker, migrations) {
|
|
2735
|
+
const result = await MigrationRunner.run(conn, migrations);
|
|
2736
|
+
if (result.applied.length > 0) await refreshTriggersAfterSchemaChange(tracker, conn);
|
|
2737
|
+
return result;
|
|
780
2738
|
}
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
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() {
|
|
2739
|
+
async function rollbackWithTriggerRefresh(conn, tracker, migrations, version) {
|
|
2740
|
+
const result = await MigrationRunner.rollback(conn, migrations, version);
|
|
2741
|
+
if (result.rolledBack.length > 0) await refreshTriggersAfterSchemaChange(tracker, conn);
|
|
2742
|
+
return result;
|
|
2743
|
+
}
|
|
2744
|
+
async function readAppliedMigrations(conn) {
|
|
2745
|
+
if (!await selectTableExists(conn, MIGRATIONS_TABLE)) return [];
|
|
2746
|
+
return selectAppliedMigrations(conn);
|
|
837
2747
|
}
|
|
838
2748
|
|
|
839
2749
|
// src/core/database.ts
|
|
@@ -851,105 +2761,53 @@ var Database = class _Database {
|
|
|
851
2761
|
closeListeners = [];
|
|
852
2762
|
_closed = false;
|
|
853
2763
|
cdc;
|
|
854
|
-
|
|
2764
|
+
sync;
|
|
2765
|
+
hookRegistry;
|
|
855
2766
|
observer;
|
|
856
2767
|
backups;
|
|
857
|
-
|
|
2768
|
+
reads;
|
|
2769
|
+
constructor(id, path, driver, runtime, options) {
|
|
858
2770
|
this.id = id;
|
|
859
2771
|
this.path = path;
|
|
860
|
-
this.pool = pool;
|
|
861
2772
|
this.driver = driver;
|
|
862
|
-
this.
|
|
863
|
-
this.
|
|
864
|
-
this.
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
2773
|
+
this.pool = runtime.pool;
|
|
2774
|
+
this.writeGate = runtime.writeGate;
|
|
2775
|
+
this.writerLock = runtime.writerLock;
|
|
2776
|
+
this.hookRegistry = runtime.hookRegistry;
|
|
2777
|
+
this.observer = runtime.observer;
|
|
2778
|
+
this.backups = runtime.backups;
|
|
2779
|
+
this.cdc = runtime.cdc;
|
|
2780
|
+
this.sync = runtime.sync;
|
|
2781
|
+
this.groupCommitter = runtime.groupCommitter;
|
|
2782
|
+
this.reads = { pool: runtime.pool, writerLock: runtime.writerLock, observer: runtime.observer };
|
|
869
2783
|
this.readOnly = options?.readOnly ?? false;
|
|
870
2784
|
this.synchronous = options?.synchronous ?? DEFAULT_SYNCHRONOUS;
|
|
871
2785
|
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
|
-
});
|
|
888
2786
|
}
|
|
889
2787
|
static async create(id, path, driver, options, internals) {
|
|
890
|
-
const
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
readOnly: options?.readOnly,
|
|
902
|
-
readPoolSize: options?.readPoolSize ?? 4,
|
|
903
|
-
walMode: options?.walMode ?? true,
|
|
904
|
-
synchronous: options?.synchronous,
|
|
905
|
-
useWriterWorker: writerWorker.enabled && !readOnly,
|
|
906
|
-
workerHostOptions: writerWorker.host
|
|
907
|
-
});
|
|
908
|
-
const writeGate = new WriteGate(writerWorker.enabled ? writerWorker.maxPendingWrites : 0, writerWorker.retryAfterMs);
|
|
909
|
-
return new _Database(id, path, pool, driver, writeGate, options, internals);
|
|
2788
|
+
const runtime = await createDatabaseRuntime(id, path, driver, options, internals);
|
|
2789
|
+
return new _Database(id, path, driver, runtime, options);
|
|
2790
|
+
}
|
|
2791
|
+
async applyChanges(batch, resolver) {
|
|
2792
|
+
this.ensureOpen();
|
|
2793
|
+
if (this.readOnly) throw new ReadOnlyError(this.id);
|
|
2794
|
+
return this.sync.applyChanges(batch, resolver);
|
|
2795
|
+
}
|
|
2796
|
+
deviceSync() {
|
|
2797
|
+
this.ensureNotClosed();
|
|
2798
|
+
return this.sync.devicePort();
|
|
910
2799
|
}
|
|
911
2800
|
async query(sql, params, options) {
|
|
912
2801
|
this.ensureOpen();
|
|
913
|
-
return this.
|
|
914
|
-
sql,
|
|
915
|
-
params,
|
|
916
|
-
options,
|
|
917
|
-
() => this.runRead(sql, (conn) => query(conn, sql, params))
|
|
918
|
-
);
|
|
2802
|
+
return readRows(this.reads, sql, params, options);
|
|
919
2803
|
}
|
|
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
2804
|
async queryForWire(sql, params, options) {
|
|
927
2805
|
this.ensureOpen();
|
|
928
|
-
return this.
|
|
929
|
-
sql,
|
|
930
|
-
params,
|
|
931
|
-
options,
|
|
932
|
-
() => this.runRead(sql, (conn) => queryForWire(conn, sql, params))
|
|
933
|
-
);
|
|
2806
|
+
return readWireRows(this.reads, sql, params, options);
|
|
934
2807
|
}
|
|
935
2808
|
async queryOne(sql, params, options) {
|
|
936
2809
|
this.ensureOpen();
|
|
937
|
-
return this.
|
|
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())));
|
|
951
|
-
}
|
|
952
|
-
return this.observer.track(sql, () => op(this.pool.acquireReader()));
|
|
2810
|
+
return readOneRow(this.reads, sql, params, options);
|
|
953
2811
|
}
|
|
954
2812
|
async execute(sql, params, options) {
|
|
955
2813
|
this.ensureOpen();
|
|
@@ -981,7 +2839,14 @@ var Database = class _Database {
|
|
|
981
2839
|
);
|
|
982
2840
|
}
|
|
983
2841
|
async runInTransaction(writer, sql, run) {
|
|
984
|
-
const result = await this.observer.track(
|
|
2842
|
+
const result = await this.observer.track(
|
|
2843
|
+
sql,
|
|
2844
|
+
() => writer.transaction(async (txConn) => {
|
|
2845
|
+
const value = await run(txConn);
|
|
2846
|
+
await this.cdc.applyStamps(txConn);
|
|
2847
|
+
return value;
|
|
2848
|
+
})
|
|
2849
|
+
);
|
|
985
2850
|
await applyDdlSideEffectsIfRelevant(this.cdc.changeTracker, writer, sql);
|
|
986
2851
|
return result;
|
|
987
2852
|
}
|
|
@@ -1062,6 +2927,13 @@ var Database = class _Database {
|
|
|
1062
2927
|
if (this._closed) return;
|
|
1063
2928
|
await this.writerLock.run(() => op(this.pool.acquireWriter()));
|
|
1064
2929
|
}
|
|
2930
|
+
async ensureChangeStamping() {
|
|
2931
|
+
this.ensureOpen();
|
|
2932
|
+
if (this.readOnly) {
|
|
2933
|
+
throw new ReadOnlyError(this.id);
|
|
2934
|
+
}
|
|
2935
|
+
await this.cdc.ensureStamping();
|
|
2936
|
+
}
|
|
1065
2937
|
async unwatch(table) {
|
|
1066
2938
|
this.ensureOpen();
|
|
1067
2939
|
await this.cdc.unwatch(table);
|
|
@@ -1070,13 +2942,26 @@ var Database = class _Database {
|
|
|
1070
2942
|
this.ensureOpen();
|
|
1071
2943
|
return this.cdc.on(table);
|
|
1072
2944
|
}
|
|
2945
|
+
async live(sql, params, options) {
|
|
2946
|
+
this.ensureOpen();
|
|
2947
|
+
if (this.readOnly) throw new ReadOnlyError(this.id);
|
|
2948
|
+
return openLiveQuery({ cdc: this.cdc, watch: (table) => this.watch(table) }, sql, params, options);
|
|
2949
|
+
}
|
|
1073
2950
|
async migrate(migrations) {
|
|
1074
2951
|
this.ensureOpen();
|
|
1075
|
-
return this.writerLock.run(
|
|
2952
|
+
return this.writerLock.run(
|
|
2953
|
+
() => migrateWithTriggerRefresh(this.pool.acquireWriter(), this.cdc.changeTracker, migrations)
|
|
2954
|
+
);
|
|
2955
|
+
}
|
|
2956
|
+
async appliedMigrations() {
|
|
2957
|
+
this.ensureNotClosed();
|
|
2958
|
+
return this.writerLock.run(() => readAppliedMigrations(this.pool.acquireWriter()));
|
|
1076
2959
|
}
|
|
1077
2960
|
async rollback(migrations, version) {
|
|
1078
2961
|
this.ensureOpen();
|
|
1079
|
-
return this.writerLock.run(
|
|
2962
|
+
return this.writerLock.run(
|
|
2963
|
+
() => rollbackWithTriggerRefresh(this.pool.acquireWriter(), this.cdc.changeTracker, migrations, version)
|
|
2964
|
+
);
|
|
1080
2965
|
}
|
|
1081
2966
|
async backup(destPath) {
|
|
1082
2967
|
this.ensureOpen();
|
|
@@ -1097,7 +2982,7 @@ var Database = class _Database {
|
|
|
1097
2982
|
this.hookRegistry.register("afterQuery", hook);
|
|
1098
2983
|
}
|
|
1099
2984
|
addCloseListener(fn) {
|
|
1100
|
-
this.
|
|
2985
|
+
this.ensureNotClosed();
|
|
1101
2986
|
this.closeListeners.push(fn);
|
|
1102
2987
|
}
|
|
1103
2988
|
async close() {
|
|
@@ -1107,6 +2992,7 @@ var Database = class _Database {
|
|
|
1107
2992
|
this.backups.cancelAll();
|
|
1108
2993
|
let poolError;
|
|
1109
2994
|
try {
|
|
2995
|
+
await this.cdc.closeLiveConnection();
|
|
1110
2996
|
await this.groupCommitter.drain();
|
|
1111
2997
|
await this.writerLock.settle();
|
|
1112
2998
|
await this.pool.close();
|
|
@@ -1130,6 +3016,15 @@ var Database = class _Database {
|
|
|
1130
3016
|
return this.pool.readerCount;
|
|
1131
3017
|
}
|
|
1132
3018
|
ensureOpen() {
|
|
3019
|
+
this.ensureNotClosed();
|
|
3020
|
+
if (this.sync.snapshotLoadBlocked) {
|
|
3021
|
+
throw new SirannonError(
|
|
3022
|
+
`Database '${this.id}' is replacing its data from a sync snapshot; retry once the snapshot load completes`,
|
|
3023
|
+
"SNAPSHOT_IN_PROGRESS"
|
|
3024
|
+
);
|
|
3025
|
+
}
|
|
3026
|
+
}
|
|
3027
|
+
ensureNotClosed() {
|
|
1133
3028
|
if (this._closed) {
|
|
1134
3029
|
throw new SirannonError(`Database '${this.id}' is closed`, "DATABASE_CLOSED");
|
|
1135
3030
|
}
|
|
@@ -1353,6 +3248,71 @@ var MetricsCollector = class {
|
|
|
1353
3248
|
}
|
|
1354
3249
|
};
|
|
1355
3250
|
|
|
3251
|
+
// src/core/migrations/from-files.ts
|
|
3252
|
+
function basename(key) {
|
|
3253
|
+
const separator = Math.max(key.lastIndexOf("/"), key.lastIndexOf("\\"));
|
|
3254
|
+
return separator === -1 ? key : key.slice(separator + 1);
|
|
3255
|
+
}
|
|
3256
|
+
function migrationsFromFiles(files, options) {
|
|
3257
|
+
const grouped = /* @__PURE__ */ new Map();
|
|
3258
|
+
for (const [key, contents] of Object.entries(files)) {
|
|
3259
|
+
const parsed = parseMigrationFilename(basename(key));
|
|
3260
|
+
if (!parsed) {
|
|
3261
|
+
throw new MigrationError(
|
|
3262
|
+
`Migration file '${key}' does not match the <version>_<name>.up.sql / <version>_<name>.down.sql convention`,
|
|
3263
|
+
0,
|
|
3264
|
+
"MIGRATION_VALIDATION_ERROR"
|
|
3265
|
+
);
|
|
3266
|
+
}
|
|
3267
|
+
if (typeof contents !== "string") {
|
|
3268
|
+
throw new MigrationError(
|
|
3269
|
+
`Migration file '${key}' must map to its SQL text; received ${typeof contents}. When bundling with import.meta.glob, pass { query: '?raw', import: 'default', eager: true }`,
|
|
3270
|
+
parsed.version,
|
|
3271
|
+
"MIGRATION_VALIDATION_ERROR"
|
|
3272
|
+
);
|
|
3273
|
+
}
|
|
3274
|
+
const sql = contents.trim();
|
|
3275
|
+
if (sql.length === 0) {
|
|
3276
|
+
throw new MigrationError(`Migration file is empty: ${key}`, parsed.version, "MIGRATION_VALIDATION_ERROR");
|
|
3277
|
+
}
|
|
3278
|
+
const existing = grouped.get(parsed.version);
|
|
3279
|
+
if (!existing) {
|
|
3280
|
+
grouped.set(parsed.version, { name: parsed.name, [parsed.direction]: sql });
|
|
3281
|
+
continue;
|
|
3282
|
+
}
|
|
3283
|
+
if (existing.name !== parsed.name) {
|
|
3284
|
+
throw new MigrationError(
|
|
3285
|
+
`Duplicate migration version ${parsed.version}: '${existing.name}' and '${parsed.name}'`,
|
|
3286
|
+
parsed.version,
|
|
3287
|
+
"MIGRATION_DUPLICATE_VERSION"
|
|
3288
|
+
);
|
|
3289
|
+
}
|
|
3290
|
+
if (existing[parsed.direction] !== void 0) {
|
|
3291
|
+
throw new MigrationError(
|
|
3292
|
+
`Duplicate ${parsed.direction} migration for version ${parsed.version} ('${parsed.name}')`,
|
|
3293
|
+
parsed.version,
|
|
3294
|
+
"MIGRATION_DUPLICATE_VERSION"
|
|
3295
|
+
);
|
|
3296
|
+
}
|
|
3297
|
+
existing[parsed.direction] = sql;
|
|
3298
|
+
}
|
|
3299
|
+
const migrations = [];
|
|
3300
|
+
for (const [version, entry] of grouped) {
|
|
3301
|
+
if (entry.up === void 0) {
|
|
3302
|
+
throw new MigrationError(
|
|
3303
|
+
`Migration version ${version} (${entry.name}) is missing an .up.sql file`,
|
|
3304
|
+
version,
|
|
3305
|
+
"MIGRATION_VALIDATION_ERROR"
|
|
3306
|
+
);
|
|
3307
|
+
}
|
|
3308
|
+
migrations.push(
|
|
3309
|
+
entry.down === void 0 ? { version, name: entry.name, up: entry.up } : { version, name: entry.name, up: entry.up, down: entry.down }
|
|
3310
|
+
);
|
|
3311
|
+
}
|
|
3312
|
+
migrations.sort((a, b) => a.version - b.version);
|
|
3313
|
+
return applyBaselineOption(migrations, options?.baseline);
|
|
3314
|
+
}
|
|
3315
|
+
|
|
1356
3316
|
// src/core/sirannon.ts
|
|
1357
3317
|
var Sirannon = class {
|
|
1358
3318
|
constructor(options) {
|
|
@@ -1369,6 +3329,8 @@ var Sirannon = class {
|
|
|
1369
3329
|
}
|
|
1370
3330
|
dbs = /* @__PURE__ */ new Map();
|
|
1371
3331
|
opening = /* @__PURE__ */ new Set();
|
|
3332
|
+
resolving = /* @__PURE__ */ new Map();
|
|
3333
|
+
migrationSet = null;
|
|
1372
3334
|
_shutdown = false;
|
|
1373
3335
|
_driver;
|
|
1374
3336
|
hookRegistry;
|
|
@@ -1400,7 +3362,19 @@ var Sirannon = class {
|
|
|
1400
3362
|
"DATABASE_OPEN_FAILED"
|
|
1401
3363
|
);
|
|
1402
3364
|
}
|
|
1403
|
-
|
|
3365
|
+
try {
|
|
3366
|
+
await this.applyRegistryMigrations(db);
|
|
3367
|
+
} catch (err) {
|
|
3368
|
+
await db.close().catch(() => {
|
|
3369
|
+
});
|
|
3370
|
+
if (err instanceof SirannonError) throw err;
|
|
3371
|
+
throw new SirannonError(
|
|
3372
|
+
`Failed to migrate database '${id}' at '${path}': ${err instanceof Error ? err.message : String(err)}`,
|
|
3373
|
+
"DATABASE_OPEN_FAILED"
|
|
3374
|
+
);
|
|
3375
|
+
} finally {
|
|
3376
|
+
this.opening.delete(id);
|
|
3377
|
+
}
|
|
1404
3378
|
if (this._shutdown) {
|
|
1405
3379
|
await db.close().catch(() => {
|
|
1406
3380
|
});
|
|
@@ -1464,7 +3438,49 @@ var Sirannon = class {
|
|
|
1464
3438
|
const db = this.get(id);
|
|
1465
3439
|
if (db) return db;
|
|
1466
3440
|
if (this._shutdown) return void 0;
|
|
1467
|
-
|
|
3441
|
+
const manager = this.lifecycleManager;
|
|
3442
|
+
if (!manager) return void 0;
|
|
3443
|
+
const pending = this.resolving.get(id);
|
|
3444
|
+
if (pending) return pending;
|
|
3445
|
+
const inFlight = manager.resolve(id).finally(() => {
|
|
3446
|
+
this.resolving.delete(id);
|
|
3447
|
+
});
|
|
3448
|
+
this.resolving.set(id, inFlight);
|
|
3449
|
+
return inFlight;
|
|
3450
|
+
}
|
|
3451
|
+
registryMigrations() {
|
|
3452
|
+
return this.loadMigrationSet();
|
|
3453
|
+
}
|
|
3454
|
+
async applyRegistryMigrations(db) {
|
|
3455
|
+
if (this.options.migrations === void 0 || db.readOnly) return;
|
|
3456
|
+
const migrations = await this.loadMigrationSet();
|
|
3457
|
+
if (migrations.length === 0) return;
|
|
3458
|
+
await db.migrate(migrations);
|
|
3459
|
+
}
|
|
3460
|
+
loadMigrationSet() {
|
|
3461
|
+
const source = this.options.migrations;
|
|
3462
|
+
if (source === void 0 || Array.isArray(source)) {
|
|
3463
|
+
return Promise.resolve(source ?? []);
|
|
3464
|
+
}
|
|
3465
|
+
if (this.migrationSet) return this.migrationSet;
|
|
3466
|
+
const loading = (async () => {
|
|
3467
|
+
const set = await source();
|
|
3468
|
+
if (!Array.isArray(set)) {
|
|
3469
|
+
throw new MigrationError(
|
|
3470
|
+
"The migrations source must return an array of migrations",
|
|
3471
|
+
0,
|
|
3472
|
+
"MIGRATION_SOURCE_INVALID"
|
|
3473
|
+
);
|
|
3474
|
+
}
|
|
3475
|
+
return set;
|
|
3476
|
+
})();
|
|
3477
|
+
loading.catch(() => {
|
|
3478
|
+
if (this.migrationSet === loading) {
|
|
3479
|
+
this.migrationSet = null;
|
|
3480
|
+
}
|
|
3481
|
+
});
|
|
3482
|
+
this.migrationSet = loading;
|
|
3483
|
+
return loading;
|
|
1468
3484
|
}
|
|
1469
3485
|
has(id) {
|
|
1470
3486
|
return this.dbs.has(id);
|
|
@@ -1512,4 +3528,12 @@ var Sirannon = class {
|
|
|
1512
3528
|
}
|
|
1513
3529
|
};
|
|
1514
3530
|
|
|
1515
|
-
|
|
3531
|
+
// src/core/operation-registry.ts
|
|
3532
|
+
function operationRef(name) {
|
|
3533
|
+
return { name };
|
|
3534
|
+
}
|
|
3535
|
+
function operationName(operation) {
|
|
3536
|
+
return typeof operation === "string" ? operation : operation.name;
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3539
|
+
export { ConnectionPool, Database, HookRegistry, LifecycleManager, MIGRATION_NAME_RE, MetricsCollector, MigrationRunner, Sirannon, createTenantResolver, migrationsFromFiles, operationName, operationRef, sanitizeTenantId, tenantPath };
|