@quereus/store 3.3.0 → 4.0.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.
Files changed (48) hide show
  1. package/README.md +436 -317
  2. package/dist/src/common/backing-host.d.ts +198 -0
  3. package/dist/src/common/backing-host.d.ts.map +1 -0
  4. package/dist/src/common/backing-host.js +452 -0
  5. package/dist/src/common/backing-host.js.map +1 -0
  6. package/dist/src/common/bytes.d.ts +16 -0
  7. package/dist/src/common/bytes.d.ts.map +1 -0
  8. package/dist/src/common/bytes.js +33 -0
  9. package/dist/src/common/bytes.js.map +1 -0
  10. package/dist/src/common/cached-kv-store.d.ts +3 -3
  11. package/dist/src/common/cached-kv-store.d.ts.map +1 -1
  12. package/dist/src/common/cached-kv-store.js +6 -4
  13. package/dist/src/common/cached-kv-store.js.map +1 -1
  14. package/dist/src/common/encoding.d.ts +9 -1
  15. package/dist/src/common/encoding.d.ts.map +1 -1
  16. package/dist/src/common/encoding.js +12 -2
  17. package/dist/src/common/encoding.js.map +1 -1
  18. package/dist/src/common/index.d.ts +8 -6
  19. package/dist/src/common/index.d.ts.map +1 -1
  20. package/dist/src/common/index.js +7 -3
  21. package/dist/src/common/index.js.map +1 -1
  22. package/dist/src/common/key-builder.d.ts +125 -4
  23. package/dist/src/common/key-builder.d.ts.map +1 -1
  24. package/dist/src/common/key-builder.js +186 -9
  25. package/dist/src/common/key-builder.js.map +1 -1
  26. package/dist/src/common/kv-store.d.ts +75 -4
  27. package/dist/src/common/kv-store.d.ts.map +1 -1
  28. package/dist/src/common/memory-store.d.ts +3 -3
  29. package/dist/src/common/memory-store.d.ts.map +1 -1
  30. package/dist/src/common/memory-store.js +4 -2
  31. package/dist/src/common/memory-store.js.map +1 -1
  32. package/dist/src/common/store-connection.d.ts +11 -4
  33. package/dist/src/common/store-connection.d.ts.map +1 -1
  34. package/dist/src/common/store-connection.js +12 -6
  35. package/dist/src/common/store-connection.js.map +1 -1
  36. package/dist/src/common/store-module.d.ts +580 -20
  37. package/dist/src/common/store-module.d.ts.map +1 -1
  38. package/dist/src/common/store-module.js +1590 -135
  39. package/dist/src/common/store-module.js.map +1 -1
  40. package/dist/src/common/store-table.d.ts +347 -14
  41. package/dist/src/common/store-table.d.ts.map +1 -1
  42. package/dist/src/common/store-table.js +751 -98
  43. package/dist/src/common/store-table.js.map +1 -1
  44. package/dist/src/common/transaction.d.ts +135 -27
  45. package/dist/src/common/transaction.d.ts.map +1 -1
  46. package/dist/src/common/transaction.js +216 -55
  47. package/dist/src/common/transaction.js.map +1 -1
  48. package/package.json +5 -5
@@ -0,0 +1,198 @@
1
+ /**
2
+ * Store implementation of the engine's backing-host capability
3
+ * (`@quereus/quereus` `vtab/backing-host.ts`) — the privileged surface that lets
4
+ * a materialized view's backing table live in a persistent store
5
+ * (`create materialized view … using store`). The memory module's
6
+ * `MemoryBackingHost` is the reference implementation; this host reproduces its
7
+ * contract over a (StoreTable, TransactionCoordinator) pair.
8
+ *
9
+ * ## Pending state = the per-table coordinator
10
+ *
11
+ * Where the memory host writes a connection's private pending
12
+ * `TransactionLayer`, this host queues ops into the module's shared
13
+ * {@link TransactionCoordinator} (addressed by the backing table's data-store
14
+ * handle) — the same pending state `StoreTable`'s read paths merge over the
15
+ * committed store (reads-own-writes). The visibility granularity is therefore
16
+ * per-module-coordinator, not per-connection: every connection this host
17
+ * creates shares one pending view. That satisfies the
18
+ * contract (later reads on the writing connection observe the ops) and is the
19
+ * store's documented RYOW posture; the per-connection invisibility the memory
20
+ * host happens to provide is an isolation property, not a contract point. Under
21
+ * the registered `IsolationModule(StoreModule)` wrapper all backing writes are
22
+ * privileged (they bypass the per-connection overlay entirely), so the single
23
+ * engine transaction sees exactly one coherent pending state.
24
+ *
25
+ * ## Incarnation pinning
26
+ *
27
+ * One host instance is bound to one StoreTable instance = one backing-table
28
+ * incarnation. The coordinator is now module-wide (shared by every table), so
29
+ * coordinator identity can no longer distinguish incarnations; pinning moves to
30
+ * the StoreTable instance. `StoreModule.destroy` evicts the table from
31
+ * `this.tables`, so a drop+recreate yields a fresh StoreTable and
32
+ * `ownsConnection`'s `conn.owner === this.table` comparison rejects the old
33
+ * incarnation's connections (whose `owner` is the evicted StoreTable) — and a
34
+ * foreign module's connection (not a StoreConnection, or a different owner)
35
+ * alike. Memory-parity pinning.
36
+ *
37
+ * ## Events: off by default, opt-in per table
38
+ *
39
+ * Privileged writes queue NO store `DataChangeEvent`s **by default**: the
40
+ * MV-over-MV cascade consumes the returned {@link BackingRowChange}s directly,
41
+ * and a local derived table (covering index, perf cache) must not replicate its
42
+ * rows (the sources replicate; each replica derives). The exception is a
43
+ * **migration target**, whose derived rows must reach old / never-upgrading
44
+ * peers that store the new table opaquely: a backing carrying the reserved tag
45
+ * `quereus.sync.replicate = true` (engine `SYNC_REPLICATE_TAG`) opts its
46
+ * maintenance writes into change recording — the host queues one local (non-
47
+ * `remote`) `DataChangeEvent` per realized {@link BackingRowChange}, so the sync
48
+ * layer records column versions / HLC stamps / tombstones exactly as for an
49
+ * ordinary table write. The value-identical-upsert suppression contract means a
50
+ * re-derivation that changes nothing emits no change and hence no event, so the
51
+ * echo loop closes itself (docs/migration.md § Synced vs. local derived tables).
52
+ * Create-fill / refresh (`replaceContents`) on a replicate-opted-in backing
53
+ * publishes the **minimal keyed diff against the committed contents** — so a
54
+ * value-identical re-fill emits nothing (the same suppression the point-op and
55
+ * `replace-all` arms have): N upgraded peers that each re-derive the same fill
56
+ * diff to zero deltas, only the first author of a cold row publishes it. A
57
+ * non-replicating backing stays event-free and byte-identical (its streaming
58
+ * direct-batch path is untouched).
59
+ *
60
+ * ## No secondary structures
61
+ *
62
+ * MV-sugar backings carry no secondary indexes, UNIQUE
63
+ * constraints, or FKs (`buildBackingTableSchema` builds none), so the host
64
+ * writes the data store only. A `create table … maintained as` backing DOES
65
+ * carry its declared constraints: CHECK / FK are validated engine-side, and
66
+ * declared secondary UNIQUEs are enforced here, post-batch, via
67
+ * {@link StoreTable.enforceSecondaryUniqueForMaintenance} (the store keeps no
68
+ * index store for UNIQUE anyway — enforcement is the same effective scan its
69
+ * DML path uses). See the engine's `vtab/backing-host.ts` § Constraint
70
+ * validation.
71
+ */
72
+ import { QuereusError, type Row, type BackingHost, type BackingScanRequest, type BackingRowChange, type MaintenanceOp, type VirtualTableConnection } from '@quereus/quereus';
73
+ import type { StoreTable } from './store-table.js';
74
+ import type { TransactionCoordinator } from './transaction.js';
75
+ /**
76
+ * Privileged per-backing-table surface over a store table — see the module
77
+ * header for the design. Resolved fresh per engine call by
78
+ * `StoreModule.getBackingHost`; never cached engine-side, so the captured
79
+ * (table, coordinator) identity always reflects the live incarnation.
80
+ */
81
+ export declare class StoreBackingHost implements BackingHost {
82
+ private readonly table;
83
+ private readonly coordinator;
84
+ constructor(table: StoreTable, coordinator: TransactionCoordinator);
85
+ /**
86
+ * True when `conn` is a StoreConnection THIS host's backing table owns.
87
+ * The coordinator is module-wide (shared by every table), so it can no longer
88
+ * pin an incarnation; ownership is the StoreTable instance, set on the
89
+ * connection's `owner` at {@link connect} time. `this.table` is evicted from
90
+ * the module on destroy, so identity comparison rejects another table's
91
+ * connection, a stale connection from a dropped+recreated incarnation (its
92
+ * `owner` is the evicted StoreTable), and a foreign module's connection (not a
93
+ * StoreConnection) alike.
94
+ */
95
+ ownsConnection(conn: VirtualTableConnection): boolean;
96
+ /**
97
+ * Fresh connection owned by this incarnation's StoreTable, on the module
98
+ * coordinator. Synchronous by design (the coordinator is handle-addressed, so
99
+ * never store-opening). The caller registers it with the Database;
100
+ * `registerConnection`'s begin + savepoint-stack replay then drives the
101
+ * coordinator into the live transaction state.
102
+ */
103
+ connect(): VirtualTableConnection;
104
+ /**
105
+ * Privileged ordered op application into the coordinator's pending state.
106
+ * Begins an implicit coordinator transaction when none is active (the lazy
107
+ * analogue of memory's `ensureTransactionLayer`); commit/rollback ride the
108
+ * registered connection. Before-images come from an effective point read
109
+ * (pending view → committed `store.get`) per op — one O(log n) read per point
110
+ * op; the store's write path doesn't otherwise know the image, and reporting
111
+ * the EFFECTIVE change is the cascade contract. Stats deltas are buffered via
112
+ * `trackPrivilegedMutation` and land at coordinator commit.
113
+ */
114
+ applyMaintenance(conn: VirtualTableConnection, ops: readonly MaintenanceOp[]): Promise<BackingRowChange[]>;
115
+ /** True when this backing opts its maintenance writes into the sync change log. */
116
+ private get replicates();
117
+ /**
118
+ * Map a realized {@link BackingRowChange} to the store {@link DataChangeEvent}
119
+ * shape, mirroring the ordinary StoreTable DML events (`store-table.ts` insert
120
+ * / update / delete). `remote` is left unset (these are local derivations — an
121
+ * inbound sync write is a different path); `changedColumns` is omitted because
122
+ * the sync layer recomputes the per-column diff from `oldRow`/`newRow` itself,
123
+ * parity with the store's own update event.
124
+ */
125
+ private toDataChangeEvent;
126
+ /**
127
+ * The `replace-all` arm: wholesale transactional replacement realized as the
128
+ * minimal keyed diff against the current effective contents. Keys are compared
129
+ * by ENCODED data-key bytes, which fold each PK column's key collation — so a
130
+ * new row whose key differs only by collation (e.g. 'apple' vs a stored 'APPLE'
131
+ * under a NOCASE-keyed PK) matches its old row and resolves to an `update`,
132
+ * mirroring memory's PK-comparator diff. Collation governs KEY identity only:
133
+ * a paired row is skipped (no storage churn, no emitted change) ONLY when its
134
+ * VALUE is byte-faithful-identical (`rowsValueIdentical`), exactly as
135
+ * `applyMaintenanceToLayer` skips them — so a collation-equal / byte-different
136
+ * paired row is an `update` that re-keys the stored bytes.
137
+ */
138
+ private applyReplaceAll;
139
+ /**
140
+ * Atomically replace the COMMITTED contents (create-fill / refresh).
141
+ *
142
+ * If the coordinator holds an open transaction, it is committed FIRST: a
143
+ * committed bulk replace inside an explicit transaction is effectively
144
+ * DDL-committing on a store-backed table, the same posture `renameTable`
145
+ * takes (and the analogue of memory's `replaceBaseLayer` draining in-flight
146
+ * transaction layers before the base swap).
147
+ *
148
+ * Duplicate encoded data keys among `rows` are detected BEFORE any write, so a
149
+ * thrown `onDuplicateKey()` (the MV "must be a set" gate) leaves the committed
150
+ * contents untorn — and, on a replicating backing, queues no events. The clear
151
+ * + rewrite then rides ONE provider batch (deletes of displaced old keys, then
152
+ * puts), so a provider with atomic batches gives concurrent readers pre- or
153
+ * post-swap state, never partial. Routed through the table's `openDataStore()`
154
+ * so the lazy first-access `saveTableDDL` fires — the catalog write a freshly
155
+ * created `using store` backing relies on to survive reopen. Stats reset to the
156
+ * exact new count.
157
+ *
158
+ * Replication seam (opt-in only): when this backing carries
159
+ * `quereus.sync.replicate = true`, the bulk path additionally diffs `rows`
160
+ * against the COMMITTED before-image and queues ONE `DataChangeEvent` per
161
+ * genuine insert / update / delete — and NOTHING for a byte-faithful-identical
162
+ * key, exactly like {@link applyReplaceAll}. That restores suppression to the
163
+ * fill path: a value-identical re-derivation (the same fill computed by another
164
+ * upgraded peer) diffs to zero deltas and emits nothing, so only the first
165
+ * author of a cold row publishes it (so it reaches never-upgrading old peers
166
+ * that store this backing opaquely). The coordinator is NOT in a transaction at
167
+ * the emit (committed at the top, or never began), so `queueEvent` emits
168
+ * immediately into the store emitter; when the engine drives this mid-
169
+ * transaction (create-fill / refresh under `db._ensureTransaction()`) the
170
+ * emitter is batching, so the deltas flush as one grouped change-set at the
171
+ * engine commit — the same place `applyMaintenance`'s events land.
172
+ *
173
+ * A non-replicating backing keeps the original streaming direct-batch path
174
+ * verbatim (no old-value deserialization, no delta list) — byte-for-byte the
175
+ * prior behavior, zero added cost for the common local-derivation case.
176
+ */
177
+ replaceContents(rows: readonly Row[], onDuplicateKey?: () => QuereusError): Promise<void>;
178
+ /**
179
+ * Reads-own-writes scan over the effective state (coordinator pending merged
180
+ * over committed), in PK order. `equalityPrefix` (leading-PK values, in
181
+ * PK-definition order) becomes encoded byte bounds — seek + early-terminate,
182
+ * the O(log n) prefix scan the cost contract requires, never a full-store
183
+ * visit. The ownership guard runs eagerly (before the first pull), matching
184
+ * the memory host's synchronous INTERNAL throw on a foreign connection.
185
+ */
186
+ scanEffective(conn: VirtualTableConnection, req: BackingScanRequest): AsyncIterable<Row>;
187
+ /** INTERNAL guard: the privileged surface only accepts this incarnation's connections. */
188
+ private assertOwned;
189
+ /** PK values (in PK-definition order) extracted from a full backing row. */
190
+ private extractPk;
191
+ /**
192
+ * Normalize the engine's `BTreeKeyForPrimary` (bare value for a single-column
193
+ * PK — see `buildPrimaryKeyFromValues` — array otherwise) to a values array in
194
+ * PK-definition order, ready for {@link StoreTable.encodeDataKey}.
195
+ */
196
+ private normalizePkValues;
197
+ }
198
+ //# sourceMappingURL=backing-host.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backing-host.d.ts","sourceRoot":"","sources":["../../../src/common/backing-host.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsEG;AAEH,OAAO,EACN,YAAY,EAIZ,KAAK,GAAG,EAGR,KAAK,WAAW,EAChB,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EACrB,KAAK,aAAa,EAClB,KAAK,sBAAsB,EAC3B,MAAM,kBAAkB,CAAC;AAE1B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AACnD,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAW/D;;;;;GAKG;AACH,qBAAa,gBAAiB,YAAW,WAAW;IAElD,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,WAAW;gBADX,KAAK,EAAE,UAAU,EACjB,WAAW,EAAE,sBAAsB;IAGrD;;;;;;;;;OASG;IACH,cAAc,CAAC,IAAI,EAAE,sBAAsB,GAAG,OAAO;IAIrD;;;;;;OAMG;IACH,OAAO,IAAI,sBAAsB;IAIjC;;;;;;;;;OASG;IACG,gBAAgB,CAAC,IAAI,EAAE,sBAAsB,EAAE,GAAG,EAAE,SAAS,aAAa,EAAE,GAAG,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAiGhH,mFAAmF;IACnF,OAAO,KAAK,UAAU,GAErB;IAED;;;;;;;OAOG;IACH,OAAO,CAAC,iBAAiB;IAYzB;;;;;;;;;;;OAWG;YACW,eAAe;IAqC7B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAqCG;IACG,eAAe,CAAC,IAAI,EAAE,SAAS,GAAG,EAAE,EAAE,cAAc,CAAC,EAAE,MAAM,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAyF/F;;;;;;;OAOG;IACH,aAAa,CAAC,IAAI,EAAE,sBAAsB,EAAE,GAAG,EAAE,kBAAkB,GAAG,aAAa,CAAC,GAAG,CAAC;IAYxF,0FAA0F;IAC1F,OAAO,CAAC,WAAW;IAUnB,4EAA4E;IAC5E,OAAO,CAAC,SAAS;IAIjB;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;CAOzB"}
@@ -0,0 +1,452 @@
1
+ /**
2
+ * Store implementation of the engine's backing-host capability
3
+ * (`@quereus/quereus` `vtab/backing-host.ts`) — the privileged surface that lets
4
+ * a materialized view's backing table live in a persistent store
5
+ * (`create materialized view … using store`). The memory module's
6
+ * `MemoryBackingHost` is the reference implementation; this host reproduces its
7
+ * contract over a (StoreTable, TransactionCoordinator) pair.
8
+ *
9
+ * ## Pending state = the per-table coordinator
10
+ *
11
+ * Where the memory host writes a connection's private pending
12
+ * `TransactionLayer`, this host queues ops into the module's shared
13
+ * {@link TransactionCoordinator} (addressed by the backing table's data-store
14
+ * handle) — the same pending state `StoreTable`'s read paths merge over the
15
+ * committed store (reads-own-writes). The visibility granularity is therefore
16
+ * per-module-coordinator, not per-connection: every connection this host
17
+ * creates shares one pending view. That satisfies the
18
+ * contract (later reads on the writing connection observe the ops) and is the
19
+ * store's documented RYOW posture; the per-connection invisibility the memory
20
+ * host happens to provide is an isolation property, not a contract point. Under
21
+ * the registered `IsolationModule(StoreModule)` wrapper all backing writes are
22
+ * privileged (they bypass the per-connection overlay entirely), so the single
23
+ * engine transaction sees exactly one coherent pending state.
24
+ *
25
+ * ## Incarnation pinning
26
+ *
27
+ * One host instance is bound to one StoreTable instance = one backing-table
28
+ * incarnation. The coordinator is now module-wide (shared by every table), so
29
+ * coordinator identity can no longer distinguish incarnations; pinning moves to
30
+ * the StoreTable instance. `StoreModule.destroy` evicts the table from
31
+ * `this.tables`, so a drop+recreate yields a fresh StoreTable and
32
+ * `ownsConnection`'s `conn.owner === this.table` comparison rejects the old
33
+ * incarnation's connections (whose `owner` is the evicted StoreTable) — and a
34
+ * foreign module's connection (not a StoreConnection, or a different owner)
35
+ * alike. Memory-parity pinning.
36
+ *
37
+ * ## Events: off by default, opt-in per table
38
+ *
39
+ * Privileged writes queue NO store `DataChangeEvent`s **by default**: the
40
+ * MV-over-MV cascade consumes the returned {@link BackingRowChange}s directly,
41
+ * and a local derived table (covering index, perf cache) must not replicate its
42
+ * rows (the sources replicate; each replica derives). The exception is a
43
+ * **migration target**, whose derived rows must reach old / never-upgrading
44
+ * peers that store the new table opaquely: a backing carrying the reserved tag
45
+ * `quereus.sync.replicate = true` (engine `SYNC_REPLICATE_TAG`) opts its
46
+ * maintenance writes into change recording — the host queues one local (non-
47
+ * `remote`) `DataChangeEvent` per realized {@link BackingRowChange}, so the sync
48
+ * layer records column versions / HLC stamps / tombstones exactly as for an
49
+ * ordinary table write. The value-identical-upsert suppression contract means a
50
+ * re-derivation that changes nothing emits no change and hence no event, so the
51
+ * echo loop closes itself (docs/migration.md § Synced vs. local derived tables).
52
+ * Create-fill / refresh (`replaceContents`) on a replicate-opted-in backing
53
+ * publishes the **minimal keyed diff against the committed contents** — so a
54
+ * value-identical re-fill emits nothing (the same suppression the point-op and
55
+ * `replace-all` arms have): N upgraded peers that each re-derive the same fill
56
+ * diff to zero deltas, only the first author of a cold row publishes it. A
57
+ * non-replicating backing stays event-free and byte-identical (its streaming
58
+ * direct-batch path is untouched).
59
+ *
60
+ * ## No secondary structures
61
+ *
62
+ * MV-sugar backings carry no secondary indexes, UNIQUE
63
+ * constraints, or FKs (`buildBackingTableSchema` builds none), so the host
64
+ * writes the data store only. A `create table … maintained as` backing DOES
65
+ * carry its declared constraints: CHECK / FK are validated engine-side, and
66
+ * declared secondary UNIQUEs are enforced here, post-batch, via
67
+ * {@link StoreTable.enforceSecondaryUniqueForMaintenance} (the store keeps no
68
+ * index store for UNIQUE anyway — enforcement is the same effective scan its
69
+ * DML path uses). See the engine's `vtab/backing-host.ts` § Constraint
70
+ * validation.
71
+ */
72
+ import { QuereusError, StatusCode, rowsValueIdentical, SYNC_REPLICATE_TAG, } from '@quereus/quereus';
73
+ import { StoreConnection } from './store-connection.js';
74
+ import { bytesToHex } from './bytes.js';
75
+ import { buildFullScanBounds } from './key-builder.js';
76
+ import { serializeRow, deserializeRow } from './serialization.js';
77
+ /**
78
+ * Privileged per-backing-table surface over a store table — see the module
79
+ * header for the design. Resolved fresh per engine call by
80
+ * `StoreModule.getBackingHost`; never cached engine-side, so the captured
81
+ * (table, coordinator) identity always reflects the live incarnation.
82
+ */
83
+ export class StoreBackingHost {
84
+ table;
85
+ coordinator;
86
+ constructor(table, coordinator) {
87
+ this.table = table;
88
+ this.coordinator = coordinator;
89
+ }
90
+ /**
91
+ * True when `conn` is a StoreConnection THIS host's backing table owns.
92
+ * The coordinator is module-wide (shared by every table), so it can no longer
93
+ * pin an incarnation; ownership is the StoreTable instance, set on the
94
+ * connection's `owner` at {@link connect} time. `this.table` is evicted from
95
+ * the module on destroy, so identity comparison rejects another table's
96
+ * connection, a stale connection from a dropped+recreated incarnation (its
97
+ * `owner` is the evicted StoreTable), and a foreign module's connection (not a
98
+ * StoreConnection) alike.
99
+ */
100
+ ownsConnection(conn) {
101
+ return conn instanceof StoreConnection && conn.owner === this.table;
102
+ }
103
+ /**
104
+ * Fresh connection owned by this incarnation's StoreTable, on the module
105
+ * coordinator. Synchronous by design (the coordinator is handle-addressed, so
106
+ * never store-opening). The caller registers it with the Database;
107
+ * `registerConnection`'s begin + savepoint-stack replay then drives the
108
+ * coordinator into the live transaction state.
109
+ */
110
+ connect() {
111
+ return new StoreConnection(`${this.table.schemaName}.${this.table.tableName}`, this.coordinator, this.table);
112
+ }
113
+ /**
114
+ * Privileged ordered op application into the coordinator's pending state.
115
+ * Begins an implicit coordinator transaction when none is active (the lazy
116
+ * analogue of memory's `ensureTransactionLayer`); commit/rollback ride the
117
+ * registered connection. Before-images come from an effective point read
118
+ * (pending view → committed `store.get`) per op — one O(log n) read per point
119
+ * op; the store's write path doesn't otherwise know the image, and reporting
120
+ * the EFFECTIVE change is the cascade contract. Stats deltas are buffered via
121
+ * `trackPrivilegedMutation` and land at coordinator commit.
122
+ */
123
+ async applyMaintenance(conn, ops) {
124
+ this.assertOwned(conn);
125
+ const changes = [];
126
+ if (ops.length === 0)
127
+ return changes;
128
+ // Resolve the backing's data-store handle once. Every coordinator op the
129
+ // backing queues is addressed by this handle (the module coordinator is
130
+ // shared, so each table's ops MUST carry their own store), and it is the
131
+ // same handle `StoreTable`'s read paths bucket under (reads-own-writes).
132
+ const store = await this.table.openDataStore();
133
+ if (!this.coordinator.isInTransaction()) {
134
+ this.coordinator.begin();
135
+ }
136
+ for (const op of ops) {
137
+ switch (op.kind) {
138
+ case 'delete-key': {
139
+ const key = this.table.encodeDataKey(this.normalizePkValues(op.key));
140
+ const existing = await this.table.readEffectiveRowByKey(key);
141
+ if (existing) {
142
+ this.coordinator.delete(key, store);
143
+ this.table.trackPrivilegedMutation(-1);
144
+ changes.push({ op: 'delete', oldRow: existing });
145
+ }
146
+ break;
147
+ }
148
+ case 'upsert': {
149
+ const key = this.table.encodeDataKey(this.extractPk(op.row));
150
+ const existing = await this.table.readEffectiveRowByKey(key);
151
+ if (existing && rowsValueIdentical(existing, op.row)) {
152
+ // Value-identical upsert (byte-faithful `rowsValueIdentical`,
153
+ // against the EFFECTIVE row): nothing changes, so queue no op and
154
+ // report nothing — the suppression contract whose normative
155
+ // statement lives in the engine's vtab/backing-host.ts. Deliberately
156
+ // collation-UNAWARE: a collation-equal / byte-different upsert is a
157
+ // real change that must replace the stored bytes and report an
158
+ // update.
159
+ break;
160
+ }
161
+ this.coordinator.put(key, serializeRow(op.row), store);
162
+ if (!existing)
163
+ this.table.trackPrivilegedMutation(+1);
164
+ changes.push(existing
165
+ ? { op: 'update', oldRow: existing, newRow: op.row }
166
+ : { op: 'insert', newRow: op.row });
167
+ break;
168
+ }
169
+ case 'delete-by-prefix': {
170
+ // Seek to the leading-PK slice (per-column DESC/collation encoded
171
+ // bounds) and early-terminate — the ordered prefix scan the cost
172
+ // contract requires. Collect-then-delete mirrors memory's arm; the
173
+ // ordered pending view is already a snapshot, so this is for clarity,
174
+ // not correctness.
175
+ const bounds = this.table.encodePkPrefixBounds(op.keyPrefix);
176
+ const matched = [];
177
+ for await (const entry of this.table.iterateEffectiveEntries(bounds)) {
178
+ matched.push({ key: entry.key, row: deserializeRow(entry.value) });
179
+ }
180
+ for (const { key, row } of matched) {
181
+ this.coordinator.delete(key, store);
182
+ this.table.trackPrivilegedMutation(-1);
183
+ changes.push({ op: 'delete', oldRow: row });
184
+ }
185
+ break;
186
+ }
187
+ case 'replace-all': {
188
+ await this.applyReplaceAll(op.rows, changes, store);
189
+ break;
190
+ }
191
+ default: {
192
+ // A new MaintenanceOp must extend this switch; never-assignment makes
193
+ // that a compile error rather than a silent no-op.
194
+ const exhaustiveCheck = op;
195
+ throw new QuereusError(`Unknown maintenance op: ${JSON.stringify(exhaustiveCheck)}`, StatusCode.INTERNAL);
196
+ }
197
+ }
198
+ }
199
+ // Declared secondary-UNIQUE enforcement, post-batch against the final
200
+ // effective contents (engine contract — vtab/backing-host.ts § Constraint
201
+ // validation). Zero overhead for constraint-less backings.
202
+ await this.table.enforceSecondaryUniqueForMaintenance(changes);
203
+ // Sync-replication opt-in: when the backing carries
204
+ // `quereus.sync.replicate = true`, publish one local DataChangeEvent per
205
+ // realized change so the sync layer records column versions / tombstones for
206
+ // the derivation write (migration target). Queued AFTER UNIQUE enforcement so
207
+ // a thrown constraint error leaves nothing queued; the coordinator buffers
208
+ // these into pendingEvents (fires on commit / discards on rollback). Default
209
+ // off — a non-replicated backing returns here having queued zero events, so
210
+ // existing MV maintenance is byte-for-byte unchanged.
211
+ if (this.replicates) {
212
+ const schema = this.table.getSchema();
213
+ for (const change of changes) {
214
+ this.coordinator.queueEvent(this.toDataChangeEvent(schema, change));
215
+ }
216
+ }
217
+ return changes;
218
+ }
219
+ /** True when this backing opts its maintenance writes into the sync change log. */
220
+ get replicates() {
221
+ return this.table.getSchema().tags?.[SYNC_REPLICATE_TAG] === true;
222
+ }
223
+ /**
224
+ * Map a realized {@link BackingRowChange} to the store {@link DataChangeEvent}
225
+ * shape, mirroring the ordinary StoreTable DML events (`store-table.ts` insert
226
+ * / update / delete). `remote` is left unset (these are local derivations — an
227
+ * inbound sync write is a different path); `changedColumns` is omitted because
228
+ * the sync layer recomputes the per-column diff from `oldRow`/`newRow` itself,
229
+ * parity with the store's own update event.
230
+ */
231
+ toDataChangeEvent(schema, change) {
232
+ const base = { schemaName: schema.schemaName, tableName: schema.name };
233
+ switch (change.op) {
234
+ case 'insert':
235
+ return { ...base, type: 'insert', key: this.extractPk(change.newRow), newRow: change.newRow };
236
+ case 'update':
237
+ return { ...base, type: 'update', key: this.extractPk(change.newRow), oldRow: change.oldRow, newRow: change.newRow };
238
+ case 'delete':
239
+ return { ...base, type: 'delete', key: this.extractPk(change.oldRow), oldRow: change.oldRow };
240
+ }
241
+ }
242
+ /**
243
+ * The `replace-all` arm: wholesale transactional replacement realized as the
244
+ * minimal keyed diff against the current effective contents. Keys are compared
245
+ * by ENCODED data-key bytes, which fold each PK column's key collation — so a
246
+ * new row whose key differs only by collation (e.g. 'apple' vs a stored 'APPLE'
247
+ * under a NOCASE-keyed PK) matches its old row and resolves to an `update`,
248
+ * mirroring memory's PK-comparator diff. Collation governs KEY identity only:
249
+ * a paired row is skipped (no storage churn, no emitted change) ONLY when its
250
+ * VALUE is byte-faithful-identical (`rowsValueIdentical`), exactly as
251
+ * `applyMaintenanceToLayer` skips them — so a collation-equal / byte-different
252
+ * paired row is an `update` that re-keys the stored bytes.
253
+ */
254
+ async applyReplaceAll(rows, changes, store) {
255
+ // Snapshot the old effective contents first (stable before-image for the
256
+ // diff). Map preserves insertion order = scan order = ascending key order,
257
+ // so the delete pass below emits in PK order like memory's.
258
+ const oldByKey = new Map();
259
+ for await (const entry of this.table.iterateEffectiveEntries(buildFullScanBounds())) {
260
+ oldByKey.set(bytesToHex(entry.key), { key: entry.key, row: deserializeRow(entry.value) });
261
+ }
262
+ const newKeys = new Set();
263
+ for (const newRow of rows) {
264
+ const key = this.table.encodeDataKey(this.extractPk(newRow));
265
+ const hex = bytesToHex(key);
266
+ newKeys.add(hex);
267
+ const existing = oldByKey.get(hex);
268
+ if (!existing) {
269
+ this.coordinator.put(key, serializeRow(newRow), store);
270
+ this.table.trackPrivilegedMutation(+1);
271
+ changes.push({ op: 'insert', newRow });
272
+ }
273
+ else if (!rowsValueIdentical(existing.row, newRow)) {
274
+ this.coordinator.put(key, serializeRow(newRow), store);
275
+ changes.push({ op: 'update', oldRow: existing.row, newRow });
276
+ }
277
+ // else: byte-identical at this key — a true no-op, no emitted change. The skip is
278
+ // byte-faithful (`rowsValueIdentical`): a collation-equal / byte-different paired
279
+ // row (a case-only rewrite under a NOCASE PK) is an `update` that re-keys the
280
+ // stored bytes, matching the point-op upsert skip above and the memory host.
281
+ }
282
+ for (const { key, row } of oldByKey.values()) {
283
+ if (newKeys.has(bytesToHex(key)))
284
+ continue;
285
+ this.coordinator.delete(key, store);
286
+ this.table.trackPrivilegedMutation(-1);
287
+ changes.push({ op: 'delete', oldRow: row });
288
+ }
289
+ }
290
+ /**
291
+ * Atomically replace the COMMITTED contents (create-fill / refresh).
292
+ *
293
+ * If the coordinator holds an open transaction, it is committed FIRST: a
294
+ * committed bulk replace inside an explicit transaction is effectively
295
+ * DDL-committing on a store-backed table, the same posture `renameTable`
296
+ * takes (and the analogue of memory's `replaceBaseLayer` draining in-flight
297
+ * transaction layers before the base swap).
298
+ *
299
+ * Duplicate encoded data keys among `rows` are detected BEFORE any write, so a
300
+ * thrown `onDuplicateKey()` (the MV "must be a set" gate) leaves the committed
301
+ * contents untorn — and, on a replicating backing, queues no events. The clear
302
+ * + rewrite then rides ONE provider batch (deletes of displaced old keys, then
303
+ * puts), so a provider with atomic batches gives concurrent readers pre- or
304
+ * post-swap state, never partial. Routed through the table's `openDataStore()`
305
+ * so the lazy first-access `saveTableDDL` fires — the catalog write a freshly
306
+ * created `using store` backing relies on to survive reopen. Stats reset to the
307
+ * exact new count.
308
+ *
309
+ * Replication seam (opt-in only): when this backing carries
310
+ * `quereus.sync.replicate = true`, the bulk path additionally diffs `rows`
311
+ * against the COMMITTED before-image and queues ONE `DataChangeEvent` per
312
+ * genuine insert / update / delete — and NOTHING for a byte-faithful-identical
313
+ * key, exactly like {@link applyReplaceAll}. That restores suppression to the
314
+ * fill path: a value-identical re-derivation (the same fill computed by another
315
+ * upgraded peer) diffs to zero deltas and emits nothing, so only the first
316
+ * author of a cold row publishes it (so it reaches never-upgrading old peers
317
+ * that store this backing opaquely). The coordinator is NOT in a transaction at
318
+ * the emit (committed at the top, or never began), so `queueEvent` emits
319
+ * immediately into the store emitter; when the engine drives this mid-
320
+ * transaction (create-fill / refresh under `db._ensureTransaction()`) the
321
+ * emitter is batching, so the deltas flush as one grouped change-set at the
322
+ * engine commit — the same place `applyMaintenance`'s events land.
323
+ *
324
+ * A non-replicating backing keeps the original streaming direct-batch path
325
+ * verbatim (no old-value deserialization, no delta list) — byte-for-byte the
326
+ * prior behavior, zero added cost for the common local-derivation case.
327
+ */
328
+ async replaceContents(rows, onDuplicateKey) {
329
+ if (this.coordinator.isInTransaction()) {
330
+ await this.coordinator.commit();
331
+ }
332
+ // Build the duplicate-checked entry set FIRST, before any write or event, so a
333
+ // thrown onDuplicateKey leaves the committed contents untorn AND (replicating
334
+ // path) queues nothing. Carry the deserialized `row` so the event `newRow` is
335
+ // available without a re-deserialize on the emit pass.
336
+ const entries = new Map();
337
+ for (const row of rows) {
338
+ const key = this.table.encodeDataKey(this.extractPk(row));
339
+ const hex = bytesToHex(key);
340
+ if (entries.has(hex)) {
341
+ throw onDuplicateKey
342
+ ? onDuplicateKey()
343
+ : new QuereusError(`UNIQUE constraint failed: ${this.table.tableName} PK.`, StatusCode.CONSTRAINT);
344
+ }
345
+ entries.set(hex, { key, value: serializeRow(row), row });
346
+ }
347
+ const store = await this.table.openDataStore();
348
+ if (!this.replicates) {
349
+ // Default (local-derivation) path: stream the committed contents, delete every
350
+ // key not in `entries`, put all entries. Byte-for-byte the prior behavior.
351
+ const batch = store.batch();
352
+ for await (const entry of store.iterate(buildFullScanBounds())) {
353
+ // Keys being rewritten are skipped (the put below covers them), so the
354
+ // batch carries no delete/put order dependence on the same key.
355
+ if (!entries.has(bytesToHex(entry.key))) {
356
+ batch.delete(entry.key);
357
+ }
358
+ }
359
+ for (const { key, value } of entries.values()) {
360
+ batch.put(key, value);
361
+ }
362
+ await batch.write();
363
+ await this.table.resetStats(rows.length);
364
+ return;
365
+ }
366
+ // Replicating path: snapshot the committed before-image and diff. The
367
+ // coordinator was committed above (or never began), so `store.iterate` yields
368
+ // exactly the committed contents (no pending state to merge) — the right
369
+ // before-image for the diff. Map insertion order = scan order = ascending key
370
+ // order, so the delete pass below emits in old-key order, mirroring applyReplaceAll.
371
+ const oldByKey = new Map();
372
+ for await (const entry of store.iterate(buildFullScanBounds())) {
373
+ oldByKey.set(bytesToHex(entry.key), { key: entry.key, row: deserializeRow(entry.value) });
374
+ }
375
+ const batch = store.batch();
376
+ const deltas = [];
377
+ for (const { key, value, row } of entries.values()) { // rows order
378
+ const existing = oldByKey.get(bytesToHex(key));
379
+ if (!existing) {
380
+ batch.put(key, value);
381
+ deltas.push({ op: 'insert', newRow: row });
382
+ }
383
+ else if (!rowsValueIdentical(existing.row, row)) {
384
+ batch.put(key, value);
385
+ deltas.push({ op: 'update', oldRow: existing.row, newRow: row });
386
+ }
387
+ // else: byte-identical at this key — a true no-op. The put is skipped (the
388
+ // stored bytes are already identical — a non-observable write reduction) and
389
+ // no delta is emitted (the suppression contract). Byte-faithful like the
390
+ // point-op / replace-all arms: a collation-equal / byte-different paired row
391
+ // is an `update` that re-keys the stored bytes, never a skip.
392
+ }
393
+ for (const { key, row } of oldByKey.values()) { // old-key order
394
+ if (entries.has(bytesToHex(key)))
395
+ continue;
396
+ batch.delete(key);
397
+ deltas.push({ op: 'delete', oldRow: row });
398
+ }
399
+ await batch.write();
400
+ // Durable-then-publish: queue events only after the batch is durable. Not in a
401
+ // transaction → each queueEvent emits immediately (batched into the engine's
402
+ // change-set when one is open, flushed at engine commit).
403
+ const schema = this.table.getSchema();
404
+ for (const delta of deltas) {
405
+ this.coordinator.queueEvent(this.toDataChangeEvent(schema, delta));
406
+ }
407
+ await this.table.resetStats(rows.length);
408
+ }
409
+ /**
410
+ * Reads-own-writes scan over the effective state (coordinator pending merged
411
+ * over committed), in PK order. `equalityPrefix` (leading-PK values, in
412
+ * PK-definition order) becomes encoded byte bounds — seek + early-terminate,
413
+ * the O(log n) prefix scan the cost contract requires, never a full-store
414
+ * visit. The ownership guard runs eagerly (before the first pull), matching
415
+ * the memory host's synchronous INTERNAL throw on a foreign connection.
416
+ */
417
+ scanEffective(conn, req) {
418
+ this.assertOwned(conn);
419
+ const bounds = this.table.encodePkPrefixBounds(req.equalityPrefix ?? []);
420
+ const reverse = req.descending ?? false;
421
+ const table = this.table;
422
+ return (async function* () {
423
+ for await (const entry of table.iterateEffectiveEntries(bounds, reverse)) {
424
+ yield deserializeRow(entry.value);
425
+ }
426
+ })();
427
+ }
428
+ /** INTERNAL guard: the privileged surface only accepts this incarnation's connections. */
429
+ assertOwned(conn) {
430
+ if (!this.ownsConnection(conn)) {
431
+ throw new QuereusError(`connection '${conn.connectionId}' does not belong to backing table `
432
+ + `'${this.table.schemaName}.${this.table.tableName}' (or to this incarnation of it)`, StatusCode.INTERNAL);
433
+ }
434
+ }
435
+ /** PK values (in PK-definition order) extracted from a full backing row. */
436
+ extractPk(row) {
437
+ return this.table.getSchema().primaryKeyDefinition.map(pk => row[pk.index]);
438
+ }
439
+ /**
440
+ * Normalize the engine's `BTreeKeyForPrimary` (bare value for a single-column
441
+ * PK — see `buildPrimaryKeyFromValues` — array otherwise) to a values array in
442
+ * PK-definition order, ready for {@link StoreTable.encodeDataKey}.
443
+ */
444
+ normalizePkValues(key) {
445
+ const pkLength = this.table.getSchema().primaryKeyDefinition.length;
446
+ if (pkLength === 1) {
447
+ return [key];
448
+ }
449
+ return key;
450
+ }
451
+ }
452
+ //# sourceMappingURL=backing-host.js.map