@syncular/server 0.22.0 → 0.24.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 +14 -2
- package/dist/admin.d.ts +3 -1
- package/dist/admin.js +7 -1
- package/dist/blob-handlers.js +1 -1
- package/dist/context.d.ts +8 -1
- package/dist/d1-storage.d.ts +11 -3
- package/dist/d1-storage.js +186 -36
- package/dist/errors.js +10 -0
- package/dist/handler.js +50 -4
- package/dist/operations.d.ts +7 -1
- package/dist/operations.js +5 -3
- package/dist/postgres-storage.d.ts +12 -4
- package/dist/postgres-storage.js +310 -59
- package/dist/push.js +31 -7
- package/dist/readiness.d.ts +9 -2
- package/dist/readiness.js +27 -2
- package/dist/realtime.js +20 -1
- package/dist/relational-rows.d.ts +31 -12
- package/dist/relational-rows.js +121 -26
- package/dist/segment-download.js +8 -0
- package/dist/sqlite-dialect.d.ts +1 -1
- package/dist/sqlite-dialect.js +15 -0
- package/dist/sqlite-storage.d.ts +18 -3
- package/dist/sqlite-storage.js +327 -82
- package/dist/storage-errors.d.ts +5 -3
- package/dist/storage-errors.js +13 -2
- package/dist/storage-query.d.ts +8 -1
- package/dist/storage-query.js +12 -0
- package/dist/storage.d.ts +153 -14
- package/dist/storage.js +40 -1
- package/dist/validate.d.ts +33 -15
- package/package.json +2 -2
- package/src/admin.ts +13 -1
- package/src/blob-handlers.ts +1 -1
- package/src/context.ts +8 -0
- package/src/d1-storage.ts +282 -37
- package/src/errors.ts +10 -0
- package/src/handler.ts +62 -4
- package/src/operations.ts +32 -14
- package/src/postgres-storage.ts +515 -68
- package/src/push.ts +31 -1
- package/src/readiness.ts +39 -4
- package/src/realtime.ts +22 -1
- package/src/relational-rows.ts +145 -30
- package/src/segment-download.ts +16 -0
- package/src/sqlite-dialect.ts +15 -0
- package/src/sqlite-storage.ts +491 -96
- package/src/storage-errors.ts +33 -3
- package/src/storage-query.ts +23 -1
- package/src/storage.ts +245 -12
- package/src/validate.ts +40 -15
package/dist/push.js
CHANGED
|
@@ -24,7 +24,10 @@ import { emitEvent } from './events.js';
|
|
|
24
24
|
import { prepareReactions, toNewReactions, } from './reactions.js';
|
|
25
25
|
import { authorizeWrite, renderScopeValue, storedScopesForRow } from './scopes.js';
|
|
26
26
|
import { decodeColumnVersions, encodeColumnVersions } from './relational-rows.js';
|
|
27
|
+
import { serveGateRefusal } from './storage.js';
|
|
27
28
|
import { StorageConstraintError } from './storage-errors.js';
|
|
29
|
+
import { transactionQuery } from './storage-query.js';
|
|
30
|
+
import { serveNotReadyError } from './readiness.js';
|
|
28
31
|
import { CommitValidationRejection, toValidateRow, ValidationRejection, } from './validate.js';
|
|
29
32
|
/**
|
|
30
33
|
* Extract the blobIds a decoded row references through its `blob_ref`
|
|
@@ -84,7 +87,7 @@ function conflictColumnsBitmap(columnCount, marked) {
|
|
|
84
87
|
* A no-op for tables with no validator — the `undefined` short-circuit
|
|
85
88
|
* keeps the feature zero-cost when off.
|
|
86
89
|
*/
|
|
87
|
-
async function runValidator(validators, table, op, rowId, values, storedValues, opIndex, partition, actorId) {
|
|
90
|
+
async function runValidator(validators, table, op, rowId, values, storedValues, opIndex, tx, partition, actorId) {
|
|
88
91
|
const validator = validators?.[table.name];
|
|
89
92
|
if (validator === undefined)
|
|
90
93
|
return undefined;
|
|
@@ -99,7 +102,11 @@ async function runValidator(validators, table, op, rowId, values, storedValues,
|
|
|
99
102
|
stored: storedValues !== undefined
|
|
100
103
|
? toValidateRow(table.columns, storedValues)
|
|
101
104
|
: undefined,
|
|
102
|
-
}, {
|
|
105
|
+
}, {
|
|
106
|
+
actorId,
|
|
107
|
+
partition,
|
|
108
|
+
queryAuthoritative: (query) => transactionQuery(tx, query),
|
|
109
|
+
});
|
|
103
110
|
}
|
|
104
111
|
catch (error) {
|
|
105
112
|
if (error instanceof ValidationRejection) {
|
|
@@ -197,7 +204,7 @@ async function applyOperation(tx, schema, resolved, op, opIndex, blobCtx, crdtMe
|
|
|
197
204
|
// stored = the row about to be removed). Only reached for an existing
|
|
198
205
|
// row — an absent-row delete is an idempotent no-op above.
|
|
199
206
|
const storedValues = decodeRow(table.columns, stored.payload);
|
|
200
|
-
const deleteReject = await runValidator(validators, table, 'delete', op.rowId, undefined, storedValues, opIndex, partition, actorId);
|
|
207
|
+
const deleteReject = await runValidator(validators, table, 'delete', op.rowId, undefined, storedValues, opIndex, tx, partition, actorId);
|
|
201
208
|
if (deleteReject !== undefined)
|
|
202
209
|
return deleteReject;
|
|
203
210
|
await tx.deleteRow(op.table, op.rowId);
|
|
@@ -328,7 +335,7 @@ async function applyOperation(tx, schema, resolved, op, opIndex, blobCtx, crdtMe
|
|
|
328
335
|
// §6.7: validate the merged row that will persist — for a crdt column
|
|
329
336
|
// the validator sees the MERGED value (§5.10.3), the state the store
|
|
330
337
|
// holds, not the raw pushed update.
|
|
331
|
-
const updateReject = await runValidator(validators, table, 'upsert', op.rowId, values, storedValues, opIndex, partition, actorId);
|
|
338
|
+
const updateReject = await runValidator(validators, table, 'upsert', op.rowId, values, storedValues, opIndex, tx, partition, actorId);
|
|
332
339
|
if (updateReject !== undefined)
|
|
333
340
|
return updateReject;
|
|
334
341
|
const columnVersions = encodeColumnVersions(nextVersions);
|
|
@@ -410,7 +417,7 @@ async function applyOperation(tx, schema, resolved, op, opIndex, blobCtx, crdtMe
|
|
|
410
417
|
// §6.7: validate the insert row (stored = undefined, so a validator can
|
|
411
418
|
// distinguish create from update); crdt columns are already merged
|
|
412
419
|
// against the empty document.
|
|
413
|
-
const insertReject = await runValidator(validators, table, 'upsert', op.rowId, values, undefined, opIndex, partition, actorId);
|
|
420
|
+
const insertReject = await runValidator(validators, table, 'upsert', op.rowId, values, undefined, opIndex, tx, partition, actorId);
|
|
414
421
|
if (insertReject !== undefined)
|
|
415
422
|
return insertReject;
|
|
416
423
|
// §5.2: an explicit insert recreates — the tombstone goes with it.
|
|
@@ -524,6 +531,7 @@ function commitValidationReader(tx, schema) {
|
|
|
524
531
|
serverVersion: stored.serverVersion,
|
|
525
532
|
}));
|
|
526
533
|
},
|
|
534
|
+
queryAuthoritative: (query) => transactionQuery(tx, query),
|
|
527
535
|
};
|
|
528
536
|
}
|
|
529
537
|
async function runCommitValidator(validator, tx, schema, clientId, clientCommitId, actorId, partition, operations) {
|
|
@@ -680,7 +688,7 @@ async function enforceReferences(tx, schema, staged, clientOpCount, cascadeLimit
|
|
|
680
688
|
const syntheticIndex = clientOpCount + originByIndex.length;
|
|
681
689
|
const storedValues = decodeRow(child.columns, childRow.payload);
|
|
682
690
|
if (reference.onDelete === 'CASCADE') {
|
|
683
|
-
const reject = await runValidator(validators, child, 'delete', childRow.rowId, undefined, storedValues, item.originOpIndex, partition, actorId);
|
|
691
|
+
const reject = await runValidator(validators, child, 'delete', childRow.rowId, undefined, storedValues, item.originOpIndex, tx, partition, actorId);
|
|
684
692
|
if (reject !== undefined) {
|
|
685
693
|
return { kind: 'terminate', record: reject.record };
|
|
686
694
|
}
|
|
@@ -704,7 +712,7 @@ async function enforceReferences(tx, schema, staged, clientOpCount, cascadeLimit
|
|
|
704
712
|
else {
|
|
705
713
|
const values = [...storedValues];
|
|
706
714
|
values[reference.columnIndex] = null;
|
|
707
|
-
const reject = await runValidator(validators, child, 'upsert', childRow.rowId, values, storedValues, item.originOpIndex, partition, actorId);
|
|
715
|
+
const reject = await runValidator(validators, child, 'upsert', childRow.rowId, values, storedValues, item.originOpIndex, tx, partition, actorId);
|
|
708
716
|
if (reject !== undefined) {
|
|
709
717
|
return { kind: 'terminate', record: reject.record };
|
|
710
718
|
}
|
|
@@ -864,6 +872,15 @@ export async function processPushOperationsWithTrace(ctx, schema, resolved, clie
|
|
|
864
872
|
throw new Error('storage transaction does not support serialized push apply, atomic rejection finalization, and configured durable reactions');
|
|
865
873
|
}
|
|
866
874
|
await lockPartitionForPush();
|
|
875
|
+
// RFC 0007: evaluate the gate on this transaction's own connection while
|
|
876
|
+
// the partition lock is held, so the migration check and the write share
|
|
877
|
+
// one transaction and no migration can interleave in between.
|
|
878
|
+
const gate = await tx.readServeGate(schema.version);
|
|
879
|
+
const refusal = serveGateRefusal(gate, schema.version, ctx.checkpoints);
|
|
880
|
+
if (refusal !== undefined) {
|
|
881
|
+
await tx.rollback();
|
|
882
|
+
throw serveNotReadyError(refusal);
|
|
883
|
+
}
|
|
867
884
|
// The optimistic lookup above may have raced another delivery. Re-check
|
|
868
885
|
// only after acquiring partition serialization and before any operation
|
|
869
886
|
// read, validation, merge, or staged write. The re-check runs on the
|
|
@@ -1015,6 +1032,13 @@ export async function processPushOperationsWithTrace(ctx, schema, resolved, clie
|
|
|
1015
1032
|
}
|
|
1016
1033
|
}
|
|
1017
1034
|
if (ctx.realtime !== undefined && changes.length > 0) {
|
|
1035
|
+
// RFC 0007 fanout decision: the notification carries the durable commit
|
|
1036
|
+
// entry itself (this commit's `changes`), not a projection read or a pull
|
|
1037
|
+
// window, so fanning it out before the request's buffered read-verify can
|
|
1038
|
+
// never serve a mixed result. Each peer's next pull is gated at the
|
|
1039
|
+
// storage seam; a refused mixed push+pull is safe because the push is
|
|
1040
|
+
// durable and replayable under its commit id. This is commit-entry
|
|
1041
|
+
// durability, not gate inheritance.
|
|
1018
1042
|
await ctx.realtime.notifyCommit(partition, {
|
|
1019
1043
|
commitSeq,
|
|
1020
1044
|
createdAtMs,
|
package/dist/readiness.d.ts
CHANGED
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* a structured failure that cannot be confused with request authentication.
|
|
8
8
|
*/
|
|
9
9
|
import type { SyncServerConfig } from './context.js';
|
|
10
|
+
import { type SyncError } from './errors.js';
|
|
11
|
+
import type { ServeGateRefusal } from './storage.js';
|
|
10
12
|
export declare const SYNC_SERVER_READINESS_ERROR_CODE: "sync.schema_not_ready";
|
|
11
|
-
export type SyncServerReadinessPhase = 'schema_compile' | 'storage_migration';
|
|
13
|
+
export type SyncServerReadinessPhase = 'schema_compile' | 'storage_migration' | 'backfill_checkpoint';
|
|
12
14
|
export declare class SyncServerReadinessError extends Error {
|
|
13
15
|
readonly name = "SyncServerReadinessError";
|
|
14
16
|
readonly code: "sync.schema_not_ready";
|
|
@@ -27,4 +29,9 @@ export declare class SyncServerReadinessError extends Error {
|
|
|
27
29
|
* exposes only a stable code, phase, and schema version; operators can inspect
|
|
28
30
|
* `cause` locally for the table/column or storage diagnostic.
|
|
29
31
|
*/
|
|
30
|
-
export declare function ensureSyncServerReady(config: Pick<SyncServerConfig, 'schema' | 'storage'>): Promise<void>;
|
|
32
|
+
export declare function ensureSyncServerReady(config: Pick<SyncServerConfig, 'schema' | 'storage' | 'checkpoints'>): Promise<void>;
|
|
33
|
+
/**
|
|
34
|
+
* RFC 0007: refuse a serve path that observed a closed gate. The projection
|
|
35
|
+
* name travels in structured details, never in the message.
|
|
36
|
+
*/
|
|
37
|
+
export declare function serveNotReadyError(refusal: ServeGateRefusal): SyncError;
|
package/dist/readiness.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { syncError } from './errors.js';
|
|
1
2
|
import { compileSchema } from './schema.js';
|
|
2
3
|
export const SYNC_SERVER_READINESS_ERROR_CODE = 'sync.schema_not_ready';
|
|
3
4
|
export class SyncServerReadinessError extends Error {
|
|
@@ -31,13 +32,37 @@ export async function ensureSyncServerReady(config) {
|
|
|
31
32
|
});
|
|
32
33
|
}
|
|
33
34
|
try {
|
|
34
|
-
await config.storage.ensureSchema(compiled);
|
|
35
|
+
await config.storage.ensureSchema(compiled, config.checkpoints);
|
|
35
36
|
}
|
|
36
37
|
catch (cause) {
|
|
38
|
+
// RFC 0007: a declared-but-incomplete checkpoint is a barrier refusal, not
|
|
39
|
+
// an ordinary migration failure — hosts distinguish it by `phase` while the
|
|
40
|
+
// client-visible identity stays `sync.schema_not_ready`.
|
|
41
|
+
const phase = cause?.code ===
|
|
42
|
+
'sync.storage.checkpoint_incomplete'
|
|
43
|
+
? 'backfill_checkpoint'
|
|
44
|
+
: 'storage_migration';
|
|
37
45
|
throw new SyncServerReadinessError({
|
|
38
|
-
phase
|
|
46
|
+
phase,
|
|
39
47
|
schemaVersion: config.schema.version,
|
|
40
48
|
cause,
|
|
41
49
|
});
|
|
42
50
|
}
|
|
43
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* RFC 0007: refuse a serve path that observed a closed gate. The projection
|
|
54
|
+
* name travels in structured details, never in the message.
|
|
55
|
+
*/
|
|
56
|
+
export function serveNotReadyError(refusal) {
|
|
57
|
+
return syncError(SYNC_SERVER_READINESS_ERROR_CODE, 'server schema is not ready for this request', refusal.kind === 'checkpoint'
|
|
58
|
+
? JSON.stringify({
|
|
59
|
+
partition: refusal.partition,
|
|
60
|
+
projection: refusal.name,
|
|
61
|
+
})
|
|
62
|
+
: refusal.kind === 'epoch'
|
|
63
|
+
? JSON.stringify({ logEpochChanged: true })
|
|
64
|
+
: JSON.stringify({
|
|
65
|
+
storedSchemaVersion: refusal.stored,
|
|
66
|
+
runningSchemaVersion: refusal.running,
|
|
67
|
+
}));
|
|
68
|
+
}
|
package/dist/realtime.js
CHANGED
|
@@ -446,7 +446,26 @@ export class RealtimeSession {
|
|
|
446
446
|
// begin its next round as soon as END *arrives* (§8.7 rule 3), so
|
|
447
447
|
// in-flight must end with the send, not with generator cleanup.
|
|
448
448
|
const iterator = stream[Symbol.asyncIterator]();
|
|
449
|
-
let step
|
|
449
|
+
let step;
|
|
450
|
+
try {
|
|
451
|
+
step = await iterator.next();
|
|
452
|
+
}
|
|
453
|
+
catch (error) {
|
|
454
|
+
// The buffered read-verify refusal is thrown on the generator's first
|
|
455
|
+
// `next()`, before any chunk is yielded (the generator builds the whole
|
|
456
|
+
// response, then verifies). Nothing has escaped, so it is delivered as
|
|
457
|
+
// the same §8.4 ERROR frame HTTP returns rather than a silent close. A
|
|
458
|
+
// throw after the first chunk still takes the §8.7 violation path.
|
|
459
|
+
if (error instanceof DecodeError || error instanceof SyncError) {
|
|
460
|
+
const sync = error instanceof SyncError
|
|
461
|
+
? error
|
|
462
|
+
: syncError(error.code, error.message);
|
|
463
|
+
finishRound();
|
|
464
|
+
await this.#sendRoundChunk(errorResponseBytes(sync, this.wireVersion, this.logEpoch));
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
throw error;
|
|
468
|
+
}
|
|
450
469
|
while (!step.done) {
|
|
451
470
|
const chunk = step.value;
|
|
452
471
|
step = await iterator.next();
|
|
@@ -75,26 +75,38 @@ export declare function columnSqlType(column: RowColumn, dialect: RelationalDial
|
|
|
75
75
|
* materialized), version, scopes, payload.
|
|
76
76
|
*/
|
|
77
77
|
export declare function tableColumnNames(table: CompiledTable): string[];
|
|
78
|
+
/** One physical column of a synced table, read from the database catalog. */
|
|
79
|
+
export interface PhysicalColumn {
|
|
80
|
+
readonly name: string;
|
|
81
|
+
/** Declared type (SQLite) or `format_type` (PostgreSQL); compared case-insensitively. */
|
|
82
|
+
readonly type: string;
|
|
83
|
+
readonly notNull: boolean;
|
|
84
|
+
/** 1-based position in the primary key; 0 when the column is not in it. */
|
|
85
|
+
readonly primaryKeyPosition: number;
|
|
86
|
+
}
|
|
78
87
|
/**
|
|
79
|
-
*
|
|
80
|
-
*
|
|
88
|
+
* Refuse a same-version database whose physical table the row store cannot
|
|
89
|
+
* read or write. The persisted marker's layouts describe the codec's app
|
|
81
90
|
* columns only and its version number is the schema's, not the database's, so
|
|
82
|
-
* a same-version database
|
|
83
|
-
* version-only bump whose ALTER never ran
|
|
84
|
-
*
|
|
91
|
+
* a same-version database whose storage-internal meta columns drifted (a
|
|
92
|
+
* version-only bump whose ALTER never ran, or a table written by another
|
|
93
|
+
* build) is only visible in the catalog. Every configured column must exist;
|
|
94
|
+
* the meta columns must also carry the type, nullability, and primary-key
|
|
95
|
+
* position `createTableDdl` declares. App-column types and nullability stay
|
|
96
|
+
* with the stored layouts: migration-added app columns are nullable by design.
|
|
97
|
+
* Fails closed without writing a migration.
|
|
85
98
|
*
|
|
86
|
-
* `
|
|
87
|
-
*
|
|
99
|
+
* `columns` must be resolved the way the unqualified row queries resolve the
|
|
100
|
+
* table: on PostgreSQL, through the session `search_path`, not
|
|
88
101
|
* `current_schema()`. A search path whose first schema is not where the
|
|
89
102
|
* configured tables live is outside the supported contract: DDL is created in
|
|
90
103
|
* that first schema, so `ensureSchema` already writes shadow tables there.
|
|
91
104
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
* persisted layouts answer for app columns.
|
|
105
|
+
* Core storage tables (`sync_tombstones`, `sync_commits`, ...) are outside
|
|
106
|
+
* this check: every open re-applies their `CREATE TABLE IF NOT EXISTS` DDL
|
|
107
|
+
* and column additions, independently of the version marker.
|
|
96
108
|
*/
|
|
97
|
-
export declare function assertPhysicalColumns(table: CompiledTable,
|
|
109
|
+
export declare function assertPhysicalColumns(table: CompiledTable, columns: readonly PhysicalColumn[], dialect: RelationalDialect): void;
|
|
98
110
|
/** CREATE TABLE IF NOT EXISTS for one app table. */
|
|
99
111
|
export declare function createTableDdl(table: CompiledTable, dialect: RelationalDialect): string;
|
|
100
112
|
/**
|
|
@@ -249,6 +261,13 @@ export type StoredLayouts = Record<string, readonly StoredColumnLayout[]>;
|
|
|
249
261
|
/** The layouts JSON persisted alongside the schema version marker. */
|
|
250
262
|
export declare function layoutsOf(schema: CompiledSchema): string;
|
|
251
263
|
export declare function parseLayouts(json: string | null | undefined): StoredLayouts;
|
|
264
|
+
/**
|
|
265
|
+
* Version equality is not layout equality: a marker written by another build
|
|
266
|
+
* at the same version describes rows the running codec cannot decode. Refuse
|
|
267
|
+
* when the stored layouts disagree with the configured schema, naming the
|
|
268
|
+
* first table (and column) that differs.
|
|
269
|
+
*/
|
|
270
|
+
export declare function assertStoredLayouts(schema: CompiledSchema, storedJson: string | null | undefined): void;
|
|
252
271
|
/**
|
|
253
272
|
* Tables present in the stored layout but absent from the configured head
|
|
254
273
|
* schema. A schema-version bump retires their relational current-row tables;
|
package/dist/relational-rows.js
CHANGED
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
* NOT NULL per the schema.
|
|
49
49
|
*/
|
|
50
50
|
import { decodeRow, encodeRow, } from '@syncular/core';
|
|
51
|
+
import { StorageQueryError } from './storage-errors.js';
|
|
51
52
|
export const SYNC_PARTITION_COLUMN = '_sync_partition';
|
|
52
53
|
export const SYNC_ROW_ID_COLUMN = '_sync_row_id';
|
|
53
54
|
export const SYNC_VERSION_COLUMN = '_sync_server_version';
|
|
@@ -170,52 +171,110 @@ export function tableColumnNames(table) {
|
|
|
170
171
|
];
|
|
171
172
|
}
|
|
172
173
|
/**
|
|
173
|
-
* The
|
|
174
|
-
*
|
|
174
|
+
* The storage-internal columns of every synced table, in `createTableDdl`
|
|
175
|
+
* order (app columns sit between the row id and the version). The same-version
|
|
176
|
+
* physical check compares a table against exactly these declarations.
|
|
177
|
+
*/
|
|
178
|
+
function syncMetaColumns(dialect) {
|
|
179
|
+
const payloadType = dialect === 'postgres' ? 'BYTEA' : 'BLOB';
|
|
180
|
+
return [
|
|
181
|
+
{ name: SYNC_PARTITION_COLUMN, type: 'TEXT', notNull: true },
|
|
182
|
+
{ name: SYNC_ROW_ID_COLUMN, type: 'TEXT', notNull: true },
|
|
183
|
+
{
|
|
184
|
+
name: SYNC_VERSION_COLUMN,
|
|
185
|
+
type: dialect === 'postgres' ? 'BIGINT' : 'INTEGER',
|
|
186
|
+
notNull: true,
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
name: SYNC_SCOPES_COLUMN,
|
|
190
|
+
type: dialect === 'postgres' ? 'JSONB' : 'TEXT',
|
|
191
|
+
notNull: true,
|
|
192
|
+
},
|
|
193
|
+
{ name: SYNC_PAYLOAD_COLUMN, type: payloadType, notNull: true },
|
|
194
|
+
{ name: SYNC_COLUMN_VERSIONS_COLUMN, type: payloadType, notNull: false },
|
|
195
|
+
];
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Refuse a same-version database whose physical table the row store cannot
|
|
199
|
+
* read or write. The persisted marker's layouts describe the codec's app
|
|
175
200
|
* columns only and its version number is the schema's, not the database's, so
|
|
176
|
-
* a same-version database
|
|
177
|
-
* version-only bump whose ALTER never ran
|
|
178
|
-
*
|
|
201
|
+
* a same-version database whose storage-internal meta columns drifted (a
|
|
202
|
+
* version-only bump whose ALTER never ran, or a table written by another
|
|
203
|
+
* build) is only visible in the catalog. Every configured column must exist;
|
|
204
|
+
* the meta columns must also carry the type, nullability, and primary-key
|
|
205
|
+
* position `createTableDdl` declares. App-column types and nullability stay
|
|
206
|
+
* with the stored layouts: migration-added app columns are nullable by design.
|
|
207
|
+
* Fails closed without writing a migration.
|
|
179
208
|
*
|
|
180
|
-
* `
|
|
181
|
-
*
|
|
209
|
+
* `columns` must be resolved the way the unqualified row queries resolve the
|
|
210
|
+
* table: on PostgreSQL, through the session `search_path`, not
|
|
182
211
|
* `current_schema()`. A search path whose first schema is not where the
|
|
183
212
|
* configured tables live is outside the supported contract: DDL is created in
|
|
184
213
|
* that first schema, so `ensureSchema` already writes shadow tables there.
|
|
185
214
|
*
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
* persisted layouts answer for app columns.
|
|
215
|
+
* Core storage tables (`sync_tombstones`, `sync_commits`, ...) are outside
|
|
216
|
+
* this check: every open re-applies their `CREATE TABLE IF NOT EXISTS` DDL
|
|
217
|
+
* and column additions, independently of the version marker.
|
|
190
218
|
*/
|
|
191
|
-
export function assertPhysicalColumns(table,
|
|
192
|
-
|
|
193
|
-
throw new
|
|
194
|
-
|
|
219
|
+
export function assertPhysicalColumns(table, columns, dialect) {
|
|
220
|
+
const refuse = (details) => {
|
|
221
|
+
throw new StorageQueryError('sync.storage.physical_layout_mismatch', {
|
|
222
|
+
table: table.name,
|
|
223
|
+
...details,
|
|
224
|
+
});
|
|
225
|
+
};
|
|
226
|
+
if (columns.length === 0)
|
|
227
|
+
refuse({ reason: 'missing_table' });
|
|
228
|
+
const byName = new Map(columns.map((column) => [column.name, column]));
|
|
195
229
|
for (const name of tableColumnNames(table)) {
|
|
196
|
-
if (
|
|
230
|
+
if (!byName.has(name))
|
|
231
|
+
refuse({ column: name, reason: 'missing_column' });
|
|
232
|
+
}
|
|
233
|
+
for (const expected of syncMetaColumns(dialect)) {
|
|
234
|
+
const actual = byName.get(expected.name);
|
|
235
|
+
if (actual === undefined)
|
|
197
236
|
continue;
|
|
198
|
-
|
|
237
|
+
if (actual.type.toUpperCase() !== expected.type) {
|
|
238
|
+
refuse({
|
|
239
|
+
column: expected.name,
|
|
240
|
+
reason: 'type',
|
|
241
|
+
expected: expected.type,
|
|
242
|
+
actual: actual.type,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
if (actual.notNull !== expected.notNull) {
|
|
246
|
+
refuse({
|
|
247
|
+
column: expected.name,
|
|
248
|
+
reason: 'nullability',
|
|
249
|
+
expected: expected.notNull ? 'not_null' : 'nullable',
|
|
250
|
+
actual: actual.notNull ? 'not_null' : 'nullable',
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
for (const column of columns) {
|
|
255
|
+
const expected = column.name === SYNC_PARTITION_COLUMN
|
|
256
|
+
? 1
|
|
257
|
+
: column.name === SYNC_ROW_ID_COLUMN
|
|
258
|
+
? 2
|
|
259
|
+
: 0;
|
|
260
|
+
if (column.primaryKeyPosition !== expected) {
|
|
261
|
+
refuse({ column: column.name, reason: 'primary_key' });
|
|
262
|
+
}
|
|
199
263
|
}
|
|
200
264
|
}
|
|
201
265
|
/** CREATE TABLE IF NOT EXISTS for one app table. */
|
|
202
266
|
export function createTableDdl(table, dialect) {
|
|
203
|
-
const
|
|
204
|
-
const scopesType = dialect === 'postgres' ? 'JSONB' : 'TEXT';
|
|
205
|
-
const payloadType = dialect === 'postgres' ? 'BYTEA' : 'BLOB';
|
|
267
|
+
const [partition, rowId, ...trailing] = syncMetaColumns(dialect).map((column) => `${quoteIdent(column.name)} ${column.type}${column.notNull ? ' NOT NULL' : ''}`);
|
|
206
268
|
const defs = [
|
|
207
|
-
|
|
208
|
-
|
|
269
|
+
partition,
|
|
270
|
+
rowId,
|
|
209
271
|
...(table.materialize
|
|
210
272
|
? table.columns.map((column) => {
|
|
211
273
|
const notNull = column.nullable ? '' : ' NOT NULL';
|
|
212
274
|
return `${quoteIdent(column.name)} ${columnSqlType(column, dialect)}${notNull}`;
|
|
213
275
|
})
|
|
214
276
|
: []),
|
|
215
|
-
|
|
216
|
-
`${quoteIdent(SYNC_SCOPES_COLUMN)} ${scopesType} NOT NULL`,
|
|
217
|
-
`${quoteIdent(SYNC_PAYLOAD_COLUMN)} ${payloadType} NOT NULL`,
|
|
218
|
-
`${quoteIdent(SYNC_COLUMN_VERSIONS_COLUMN)} ${payloadType}`,
|
|
277
|
+
...trailing,
|
|
219
278
|
`PRIMARY KEY (${quoteIdent(SYNC_PARTITION_COLUMN)}, ${quoteIdent(SYNC_ROW_ID_COLUMN)})`,
|
|
220
279
|
];
|
|
221
280
|
return `CREATE TABLE IF NOT EXISTS ${quoteIdent(table.name)} (${defs.join(', ')})`;
|
|
@@ -555,6 +614,42 @@ export function parseLayouts(json) {
|
|
|
555
614
|
return {};
|
|
556
615
|
return JSON.parse(json);
|
|
557
616
|
}
|
|
617
|
+
/**
|
|
618
|
+
* Version equality is not layout equality: a marker written by another build
|
|
619
|
+
* at the same version describes rows the running codec cannot decode. Refuse
|
|
620
|
+
* when the stored layouts disagree with the configured schema, naming the
|
|
621
|
+
* first table (and column) that differs.
|
|
622
|
+
*/
|
|
623
|
+
export function assertStoredLayouts(schema, storedJson) {
|
|
624
|
+
const storedLayouts = parseLayouts(storedJson);
|
|
625
|
+
const configuredLayouts = parseLayouts(layoutsOf(schema));
|
|
626
|
+
for (const [tableName, columns] of Object.entries(configuredLayouts)) {
|
|
627
|
+
const stored = storedLayouts[tableName];
|
|
628
|
+
const columnCount = Math.max(columns.length, stored?.length ?? 0);
|
|
629
|
+
for (let index = 0; index < columnCount; index++) {
|
|
630
|
+
const expected = columns[index];
|
|
631
|
+
const actual = stored?.[index];
|
|
632
|
+
if (expected !== undefined &&
|
|
633
|
+
actual !== undefined &&
|
|
634
|
+
actual.name === expected.name &&
|
|
635
|
+
actual.type === expected.type &&
|
|
636
|
+
actual.nullable === expected.nullable) {
|
|
637
|
+
continue;
|
|
638
|
+
}
|
|
639
|
+
throw new StorageQueryError('sync.storage.stored_layout_mismatch', {
|
|
640
|
+
table: tableName,
|
|
641
|
+
column: expected?.name ?? actual?.name ?? '',
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
for (const tableName of Object.keys(storedLayouts)) {
|
|
646
|
+
if (!(tableName in configuredLayouts)) {
|
|
647
|
+
throw new StorageQueryError('sync.storage.stored_layout_mismatch', {
|
|
648
|
+
table: tableName,
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
}
|
|
558
653
|
/**
|
|
559
654
|
* Tables present in the stored layout but absent from the configured head
|
|
560
655
|
* schema. A schema-version bump retires their relational current-row tables;
|
package/dist/segment-download.js
CHANGED
|
@@ -136,6 +136,14 @@ async function downloadSegment(ctx, request) {
|
|
|
136
136
|
throw syncError('sync.invalid_subscription', 'invalid requested scopes (§3.2)');
|
|
137
137
|
}
|
|
138
138
|
const publication = candidates[candidates.length - 1];
|
|
139
|
+
// RFC 0007: a descriptor minted before this partition declared a backfill
|
|
140
|
+
// checkpoint is not a complete window once the projection becomes
|
|
141
|
+
// checkpointed. SPEC §9 mints segments per schema version, so refuse and
|
|
142
|
+
// let the client re-pull for a fresh descriptor.
|
|
143
|
+
const checkpoints = await ctx.storage.readCheckpoints(ctx.partition);
|
|
144
|
+
if (checkpoints.some((checkpoint) => checkpoint.schemaVersion > publication.schemaVersion)) {
|
|
145
|
+
throw syncError('sync.segment_expired', 'segment predates the partition backfill checkpoint — re-pull (§5.1)');
|
|
146
|
+
}
|
|
139
147
|
return {
|
|
140
148
|
// The returned record is projected onto the selected publication, so its
|
|
141
149
|
// `scopeDigests`/`publications` compatibility view cannot union a grant
|
package/dist/sqlite-dialect.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ import type { StoredChange, StoredCommit, StoredPushResult, StoredRow } from './
|
|
|
36
36
|
* inverted index. Declared here so every SQLite-family storage (and D1,
|
|
37
37
|
* which applies this same DDL) keeps the same plan shape as Postgres.
|
|
38
38
|
*/
|
|
39
|
-
export declare const SQLITE_DDL = "\nCREATE TABLE IF NOT EXISTS sync_partitions(\n partition TEXT PRIMARY KEY,\n max_commit_seq INTEGER NOT NULL DEFAULT 0,\n horizon_seq INTEGER NOT NULL DEFAULT 0\n);\nCREATE TABLE IF NOT EXISTS sync_partition_registry(\n partition TEXT PRIMARY KEY,\n log_epoch TEXT NOT NULL,\n epoch_required INTEGER NOT NULL DEFAULT 0,\n last_authenticated_at_ms INTEGER NOT NULL\n);\nCREATE TABLE IF NOT EXISTS sync_row_scopes(\n partition TEXT NOT NULL, tbl TEXT NOT NULL,\n var TEXT NOT NULL, value TEXT NOT NULL, row_id TEXT NOT NULL,\n PRIMARY KEY(partition, tbl, var, value, row_id)\n);\nCREATE INDEX IF NOT EXISTS sync_row_scopes_by_row\n ON sync_row_scopes(partition, tbl, row_id);\nCREATE TABLE IF NOT EXISTS sync_commits(\n partition TEXT NOT NULL, commit_seq INTEGER NOT NULL,\n client_id TEXT NOT NULL, client_commit_id TEXT NOT NULL,\n actor_id TEXT NOT NULL, created_at_ms INTEGER NOT NULL,\n PRIMARY KEY(partition, commit_seq)\n);\nCREATE INDEX IF NOT EXISTS sync_commits_by_time\n ON sync_commits(partition, created_at_ms);\nCREATE TABLE IF NOT EXISTS sync_changes(\n partition TEXT NOT NULL, commit_seq INTEGER NOT NULL, idx INTEGER NOT NULL,\n tbl TEXT NOT NULL, row_id TEXT NOT NULL, op INTEGER NOT NULL,\n row_version INTEGER, scopes TEXT NOT NULL, payload BLOB,\n PRIMARY KEY(partition, commit_seq, idx)\n);\nCREATE INDEX IF NOT EXISTS sync_changes_by_table\n ON sync_changes(partition, commit_seq, tbl, idx);\nCREATE TABLE IF NOT EXISTS sync_change_scopes(\n partition TEXT NOT NULL, tbl TEXT NOT NULL,\n var TEXT NOT NULL, value TEXT NOT NULL, commit_seq INTEGER NOT NULL,\n PRIMARY KEY(partition, tbl, var, value, commit_seq)\n);\nCREATE TABLE IF NOT EXISTS sync_push_results(\n partition TEXT NOT NULL, client_id TEXT NOT NULL,\n client_commit_id TEXT NOT NULL, result TEXT NOT NULL,\n PRIMARY KEY(partition, client_id, client_commit_id)\n);\nCREATE TABLE IF NOT EXISTS sync_reactions(\n partition TEXT NOT NULL, idempotency_key TEXT NOT NULL,\n type TEXT NOT NULL, version INTEGER NOT NULL, payload TEXT NOT NULL,\n source_client_id TEXT NOT NULL, source_client_commit_id TEXT NOT NULL,\n source_commit_seq INTEGER NOT NULL, created_at_ms INTEGER NOT NULL,\n available_at_ms INTEGER NOT NULL, status TEXT NOT NULL,\n attempts INTEGER NOT NULL, max_attempts INTEGER NOT NULL,\n lease_owner TEXT, lease_expires_at_ms INTEGER, completed_at_ms INTEGER,\n last_failure TEXT,\n PRIMARY KEY(partition, idempotency_key),\n CHECK(status IN ('pending', 'leased', 'completed', 'dead-letter'))\n);\nCREATE INDEX IF NOT EXISTS sync_reactions_due\n ON sync_reactions(partition, status, available_at_ms, created_at_ms, idempotency_key);\nCREATE INDEX IF NOT EXISTS sync_reactions_lease\n ON sync_reactions(partition, status, lease_expires_at_ms);\nCREATE INDEX IF NOT EXISTS sync_reactions_completed\n ON sync_reactions(partition, status, completed_at_ms, idempotency_key);\nCREATE INDEX IF NOT EXISTS sync_reactions_dead_letter\n ON sync_reactions(partition, status, available_at_ms, idempotency_key);\nCREATE TABLE IF NOT EXISTS sync_clients(\n partition TEXT NOT NULL, client_id TEXT NOT NULL, actor_id TEXT NOT NULL,\n wire_version INTEGER NOT NULL DEFAULT 1,\n cursor INTEGER NOT NULL, subscriptions TEXT NOT NULL,\n updated_at_ms INTEGER NOT NULL,\n PRIMARY KEY(partition, client_id)\n);\nCREATE TABLE IF NOT EXISTS sync_blob_refs(\n partition TEXT NOT NULL, tbl TEXT NOT NULL, row_id TEXT NOT NULL,\n blob_id TEXT NOT NULL,\n PRIMARY KEY(partition, tbl, row_id, blob_id)\n);\nCREATE INDEX IF NOT EXISTS sync_blob_refs_by_blob\n ON sync_blob_refs(partition, blob_id);\nCREATE TABLE IF NOT EXISTS sync_tombstones(\n partition TEXT NOT NULL, tbl TEXT NOT NULL, row_id TEXT NOT NULL,\n commit_seq INTEGER NOT NULL,\n PRIMARY KEY(partition, tbl, row_id)\n);\n";
|
|
39
|
+
export declare const SQLITE_DDL = "\nCREATE TABLE IF NOT EXISTS sync_partitions(\n partition TEXT PRIMARY KEY,\n max_commit_seq INTEGER NOT NULL DEFAULT 0,\n horizon_seq INTEGER NOT NULL DEFAULT 0\n);\nCREATE TABLE IF NOT EXISTS sync_partition_registry(\n partition TEXT PRIMARY KEY,\n log_epoch TEXT NOT NULL,\n epoch_required INTEGER NOT NULL DEFAULT 0,\n last_authenticated_at_ms INTEGER NOT NULL\n);\nCREATE TABLE IF NOT EXISTS sync_row_scopes(\n partition TEXT NOT NULL, tbl TEXT NOT NULL,\n var TEXT NOT NULL, value TEXT NOT NULL, row_id TEXT NOT NULL,\n PRIMARY KEY(partition, tbl, var, value, row_id)\n);\nCREATE INDEX IF NOT EXISTS sync_row_scopes_by_row\n ON sync_row_scopes(partition, tbl, row_id);\nCREATE TABLE IF NOT EXISTS sync_commits(\n partition TEXT NOT NULL, commit_seq INTEGER NOT NULL,\n client_id TEXT NOT NULL, client_commit_id TEXT NOT NULL,\n actor_id TEXT NOT NULL, created_at_ms INTEGER NOT NULL,\n writer_version INTEGER,\n PRIMARY KEY(partition, commit_seq)\n);\nCREATE INDEX IF NOT EXISTS sync_commits_by_time\n ON sync_commits(partition, created_at_ms);\nCREATE TABLE IF NOT EXISTS sync_changes(\n partition TEXT NOT NULL, commit_seq INTEGER NOT NULL, idx INTEGER NOT NULL,\n tbl TEXT NOT NULL, row_id TEXT NOT NULL, op INTEGER NOT NULL,\n row_version INTEGER, scopes TEXT NOT NULL, payload BLOB,\n PRIMARY KEY(partition, commit_seq, idx)\n);\nCREATE INDEX IF NOT EXISTS sync_changes_by_table\n ON sync_changes(partition, commit_seq, tbl, idx);\nCREATE TABLE IF NOT EXISTS sync_change_scopes(\n partition TEXT NOT NULL, tbl TEXT NOT NULL,\n var TEXT NOT NULL, value TEXT NOT NULL, commit_seq INTEGER NOT NULL,\n PRIMARY KEY(partition, tbl, var, value, commit_seq)\n);\nCREATE TABLE IF NOT EXISTS sync_push_results(\n partition TEXT NOT NULL, client_id TEXT NOT NULL,\n client_commit_id TEXT NOT NULL, result TEXT NOT NULL,\n PRIMARY KEY(partition, client_id, client_commit_id)\n);\nCREATE TABLE IF NOT EXISTS sync_reactions(\n partition TEXT NOT NULL, idempotency_key TEXT NOT NULL,\n type TEXT NOT NULL, version INTEGER NOT NULL, payload TEXT NOT NULL,\n source_client_id TEXT NOT NULL, source_client_commit_id TEXT NOT NULL,\n source_commit_seq INTEGER NOT NULL, created_at_ms INTEGER NOT NULL,\n available_at_ms INTEGER NOT NULL, status TEXT NOT NULL,\n attempts INTEGER NOT NULL, max_attempts INTEGER NOT NULL,\n lease_owner TEXT, lease_expires_at_ms INTEGER, completed_at_ms INTEGER,\n last_failure TEXT,\n PRIMARY KEY(partition, idempotency_key),\n CHECK(status IN ('pending', 'leased', 'completed', 'dead-letter'))\n);\nCREATE INDEX IF NOT EXISTS sync_reactions_due\n ON sync_reactions(partition, status, available_at_ms, created_at_ms, idempotency_key);\nCREATE INDEX IF NOT EXISTS sync_reactions_lease\n ON sync_reactions(partition, status, lease_expires_at_ms);\nCREATE INDEX IF NOT EXISTS sync_reactions_completed\n ON sync_reactions(partition, status, completed_at_ms, idempotency_key);\nCREATE INDEX IF NOT EXISTS sync_reactions_dead_letter\n ON sync_reactions(partition, status, available_at_ms, idempotency_key);\nCREATE TABLE IF NOT EXISTS sync_clients(\n partition TEXT NOT NULL, client_id TEXT NOT NULL, actor_id TEXT NOT NULL,\n wire_version INTEGER NOT NULL DEFAULT 1,\n cursor INTEGER NOT NULL, subscriptions TEXT NOT NULL,\n updated_at_ms INTEGER NOT NULL,\n PRIMARY KEY(partition, client_id)\n);\nCREATE TABLE IF NOT EXISTS sync_blob_refs(\n partition TEXT NOT NULL, tbl TEXT NOT NULL, row_id TEXT NOT NULL,\n blob_id TEXT NOT NULL,\n PRIMARY KEY(partition, tbl, row_id, blob_id)\n);\nCREATE INDEX IF NOT EXISTS sync_blob_refs_by_blob\n ON sync_blob_refs(partition, blob_id);\nCREATE TABLE IF NOT EXISTS sync_tombstones(\n partition TEXT NOT NULL, tbl TEXT NOT NULL, row_id TEXT NOT NULL,\n commit_seq INTEGER NOT NULL,\n PRIMARY KEY(partition, tbl, row_id)\n);\nCREATE TABLE IF NOT EXISTS sync_backfill_checkpoints(\n partition TEXT NOT NULL, name TEXT NOT NULL,\n schema_version INTEGER NOT NULL,\n state TEXT NOT NULL CHECK(state IN ('declared','backfilling','activated')),\n watermark INTEGER NOT NULL DEFAULT 0,\n owner_epoch INTEGER NOT NULL DEFAULT 0,\n observed_rows INTEGER NOT NULL DEFAULT 0,\n updated_at_ms INTEGER NOT NULL,\n PRIMARY KEY(partition, name)\n);\nCREATE TABLE IF NOT EXISTS sync_writer_fence(\n partition TEXT PRIMARY KEY,\n required_writer_version INTEGER NOT NULL\n);\n";
|
|
40
40
|
/** Split the DDL into individual statements (D1 applies them one by one). */
|
|
41
41
|
export declare function sqliteDdlStatements(): string[];
|
|
42
42
|
/** `?,?,…` for an `IN (…)` clause of `count` positional parameters. */
|
package/dist/sqlite-dialect.js
CHANGED
|
@@ -41,6 +41,7 @@ CREATE TABLE IF NOT EXISTS sync_commits(
|
|
|
41
41
|
partition TEXT NOT NULL, commit_seq INTEGER NOT NULL,
|
|
42
42
|
client_id TEXT NOT NULL, client_commit_id TEXT NOT NULL,
|
|
43
43
|
actor_id TEXT NOT NULL, created_at_ms INTEGER NOT NULL,
|
|
44
|
+
writer_version INTEGER,
|
|
44
45
|
PRIMARY KEY(partition, commit_seq)
|
|
45
46
|
);
|
|
46
47
|
CREATE INDEX IF NOT EXISTS sync_commits_by_time
|
|
@@ -102,6 +103,20 @@ CREATE TABLE IF NOT EXISTS sync_tombstones(
|
|
|
102
103
|
commit_seq INTEGER NOT NULL,
|
|
103
104
|
PRIMARY KEY(partition, tbl, row_id)
|
|
104
105
|
);
|
|
106
|
+
CREATE TABLE IF NOT EXISTS sync_backfill_checkpoints(
|
|
107
|
+
partition TEXT NOT NULL, name TEXT NOT NULL,
|
|
108
|
+
schema_version INTEGER NOT NULL,
|
|
109
|
+
state TEXT NOT NULL CHECK(state IN ('declared','backfilling','activated')),
|
|
110
|
+
watermark INTEGER NOT NULL DEFAULT 0,
|
|
111
|
+
owner_epoch INTEGER NOT NULL DEFAULT 0,
|
|
112
|
+
observed_rows INTEGER NOT NULL DEFAULT 0,
|
|
113
|
+
updated_at_ms INTEGER NOT NULL,
|
|
114
|
+
PRIMARY KEY(partition, name)
|
|
115
|
+
);
|
|
116
|
+
CREATE TABLE IF NOT EXISTS sync_writer_fence(
|
|
117
|
+
partition TEXT PRIMARY KEY,
|
|
118
|
+
required_writer_version INTEGER NOT NULL
|
|
119
|
+
);
|
|
105
120
|
`;
|
|
106
121
|
/** Split the DDL into individual statements (D1 applies them one by one). */
|
|
107
122
|
export function sqliteDdlStatements() {
|
package/dist/sqlite-storage.d.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { CommitPruneQuery, CommitPruneResult } from './storage.js';
|
|
2
2
|
import type { CompiledSchema, CompiledTable } from './schema.js';
|
|
3
3
|
import { type SqliteDatabase } from './sqlite-driver.js';
|
|
4
|
-
import type { AuthoritativeQueryRequest, AuthoritativeQueryResult, ClientCursorInfo, ClientRecord, CommitMetadata, CommitMetadataQuery, CommitWindowQuery, IndexRowScanQuery, PartitionRegistryEntry, PrunedReactionCounts, ReactionClaimQuery, ReactionFailureUpdate, ReactionListQuery, ReactionPruneQuery, RowScanQuery, ScopeActivityQuery, ScopeCommitActivity, ServerStorage, StorageTransaction, StoredCommit, StoredPushResult, StoredReaction, StoredRow } from './storage.js';
|
|
4
|
+
import type { AuthoritativeQueryRequest, AuthoritativeQueryResult, CheckpointDeclaration, ClientCursorInfo, ClientRecord, CommitMetadata, CommitMetadataQuery, CommitWindowQuery, IndexRowScanQuery, PartitionRegistryEntry, PrunedReactionCounts, ReactionClaimQuery, ReactionFailureUpdate, ReactionListQuery, ReactionPruneQuery, RowScanQuery, ScopeActivityQuery, ScopeCommitActivity, ServerStorage, ServeGate, StorageTransaction, StoredCommit, StoredCheckpoint, StoredPushResult, StoredReaction, StoredRow } from './storage.js';
|
|
5
5
|
export declare class SqliteServerStorage implements ServerStorage {
|
|
6
6
|
#private;
|
|
7
7
|
readonly db: SqliteDatabase;
|
|
8
8
|
constructor(db?: SqliteDatabase | string);
|
|
9
9
|
/** Resolve a table's compiled schema; row operations require `ensureSchema`. */
|
|
10
10
|
table(name: string): CompiledTable;
|
|
11
|
-
ensureSchema(schema: CompiledSchema): Promise<void>;
|
|
11
|
+
ensureSchema(schema: CompiledSchema, checkpoints?: readonly CheckpointDeclaration[]): Promise<void>;
|
|
12
12
|
touchPartition(partition: string, authenticatedAtMs: number, initialLogEpoch: string): Promise<PartitionRegistryEntry>;
|
|
13
13
|
rotatePartitionLogEpoch(partition: string, logEpoch: string, authenticatedAtMs: number): Promise<PartitionRegistryEntry>;
|
|
14
14
|
listPartitionRegistry(): Promise<PartitionRegistryEntry[]>;
|
|
@@ -16,12 +16,27 @@ export declare class SqliteServerStorage implements ServerStorage {
|
|
|
16
16
|
/** Internal: write a row + refresh its scope-index entries. */
|
|
17
17
|
writeRow(partition: string, table: string, row: StoredRow): void;
|
|
18
18
|
getMaxCommitSeq(partition: string): Promise<number>;
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Internal: rewrite and run one registered query on the shared connection.
|
|
21
|
+
* The caller owns the transaction: `queryAuthoritative` opens a read
|
|
22
|
+
* transaction behind the FIFO; `SqliteTransaction.queryAuthoritative` runs
|
|
23
|
+
* inside the push transaction's `BEGIN IMMEDIATE`.
|
|
24
|
+
*/
|
|
25
|
+
runAuthoritativeQuery(partition: string, query: AuthoritativeQueryRequest): AuthoritativeQueryResult;
|
|
26
|
+
queryAuthoritative(partition: string, query: AuthoritativeQueryRequest, checkpoints?: readonly CheckpointDeclaration[]): Promise<AuthoritativeQueryResult>;
|
|
20
27
|
getPartitionLogEpoch(partition: string): Promise<string | undefined>;
|
|
21
28
|
getHorizonSeq(partition: string): Promise<number>;
|
|
22
29
|
setHorizonSeq(partition: string, seq: number): Promise<void>;
|
|
23
30
|
pruneCommitsThrough(partition: string, query: CommitPruneQuery): Promise<CommitPruneResult>;
|
|
24
31
|
getCommitSeqBefore(partition: string, createdBeforeMs: number): Promise<number>;
|
|
32
|
+
readServeGate(partition: string, runningSchemaVersion: number): Promise<ServeGate>;
|
|
33
|
+
readCheckpoints(partition: string): Promise<StoredCheckpoint[]>;
|
|
34
|
+
declareCheckpoint(partition: string, name: string, schemaVersion: number, nowMs: number): Promise<StoredCheckpoint>;
|
|
35
|
+
activateCheckpoint(partition: string, name: string, ownerEpoch: number, watermark: number, sources: readonly string[], nowMs: number): Promise<'activated' | 'stale' | 'unverifiable'>;
|
|
36
|
+
claimCheckpoint(partition: string, name: string, schemaVersion: number, nowMs: number): Promise<StoredCheckpoint>;
|
|
37
|
+
advanceCheckpoint(partition: string, name: string, ownerEpoch: number, watermark: number, observedRows: number, nowMs: number): Promise<boolean>;
|
|
38
|
+
sourceCoverageSeq(partition: string, tables: readonly string[]): Promise<number>;
|
|
39
|
+
hasSourceChangesAbove(partition: string, tables: readonly string[], seq: number): Promise<'clean' | 'changed' | 'unverifiable'>;
|
|
25
40
|
getRow(partition: string, table: string, rowId: string): Promise<StoredRow | undefined>;
|
|
26
41
|
getPushResult(partition: string, clientId: string, clientCommitId: string): Promise<StoredPushResult | undefined>;
|
|
27
42
|
claimReactions(partition: string, query: ReactionClaimQuery): Promise<StoredReaction[]>;
|