@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.
- package/README.md +436 -317
- package/dist/src/common/backing-host.d.ts +198 -0
- package/dist/src/common/backing-host.d.ts.map +1 -0
- package/dist/src/common/backing-host.js +452 -0
- package/dist/src/common/backing-host.js.map +1 -0
- package/dist/src/common/bytes.d.ts +16 -0
- package/dist/src/common/bytes.d.ts.map +1 -0
- package/dist/src/common/bytes.js +33 -0
- package/dist/src/common/bytes.js.map +1 -0
- package/dist/src/common/cached-kv-store.d.ts +3 -3
- package/dist/src/common/cached-kv-store.d.ts.map +1 -1
- package/dist/src/common/cached-kv-store.js +6 -4
- package/dist/src/common/cached-kv-store.js.map +1 -1
- package/dist/src/common/encoding.d.ts +9 -1
- package/dist/src/common/encoding.d.ts.map +1 -1
- package/dist/src/common/encoding.js +12 -2
- package/dist/src/common/encoding.js.map +1 -1
- package/dist/src/common/index.d.ts +8 -6
- package/dist/src/common/index.d.ts.map +1 -1
- package/dist/src/common/index.js +7 -3
- package/dist/src/common/index.js.map +1 -1
- package/dist/src/common/key-builder.d.ts +125 -4
- package/dist/src/common/key-builder.d.ts.map +1 -1
- package/dist/src/common/key-builder.js +186 -9
- package/dist/src/common/key-builder.js.map +1 -1
- package/dist/src/common/kv-store.d.ts +75 -4
- package/dist/src/common/kv-store.d.ts.map +1 -1
- package/dist/src/common/memory-store.d.ts +3 -3
- package/dist/src/common/memory-store.d.ts.map +1 -1
- package/dist/src/common/memory-store.js +4 -2
- package/dist/src/common/memory-store.js.map +1 -1
- package/dist/src/common/store-connection.d.ts +11 -4
- package/dist/src/common/store-connection.d.ts.map +1 -1
- package/dist/src/common/store-connection.js +12 -6
- package/dist/src/common/store-connection.js.map +1 -1
- package/dist/src/common/store-module.d.ts +580 -20
- package/dist/src/common/store-module.d.ts.map +1 -1
- package/dist/src/common/store-module.js +1590 -135
- package/dist/src/common/store-module.js.map +1 -1
- package/dist/src/common/store-table.d.ts +347 -14
- package/dist/src/common/store-table.d.ts.map +1 -1
- package/dist/src/common/store-table.js +751 -98
- package/dist/src/common/store-table.js.map +1 -1
- package/dist/src/common/transaction.d.ts +135 -27
- package/dist/src/common/transaction.d.ts.map +1 -1
- package/dist/src/common/transaction.js +216 -55
- package/dist/src/common/transaction.js.map +1 -1
- 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
|