@quereus/store 4.4.0 → 4.5.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 +512 -486
- package/dist/src/common/cached-kv-store.js +4 -0
- package/dist/src/common/cached-kv-store.js.map +1 -1
- package/dist/src/common/encoding.d.ts +32 -1
- package/dist/src/common/encoding.d.ts.map +1 -1
- package/dist/src/common/encoding.js +42 -10
- package/dist/src/common/encoding.js.map +1 -1
- package/dist/src/common/index.d.ts +3 -2
- package/dist/src/common/index.d.ts.map +1 -1
- package/dist/src/common/index.js +3 -1
- package/dist/src/common/index.js.map +1 -1
- package/dist/src/common/json-key.d.ts +96 -0
- package/dist/src/common/json-key.d.ts.map +1 -0
- package/dist/src/common/json-key.js +204 -0
- package/dist/src/common/json-key.js.map +1 -0
- package/dist/src/common/key-builder.d.ts +41 -5
- package/dist/src/common/key-builder.d.ts.map +1 -1
- package/dist/src/common/key-builder.js +60 -21
- package/dist/src/common/key-builder.js.map +1 -1
- package/dist/src/common/kv-store.d.ts +12 -2
- package/dist/src/common/kv-store.d.ts.map +1 -1
- package/dist/src/common/store-module.d.ts +64 -15
- package/dist/src/common/store-module.d.ts.map +1 -1
- package/dist/src/common/store-module.js +439 -133
- package/dist/src/common/store-module.js.map +1 -1
- package/dist/src/common/store-table.d.ts +226 -12
- package/dist/src/common/store-table.d.ts.map +1 -1
- package/dist/src/common/store-table.js +454 -37
- package/dist/src/common/store-table.js.map +1 -1
- package/dist/src/testing/kv-conformance.d.ts.map +1 -1
- package/dist/src/testing/kv-conformance.js +32 -0
- package/dist/src/testing/kv-conformance.js.map +1 -1
- package/package.json +6 -6
|
@@ -9,11 +9,12 @@
|
|
|
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, QuereusError, StatusCode, compareSqlValues, compareSqlValuesFast, resolveUniqueEnforcementCollations, BINARY_COLLATION, rowsValueIdentical,
|
|
12
|
+
import { VirtualTable, IndexConstraintOp, ConflictResolution, QuereusError, StatusCode, compareSqlValues, compareSqlValuesFast, createTypedComparator, hasSemanticOrdering, semanticKeyTransform, resolveUniqueEnforcementCollations, uniqueEnforcementComparators, BINARY_COLLATION, rowsValueIdentical, coerceRowToSchema, compilePredicate, decodeIdxStr, planKindFromCode, maintainedTableUniqueViolationError, uniqueEnforcementCollations, logicalTypeCanHoldText, pkKeyCollationName, JSON_TYPE, } from '@quereus/quereus';
|
|
13
13
|
import { bytesEqual, bytesToHex, compareBytes } from './bytes.js';
|
|
14
14
|
import { StoreConnection } from './store-connection.js';
|
|
15
15
|
import { buildDataKey, buildIndexKey, buildIndexPrefixBounds, buildFullScanBounds, buildPkPrefixBounds, buildStatsKey, } from './key-builder.js';
|
|
16
16
|
import { serializeRow, deserializeRow, serializeStats, deserializeStats, } from './serialization.js';
|
|
17
|
+
import { jsonStructuralKey } from './json-key.js';
|
|
17
18
|
/** Number of mutations before persisting statistics. */
|
|
18
19
|
const STATS_FLUSH_INTERVAL = 100;
|
|
19
20
|
/** True when `key` falls within the (gte/gt/lte/lt) window of `bounds`. */
|
|
@@ -87,6 +88,76 @@ export function resolvePkKeyCollations(pkDef, columns, fallback) {
|
|
|
87
88
|
return name === undefined ? undefined : (name || fallback).toUpperCase();
|
|
88
89
|
});
|
|
89
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* The store's per-type key VALUE TRANSFORM for a semantic-ordering logical type, or
|
|
93
|
+
* `undefined` when raw values already key faithfully.
|
|
94
|
+
*
|
|
95
|
+
* The contract is TWO-sided — identity AND order. `StoreTable` implements no
|
|
96
|
+
* `comparePrimaryKey`, so the isolation layer merges overlay against underlying with
|
|
97
|
+
* its own fallback comparator (the PK type's `compare` via `createTypedComparator`)
|
|
98
|
+
* and needs this table's key BYTE order to agree with it; and byte equality must be
|
|
99
|
+
* exactly the type's equality, or two spellings of one value land on two keys.
|
|
100
|
+
*
|
|
101
|
+
* - JSON → the store-local structural encoder ({@link jsonStructuralKey}): a
|
|
102
|
+
* `Uint8Array` whose memcmp order reproduces the type's structural `compare`
|
|
103
|
+
* (canonical-text bytes are identity-faithful but sort `[10]` before `[2]`,
|
|
104
|
+
* which broke overlay shadowing — see json-key.ts).
|
|
105
|
+
* - Everything else → the engine's {@link semanticKeyTransform} (the type's
|
|
106
|
+
* `groupKey`). Today that is TIMESPAN: total seconds against the same reference
|
|
107
|
+
* date as `TIMESPAN.compare`, so 'PT1H' and 'PT60M' collide on one key AND byte
|
|
108
|
+
* order is elapsed-time order.
|
|
109
|
+
*
|
|
110
|
+
* JSON deliberately gets NO engine `groupKey` — `groupKey` also drives GROUP BY /
|
|
111
|
+
* `IN` / hash-join identity, where canonical-text identity is already correct and a
|
|
112
|
+
* `Uint8Array` key would be a behaviour change for no benefit. Hence this
|
|
113
|
+
* store-local seam. A semantic-ordering type with NEITHER a `groupKey` nor an entry
|
|
114
|
+
* here cannot key a persisted structure soundly; `StoreTable` rejects it at DDL time
|
|
115
|
+
* (see `validateSemanticKeyTransforms`).
|
|
116
|
+
*/
|
|
117
|
+
export function storeSemanticKeyTransform(type) {
|
|
118
|
+
// Matched by NAME, not object identity (`type === JSON_TYPE`): the engine and this
|
|
119
|
+
// package can be loaded as two module instances (e.g. the logic suite runs the
|
|
120
|
+
// engine from src via ts-node while the store resolves `@quereus/quereus` to dist),
|
|
121
|
+
// and each instance carries its own JSON_TYPE singleton. The `hasSemanticOrdering`
|
|
122
|
+
// gate keeps a hypothetical plain custom type named 'JSON' out of this branch.
|
|
123
|
+
if (hasSemanticOrdering(type) && type.name === JSON_TYPE.name)
|
|
124
|
+
return jsonStructuralKey;
|
|
125
|
+
return semanticKeyTransform(type);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Resolve the per-column key value transform for each primary-key column —
|
|
129
|
+
* {@link storeSemanticKeyTransform} of the member's logical type. One entry per PK
|
|
130
|
+
* member in `pkDef` order; `undefined` for every type whose raw value already keys
|
|
131
|
+
* faithfully. The companion of {@link resolvePkKeyCollations}: every call site that
|
|
132
|
+
* threads the collations MUST thread these too, or key identity/order drifts
|
|
133
|
+
* between the write path and the rebuild/lookup path.
|
|
134
|
+
*/
|
|
135
|
+
export function resolvePkKeyTransforms(pkDef, columns) {
|
|
136
|
+
return pkDef.map(def => storeSemanticKeyTransform(columns[def.index]?.logicalType));
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Per-column key value transforms for a secondary index's OWN columns (the
|
|
140
|
+
* index half of `buildIndexKey`), positionally aligned with `index.columns`.
|
|
141
|
+
* Same rule as {@link resolvePkKeyTransforms}: a semantic-ordering member
|
|
142
|
+
* canonicalizes through {@link storeSemanticKeyTransform}, everything else
|
|
143
|
+
* encodes raw.
|
|
144
|
+
*/
|
|
145
|
+
export function resolveIndexKeyTransforms(index, columns) {
|
|
146
|
+
return index.columns.map(col => storeSemanticKeyTransform(columns[col.index]?.logicalType));
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Per-PK-column typed equality comparator for semantic-ordering members, `undefined`
|
|
150
|
+
* elsewhere. Backs {@link StoreTable.keysEqual}: two PK arrays naming the same
|
|
151
|
+
* physical key under {@link resolvePkKeyTransforms} ('PT1H' / 'PT60M') must also
|
|
152
|
+
* answer "same row" here, or self-PK exclusion in the UNIQUE checks false-conflicts
|
|
153
|
+
* a row against its own differently-spelled image.
|
|
154
|
+
*/
|
|
155
|
+
function resolvePkSemanticEquality(pkDef, columns) {
|
|
156
|
+
return pkDef.map(def => {
|
|
157
|
+
const logicalType = columns[def.index]?.logicalType;
|
|
158
|
+
return hasSemanticOrdering(logicalType) ? createTypedComparator(logicalType) : undefined;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
90
161
|
/**
|
|
91
162
|
* True when `col` can produce a TEXT value at runtime, and so when its physical
|
|
92
163
|
* key bytes are produced by a collation normalizer rather than a type-native
|
|
@@ -128,6 +199,17 @@ export function columnCanHoldText(col) {
|
|
|
128
199
|
* "mark handled" and "build a window" decisions can never disagree.
|
|
129
200
|
*/
|
|
130
201
|
export function keyOrderMatchesCollation(db, column, keyCollation, compareCollation) {
|
|
202
|
+
// A semantic-ordering logical type (TIMESPAN, JSON — see docs/types.md "Semantic
|
|
203
|
+
// ordering") is ordered by its `compare` (elapsed time / structural), which the
|
|
204
|
+
// engine's Sort, comparison operators, and the memory backend all use. Both types'
|
|
205
|
+
// key bytes now encode through an order-preserving transform (TIMESPAN's `groupKey`
|
|
206
|
+
// total seconds; JSON's structural encoding — see `storeSemanticKeyTransform`), so
|
|
207
|
+
// byte order DOES match the type's `compare` for every well-formed value and this
|
|
208
|
+
// blanket decline is conservative, costing the seek/sort-elision, never a row.
|
|
209
|
+
// Re-opening the window/advertisement (and the byte-EQ arms in `analyzePKAccess` /
|
|
210
|
+
// `analyzeIndexAccess`) is tracked in backlog `feat-reopen-timespan-store-seeks`.
|
|
211
|
+
if (hasSemanticOrdering(column?.logicalType))
|
|
212
|
+
return false;
|
|
131
213
|
if (!columnCanHoldText(column))
|
|
132
214
|
return true;
|
|
133
215
|
if (compareCollation.toUpperCase() !== keyCollation.toUpperCase())
|
|
@@ -145,13 +227,12 @@ export function keyOrderMatchesCollation(db, column, keyCollation, compareCollat
|
|
|
145
227
|
* already aligned with the key collation for every *textual* PK member.
|
|
146
228
|
*
|
|
147
229
|
* What the advertisement is measured against is the order the planner's `Sort` would have
|
|
148
|
-
* produced
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
* `bug-memory-pk-btree-orders-by-logical-type-compare`, not something the store should copy.
|
|
230
|
+
* produced. For most members Sort orders under the operand's COLLATION, so a collation match
|
|
231
|
+
* is the question; for a member whose logical type carries SEMANTIC ordering (TIMESPAN, JSON
|
|
232
|
+
* — see docs/types.md "Semantic ordering") Sort now ranks by `logicalType.compare`, which no
|
|
233
|
+
* text-byte encoding reproduces, and {@link keyOrderMatchesCollation} closes the gate for
|
|
234
|
+
* such members outright (the memory backend's typed BTree order is the canonical order under
|
|
235
|
+
* that ruling; the store declines rather than advertising a divergent byte order).
|
|
155
236
|
*
|
|
156
237
|
* `0` ⇒ even the leading member is unsafe: no range seek, no PK-order advertisement.
|
|
157
238
|
* A prefix shorter than the PK truncates the ordering advertisement rather than voiding it.
|
|
@@ -279,6 +360,21 @@ export class StoreTable extends VirtualTable {
|
|
|
279
360
|
* {@link updateSchema} (an ALTER COLUMN SET COLLATE on a PK member changes it).
|
|
280
361
|
*/
|
|
281
362
|
pkKeyCollations;
|
|
363
|
+
/**
|
|
364
|
+
* Per-PK-column key-identity value transform (see {@link resolvePkKeyTransforms}).
|
|
365
|
+
* Threaded alongside {@link pkKeyCollations} into every data-key / PK-suffix /
|
|
366
|
+
* PK-prefix-bounds encode, so a semantic-ordering PK member (TIMESPAN) keys
|
|
367
|
+
* semantically-equal spellings to one physical key. Recomputed on every
|
|
368
|
+
* {@link updateSchema}.
|
|
369
|
+
*/
|
|
370
|
+
pkKeyTransforms;
|
|
371
|
+
/**
|
|
372
|
+
* Per-PK-column typed EQUALITY comparator for the semantic-ordering members only —
|
|
373
|
+
* `undefined` for every other member, which {@link keysEqual} still compares with
|
|
374
|
+
* strict `!==` as before. Keeps self-PK exclusion and PK-identity questions in
|
|
375
|
+
* agreement with the physical key identity above ('PT1H' row IS the 'PT60M' row).
|
|
376
|
+
*/
|
|
377
|
+
pkSemanticEquality;
|
|
282
378
|
/**
|
|
283
379
|
* `db.getCollationResolver()`, bound once at construction. Every collation-aware
|
|
284
380
|
* VALUE comparison this table makes — pushed-constraint re-check, UNIQUE conflict
|
|
@@ -345,9 +441,12 @@ export class StoreTable extends VirtualTable {
|
|
|
345
441
|
};
|
|
346
442
|
this.pkDirections = tableSchema.primaryKeyDefinition.map(pk => !!pk.desc);
|
|
347
443
|
this.pkKeyCollations = resolvePkKeyCollations(tableSchema.primaryKeyDefinition, tableSchema.columns, this.encodeOptions.collation ?? 'NOCASE');
|
|
444
|
+
this.pkKeyTransforms = resolvePkKeyTransforms(tableSchema.primaryKeyDefinition, tableSchema.columns);
|
|
445
|
+
this.pkSemanticEquality = resolvePkSemanticEquality(tableSchema.primaryKeyDefinition, tableSchema.columns);
|
|
348
446
|
// Validate the MATERIALIZED schema, so a `_uc_*` over a text column that needs the
|
|
349
447
|
// table key collation K is rejected up front just like an explicit index would be.
|
|
350
448
|
this.validateKeyCollations(this.materializedSchema, this.pkKeyCollations);
|
|
449
|
+
this.validateSemanticKeyTransforms(this.materializedSchema);
|
|
351
450
|
this.ddlSaved = isConnected;
|
|
352
451
|
}
|
|
353
452
|
/**
|
|
@@ -397,6 +496,47 @@ export class StoreTable extends VirtualTable {
|
|
|
397
496
|
+ `— pass { normalizer } to registerCollation`, StatusCode.ERROR);
|
|
398
497
|
}
|
|
399
498
|
}
|
|
499
|
+
/**
|
|
500
|
+
* Reject, at DDL time, a key member whose semantic-ordering logical type has no
|
|
501
|
+
* store key transform — before a single row is keyed under bytes whose order the
|
|
502
|
+
* type's `compare` (and so the isolation merge, the memory oracle, and Sort)
|
|
503
|
+
* disagrees with.
|
|
504
|
+
*
|
|
505
|
+
* A semantic-ordering type keys soundly only through an order- and
|
|
506
|
+
* identity-preserving transform ({@link storeSemanticKeyTransform}: the type's
|
|
507
|
+
* `groupKey`, or a store-local encoder like JSON's structural one). This gap used
|
|
508
|
+
* to be undetectable — JSON sat here transform-less for a release, keying in
|
|
509
|
+
* canonical-text order while every comparator ordered structurally, and the only
|
|
510
|
+
* symptom was a misaligned overlay merge deep inside a transaction. Unreachable
|
|
511
|
+
* today (TIMESPAN and JSON are both covered); it exists so the NEXT
|
|
512
|
+
* semantic-ordering type fails loudly at CREATE TABLE instead.
|
|
513
|
+
*
|
|
514
|
+
* Checked over the same materialized schema as {@link validateKeyCollations}:
|
|
515
|
+
* every PK member and every (explicit or hidden `_uc_*`) index column.
|
|
516
|
+
*
|
|
517
|
+
* CAVEAT: this can only check that a transform EXISTS — a `groupKey` that is
|
|
518
|
+
* identity-faithful but not order-preserving passes undetected, so a new type's
|
|
519
|
+
* transform still needs its byte order argued against its `compare` (see
|
|
520
|
+
* json-key.ts for the shape of that argument).
|
|
521
|
+
*/
|
|
522
|
+
validateSemanticKeyTransforms(schema) {
|
|
523
|
+
const check = (colIndex, member) => {
|
|
524
|
+
const col = schema.columns[colIndex];
|
|
525
|
+
const type = col?.logicalType;
|
|
526
|
+
if (!hasSemanticOrdering(type) || storeSemanticKeyTransform(type) !== undefined)
|
|
527
|
+
return;
|
|
528
|
+
throw new QuereusError(`column ${col?.name ?? String(colIndex)} (${member}): semantic-ordering type `
|
|
529
|
+
+ `${type.name} cannot key a persisted structure: its raw encoding would not `
|
|
530
|
+
+ `reproduce the type's compare order — the type needs an order-preserving `
|
|
531
|
+
+ `groupKey or a store key encoder (see storeSemanticKeyTransform)`, StatusCode.ERROR);
|
|
532
|
+
};
|
|
533
|
+
for (const def of schema.primaryKeyDefinition)
|
|
534
|
+
check(def.index, 'primary key');
|
|
535
|
+
for (const index of schema.indexes ?? []) {
|
|
536
|
+
for (const col of index.columns)
|
|
537
|
+
check(col.index, `index ${index.name}`);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
400
540
|
/** Get the table configuration. */
|
|
401
541
|
getConfig() {
|
|
402
542
|
return this.config;
|
|
@@ -427,10 +567,13 @@ export class StoreTable extends VirtualTable {
|
|
|
427
567
|
const materialized = withImplicitUniqueIndexes(newSchema);
|
|
428
568
|
const pkKeyCollations = resolvePkKeyCollations(newSchema.primaryKeyDefinition, newSchema.columns, this.encodeOptions.collation ?? 'NOCASE');
|
|
429
569
|
this.validateKeyCollations(materialized, pkKeyCollations);
|
|
570
|
+
this.validateSemanticKeyTransforms(materialized);
|
|
430
571
|
this.tableSchema = newSchema;
|
|
431
572
|
this.materializedSchema = materialized;
|
|
432
573
|
this.pkDirections = newSchema.primaryKeyDefinition.map(pk => !!pk.desc);
|
|
433
574
|
this.pkKeyCollations = pkKeyCollations;
|
|
575
|
+
this.pkKeyTransforms = resolvePkKeyTransforms(newSchema.primaryKeyDefinition, newSchema.columns);
|
|
576
|
+
this.pkSemanticEquality = resolvePkSemanticEquality(newSchema.primaryKeyDefinition, newSchema.columns);
|
|
434
577
|
}
|
|
435
578
|
/**
|
|
436
579
|
* Mark the table's DDL as already persisted to the catalog, so the lazy
|
|
@@ -566,9 +709,13 @@ export class StoreTable extends VirtualTable {
|
|
|
566
709
|
const bounds = buildFullScanBounds();
|
|
567
710
|
const newPkDirections = newPkDef.map(pk => !!pk.desc);
|
|
568
711
|
const newPkCollations = resolvePkKeyCollations(newPkDef, newColumns, this.encodeOptions.collation ?? 'NOCASE');
|
|
712
|
+
// Post-ALTER key-identity transforms too: an ALTER PRIMARY KEY onto (or a SET
|
|
713
|
+
// DATA TYPE creating) a semantic-ordering member must collapse equal spellings
|
|
714
|
+
// — pass 1 then rejects 'PT1H'/'PT60M' as a duplicate PK, all-or-nothing.
|
|
715
|
+
const newPkTransforms = resolvePkKeyTransforms(newPkDef, newColumns);
|
|
569
716
|
// Both passes key rows through this one helper, so a collision judged in pass 1
|
|
570
717
|
// is byte-identical to the key pass 2 writes.
|
|
571
|
-
const computeNewKey = (row) => buildDataKey(newPkDef.map(pk => row[pk.index]), this.encodeOptions, newPkDirections, newPkCollations);
|
|
718
|
+
const computeNewKey = (row) => buildDataKey(newPkDef.map(pk => row[pk.index]), this.encodeOptions, newPkDirections, newPkCollations, newPkTransforms);
|
|
572
719
|
// Pass 1 — collision detection only. Hold one hex signature per new key, never
|
|
573
720
|
// the row or old key. On a repeat, reject before any write; the store is
|
|
574
721
|
// untouched on rejection.
|
|
@@ -783,11 +930,7 @@ export class StoreTable extends VirtualTable {
|
|
|
783
930
|
* objects before PK extraction, serialization, and index-key construction.
|
|
784
931
|
*/
|
|
785
932
|
coerceRow(row) {
|
|
786
|
-
|
|
787
|
-
if (row.length > cols.length) {
|
|
788
|
-
throw new QuereusError(`Too many values for ${this.schemaName}.${this.tableName}: expected ${cols.length}, got ${row.length}`, StatusCode.ERROR);
|
|
789
|
-
}
|
|
790
|
-
return row.map((v, i) => validateAndParse(v, cols[i].logicalType, cols[i].name));
|
|
933
|
+
return coerceRowToSchema(row, this.tableSchema.columns, `${this.schemaName}.${this.tableName}`);
|
|
791
934
|
}
|
|
792
935
|
/**
|
|
793
936
|
* Query the table with optional filters.
|
|
@@ -800,6 +943,16 @@ export class StoreTable extends VirtualTable {
|
|
|
800
943
|
*/
|
|
801
944
|
async *query(filterInfo) {
|
|
802
945
|
const store = await this.ensureStore();
|
|
946
|
+
// Multi-seek (`plan=5`, an IN-list served as N point seeks) dispatches FIRST —
|
|
947
|
+
// before analyzePKAccess: the FilterInfo carries N*W EQ constraints, so on a
|
|
948
|
+
// table whose PK column is also the leading column of the chosen index,
|
|
949
|
+
// analyzePKAccess would match the FIRST equality and answer a single-value
|
|
950
|
+
// point lookup — one list value's rows, a silent wrong result.
|
|
951
|
+
const idxSpec = decodeIdxStr(filterInfo.idxStr);
|
|
952
|
+
if (idxSpec && planKindFromCode(idxSpec.plan) === 'multiSeek') {
|
|
953
|
+
yield* this.scanMultiSeek(idxSpec, filterInfo);
|
|
954
|
+
return;
|
|
955
|
+
}
|
|
803
956
|
const pkAccess = this.analyzePKAccess(filterInfo);
|
|
804
957
|
if (pkAccess.type === 'point') {
|
|
805
958
|
const row = await this.readLiveRowByPk(pkAccess.values);
|
|
@@ -891,6 +1044,22 @@ export class StoreTable extends VirtualTable {
|
|
|
891
1044
|
leadingPkRangeIsOrderSafe() {
|
|
892
1045
|
return pkOrderPreservingPrefixLength(this.db, this.tableSchema, this.pkKeyCollations, this.encodeOptions.collation ?? 'NOCASE') >= 1;
|
|
893
1046
|
}
|
|
1047
|
+
/**
|
|
1048
|
+
* True when some PK member's logical type carries semantic ordering (TIMESPAN,
|
|
1049
|
+
* JSON). Such PKs decline the point arm and full-scan, where
|
|
1050
|
+
* {@link matchesFilters} applies the type's compare. Historically required —
|
|
1051
|
+
* byte-key equality was a strict subset of the type's equality ('PT1H' ≡
|
|
1052
|
+
* 'PT60M' elapsed-time-equal, byte-distinct) and a point window under-fetched
|
|
1053
|
+
* with no residual able to resurrect a skipped row. Both types' key transforms
|
|
1054
|
+
* (see {@link storeSemanticKeyTransform}) now make byte equality the type's
|
|
1055
|
+
* equality, so the decline is conservative; re-opening is tracked in backlog
|
|
1056
|
+
* `feat-reopen-timespan-store-seeks`. (Range windows and ordering
|
|
1057
|
+
* advertisements are already declined by {@link keyOrderMatchesCollation}.)
|
|
1058
|
+
*/
|
|
1059
|
+
pkHasSemanticOrderingMember() {
|
|
1060
|
+
const schema = this.tableSchema;
|
|
1061
|
+
return schema.primaryKeyDefinition.some(def => hasSemanticOrdering(schema.columns[def.index]?.logicalType));
|
|
1062
|
+
}
|
|
894
1063
|
/** Analyze filter info to determine PK access pattern. */
|
|
895
1064
|
analyzePKAccess(filterInfo) {
|
|
896
1065
|
const schema = this.tableSchema;
|
|
@@ -912,7 +1081,7 @@ export class StoreTable extends VirtualTable {
|
|
|
912
1081
|
break;
|
|
913
1082
|
}
|
|
914
1083
|
}
|
|
915
|
-
if (allEq) {
|
|
1084
|
+
if (allEq && !this.pkHasSemanticOrderingMember()) {
|
|
916
1085
|
return { type: 'point', values: eqValues };
|
|
917
1086
|
}
|
|
918
1087
|
// Check for range constraints on first PK column
|
|
@@ -1068,9 +1237,14 @@ export class StoreTable extends VirtualTable {
|
|
|
1068
1237
|
return null;
|
|
1069
1238
|
const indexCols = index.columns.map(c => c.index);
|
|
1070
1239
|
const indexDirections = index.columns.map(c => !!c.desc);
|
|
1071
|
-
// Contiguous leading-prefix EQ → point/prefix window.
|
|
1240
|
+
// Contiguous leading-prefix EQ → point/prefix window. A prefix member whose
|
|
1241
|
+
// logical type carries semantic ordering stops the prefix: its byte-equality
|
|
1242
|
+
// window under-fetches the type's equality (see pkHasSemanticOrderingMember),
|
|
1243
|
+
// and a skipped row cannot be resurrected by the residual.
|
|
1072
1244
|
const eqValues = [];
|
|
1073
1245
|
for (let i = 0; i < indexCols.length; i++) {
|
|
1246
|
+
if (hasSemanticOrdering(this.tableSchema.columns[indexCols[i]]?.logicalType))
|
|
1247
|
+
break;
|
|
1074
1248
|
const eq = filterInfo.constraints?.find(c => c.constraint.iColumn === indexCols[i]
|
|
1075
1249
|
&& c.constraint.op === IndexConstraintOp.EQ
|
|
1076
1250
|
&& c.argvIndex > 0);
|
|
@@ -1170,12 +1344,13 @@ export class StoreTable extends VirtualTable {
|
|
|
1170
1344
|
* a superset, and a stale entry whose indexed column no longer matches is
|
|
1171
1345
|
* dropped).
|
|
1172
1346
|
*/
|
|
1173
|
-
async *scanIndex(indexStore, access, filterInfo) {
|
|
1347
|
+
async *scanIndex(indexStore, access, filterInfo, multi) {
|
|
1174
1348
|
// Re-check each resolved row under the INDEX's per-column collation (see
|
|
1175
1349
|
// matchesFilters): the planner dropped the residual based on the index
|
|
1176
1350
|
// column's collation, which an explicit index `COLLATE` can make differ from
|
|
1177
1351
|
// the table column's declared collation.
|
|
1178
|
-
const indexCollations =
|
|
1352
|
+
const indexCollations = multi?.collations
|
|
1353
|
+
?? this.resolveFilterCollations(filterInfo, this.indexColumnCollations(access.index));
|
|
1179
1354
|
for await (const entry of this.iterateEffective(indexStore, access.bounds)) {
|
|
1180
1355
|
// NOTE: a legacy index store (written before index values carried the data
|
|
1181
1356
|
// key) holds EMPTY values; a zero-length data key is not a row key, so skip
|
|
@@ -1190,6 +1365,14 @@ export class StoreTable extends VirtualTable {
|
|
|
1190
1365
|
// fall back to a full scan the first time an empty value is seen.
|
|
1191
1366
|
if (entry.value.length === 0)
|
|
1192
1367
|
continue;
|
|
1368
|
+
// Cross-window dedup for a multi-seek: a data key an earlier window already
|
|
1369
|
+
// YIELDED is skipped before the data-store read, so a duplicate costs no
|
|
1370
|
+
// extra `get`. The seen-set is only ever ADDED to on a yield (below) — adding
|
|
1371
|
+
// at visit time would let a stale index entry that fails its residual poison
|
|
1372
|
+
// the set and suppress the row's live entry in a later window.
|
|
1373
|
+
const dataKeyHex = multi ? bytesToHex(entry.value) : undefined;
|
|
1374
|
+
if (dataKeyHex !== undefined && multi.seen.has(dataKeyHex))
|
|
1375
|
+
continue;
|
|
1193
1376
|
// NOTE: one extra data-store `get` per matched index entry — the row lives
|
|
1194
1377
|
// in the data store, not the index (the index value carries only the data
|
|
1195
1378
|
// key, no covering payload). Fine now; if index-covered scans ever dominate
|
|
@@ -1197,7 +1380,204 @@ export class StoreTable extends VirtualTable {
|
|
|
1197
1380
|
// at the cost of an index rewrite on EVERY column change (not just indexed
|
|
1198
1381
|
// columns) — deliberately not done here.
|
|
1199
1382
|
const row = await this.readEffectiveRowByKey(entry.value);
|
|
1200
|
-
if (row
|
|
1383
|
+
if (!row)
|
|
1384
|
+
continue;
|
|
1385
|
+
if (this.matchesFilters(row, filterInfo, indexCollations)
|
|
1386
|
+
|| (multi !== undefined && multi.extraTuples.some(fi => this.matchesFilters(row, fi, indexCollations)))) {
|
|
1387
|
+
if (dataKeyHex !== undefined)
|
|
1388
|
+
multi.seen.add(dataKeyHex);
|
|
1389
|
+
yield row;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
/**
|
|
1394
|
+
* Fail loudly on a multi-seek FilterInfo this table cannot serve. The plan already
|
|
1395
|
+
* dropped the residual Filter, and {@link matchesFilters} ANDs every pushed
|
|
1396
|
+
* constraint — so falling through to the scan arm would AND N mutually-exclusive
|
|
1397
|
+
* equalities and return zero rows: a silent wrong answer, not a slow one.
|
|
1398
|
+
*/
|
|
1399
|
+
multiSeekMalformed(filterInfo, why) {
|
|
1400
|
+
throw new QuereusError(`Malformed multi-seek FilterInfo (idxStr '${filterInfo.idxStr}') on ${this.schemaName}.${this.tableName}: ${why}`, StatusCode.INTERNAL);
|
|
1401
|
+
}
|
|
1402
|
+
/**
|
|
1403
|
+
* Decode a multi-seek FilterInfo into its N seek tuples of width W. Tuple i pairs
|
|
1404
|
+
* `args[i*W … i*W+W-1]` with the same slice of `constraints` (argvIndex runs
|
|
1405
|
+
* 1…N*W in order — see rule-select-access-path's multiSeek emission). A tuple with
|
|
1406
|
+
* a NULL/undefined component is dropped: IN is set membership and a NULL component
|
|
1407
|
+
* matches nothing. The planner pre-reduces literal lists, but a parameter-bound /
|
|
1408
|
+
* mixed-binding list reaches here unreduced, so this skip is the only line of
|
|
1409
|
+
* defense for those.
|
|
1410
|
+
*/
|
|
1411
|
+
decodeMultiSeekTuples(spec, filterInfo) {
|
|
1412
|
+
const seekWidth = Number.parseInt(spec.params.get('seekWidth') ?? '1', 10);
|
|
1413
|
+
const inCount = Number.parseInt(spec.params.get('inCount') ?? '0', 10);
|
|
1414
|
+
if (!Number.isInteger(seekWidth) || seekWidth < 1) {
|
|
1415
|
+
this.multiSeekMalformed(filterInfo, `seekWidth=${spec.params.get('seekWidth')}`);
|
|
1416
|
+
}
|
|
1417
|
+
if (!Number.isInteger(inCount) || inCount < 1) {
|
|
1418
|
+
this.multiSeekMalformed(filterInfo, `inCount=${spec.params.get('inCount')}`);
|
|
1419
|
+
}
|
|
1420
|
+
const constraints = filterInfo.constraints ?? [];
|
|
1421
|
+
if (constraints.length < inCount * seekWidth) {
|
|
1422
|
+
this.multiSeekMalformed(filterInfo, `${constraints.length} constraints for ${inCount}×${seekWidth} seek keys`);
|
|
1423
|
+
}
|
|
1424
|
+
if (filterInfo.args.length < inCount * seekWidth) {
|
|
1425
|
+
this.multiSeekMalformed(filterInfo, `${filterInfo.args.length} args for ${inCount}×${seekWidth} seek keys`);
|
|
1426
|
+
}
|
|
1427
|
+
const tuples = [];
|
|
1428
|
+
for (let t = 0; t < inCount; t++) {
|
|
1429
|
+
const entries = constraints.slice(t * seekWidth, (t + 1) * seekWidth);
|
|
1430
|
+
if (entries.some(e => e.constraint.op !== IndexConstraintOp.EQ || e.argvIndex <= 0)) {
|
|
1431
|
+
this.multiSeekMalformed(filterInfo, `seek tuple ${t} carries a non-EQ or unbound constraint`);
|
|
1432
|
+
}
|
|
1433
|
+
const values = entries.map(e => filterInfo.args[e.argvIndex - 1]);
|
|
1434
|
+
if (values.some(v => v === null || v === undefined))
|
|
1435
|
+
continue;
|
|
1436
|
+
tuples.push({ entries, values });
|
|
1437
|
+
}
|
|
1438
|
+
return { tuples, seekWidth };
|
|
1439
|
+
}
|
|
1440
|
+
/**
|
|
1441
|
+
* Order one tuple's values to match `keyCols` (the leading index columns, or the
|
|
1442
|
+
* PK definition) using each constraint entry's column. The plan's seek columns and
|
|
1443
|
+
* the constraint list must agree; a tuple that does not cover exactly those
|
|
1444
|
+
* columns is malformed.
|
|
1445
|
+
*/
|
|
1446
|
+
orderTupleValues(tuple, keyCols, filterInfo) {
|
|
1447
|
+
const byCol = new Map();
|
|
1448
|
+
tuple.entries.forEach((e, i) => byCol.set(e.constraint.iColumn, tuple.values[i]));
|
|
1449
|
+
if (byCol.size !== keyCols.length) {
|
|
1450
|
+
this.multiSeekMalformed(filterInfo, `seek tuple covers ${byCol.size} columns, expected ${keyCols.length}`);
|
|
1451
|
+
}
|
|
1452
|
+
return keyCols.map(colIdx => {
|
|
1453
|
+
if (!byCol.has(colIdx))
|
|
1454
|
+
this.multiSeekMalformed(filterInfo, `seek tuple missing column ${colIdx}`);
|
|
1455
|
+
return byCol.get(colIdx);
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
/**
|
|
1459
|
+
* Serve a multi-seek (`plan=5`) FilterInfo: one point window per distinct
|
|
1460
|
+
* IN-list tuple, scanned in ascending encoded-key order.
|
|
1461
|
+
*
|
|
1462
|
+
* Window order is not cosmetic. Encoded-byte order IS index-key order (per-column
|
|
1463
|
+
* DESC inversion is baked into the bytes), and the isolation overlay merges an
|
|
1464
|
+
* index scan with its pending rows by (indexKey, PK) — an out-of-order underlying
|
|
1465
|
+
* stream misplaces overlay rows in the output. Two overlap hazards are folded
|
|
1466
|
+
* away before scanning:
|
|
1467
|
+
* - Tuples whose K-encoded prefix byte-matches (duplicate bound parameters, or
|
|
1468
|
+
* case variants under a NOCASE table key collation) share ONE window, each
|
|
1469
|
+
* kept as a residual alternative — see {@link MultiSeekWindowContext} for why
|
|
1470
|
+
* a single tuple's residual would drop rows when the comparison collation is
|
|
1471
|
+
* finer than K.
|
|
1472
|
+
* - A window with no finite upper bound (all-0xff prefix — see
|
|
1473
|
+
* buildIndexPrefixBounds) contains every later-sorting window outright (any
|
|
1474
|
+
* key ≥ an all-0xff prefix necessarily starts with it), so those windows fold
|
|
1475
|
+
* into it instead of re-scanning an overlapping range out of order.
|
|
1476
|
+
*
|
|
1477
|
+
* Emission is lazy per window — `… in (…) limit 1` stops after the first yielded
|
|
1478
|
+
* row without materializing the remaining windows. Each window goes through
|
|
1479
|
+
* {@link scanIndex} → {@link iterateEffective}, so read-your-own-writes and the
|
|
1480
|
+
* stale-entry / deleted-row defenses hold per seek key.
|
|
1481
|
+
*/
|
|
1482
|
+
async *scanMultiSeek(spec, filterInfo) {
|
|
1483
|
+
const { tuples, seekWidth } = this.decodeMultiSeekTuples(spec, filterInfo);
|
|
1484
|
+
if (spec.indexName === '_primary_') {
|
|
1485
|
+
yield* this.scanMultiSeekPrimary(tuples, seekWidth, filterInfo);
|
|
1486
|
+
return;
|
|
1487
|
+
}
|
|
1488
|
+
const index = this.resolveIndexFromIdxStr(filterInfo.idxStr);
|
|
1489
|
+
if (!index)
|
|
1490
|
+
this.multiSeekMalformed(filterInfo, `no index named '${spec.indexName}'`);
|
|
1491
|
+
const indexCols = index.columns.map(c => c.index);
|
|
1492
|
+
const indexDirections = index.columns.map(c => !!c.desc);
|
|
1493
|
+
if (seekWidth > indexCols.length) {
|
|
1494
|
+
this.multiSeekMalformed(filterInfo, `seekWidth ${seekWidth} exceeds ${index.name}'s ${indexCols.length} columns`);
|
|
1495
|
+
}
|
|
1496
|
+
const seekCols = indexCols.slice(0, seekWidth);
|
|
1497
|
+
// The K-encoded byte windows below carry no residual able to resurrect a
|
|
1498
|
+
// skipped row, so a seek column whose byte equality under-fetches the type's
|
|
1499
|
+
// equality (TIMESPAN, JSON — see pkHasSemanticOrderingMember) cannot be
|
|
1500
|
+
// multi-seeked. StoreModule.tryIndexAccessPlan declines such plans; one
|
|
1501
|
+
// arriving anyway is malformed.
|
|
1502
|
+
if (seekCols.some(colIdx => hasSemanticOrdering(this.tableSchema.columns[colIdx]?.logicalType))) {
|
|
1503
|
+
this.multiSeekMalformed(filterInfo, 'semantic-ordering seek column');
|
|
1504
|
+
}
|
|
1505
|
+
// One window per distinct K-encoded tuple prefix; K-equal tuples merge into
|
|
1506
|
+
// one window, each kept as a residual alternative.
|
|
1507
|
+
const windows = new Map();
|
|
1508
|
+
for (const tuple of tuples) {
|
|
1509
|
+
const ordered = this.orderTupleValues(tuple, seekCols, filterInfo);
|
|
1510
|
+
const bounds = buildIndexPrefixBounds(ordered, this.encodeOptions, indexDirections.slice(0, seekWidth));
|
|
1511
|
+
const hex = bytesToHex(bounds.gte);
|
|
1512
|
+
const info = { ...filterInfo, constraints: tuple.entries };
|
|
1513
|
+
const existing = windows.get(hex);
|
|
1514
|
+
if (existing)
|
|
1515
|
+
existing.infos.push(info);
|
|
1516
|
+
else
|
|
1517
|
+
windows.set(hex, { bounds, infos: [info] });
|
|
1518
|
+
}
|
|
1519
|
+
if (windows.size === 0)
|
|
1520
|
+
return; // every tuple had a NULL component — nothing matches
|
|
1521
|
+
const sorted = [...windows.values()].sort((a, b) => compareBytes(a.bounds.gte, b.bounds.gte));
|
|
1522
|
+
const disjoint = [];
|
|
1523
|
+
for (const w of sorted) {
|
|
1524
|
+
const last = disjoint[disjoint.length - 1];
|
|
1525
|
+
if (last && last.bounds.lt === undefined)
|
|
1526
|
+
last.infos.push(...w.infos);
|
|
1527
|
+
else
|
|
1528
|
+
disjoint.push(w);
|
|
1529
|
+
}
|
|
1530
|
+
const indexStore = await this.ensureIndexStore(index.name);
|
|
1531
|
+
// Identical for every window (resolveFilterCollations dedups by column), so
|
|
1532
|
+
// resolve once and thread it through.
|
|
1533
|
+
const collations = this.resolveFilterCollations(filterInfo, this.indexColumnCollations(index));
|
|
1534
|
+
const seen = new Set();
|
|
1535
|
+
for (const w of disjoint) {
|
|
1536
|
+
yield* this.scanIndex(indexStore, { index, type: 'point', bounds: w.bounds }, w.infos[0], { seen, collations, extraTuples: w.infos.slice(1) });
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
/**
|
|
1540
|
+
* Multi-seek over the PRIMARY key: each tuple is a full PK, resolved by a point
|
|
1541
|
+
* read in ascending encoded-data-key order (the table's native emission order).
|
|
1542
|
+
*
|
|
1543
|
+
* NOT reachable from this module's own plans today: `StoreModule.computeBestAccessPlan`
|
|
1544
|
+
* claims IN-list filters only for secondary indexes (see its EQ_OPS vs EQ_OR_IN_OPS
|
|
1545
|
+
* split and tickets/backlog/feat-store-pk-in-list-multiseek). The branch exists so
|
|
1546
|
+
* a `_primary_` multi-seek arriving from a future plan gets a correct answer rather
|
|
1547
|
+
* than the scan arm's silent zero rows, and it is what PK-IN enablement will build on.
|
|
1548
|
+
*/
|
|
1549
|
+
async *scanMultiSeekPrimary(tuples, seekWidth, filterInfo) {
|
|
1550
|
+
const pkColumns = this.tableSchema.primaryKeyDefinition.map(pk => pk.index);
|
|
1551
|
+
if (pkColumns.length === 0) {
|
|
1552
|
+
this.multiSeekMalformed(filterInfo, 'primary-key multi-seek on a table with no primary key');
|
|
1553
|
+
}
|
|
1554
|
+
if (seekWidth !== pkColumns.length) {
|
|
1555
|
+
this.multiSeekMalformed(filterInfo, `seekWidth ${seekWidth} does not cover the ${pkColumns.length}-column primary key`);
|
|
1556
|
+
}
|
|
1557
|
+
// Mirrors analyzePKAccess's conservative decline of the point arm for such PKs
|
|
1558
|
+
// (see pkHasSemanticOrderingMember); with the residual gone there is no scan to
|
|
1559
|
+
// degrade to, so fail loudly rather than risk an under-fetch.
|
|
1560
|
+
if (this.pkHasSemanticOrderingMember()) {
|
|
1561
|
+
this.multiSeekMalformed(filterInfo, 'semantic-ordering primary-key member');
|
|
1562
|
+
}
|
|
1563
|
+
// Dedup by encoded data key (collapses K-equal tuples), keeping every merged
|
|
1564
|
+
// tuple's residual as an alternative — same rationale as the index branch.
|
|
1565
|
+
const points = new Map();
|
|
1566
|
+
for (const tuple of tuples) {
|
|
1567
|
+
const ordered = this.orderTupleValues(tuple, pkColumns, filterInfo);
|
|
1568
|
+
const key = this.encodeDataKey(ordered);
|
|
1569
|
+
const hex = bytesToHex(key);
|
|
1570
|
+
const info = { ...filterInfo, constraints: tuple.entries };
|
|
1571
|
+
const existing = points.get(hex);
|
|
1572
|
+
if (existing)
|
|
1573
|
+
existing.infos.push(info);
|
|
1574
|
+
else
|
|
1575
|
+
points.set(hex, { key, infos: [info] });
|
|
1576
|
+
}
|
|
1577
|
+
const collations = this.resolveFilterCollations(filterInfo);
|
|
1578
|
+
for (const p of [...points.values()].sort((a, b) => compareBytes(a.key, b.key))) {
|
|
1579
|
+
const row = await this.readEffectiveRowByKey(p.key);
|
|
1580
|
+
if (row && p.infos.some(fi => this.matchesFilters(row, fi, collations))) {
|
|
1201
1581
|
yield row;
|
|
1202
1582
|
}
|
|
1203
1583
|
}
|
|
@@ -1225,7 +1605,16 @@ export class StoreTable extends VirtualTable {
|
|
|
1225
1605
|
const rowValue = row[constraint.iColumn];
|
|
1226
1606
|
const filterValue = filterInfo.args[argvIndex - 1];
|
|
1227
1607
|
const collation = collations.get(constraint.iColumn) ?? BINARY_COLLATION;
|
|
1228
|
-
|
|
1608
|
+
// A semantic-ordering column (TIMESPAN, JSON) must filter under the type's
|
|
1609
|
+
// compare — the same order the engine's operators and Sort use — or a pushed
|
|
1610
|
+
// `d > 'PT90M'` would text-compare and drop rows the predicate admits. The
|
|
1611
|
+
// comparator is built per row only on this rare column kind; if a profile
|
|
1612
|
+
// ever shows it, resolve it once per scan alongside the collations.
|
|
1613
|
+
const logicalType = this.tableSchema.columns[constraint.iColumn]?.logicalType;
|
|
1614
|
+
const compare = hasSemanticOrdering(logicalType)
|
|
1615
|
+
? createTypedComparator(logicalType, collation)
|
|
1616
|
+
: undefined;
|
|
1617
|
+
if (!this.compareValues(rowValue, constraint.op, filterValue, collation, compare)) {
|
|
1229
1618
|
return false;
|
|
1230
1619
|
}
|
|
1231
1620
|
}
|
|
@@ -1296,11 +1685,11 @@ export class StoreTable extends VirtualTable {
|
|
|
1296
1685
|
* NULL on either side fails every operator except EQ-with-both-NULL (the
|
|
1297
1686
|
* internal point-lookup convention; the planner never pushes `= NULL`).
|
|
1298
1687
|
*/
|
|
1299
|
-
compareValues(a, op, b, collationFunc) {
|
|
1688
|
+
compareValues(a, op, b, collationFunc, semanticCompare) {
|
|
1300
1689
|
if (a === null || b === null) {
|
|
1301
1690
|
return op === IndexConstraintOp.EQ ? a === b : false;
|
|
1302
1691
|
}
|
|
1303
|
-
const cmp = compareSqlValuesFast(a, b, collationFunc);
|
|
1692
|
+
const cmp = semanticCompare ? semanticCompare(a, b) : compareSqlValuesFast(a, b, collationFunc);
|
|
1304
1693
|
switch (op) {
|
|
1305
1694
|
case IndexConstraintOp.EQ: return cmp === 0;
|
|
1306
1695
|
case IndexConstraintOp.NE: return cmp !== 0;
|
|
@@ -1324,7 +1713,7 @@ export class StoreTable extends VirtualTable {
|
|
|
1324
1713
|
throw new QuereusError('INSERT requires values', StatusCode.MISUSE);
|
|
1325
1714
|
const coerced = args.preCoerced ? values : this.coerceRow(values);
|
|
1326
1715
|
const pk = this.extractPK(coerced);
|
|
1327
|
-
const key =
|
|
1716
|
+
const key = this.encodeDataKey(pk);
|
|
1328
1717
|
// Check for existing row (for conflict handling).
|
|
1329
1718
|
// Resolve PK-conflict action: statement OR > per-constraint default > ABORT.
|
|
1330
1719
|
const pkEffective = args.onConflict ?? resolvePkDefaultConflict(schema) ?? ConflictResolution.ABORT;
|
|
@@ -1442,8 +1831,8 @@ export class StoreTable extends VirtualTable {
|
|
|
1442
1831
|
const coerced = args.preCoerced ? values : this.coerceRow(values);
|
|
1443
1832
|
const oldPk = this.extractPK(oldKeyValues);
|
|
1444
1833
|
const newPk = this.extractPK(coerced);
|
|
1445
|
-
const oldKey =
|
|
1446
|
-
const newKey =
|
|
1834
|
+
const oldKey = this.encodeDataKey(oldPk);
|
|
1835
|
+
const newKey = this.encodeDataKey(newPk);
|
|
1447
1836
|
// Get old row for index updates. Read the effective
|
|
1448
1837
|
// (pending-over-committed) image UNCONDITIONALLY — including the trusted
|
|
1449
1838
|
// flush path — so an old image written earlier in the same transaction is
|
|
@@ -1558,7 +1947,7 @@ export class StoreTable extends VirtualTable {
|
|
|
1558
1947
|
if (!oldKeyValues)
|
|
1559
1948
|
throw new QuereusError('DELETE requires oldKeyValues', StatusCode.MISUSE);
|
|
1560
1949
|
const pk = this.extractPK(oldKeyValues);
|
|
1561
|
-
const key =
|
|
1950
|
+
const key = this.encodeDataKey(pk);
|
|
1562
1951
|
// Get old row for index cleanup. Read the effective
|
|
1563
1952
|
// (pending-over-committed) image so a row inserted earlier in the same
|
|
1564
1953
|
// transaction is seen: this fixes index cleanup, the `-1` stats delta
|
|
@@ -1614,6 +2003,9 @@ export class StoreTable extends VirtualTable {
|
|
|
1614
2003
|
const indexStore = await this.ensureIndexStore(index.name);
|
|
1615
2004
|
const indexCols = index.columns.map(c => c.index);
|
|
1616
2005
|
const indexDirections = index.columns.map(c => !!c.desc);
|
|
2006
|
+
// Canonicalize semantic-ordering index members ('PT1H'/'PT60M' → one key) so a
|
|
2007
|
+
// UNIQUE enforcement seek and delete-then-insert maintenance address one entry.
|
|
2008
|
+
const indexTransforms = resolveIndexKeyTransforms(index, this.tableSchema.columns);
|
|
1617
2009
|
// Partial index: only rows the predicate unambiguously accepts are
|
|
1618
2010
|
// indexed (mirrors buildIndexEntries' build-time filtering). Guarding both
|
|
1619
2011
|
// halves keeps a row that transitions across the predicate scope on UPDATE
|
|
@@ -1624,7 +2016,7 @@ export class StoreTable extends VirtualTable {
|
|
|
1624
2016
|
// Remove old index entry (only if the old row was within scope).
|
|
1625
2017
|
if (oldRow && (!predicate || predicate.evaluate(oldRow) === true)) {
|
|
1626
2018
|
const oldIndexValues = indexCols.map(i => oldRow[i]);
|
|
1627
|
-
const oldIndexKey = buildIndexKey(oldIndexValues, oldPk, this.encodeOptions, indexDirections, this.pkDirections, this.pkKeyCollations);
|
|
2019
|
+
const oldIndexKey = buildIndexKey(oldIndexValues, oldPk, this.encodeOptions, indexDirections, this.pkDirections, this.pkKeyCollations, indexTransforms, this.pkKeyTransforms);
|
|
1628
2020
|
if (inTransaction && this.coordinator) {
|
|
1629
2021
|
this.coordinator.delete(oldIndexKey, indexStore);
|
|
1630
2022
|
}
|
|
@@ -1635,7 +2027,7 @@ export class StoreTable extends VirtualTable {
|
|
|
1635
2027
|
// Add new index entry (only if the new row is within scope).
|
|
1636
2028
|
if (newRow && (!predicate || predicate.evaluate(newRow) === true)) {
|
|
1637
2029
|
const newIndexValues = indexCols.map(i => newRow[i]);
|
|
1638
|
-
const newIndexKey = buildIndexKey(newIndexValues, newPk, this.encodeOptions, indexDirections, this.pkDirections, this.pkKeyCollations);
|
|
2030
|
+
const newIndexKey = buildIndexKey(newIndexValues, newPk, this.encodeOptions, indexDirections, this.pkDirections, this.pkKeyCollations, indexTransforms, this.pkKeyTransforms);
|
|
1639
2031
|
// Index value = the row's encoded DATA key. The index-entry key can
|
|
1640
2032
|
// locate a row's byte window, but its PK suffix is not losslessly
|
|
1641
2033
|
// recoverable to SqlValues (a NOCASE/RTRIM PK column encodes lossily)
|
|
@@ -1684,16 +2076,36 @@ export class StoreTable extends VirtualTable {
|
|
|
1684
2076
|
}
|
|
1685
2077
|
return compiled;
|
|
1686
2078
|
}
|
|
1687
|
-
/**
|
|
2079
|
+
/**
|
|
2080
|
+
* Check if two PK arrays name the same row. A semantic-ordering member compares
|
|
2081
|
+
* through its type's `compare` (see {@link resolvePkSemanticEquality}) so
|
|
2082
|
+
* differently-spelled equal values ('PT1H' / 'PT60M') answer "same row",
|
|
2083
|
+
* agreeing with the physical key identity {@link encodeDataKey} produces; every
|
|
2084
|
+
* other member keeps the original strict `!==`.
|
|
2085
|
+
*/
|
|
1688
2086
|
keysEqual(a, b) {
|
|
1689
2087
|
if (a.length !== b.length)
|
|
1690
2088
|
return false;
|
|
1691
2089
|
for (let i = 0; i < a.length; i++) {
|
|
1692
|
-
|
|
2090
|
+
const semantic = this.pkSemanticEquality[i];
|
|
2091
|
+
if (semantic ? semantic(a[i], b[i]) !== 0 : a[i] !== b[i])
|
|
1693
2092
|
return false;
|
|
1694
2093
|
}
|
|
1695
2094
|
return true;
|
|
1696
2095
|
}
|
|
2096
|
+
/**
|
|
2097
|
+
* Per-constrained-column comparison functions for one UNIQUE constraint: the
|
|
2098
|
+
* type's `compare` for a semantic-ordering column (so 'PT1H' conflicts with
|
|
2099
|
+
* 'PT60M', matching the memory backend's typed BTree), else the enforcement
|
|
2100
|
+
* collation the caller resolved (`resolveUniqueEnforcementCollations`) through
|
|
2101
|
+
* `compareSqlValuesFast` — the exact comparison every finder used before.
|
|
2102
|
+
*
|
|
2103
|
+
* Thin wrapper over the engine's `uniqueEnforcementComparators`, which memory
|
|
2104
|
+
* and the isolation overlay share so the three backends cannot drift.
|
|
2105
|
+
*/
|
|
2106
|
+
uniqueColumnComparators(uc, collations) {
|
|
2107
|
+
return uniqueEnforcementComparators(this.tableSchema.columns, uc.columns, collations);
|
|
2108
|
+
}
|
|
1697
2109
|
/**
|
|
1698
2110
|
* Returns true if any column covered by a UNIQUE constraint differs between
|
|
1699
2111
|
* oldRow and newRow, or — for partial UNIQUE — any column referenced by the
|
|
@@ -1927,7 +2339,10 @@ export class StoreTable extends VirtualTable {
|
|
|
1927
2339
|
// unregistered name and cannot be inlined, so a per-candidate call would be
|
|
1928
2340
|
// pure overhead.
|
|
1929
2341
|
const collations = resolveUniqueEnforcementCollations(this.tableSchema, uc, this.collationResolver);
|
|
1930
|
-
const
|
|
2342
|
+
const compares = this.uniqueColumnComparators(uc, collations);
|
|
2343
|
+
// Same per-column transforms `updateSecondaryIndexes` wrote the entries under, so
|
|
2344
|
+
// the probe for 'PT60M' lands on the window holding the 'PT1H' entry.
|
|
2345
|
+
const bounds = buildIndexPrefixBounds(uc.columns.map(c => newRow[c]), this.encodeOptions, index.columns.map(c => !!c.desc), resolveIndexKeyTransforms(index, this.tableSchema.columns));
|
|
1931
2346
|
for await (const entry of this.iterateEffective(indexStore, bounds)) {
|
|
1932
2347
|
// A legacy index store (written before index values carried the data key)
|
|
1933
2348
|
// holds EMPTY values. `scanIndex` may skip such an entry — a read that
|
|
@@ -1944,7 +2359,7 @@ export class StoreTable extends VirtualTable {
|
|
|
1944
2359
|
const pk = this.extractPK(candidate);
|
|
1945
2360
|
if (selfPks.some(skip => this.keysEqual(pk, skip)))
|
|
1946
2361
|
continue;
|
|
1947
|
-
if (uc.columns.some((c, i) =>
|
|
2362
|
+
if (uc.columns.some((c, i) => compares[i](newRow[c], candidate[c]) !== 0))
|
|
1948
2363
|
continue;
|
|
1949
2364
|
if (predicate && predicate.evaluate(candidate) !== true)
|
|
1950
2365
|
continue;
|
|
@@ -1971,6 +2386,7 @@ export class StoreTable extends VirtualTable {
|
|
|
1971
2386
|
// COLLATE for an index-derived UNIQUE, else the declared column collation.
|
|
1972
2387
|
// Resolved once here, not per candidate row.
|
|
1973
2388
|
const collations = resolveUniqueEnforcementCollations(this.tableSchema, uc, this.collationResolver);
|
|
2389
|
+
const compares = this.uniqueColumnComparators(uc, collations);
|
|
1974
2390
|
const matches = (candidate) => {
|
|
1975
2391
|
const pk = this.extractPK(candidate);
|
|
1976
2392
|
for (const skip of selfPks) {
|
|
@@ -1979,7 +2395,7 @@ export class StoreTable extends VirtualTable {
|
|
|
1979
2395
|
}
|
|
1980
2396
|
for (let i = 0; i < constrainedCols.length; i++) {
|
|
1981
2397
|
const idx = constrainedCols[i];
|
|
1982
|
-
if (
|
|
2398
|
+
if (compares[i](newRow[idx], candidate[idx]) !== 0)
|
|
1983
2399
|
return null;
|
|
1984
2400
|
}
|
|
1985
2401
|
// Partial UNIQUE: candidate must also be in the predicate's scope to conflict.
|
|
@@ -2024,6 +2440,7 @@ export class StoreTable extends VirtualTable {
|
|
|
2024
2440
|
const newSourcePk = this.extractPK(newRow);
|
|
2025
2441
|
// Resolved once, above the candidate loop.
|
|
2026
2442
|
const collations = resolveUniqueEnforcementCollations(this.tableSchema, uc, this.collationResolver);
|
|
2443
|
+
const compares = this.uniqueColumnComparators(uc, collations);
|
|
2027
2444
|
const candidates = await this.db._lookupCoveringConflicts(mv, uc, newRow, newSourcePk);
|
|
2028
2445
|
for (const cand of candidates) {
|
|
2029
2446
|
const liveRow = await this.readLiveRowByPk(cand.pk);
|
|
@@ -2041,7 +2458,7 @@ export class StoreTable extends VirtualTable {
|
|
|
2041
2458
|
// index over a BINARY column) is declined upstream by the collation gate in
|
|
2042
2459
|
// findRowTimeCoveringStructure, so only BINARY-floor or equal-collation MVs
|
|
2043
2460
|
// reach here — the superset this re-validation can soundly filter.
|
|
2044
|
-
if (uc.columns.some((c, i) =>
|
|
2461
|
+
if (uc.columns.some((c, i) => compares[i](newRow[c], liveRow[c]) !== 0))
|
|
2045
2462
|
continue;
|
|
2046
2463
|
if (predicate && predicate.evaluate(liveRow) !== true)
|
|
2047
2464
|
continue;
|
|
@@ -2065,7 +2482,7 @@ export class StoreTable extends VirtualTable {
|
|
|
2065
2482
|
// the table's own read/write paths.
|
|
2066
2483
|
/** Encode `pkValues` (in PK-definition order) exactly as the data store keys rows. */
|
|
2067
2484
|
encodeDataKey(pkValues) {
|
|
2068
|
-
return buildDataKey(pkValues, this.encodeOptions, this.pkDirections, this.pkKeyCollations);
|
|
2485
|
+
return buildDataKey(pkValues, this.encodeOptions, this.pkDirections, this.pkKeyCollations, this.pkKeyTransforms);
|
|
2069
2486
|
}
|
|
2070
2487
|
/**
|
|
2071
2488
|
* Byte bounds covering every data key whose leading PK columns equal
|
|
@@ -2074,7 +2491,7 @@ export class StoreTable extends VirtualTable {
|
|
|
2074
2491
|
* exact slice. An empty prefix yields full-scan bounds.
|
|
2075
2492
|
*/
|
|
2076
2493
|
encodePkPrefixBounds(prefixValues) {
|
|
2077
|
-
return buildPkPrefixBounds(prefixValues, this.encodeOptions, this.pkDirections.slice(0, prefixValues.length), this.pkKeyCollations.slice(0, prefixValues.length));
|
|
2494
|
+
return buildPkPrefixBounds(prefixValues, this.encodeOptions, this.pkDirections.slice(0, prefixValues.length), this.pkKeyCollations.slice(0, prefixValues.length), this.pkKeyTransforms.slice(0, prefixValues.length));
|
|
2078
2495
|
}
|
|
2079
2496
|
/**
|
|
2080
2497
|
* Effective (pending-over-committed) point read by encoded data key: a pending
|
|
@@ -2284,7 +2701,7 @@ export class StoreTable extends VirtualTable {
|
|
|
2284
2701
|
*/
|
|
2285
2702
|
async deleteRowAt(inTransaction, pk, oldRow) {
|
|
2286
2703
|
const store = await this.ensureStore();
|
|
2287
|
-
const key =
|
|
2704
|
+
const key = this.encodeDataKey(pk);
|
|
2288
2705
|
if (inTransaction && this.coordinator) {
|
|
2289
2706
|
this.coordinator.delete(key, store);
|
|
2290
2707
|
}
|