@quereus/store 4.3.0 → 4.3.2
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 +486 -436
- package/dist/src/common/bytes.d.ts +4 -1
- package/dist/src/common/bytes.d.ts.map +1 -1
- package/dist/src/common/bytes.js +23 -2
- package/dist/src/common/bytes.js.map +1 -1
- package/dist/src/common/cached-kv-store.d.ts.map +1 -1
- package/dist/src/common/cached-kv-store.js +6 -13
- package/dist/src/common/cached-kv-store.js.map +1 -1
- package/dist/src/common/encoding.d.ts +69 -26
- package/dist/src/common/encoding.d.ts.map +1 -1
- package/dist/src/common/encoding.js +292 -230
- package/dist/src/common/encoding.js.map +1 -1
- package/dist/src/common/index.d.ts +1 -1
- package/dist/src/common/index.d.ts.map +1 -1
- package/dist/src/common/index.js +1 -1
- package/dist/src/common/index.js.map +1 -1
- package/dist/src/common/key-builder.d.ts +5 -1
- package/dist/src/common/key-builder.d.ts.map +1 -1
- package/dist/src/common/key-builder.js +30 -2
- package/dist/src/common/key-builder.js.map +1 -1
- package/dist/src/common/kv-store.d.ts +1 -1
- package/dist/src/common/memory-store.d.ts.map +1 -1
- package/dist/src/common/memory-store.js +21 -6
- package/dist/src/common/memory-store.js.map +1 -1
- package/dist/src/common/serialization.d.ts.map +1 -1
- package/dist/src/common/serialization.js +6 -3
- package/dist/src/common/serialization.js.map +1 -1
- package/dist/src/common/store-module.d.ts +293 -26
- package/dist/src/common/store-module.d.ts.map +1 -1
- package/dist/src/common/store-module.js +1543 -612
- package/dist/src/common/store-module.js.map +1 -1
- package/dist/src/common/store-table.d.ts +515 -36
- package/dist/src/common/store-table.d.ts.map +1 -1
- package/dist/src/common/store-table.js +873 -100
- package/dist/src/common/store-table.js.map +1 -1
- package/dist/src/common/transaction.d.ts +8 -5
- package/dist/src/common/transaction.d.ts.map +1 -1
- package/dist/src/common/transaction.js +32 -5
- package/dist/src/common/transaction.js.map +1 -1
- package/dist/src/testing/kv-conformance.d.ts +47 -0
- package/dist/src/testing/kv-conformance.d.ts.map +1 -0
- package/dist/src/testing/kv-conformance.js +418 -0
- package/dist/src/testing/kv-conformance.js.map +1 -0
- package/package.json +17 -5
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* - Index stores: {schema}.{table}_idx_{name} - one per secondary index
|
|
10
10
|
* - Stats store: __stats__ - unified store for all table statistics, keyed by {schema}.{table}
|
|
11
11
|
*/
|
|
12
|
-
import { VirtualTable, IndexConstraintOp, ConflictResolution, type Database, type ColumnSchema, type TableSchema, type Row, type FilterInfo, type SqlValue, type VirtualTableConnection, type UpdateArgs, type UpdateResult, type BackingRowChange } from '@quereus/quereus';
|
|
12
|
+
import { VirtualTable, IndexConstraintOp, ConflictResolution, type Database, type CollationFunction, type CollationResolver, type ColumnSchema, type TableSchema, type TableIndexSchema, type UniqueConstraintSchema, type Row, type FilterInfo, type SqlValue, type VirtualTableConnection, type UpdateArgs, type UpdateResult, type BackingRowChange } from '@quereus/quereus';
|
|
13
13
|
import type { IterateOptions, KVEntry, KVStore } from './kv-store.js';
|
|
14
14
|
import type { StoreEventEmitter } from './events.js';
|
|
15
15
|
import type { TransactionCoordinator } from './transaction.js';
|
|
@@ -19,32 +19,144 @@ import { type EncodeOptions } from './encoding.js';
|
|
|
19
19
|
/**
|
|
20
20
|
* Resolve the per-column KEY collation for each primary-key column.
|
|
21
21
|
*
|
|
22
|
-
* The store encodes PRIMARY KEY uniqueness/ordering PHYSICALLY in the key bytes,
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* - text member → its declared `collation` (normalized upper-case), or
|
|
22
|
+
* The store encodes PRIMARY KEY uniqueness/ordering PHYSICALLY in the key bytes, so each
|
|
23
|
+
* text-capable PK column's key must be encoded under the collation the engine actually
|
|
24
|
+
* COMPARES that column under. Returns one entry per PK member, in `pkDef` order:
|
|
25
|
+
* - `isTextual` member (`text`) → its declared `collation` (normalized upper-case), or
|
|
27
26
|
* `fallback` (the table key collation K) when the column carries none.
|
|
28
|
-
* -
|
|
27
|
+
* - text-capable but not `isTextual` member — `any`, `json`, and the temporal types
|
|
28
|
+
* (`date` / `time` / `datetime` / `timespan`) → hard-coded `'BINARY'`, see below.
|
|
29
|
+
* - never-text member → `undefined`: collation is meaningless for
|
|
29
30
|
* integer/real/blob keys (they encode type-natively), so the encoder ignores
|
|
30
31
|
* it and the data/index key bytes are identical regardless.
|
|
31
32
|
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
33
|
+
* Note that `undefined` does NOT mean "encode type-natively" to `encodeValue` — it means
|
|
34
|
+
* "fall back to `options.collation`", the table key collation K. Only a never-text column
|
|
35
|
+
* may be left `undefined`, because only it never reaches the collation branch.
|
|
36
|
+
*
|
|
37
|
+
* Every name returned here is encoded through the key-normalizer resolver carried
|
|
38
|
+
* in `EncodeOptions.normalizers` (`db.getKeyNormalizerResolver()`), so a collation
|
|
39
|
+
* registered or overridden with `db.registerCollation` produces key bytes that agree
|
|
40
|
+
* with the comparator the store's UNIQUE enforcement uses. A collation that cannot
|
|
41
|
+
* key — unregistered, or registered with a comparator but no normalizer — is rejected
|
|
42
|
+
* at DDL time by {@link StoreTable.validateKeyCollations}, so no encode call ever has
|
|
43
|
+
* to fall back. Shared by {@link StoreTable} (data-key + index-maintenance) and
|
|
36
44
|
* `StoreModule.buildIndexEntries` (index rebuild) so the PK suffix encoding can
|
|
37
45
|
* never drift between the two.
|
|
38
46
|
*/
|
|
39
47
|
export declare function resolvePkKeyCollations(pkDef: ReadonlyArray<{
|
|
40
48
|
index: number;
|
|
41
49
|
}>, columns: ReadonlyArray<ColumnSchema>, fallback: string): (string | undefined)[];
|
|
50
|
+
/**
|
|
51
|
+
* True when `col` can produce a TEXT value at runtime, and so when its physical
|
|
52
|
+
* key bytes are produced by a collation normalizer rather than a type-native
|
|
53
|
+
* encoding. The `ColumnSchema`-shaped wrapper over the engine's
|
|
54
|
+
* {@link logicalTypeCanHoldText}; both collation-safety guards over the store's
|
|
55
|
+
* secondary indexes — the write-side {@link StoreTable.indexSeekHonorsEnforcementCollation}
|
|
56
|
+
* and the read-side `StoreModule.tryIndexAccessPlan` — exempt a never-text column
|
|
57
|
+
* from their K-vs-C comparison, so a false "non-text" answer is a silent
|
|
58
|
+
* wrong-result (a seek under the wrong collation, with the residual dropped).
|
|
59
|
+
*/
|
|
60
|
+
export declare function columnCanHoldText(col: ColumnSchema | undefined): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* True when a byte window — or a byte-order advertisement — over `column` is SOUND.
|
|
63
|
+
*
|
|
64
|
+
* The store physically orders rows by memcmp of each column's key bytes, which for a
|
|
65
|
+
* text-capable column are produced by `keyCollation`'s normalizer. A range seek (and the
|
|
66
|
+
* `providesOrdering` / `monotonicOn` advertisement) equates that byte order with the order
|
|
67
|
+
* `compareCollation`'s comparator defines. Two things must hold for that equation:
|
|
68
|
+
*
|
|
69
|
+
* - **Same collation on both sides** (`compareCollation === keyCollation`). A `keyCollation`
|
|
70
|
+
* merely COARSER than `compareCollation` — the relaxation the EQUALITY seek is allowed to
|
|
71
|
+
* make, see `StoreModule.tryIndexAccessPlan` — is not enough here: it would need the key
|
|
72
|
+
* normalizer to be monotone with respect to the OTHER collation's order. It generally is
|
|
73
|
+
* not, even for built-ins: with `keyCollation = NOCASE` and `compareCollation = BINARY`,
|
|
74
|
+
* 'K' (U+212A KELVIN SIGN) is `> 'z'` under BINARY, yet its key bytes are
|
|
75
|
+
* `toLowerCase('K') = 'k'`, which sorts BEFORE 'z' — the row falls outside a `> 'z'`
|
|
76
|
+
* window and is silently dropped.
|
|
77
|
+
* - **The normalizer preserves order** (`db._isCollationOrderPreserving`). `registerCollation`
|
|
78
|
+
* promises only that a normalizer partitions strings the way the comparator calls them
|
|
79
|
+
* equal; a custom pair may agree on equality and disagree on order.
|
|
80
|
+
*
|
|
81
|
+
* A never-text column is exempt: its key bytes are type-native and collation-independent.
|
|
82
|
+
*
|
|
83
|
+
* Returning `false` costs an optimization (the caller full-scans and lets the
|
|
84
|
+
* collation-aware `matchesFilters` residual decide); returning `true` wrongly is a silent
|
|
85
|
+
* wrong-result. Shared by `StoreTable`'s read arms and `StoreModule`'s access planner so the
|
|
86
|
+
* "mark handled" and "build a window" decisions can never disagree.
|
|
87
|
+
*/
|
|
88
|
+
export declare function keyOrderMatchesCollation(db: Database, column: ColumnSchema | undefined, keyCollation: string, compareCollation: string): boolean;
|
|
89
|
+
/**
|
|
90
|
+
* How many LEADING primary-key members have key bytes whose memcmp order matches their
|
|
91
|
+
* declared collation's comparator order, per {@link keyOrderMatchesCollation}.
|
|
92
|
+
*
|
|
93
|
+
* `pkKeyCollations` is {@link resolvePkKeyCollations}' output for this table; an `undefined`
|
|
94
|
+
* entry belongs to a never-text member, which {@link keyOrderMatchesCollation} exempts
|
|
95
|
+
* outright — `tableKeyCollation` is passed only so the exempt branch has a name to ignore.
|
|
96
|
+
* The comparison collation is the column's declared one, which `reconcilePkCollations` has
|
|
97
|
+
* already aligned with the key collation for every *textual* PK member.
|
|
98
|
+
*
|
|
99
|
+
* What the advertisement is measured against is the order the planner's `Sort` would have
|
|
100
|
+
* produced, and `Sort` (like every scalar comparison) orders under the operand's COLLATION via
|
|
101
|
+
* `compareSqlValuesFast` — never through `logicalType.compare`. So a collation match is the
|
|
102
|
+
* whole question, even for a member whose type declares a `compare` that orders differently
|
|
103
|
+
* (`TIMESPAN.compare` ranks by `Temporal.Duration` total, `JSON_TYPE.compare` structurally).
|
|
104
|
+
* `MemoryTable`, whose primary-key BTree *is* keyed by `createTypedComparator`, advertises the
|
|
105
|
+
* type's order instead and so disagrees with its own `Sort` — tracked as
|
|
106
|
+
* `bug-memory-pk-btree-orders-by-logical-type-compare`, not something the store should copy.
|
|
107
|
+
*
|
|
108
|
+
* `0` ⇒ even the leading member is unsafe: no range seek, no PK-order advertisement.
|
|
109
|
+
* A prefix shorter than the PK truncates the ordering advertisement rather than voiding it.
|
|
110
|
+
*/
|
|
111
|
+
export declare function pkOrderPreservingPrefixLength(db: Database, schema: TableSchema, pkKeyCollations: ReadonlyArray<string | undefined>, tableKeyCollation: string): number;
|
|
112
|
+
/**
|
|
113
|
+
* Deterministic name of the implicit covering index materialized for a
|
|
114
|
+
* NON-DERIVED UNIQUE constraint `uc`: the constraint's own name when it has one,
|
|
115
|
+
* else the auto-name `_uc_<colNames>`. This MUST equal the engine's
|
|
116
|
+
* `catalog.ts` `implicitIndexName`, so a name that ever reaches the catalog
|
|
117
|
+
* bundle generator is recognized as hidden / exposed-implicit rather than a user
|
|
118
|
+
* index.
|
|
119
|
+
*/
|
|
120
|
+
export declare function implicitUniqueIndexName(schema: TableSchema, uc: UniqueConstraintSchema): string;
|
|
121
|
+
/**
|
|
122
|
+
* Return `schema` with a synthetic secondary index materialized into
|
|
123
|
+
* `schema.indexes` for every NON-DERIVED UNIQUE constraint that lacks one — the
|
|
124
|
+
* store analogue of `MemoryTableManager.ensureUniqueConstraintIndexes`.
|
|
125
|
+
*
|
|
126
|
+
* This is what lets a plain column- or table-level `UNIQUE` be enforced by a
|
|
127
|
+
* bounded index point-seek ({@link StoreTable.findUniqueConflictViaIndex})
|
|
128
|
+
* instead of an O(rows) full scan: DML maintenance
|
|
129
|
+
* ({@link StoreTable.updateSecondaryIndexes}) and the UNIQUE check both iterate
|
|
130
|
+
* `schema.indexes`, so once the `_uc_*` entry is present the physical index is
|
|
131
|
+
* maintained and seeked with no new code path.
|
|
132
|
+
*
|
|
133
|
+
* The result is held ONLY in {@link StoreTable.materializedSchema} (the
|
|
134
|
+
* enforcement copy), never in the engine-facing `tableSchema` the store returns
|
|
135
|
+
* from `getSchema()` / registers with the engine. So the read-query planner and
|
|
136
|
+
* the catalog bundle never see `_uc_*` (it is fully derived on open from
|
|
137
|
+
* `uniqueConstraints`); like the memory backend, a plain UNIQUE gets no read-side
|
|
138
|
+
* plan from `_uc_*`, only enforcement / maintenance.
|
|
139
|
+
*
|
|
140
|
+
* Idempotent — a name already present is left untouched — and returns a frozen
|
|
141
|
+
* schema (or the input unchanged when there is nothing to add). A
|
|
142
|
+
* `derivedFromIndex` UC is skipped (its `CREATE UNIQUE INDEX` is already in
|
|
143
|
+
* `schema.indexes`). A partial UNIQUE's synthetic index carries the SAME
|
|
144
|
+
* `uc.predicate` object reference, which
|
|
145
|
+
* {@link StoreTable.findIndexForUniqueConstraint} matches on identity.
|
|
146
|
+
*/
|
|
147
|
+
export declare function withImplicitUniqueIndexes(schema: TableSchema): TableSchema;
|
|
42
148
|
/**
|
|
43
149
|
* Configuration for a store table.
|
|
44
150
|
*/
|
|
45
151
|
export interface StoreTableConfig {
|
|
46
152
|
/** Collation for text keys. Default: 'NOCASE'. */
|
|
47
153
|
collation?: 'BINARY' | 'NOCASE';
|
|
154
|
+
/**
|
|
155
|
+
* Serialized-byte budget for a single index-build write batch (see
|
|
156
|
+
* `StoreModule.buildIndexEntries`). Parsed from the `max_batch_bytes` module
|
|
157
|
+
* arg; undefined falls back to the module default.
|
|
158
|
+
*/
|
|
159
|
+
maxBatchBytes?: number;
|
|
48
160
|
/** Additional platform-specific options. */
|
|
49
161
|
[key: string]: unknown;
|
|
50
162
|
}
|
|
@@ -54,7 +166,7 @@ export interface StoreTableConfig {
|
|
|
54
166
|
*/
|
|
55
167
|
export interface StoreTableModule {
|
|
56
168
|
/** Get the data store for a table. */
|
|
57
|
-
getStore(
|
|
169
|
+
getStore(schemaName: string, tableName: string, config: StoreTableConfig): Promise<KVStore>;
|
|
58
170
|
/** Get an index store for a table. */
|
|
59
171
|
getIndexStore(schemaName: string, tableName: string, indexName: string): Promise<KVStore>;
|
|
60
172
|
/** Get the stats store for a table. */
|
|
@@ -117,6 +229,22 @@ export declare class StoreTable extends VirtualTable {
|
|
|
117
229
|
* {@link updateSchema} (an ALTER COLUMN SET COLLATE on a PK member changes it).
|
|
118
230
|
*/
|
|
119
231
|
protected pkKeyCollations: (string | undefined)[];
|
|
232
|
+
/**
|
|
233
|
+
* `db.getCollationResolver()`, bound once at construction. Every collation-aware
|
|
234
|
+
* VALUE comparison this table makes — pushed-constraint re-check, UNIQUE conflict
|
|
235
|
+
* detection — resolves names through it, so a collation registered with
|
|
236
|
+
* `db.registerCollation` on *this* connection is honored rather than silently
|
|
237
|
+
* degraded to BINARY by the process-global built-in registry.
|
|
238
|
+
*
|
|
239
|
+
* Only the resolver closure is cached: it reads the live registry, so a collation
|
|
240
|
+
* registered after connect is still visible. Resolved *functions* are hoisted no
|
|
241
|
+
* further than the comparator or constraint check that resolved them.
|
|
242
|
+
*
|
|
243
|
+
* Its key-bytes counterpart, `db.getKeyNormalizerResolver()`, is bound into
|
|
244
|
+
* {@link encodeOptions} so the names in {@link pkKeyCollations} resolve to the same
|
|
245
|
+
* per-connection registry.
|
|
246
|
+
*/
|
|
247
|
+
protected readonly collationResolver: CollationResolver;
|
|
120
248
|
protected ddlSaved: boolean;
|
|
121
249
|
protected cachedStats: TableStats | null;
|
|
122
250
|
protected pendingStatsDelta: number;
|
|
@@ -124,12 +252,66 @@ export declare class StoreTable extends VirtualTable {
|
|
|
124
252
|
protected statsFlushPending: boolean;
|
|
125
253
|
private readonly predicateCache;
|
|
126
254
|
private readonly indexPredicateCache;
|
|
255
|
+
/**
|
|
256
|
+
* {@link tableSchema} with a hidden `_uc_*` secondary index materialized per
|
|
257
|
+
* non-derived UNIQUE constraint (see {@link withImplicitUniqueIndexes}) — the
|
|
258
|
+
* schema that drives UNIQUE ENFORCEMENT ({@link findIndexForUniqueConstraint})
|
|
259
|
+
* and physical index MAINTENANCE ({@link updateSecondaryIndexes}).
|
|
260
|
+
*
|
|
261
|
+
* Deliberately SEPARATE from the engine-facing {@link tableSchema}: the engine
|
|
262
|
+
* reads `tableInstance.tableSchema` at CREATE (`SchemaManager.finalizeCreatedTableSchema`)
|
|
263
|
+
* and the read-query planner reads the registered schema, and NEITHER may see
|
|
264
|
+
* `_uc_*` (mirrors the memory backend keeping its materialized schema
|
|
265
|
+
* manager-local; a plain UNIQUE gets no read-side plan from `_uc_*`, only
|
|
266
|
+
* enforcement). Recomputed alongside `tableSchema` in the constructor and
|
|
267
|
+
* {@link updateSchema}.
|
|
268
|
+
*/
|
|
269
|
+
private materializedSchema;
|
|
127
270
|
constructor(db: Database, storeModule: StoreTableModule, tableSchema: TableSchema, config: StoreTableConfig, eventEmitter?: StoreEventEmitter, isConnected?: boolean);
|
|
271
|
+
/**
|
|
272
|
+
* Reject, at DDL time, any collation this table's key encoding would need but cannot
|
|
273
|
+
* use — before a single row is written under bytes the connection cannot reproduce.
|
|
274
|
+
*
|
|
275
|
+
* A collation keys a persisted structure only if it carries a KEY NORMALIZER (the
|
|
276
|
+
* `(s) => string` whose output equality partitions strings exactly as its comparator
|
|
277
|
+
* does). `db.registerCollation(name, cmp)` with no `{ normalizer }` gives a collation
|
|
278
|
+
* that can order rows but not key them; an unregistered name gives nothing at all.
|
|
279
|
+
* Both raise here rather than at the first insert.
|
|
280
|
+
*
|
|
281
|
+
* Checked over exactly the collations the key encoding actually uses:
|
|
282
|
+
* - every defined `pkKeyCollations` entry (text-capable PK columns);
|
|
283
|
+
* - the table key collation K, but only when it is reachable — a secondary index
|
|
284
|
+
* encodes a TEXT-CAPABLE index column's bytes under K (`buildIndexKey`).
|
|
285
|
+
*
|
|
286
|
+
* So neither a table with an integer PK and no secondary index, nor one whose every
|
|
287
|
+
* index column is type-natively keyed, is made unopenable by a K it never encodes with.
|
|
288
|
+
*
|
|
289
|
+
* Blast radius: this also fires on catalog rehydration. Reopening a persisted database
|
|
290
|
+
* from a connection that has not re-registered its custom collation now throws at
|
|
291
|
+
* CREATE-TABLE-from-catalog rather than silently reading rows under a key layout it
|
|
292
|
+
* cannot reproduce. See `docs/plugins.md`.
|
|
293
|
+
*
|
|
294
|
+
* Takes `pkKeyCollations` rather than reading the field so `updateSchema` can validate
|
|
295
|
+
* the incoming schema BEFORE it overwrites its own state with it.
|
|
296
|
+
*/
|
|
297
|
+
private validateKeyCollations;
|
|
128
298
|
/** Get the table configuration. */
|
|
129
299
|
getConfig(): StoreTableConfig;
|
|
130
|
-
/** Get the table schema. */
|
|
300
|
+
/** Get the table schema (engine-facing, WITHOUT the hidden `_uc_*` indexes). */
|
|
131
301
|
getSchema(): TableSchema;
|
|
132
|
-
/**
|
|
302
|
+
/**
|
|
303
|
+
* Get the enforcement schema — {@link getSchema} plus the hidden `_uc_*`
|
|
304
|
+
* index materialized per non-derived UNIQUE. Used by the module to resolve the
|
|
305
|
+
* physical index store to build/tear down during ALTER; never registered with
|
|
306
|
+
* the engine.
|
|
307
|
+
*/
|
|
308
|
+
getMaterializedSchema(): TableSchema;
|
|
309
|
+
/**
|
|
310
|
+
* Update the table schema after an ALTER TABLE / CREATE INDEX operation.
|
|
311
|
+
*
|
|
312
|
+
* Validates the incoming schema's key collations before adopting any of it, so a
|
|
313
|
+
* rejection leaves this table on its previous, consistent schema.
|
|
314
|
+
*/
|
|
133
315
|
updateSchema(newSchema: TableSchema): void;
|
|
134
316
|
/**
|
|
135
317
|
* Mark the table's DDL as already persisted to the catalog, so the lazy
|
|
@@ -141,26 +323,68 @@ export declare class StoreTable extends VirtualTable {
|
|
|
141
323
|
/** Close and forget a cached index-store handle, if any. */
|
|
142
324
|
releaseIndexStore(indexName: string): Promise<void>;
|
|
143
325
|
/**
|
|
144
|
-
* Returns true if the table has at least one
|
|
326
|
+
* Returns true if the table has at least one LIVE row — the open transaction's
|
|
327
|
+
* pending overlay over the committed store. Stops after the first hit.
|
|
328
|
+
*
|
|
329
|
+
* Effective (not committed-only) because its one caller, `ADD COLUMN`'s NOT
|
|
330
|
+
* NULL-without-DEFAULT rejection, must see rows the same transaction inserted:
|
|
331
|
+
* they are migrated too (the arm DDL-commits before `migrateRows`), and a
|
|
332
|
+
* committed-only probe would wave them through only to give them a NULL in a
|
|
333
|
+
* NOT NULL column. Conversely, a pending delete of the last row makes the table
|
|
334
|
+
* legitimately empty. Reading effectively keeps the check throw-only and
|
|
335
|
+
* BEFORE the DDL-commit, so a rejection leaves the transaction intact.
|
|
145
336
|
*/
|
|
146
337
|
hasAnyRows(): Promise<boolean>;
|
|
147
338
|
/**
|
|
148
|
-
*
|
|
339
|
+
* Yield the LIVE value of `colIndex` for every row — the open transaction's
|
|
340
|
+
* pending overlay over the committed store. For throw-only ALTER COLUMN probes
|
|
341
|
+
* that must run BEFORE the arm's DDL-commit (see `StoreModule.ddlCommitPendingOps`):
|
|
342
|
+
* they have to see rows this transaction wrote, because those rows are rewritten
|
|
343
|
+
* too, and a rejection here must leave the transaction intact.
|
|
344
|
+
*/
|
|
345
|
+
iterateEffectiveValuesAtIndex(colIndex: number): AsyncIterable<SqlValue>;
|
|
346
|
+
/**
|
|
347
|
+
* Count the LIVE rows whose column at `colIndex` holds NULL (see
|
|
348
|
+
* {@link iterateEffectiveValuesAtIndex} for why this reads effectively).
|
|
149
349
|
* Used by ALTER COLUMN SET NOT NULL to decide whether the tightening is safe.
|
|
150
350
|
*/
|
|
151
351
|
rowsWithNullAtIndex(colIndex: number): Promise<number>;
|
|
152
352
|
/**
|
|
153
353
|
* Apply a per-row mapping function to every stored row, in place (re-writing
|
|
154
|
-
* the same key). The mapper may throw QuereusError — propagated to the caller
|
|
354
|
+
* the same key). The mapper may throw QuereusError — propagated to the caller;
|
|
355
|
+
* the batch is written only after every row maps, so a throw leaves the store
|
|
356
|
+
* untouched.
|
|
357
|
+
*
|
|
358
|
+
* NOTE: reads and writes the COMMITTED store, outside the coordinator. Sound
|
|
359
|
+
* only because every caller calls `StoreModule.ddlCommitPendingOps` first, so
|
|
360
|
+
* "committed" is "everything live". A caller that skips that flush would leave
|
|
361
|
+
* its transaction's pending rows unmapped, and they would replay unconverted
|
|
362
|
+
* over the rewritten store at commit.
|
|
155
363
|
*/
|
|
156
364
|
mapRowsAtIndex(colIndex: number, mapper: (value: SqlValue) => SqlValue): Promise<void>;
|
|
157
365
|
/**
|
|
158
366
|
* Re-key every stored row under a new primary-key definition.
|
|
159
367
|
*
|
|
160
|
-
* Two-pass: the first pass
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
368
|
+
* Two-pass, signatures-only pass 1: the first pass computes each row's new data
|
|
369
|
+
* key and retains only a `Set` of key SIGNATURES (hex of the key bytes) to detect
|
|
370
|
+
* collisions — two distinct old keys collapsing to one new key under a coarser
|
|
371
|
+
* collation or a narrower PK. On collision we throw `CONSTRAINT` without touching
|
|
372
|
+
* the store. The second pass RE-SCANS the same committed store, recomputes each
|
|
373
|
+
* new key, and batches deletes of displaced old keys + puts of new (key, row)
|
|
374
|
+
* pairs into ONE atomic batch. Rows whose new key matches the old key are no-ops.
|
|
375
|
+
*
|
|
376
|
+
* Holding signatures instead of whole rows halves peak memory: the prior design
|
|
377
|
+
* retained the entire table in a map AND again in the batch. The re-scan trades
|
|
378
|
+
* O(rows) CPU (a second iterate + newKey recompute) for not buffering the table
|
|
379
|
+
* twice. Pass 1 and pass 2 iterate the SAME bounds over the SAME committed store
|
|
380
|
+
* and see identical rows: nothing writes between them — we are single-threaded
|
|
381
|
+
* within the ALTER, outside the coordinator, and every caller ran
|
|
382
|
+
* `StoreModule.ddlCommitPendingOps` first so "committed" is "everything live".
|
|
383
|
+
*
|
|
384
|
+
* The final single `batch.write()` is the ONLY thing making the re-key
|
|
385
|
+
* all-or-nothing — do not chunk-flush it. Its residual peak (the batch still holds
|
|
386
|
+
* every changed row) is irreducible without breaking atomicity; tracked separately
|
|
387
|
+
* in `debt-store-atomic-batch-bounded-memory`.
|
|
164
388
|
*
|
|
165
389
|
* Only the data store is rewritten — secondary indexes are rebuilt by the
|
|
166
390
|
* caller (the keys embed the PK suffix, so they must be rebuilt whenever
|
|
@@ -287,6 +511,13 @@ export declare class StoreTable extends VirtualTable {
|
|
|
287
511
|
* a sibling table's pending ops (a different data-store handle) never bleed in.
|
|
288
512
|
*/
|
|
289
513
|
protected iterateEffective(store: KVStore, bounds: IterateOptions, reverse?: boolean): AsyncIterable<KVEntry>;
|
|
514
|
+
/**
|
|
515
|
+
* True when a byte window over the LEADING PK column reproduces the comparator's order
|
|
516
|
+
* — the precondition {@link buildPKRangeBounds} needs, and the exact condition
|
|
517
|
+
* `StoreModule.computeBestAccessPlan` uses before claiming the range filters handled.
|
|
518
|
+
* When false, the range arm degrades to a full scan + `matchesFilters` residual.
|
|
519
|
+
*/
|
|
520
|
+
protected leadingPkRangeIsOrderSafe(): boolean;
|
|
290
521
|
/** Analyze filter info to determine PK access pattern. */
|
|
291
522
|
protected analyzePKAccess(filterInfo: FilterInfo): PKAccessPattern;
|
|
292
523
|
/**
|
|
@@ -316,11 +547,16 @@ export declare class StoreTable extends VirtualTable {
|
|
|
316
547
|
* row filter. A NULL/missing bound value is likewise skipped (the planner never
|
|
317
548
|
* pushes `= NULL`, and a range op against NULL rejects every row in matchesFilters).
|
|
318
549
|
*
|
|
319
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
550
|
+
* NOTE: a range window over a text PK column is sound only when the column's key
|
|
551
|
+
* normalizer is ORDER-preserving with respect to its comparator — i.e. the comparator
|
|
552
|
+
* orders two strings the way memcmp orders their normalized bytes. That is NOT implied
|
|
553
|
+
* by `db.registerCollation`'s normalizer contract, which promises only an equality
|
|
554
|
+
* partition, and a built-in NAME may be re-registered with a comparator + normalizer
|
|
555
|
+
* pair that preserves equality while inverting order. This method is therefore only
|
|
556
|
+
* ever reached under {@link leadingPkRangeIsOrderSafe} — enforced by
|
|
557
|
+
* {@link analyzePKAccess} here and mirrored by `StoreModule.computeBestAccessPlan`
|
|
558
|
+
* before it claims the range filters handled. A collation without the
|
|
559
|
+
* `orderPreserving` assertion costs the seek, never a row.
|
|
324
560
|
*/
|
|
325
561
|
protected buildPKRangeBounds(access: PKAccessPattern): IterateOptions;
|
|
326
562
|
/**
|
|
@@ -336,18 +572,148 @@ export declare class StoreTable extends VirtualTable {
|
|
|
336
572
|
* merge to the same `bounds`, so read-your-own-writes holds on the narrowed window.
|
|
337
573
|
*/
|
|
338
574
|
protected scanPKRange(store: KVStore, access: PKAccessPattern, filterInfo: FilterInfo): AsyncIterable<Row>;
|
|
339
|
-
/** Check if a row matches the filter constraints. */
|
|
340
|
-
protected matchesFilters(row: Row, filterInfo: FilterInfo): boolean;
|
|
341
575
|
/**
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
* `
|
|
345
|
-
*
|
|
576
|
+
* Resolve the secondary index chosen by the planner from `filterInfo.idxStr`.
|
|
577
|
+
*
|
|
578
|
+
* The planner emits `idx=<name>(<n>);plan=…` when its access plan set both an
|
|
579
|
+
* `indexName` and `seekColumnIndexes` (see `getBestAccessPlan` and
|
|
580
|
+
* rule-select-access-path.ts). Decoding goes through the engine's shared
|
|
581
|
+
* `decodeIdxStr`, so the store, the in-memory vtab, and the isolation overlay all
|
|
582
|
+
* read one format. Returns null for the PK/scan sentinels (`_primary_`, `fullscan`,
|
|
583
|
+
* `empty`), an idxStr that names no index, or a name absent from `schema.indexes` —
|
|
584
|
+
* every one of which routes back to a PK/full-scan arm.
|
|
585
|
+
*/
|
|
586
|
+
protected resolveIndexFromIdxStr(idxStr: string | null): TableIndexSchema | null;
|
|
587
|
+
/**
|
|
588
|
+
* Analyze filter info to determine a secondary-index access pattern, mirroring
|
|
589
|
+
* {@link analyzePKAccess} but over the index chosen in `idxStr`.
|
|
590
|
+
*
|
|
591
|
+
* A contiguous leading-prefix EQ on the index columns yields a `point` window
|
|
592
|
+
* (the prefix covers every entry sharing those leading values — an index seek
|
|
593
|
+
* is a PREFIX scan, not a single row, since the index need not be unique and
|
|
594
|
+
* the PK suffix varies); otherwise a range (LT/LE/GT/GE) on the LEADING index
|
|
595
|
+
* column yields a `range` window. Returns null when neither applies or when the
|
|
596
|
+
* index is unresolved. {@link matchesFilters} stays the authoritative row filter,
|
|
597
|
+
* so the window need only be a SUPERSET.
|
|
598
|
+
*
|
|
599
|
+
* Index-column bytes are encoded under the table key collation K (NOT the index's
|
|
600
|
+
* per-column declared collation — see `buildIndexKey`), while `matchesFilters` compares
|
|
601
|
+
* under the index column's effective collation C. The EQ/prefix window is a superset
|
|
602
|
+
* whenever K is coarser-or-equal to C, which `StoreModule.tryIndexAccessPlan` checked
|
|
603
|
+
* before it named this index. The RANGE window additionally needs byte order to BE
|
|
604
|
+
* comparator order, so it is gated on {@link keyOrderMatchesCollation} (which demands
|
|
605
|
+
* `C === K` plus K's `orderPreserving` assertion); when that fails we return null and
|
|
606
|
+
* the caller full-scans.
|
|
607
|
+
*/
|
|
608
|
+
protected analyzeIndexAccess(filterInfo: FilterInfo): IndexAccessPattern | null;
|
|
609
|
+
/**
|
|
610
|
+
* True when a byte window over `leadingCol` of `index` reproduces the comparator's
|
|
611
|
+
* order. Mirrors the range arm of `StoreModule.tryIndexAccessPlan`: the window's bytes
|
|
612
|
+
* come from the table key collation K, the residual compares under the index column's
|
|
613
|
+
* effective collation C, so both must be the same order-preserving collation.
|
|
614
|
+
*/
|
|
615
|
+
protected indexRangeIsOrderSafe(index: TableIndexSchema, leadingCol: number): boolean;
|
|
616
|
+
/**
|
|
617
|
+
* Convert leading-index-column LT/LE/GT/GE constraints into one encoded-byte
|
|
618
|
+
* `gte`/`lt` window — the secondary-index analogue of {@link buildPKRangeBounds}.
|
|
619
|
+
*
|
|
620
|
+
* Each bound value is encoded under {@link encodeOptions} (the table key collation K
|
|
621
|
+
* and its normalizer resolver) and the leading index column's DESC `dir` — exactly as
|
|
622
|
+
* {@link buildIndexKey} encodes
|
|
623
|
+
* that column — via {@link buildIndexPrefixBounds}, giving the byte region
|
|
624
|
+
* `[lo, hi)` whose leading column equals that value. The op maps that region's
|
|
625
|
+
* endpoints onto `gte`/`lt`, with the same DESC lower/upper SWAP as the PK path:
|
|
626
|
+
*
|
|
627
|
+
* | op | ASC | DESC |
|
|
628
|
+
* |----|----------|----------|
|
|
629
|
+
* | GE | gte = lo | lt = hi |
|
|
630
|
+
* | GT | gte = hi | lt = lo |
|
|
631
|
+
* | LE | lt = hi | gte = lo |
|
|
632
|
+
* | LT | lt = lo | gte = hi |
|
|
633
|
+
*
|
|
634
|
+
* Across constraints keep the MAX lower and MIN upper. An `undefined` upper (an
|
|
635
|
+
* `hi` whose increment overflowed all-0xff) leaves that side unbounded — a safe
|
|
636
|
+
* SUPERSET. A NULL/missing bound value is skipped (the planner never pushes
|
|
637
|
+
* `= NULL`, and a range op against NULL rejects every row in matchesFilters).
|
|
638
|
+
*/
|
|
639
|
+
protected buildIndexRangeBounds(constraints: Array<{
|
|
640
|
+
op: IndexConstraintOp;
|
|
641
|
+
value?: SqlValue;
|
|
642
|
+
}>, dir: boolean): IterateOptions;
|
|
643
|
+
/**
|
|
644
|
+
* Scan a secondary index over `access.bounds`, resolving each index entry to its
|
|
645
|
+
* base row and re-filtering.
|
|
646
|
+
*
|
|
647
|
+
* {@link iterateEffective} yields the committed index entries merged with this
|
|
648
|
+
* transaction's pending index puts/deletes (read-your-own-writes over the
|
|
649
|
+
* index), in index-key byte order — the order the isolation overlay merge relies
|
|
650
|
+
* on (`isolated-table.ts` § buildSortKey). We resolve each entry to its row via
|
|
651
|
+
* its stored data-key value WITHOUT reordering, so index-key order is preserved.
|
|
652
|
+
*
|
|
653
|
+
* Defense in depth mirroring the memory layer's live-recheck: a resolved-null
|
|
654
|
+
* row (the entry's row was deleted — a pending index delete would normally
|
|
655
|
+
* suppress the entry, but a committed entry can lag) is skipped, and every
|
|
656
|
+
* resolved row is re-checked by {@link matchesFilters} (the byte window is only
|
|
657
|
+
* a superset, and a stale entry whose indexed column no longer matches is
|
|
658
|
+
* dropped).
|
|
659
|
+
*/
|
|
660
|
+
protected scanIndex(indexStore: KVStore, access: IndexAccessPattern, filterInfo: FilterInfo): AsyncIterable<Row>;
|
|
661
|
+
/**
|
|
662
|
+
* Check if a row matches the filter constraints.
|
|
663
|
+
*
|
|
664
|
+
* `collations` maps a constrained column index to the comparison function that
|
|
665
|
+
* column must be re-checked under; every caller builds it once per scan with
|
|
666
|
+
* {@link resolveFilterCollations}, whose doc comment explains how the name is
|
|
667
|
+
* chosen. A column absent from the map compares BINARY — the same result
|
|
668
|
+
* {@link resolveFilterCollations} produces for an undeclared collation, and the
|
|
669
|
+
* only reachable absence, since both walk the constraint list under identical
|
|
670
|
+
* skip conditions.
|
|
671
|
+
*/
|
|
672
|
+
protected matchesFilters(row: Row, filterInfo: FilterInfo, collations: ReadonlyMap<number, CollationFunction>): boolean;
|
|
673
|
+
/**
|
|
674
|
+
* Resolve, ONCE per scan, the comparison collation function for every pushed
|
|
675
|
+
* constraint column, keyed by column index.
|
|
676
|
+
*
|
|
677
|
+
* The name for a column is its DECLARED collation (absent ⇒ BINARY) — the same
|
|
678
|
+
* resolution the access path's collation-cover analysis uses
|
|
679
|
+
* (indexColumnCollationLookup / primaryKeyCollationLookup) when it decides a
|
|
680
|
+
* pushed constraint is fully covered, and the same source this file's UNIQUE
|
|
681
|
+
* checks compare under. On a collation MATCH the planner drops the residual
|
|
682
|
+
* Filter, so {@link matchesFilters} alone must reproduce the predicate.
|
|
683
|
+
*
|
|
684
|
+
* `indexCollationNames` (from {@link indexColumnCollations}) overrides the
|
|
685
|
+
* declared name for the secondary-index scan arm: the planner's MATCH there is
|
|
686
|
+
* against the INDEX column's collation, which an explicit `COLLATE` on the index
|
|
687
|
+
* can make differ from the table column's.
|
|
688
|
+
*
|
|
689
|
+
* Names resolve against {@link collationResolver}, so an unregistered collation
|
|
690
|
+
* raises `no such collation sequence` at scan setup rather than byte-ordering
|
|
691
|
+
* every row.
|
|
692
|
+
*
|
|
693
|
+
* NOTE: rebuilt on every `query()` / `scanPKRange()` / `scanIndex()` call — one
|
|
694
|
+
* registry lookup per distinct constrained column, dwarfed by the scan's I/O. If a
|
|
695
|
+
* point-lookup-heavy profile ever shows it, memoize on the `FilterInfo`.
|
|
696
|
+
*/
|
|
697
|
+
protected resolveFilterCollations(filterInfo: FilterInfo, indexCollationNames?: ReadonlyMap<number, string | undefined>): ReadonlyMap<number, CollationFunction>;
|
|
698
|
+
/**
|
|
699
|
+
* Effective per-column comparison collation for a secondary index's columns:
|
|
700
|
+
* the index column's own `COLLATE` when present, else the underlying table
|
|
701
|
+
* column's declared collation. Mirrors the resolution `StoreModule`'s
|
|
702
|
+
* index-maintenance UNIQUE dedup uses (`indexCol.collation ?? tableColumn`), so
|
|
703
|
+
* a re-checked index-scan row compares under the same collation the planner used
|
|
704
|
+
* to justify dropping (or keeping) the residual Filter.
|
|
705
|
+
*/
|
|
706
|
+
protected indexColumnCollations(index: TableIndexSchema): Map<number, string | undefined>;
|
|
707
|
+
/**
|
|
708
|
+
* Compare two values according to an operator, under `collationFunc` (already
|
|
709
|
+
* resolved by {@link resolveFilterCollations} against this database's collation
|
|
710
|
+
* registry). So the LT/LE/GT/GE range bounds honour a NOCASE/RTRIM/custom column
|
|
711
|
+
* collation rather than a raw BINARY JS comparison — the capability
|
|
346
712
|
* `StoreModule.getBestAccessPlan` advertises via `honorsCollatedRangeBounds`.
|
|
347
713
|
* NULL on either side fails every operator except EQ-with-both-NULL (the
|
|
348
714
|
* internal point-lookup convention; the planner never pushes `= NULL`).
|
|
349
715
|
*/
|
|
350
|
-
protected compareValues(a: SqlValue, op: IndexConstraintOp, b: SqlValue,
|
|
716
|
+
protected compareValues(a: SqlValue, op: IndexConstraintOp, b: SqlValue, collationFunc: CollationFunction): boolean;
|
|
351
717
|
/** Perform an update operation (INSERT, UPDATE, DELETE). */
|
|
352
718
|
update(args: UpdateArgs): Promise<UpdateResult>;
|
|
353
719
|
/**
|
|
@@ -397,6 +763,105 @@ export declare class StoreTable extends VirtualTable {
|
|
|
397
763
|
* (change-tracking, row-time MV maintenance, FK cascade, auto-events).
|
|
398
764
|
*/
|
|
399
765
|
protected checkUniqueConstraints(inTransaction: boolean, newRow: Row, selfPks: SqlValue[][], onConflict: ConflictResolution | undefined, evicted: Row[]): Promise<UpdateResult | null>;
|
|
766
|
+
/**
|
|
767
|
+
* Route one UNIQUE constraint's conflict search to the cheapest SOUND finder,
|
|
768
|
+
* in descending preference:
|
|
769
|
+
*
|
|
770
|
+
* 1. A linked row-time covering MV — its backing table (hosted by any
|
|
771
|
+
* backing-host-capable module — memory by default, this store module under
|
|
772
|
+
* `using store` — queried through the db with reads-own-writes) answers the
|
|
773
|
+
* uniqueness question, mirroring the memory enforcement path.
|
|
774
|
+
* 2. A physical secondary index realizing the constraint — one prefix seek
|
|
775
|
+
* instead of a full table scan (see {@link findIndexForUniqueConstraint}).
|
|
776
|
+
* 3. The full data-store scan ({@link findUniqueConflict}) — always correct,
|
|
777
|
+
* O(rows) per checked row.
|
|
778
|
+
*
|
|
779
|
+
* Every finder returns the SAME `{pk, row}` shape, so the caller's conflict
|
|
780
|
+
* action (ABORT / IGNORE / REPLACE eviction) is finder-independent.
|
|
781
|
+
*/
|
|
782
|
+
private findUniqueConflictFor;
|
|
783
|
+
/**
|
|
784
|
+
* The `schema.indexes` entry whose physical index store can serve `uc`'s
|
|
785
|
+
* conflict search as a point seek, or undefined when none can.
|
|
786
|
+
*
|
|
787
|
+
* Every non-derived UNIQUE now carries a materialized implicit index
|
|
788
|
+
* (`_uc_*`, see {@link withImplicitUniqueIndexes}), so — like the memory
|
|
789
|
+
* backend — a plain column- or table-level `UNIQUE` is index-servable, not
|
|
790
|
+
* only an explicit `CREATE INDEX`. A UC is index-servable when:
|
|
791
|
+
*
|
|
792
|
+
* - it is index-derived (`derivedFromIndex`, from `CREATE UNIQUE INDEX`) and
|
|
793
|
+
* its named index is still present — the index's partial predicate then
|
|
794
|
+
* equals the constraint's by construction (`appendIndexToTableSchema`); or
|
|
795
|
+
* - some index's columns equal `uc.columns` positionally AND whose predicate
|
|
796
|
+
* is the SAME object as `uc.predicate` (reference identity — the implicit
|
|
797
|
+
* materializer sets it that way). For a FULL UNIQUE that is
|
|
798
|
+
* `undefined === undefined`, which also admits any user full index over the
|
|
799
|
+
* same columns. For a PARTIAL `unique(...) where p` it admits only the
|
|
800
|
+
* co-scoped `_uc_*` (which holds exactly the in-scope rows); an arbitrary
|
|
801
|
+
* user partial index with a DIFFERENT predicate object stays conservative
|
|
802
|
+
* (declined → full scan), because it physically omits rows the constraint
|
|
803
|
+
* still covers. The serving index need not be UNIQUE — a plain index over
|
|
804
|
+
* the constrained columns still holds every in-scope row.
|
|
805
|
+
*
|
|
806
|
+
* Collation guard (see {@link indexSeekHonorsEnforcementCollation}) may still
|
|
807
|
+
* reject the found index, which routes the check back to the full scan.
|
|
808
|
+
*/
|
|
809
|
+
private findIndexForUniqueConstraint;
|
|
810
|
+
/**
|
|
811
|
+
* True when a point seek into the index realizing `uc` returns a SUPERSET of
|
|
812
|
+
* the constraint's true conflict set — the only condition under which the
|
|
813
|
+
* seek may replace the full scan.
|
|
814
|
+
*
|
|
815
|
+
* An index key's leading (index-column) bytes are encoded under the TABLE KEY
|
|
816
|
+
* collation K (`buildIndexKey` passes `this.encodeOptions`), NOT the index's
|
|
817
|
+
* declared per-column COLLATE and NOT the constraint's enforcement collation
|
|
818
|
+
* C (`uniqueEnforcementCollations` — the index's per-column COLLATE for an
|
|
819
|
+
* index-derived UC, else the declared column collation). A seek therefore
|
|
820
|
+
* fetches exactly `{rows K-equal to newRow}` while the re-validation keeps
|
|
821
|
+
* `{rows C-equal to newRow}`. Soundness needs
|
|
822
|
+
* `{C-equal} ⊆ {K-equal}`, i.e. K must be COARSER-OR-EQUAL to C per column:
|
|
823
|
+
*
|
|
824
|
+
* - non-text column → its bytes are type-native, collation-independent: safe.
|
|
825
|
+
* - C == K → the sets coincide: safe.
|
|
826
|
+
* - K = NOCASE, C = BINARY → K strictly coarser: safe superset.
|
|
827
|
+
* - otherwise (K = BINARY over C = NOCASE/RTRIM; K = NOCASE over C = RTRIM)
|
|
828
|
+
* the seek UNDER-fetches and a real duplicate would be silently accepted:
|
|
829
|
+
* reject, so the caller full-scans.
|
|
830
|
+
*
|
|
831
|
+
* Same direction and same admitted cases as the read-side guard in
|
|
832
|
+
* `StoreModule.tryIndexAccessPlan`; conservative rather than exhaustive
|
|
833
|
+
* (K = RTRIM over C = BINARY is provably safe but declined), which costs an
|
|
834
|
+
* optimization, never correctness. The coarseness test is only sound for the
|
|
835
|
+
* built-in names: a custom K equals a custom C only when the index column names
|
|
836
|
+
* that same collation, and otherwise falls through to the full scan.
|
|
837
|
+
*/
|
|
838
|
+
private indexSeekHonorsEnforcementCollation;
|
|
839
|
+
/**
|
|
840
|
+
* The index analogue of {@link findUniqueConflict}: seek the index realizing
|
|
841
|
+
* `uc` at the point formed by `newRow`'s constrained-column values, and
|
|
842
|
+
* re-validate each resolved candidate exactly as the full scan does.
|
|
843
|
+
*
|
|
844
|
+
* The seek encodes ALL of `uc.columns` (positionally aligned with the index's
|
|
845
|
+
* columns, guaranteed by `appendIndexToTableSchema` / the column-set match in
|
|
846
|
+
* {@link findIndexForUniqueConstraint}) as a leading prefix of the index key,
|
|
847
|
+
* under the same key collation and per-column DESC directions
|
|
848
|
+
* {@link updateSecondaryIndexes} used to write it. The remaining suffix is the
|
|
849
|
+
* row's PK, so the window spans every entry sharing those column values.
|
|
850
|
+
* {@link iterateEffective} merges this transaction's pending index puts/deletes
|
|
851
|
+
* over the committed entries, giving read-your-own-writes; each entry resolves
|
|
852
|
+
* to its LIVE row through the data key stored as the entry's value.
|
|
853
|
+
*
|
|
854
|
+
* The seek only narrows the CANDIDATE set to a superset (guaranteed by
|
|
855
|
+
* {@link indexSeekHonorsEnforcementCollation}); the authoritative comparison is
|
|
856
|
+
* the identical self-PK exclusion, per-column enforcement-collation compare,
|
|
857
|
+
* and partial-predicate scope check the full scan performs. A partial index
|
|
858
|
+
* already excludes out-of-scope rows physically — the predicate re-check is
|
|
859
|
+
* kept as defense in depth.
|
|
860
|
+
*
|
|
861
|
+
* Returns {@link INDEX_UNUSABLE} rather than a (possibly wrong) answer when an
|
|
862
|
+
* entry carries a legacy empty value.
|
|
863
|
+
*/
|
|
864
|
+
private findUniqueConflictViaIndex;
|
|
400
865
|
/**
|
|
401
866
|
* Scan committed + pending data rows for a row matching `newRow` on
|
|
402
867
|
* `uc.columns` whose PK is not in `selfPks`. For partial UNIQUE, candidates
|
|
@@ -451,7 +916,7 @@ export declare class StoreTable extends VirtualTable {
|
|
|
451
916
|
* Declared secondary-UNIQUE enforcement for maintenance writes — the store
|
|
452
917
|
* mirror of the memory manager's `enforceSecondaryUniqueOnMaintenance` (see
|
|
453
918
|
* `vtab/backing-host.ts` § Constraint validation for the contract and
|
|
454
|
-
* docs/
|
|
919
|
+
* docs/mv-constraints.md § Derived-row constraint validation for the
|
|
455
920
|
* semantics). Called by `StoreBackingHost.applyMaintenance` AFTER the op
|
|
456
921
|
* batch lands in the coordinator's pending state: post-batch is load-bearing
|
|
457
922
|
* (a `replace-all` diff applies puts before deletes, so a per-op check would
|
|
@@ -466,8 +931,10 @@ export declare class StoreTable extends VirtualTable {
|
|
|
466
931
|
* and would miss a same-batch colliding pair. The conflict action is a hard
|
|
467
932
|
* abort (a derivation write carries no user OR clause, and a declared
|
|
468
933
|
* `on conflict replace`/`ignore` default must not evict or drop derived
|
|
469
|
-
* rows). Per-image cost is one effective scan
|
|
470
|
-
*
|
|
934
|
+
* rows). Per-image cost is one effective full scan: unlike the DML path
|
|
935
|
+
* ({@link findUniqueConflictFor}), this one is NOT routed through
|
|
936
|
+
* {@link findUniqueConflictViaIndex}, because a backing table keeps no
|
|
937
|
+
* secondary indexes by design — there is never an index store to seek.
|
|
471
938
|
*
|
|
472
939
|
* Zero overhead when the table declares no secondary UNIQUE (every MV-sugar
|
|
473
940
|
* backing, and most maintained tables): one empty-array check.
|
|
@@ -585,5 +1052,17 @@ interface PKAccessPattern {
|
|
|
585
1052
|
value?: SqlValue;
|
|
586
1053
|
}>;
|
|
587
1054
|
}
|
|
1055
|
+
/**
|
|
1056
|
+
* Secondary-index access pattern analysis result: the chosen index plus the
|
|
1057
|
+
* encoded byte window {@link StoreTable.scanIndex} iterates. `point` is a
|
|
1058
|
+
* leading-prefix EQ window, `range` a leading-column LT/LE/GT/GE window; both
|
|
1059
|
+
* resolve to a `bounds` scan (an index seek is always a prefix scan, never a
|
|
1060
|
+
* single entry).
|
|
1061
|
+
*/
|
|
1062
|
+
interface IndexAccessPattern {
|
|
1063
|
+
index: TableIndexSchema;
|
|
1064
|
+
type: 'point' | 'range';
|
|
1065
|
+
bounds: IterateOptions;
|
|
1066
|
+
}
|
|
588
1067
|
export {};
|
|
589
1068
|
//# sourceMappingURL=store-table.d.ts.map
|