@quereus/isolation 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 +31 -1
- package/dist/src/flush.d.ts.map +1 -1
- package/dist/src/flush.js +16 -3
- package/dist/src/flush.js.map +1 -1
- package/dist/src/isolated-table.d.ts +23 -9
- package/dist/src/isolated-table.d.ts.map +1 -1
- package/dist/src/isolated-table.js +128 -58
- package/dist/src/isolated-table.js.map +1 -1
- package/dist/src/isolation-module.d.ts +491 -118
- package/dist/src/isolation-module.d.ts.map +1 -1
- package/dist/src/isolation-module.js +1007 -291
- package/dist/src/isolation-module.js.map +1 -1
- package/dist/src/isolation-types.d.ts +8 -0
- package/dist/src/isolation-types.d.ts.map +1 -1
- package/dist/src/overlay-rows.d.ts +10 -1
- package/dist/src/overlay-rows.d.ts.map +1 -1
- package/dist/src/overlay-rows.js +16 -3
- package/dist/src/overlay-rows.js.map +1 -1
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# @quereus/isolation
|
|
2
2
|
|
|
3
|
+
> **Stability: Beta** — complete and tested, but the surface is still being shaped; a
|
|
4
|
+
> breaking change may land in a minor release. See
|
|
5
|
+
> [Stability Tiers](../../docs/stability.md#tiers).
|
|
6
|
+
|
|
3
7
|
Generic transaction isolation layer for Quereus virtual table modules.
|
|
4
8
|
|
|
5
9
|
## Overview
|
|
@@ -80,6 +84,30 @@ The isolation layer operates at the **row level**, merging query results from tw
|
|
|
80
84
|
└─────────────────────────────────────────────────────────┘
|
|
81
85
|
```
|
|
82
86
|
|
|
87
|
+
### Reading a pushed index window
|
|
88
|
+
|
|
89
|
+
When the underlying serves a read through an index seek, the overlay must be narrowed to
|
|
90
|
+
the same window — otherwise a staged row outside it would leak into the answer. The
|
|
91
|
+
overlay is full-scanned (it cannot resolve an index name the underlying minted for its own
|
|
92
|
+
plan), and `IsolatedTable`'s constraint matcher re-applies the pushed window itself. Several
|
|
93
|
+
equality constraints on the *same* column are read as an **IN set**, not as an AND: that is
|
|
94
|
+
how an `IN`-list seek is encoded — one equality per seek value — so an AND reading would
|
|
95
|
+
match nothing and silently drop every staged row.
|
|
96
|
+
|
|
97
|
+
This holds for a set of values that only exists at run time (`where v in (select …)`,
|
|
98
|
+
which the engine may materialize and push down as a seek) exactly as it does for a literal
|
|
99
|
+
`in (1, 2, 3)`: the pushed window has the same shape either way, and this layer cannot —
|
|
100
|
+
and need not — tell them apart.
|
|
101
|
+
|
|
102
|
+
One ordering caveat. The **primary-key** merge walks both streams in the primary key's own
|
|
103
|
+
declared order — ascending, or descending for a `primary key (… desc)`. The engine sorts a
|
|
104
|
+
run-time key set into that same order before pushing it, so that path arrives in order. A
|
|
105
|
+
**literal** list does not: `where pk in (3, 1, 2)` is visited in list order, and with rows
|
|
106
|
+
staged in the transaction the merge mis-pairs them — a staged update can surface alongside
|
|
107
|
+
the stale stored row, and a staged delete can reappear. That is tracked as
|
|
108
|
+
`backlog/bug-isolation-multiseek-merge-order`; the secondary-index merge is unaffected
|
|
109
|
+
(it sorts the overlay itself and excludes shadowed rows by primary key).
|
|
110
|
+
|
|
83
111
|
## Isolation Level
|
|
84
112
|
|
|
85
113
|
The guarantee this layer actually provides is closer to **read-committed with
|
|
@@ -106,11 +134,13 @@ read-your-own-writes** than to snapshot isolation:
|
|
|
106
134
|
|
|
107
135
|
**Lazy overlay creation** — No memory overhead until the first write in a transaction.
|
|
108
136
|
|
|
109
|
-
**Transparent hook forwarding** — `IsolationModule` is a wrapper, so the engine/planner/lens machinery reaches *it* (the registered module) rather than the underlying. Optional `VirtualTableModule` hooks whose behavior is isolation-transparent are forwarded straight through to the underlying: `getMappingAdvertisements` (decomposition shape), `getBestAccessPlan` (index awareness), the `beginSchemaBatch` / `endSchemaBatch` APPLY SCHEMA batch hooks (single-substrate-commit batching of migration DDL), and `getBackingHost` (the materialized-view backing-host capability — assigned in the constructor only when the underlying implements it, so method *presence* mirrors the underlying; backing writes are privileged and bypass the per-connection overlay entirely, making the underlying host's pending state the right surface). Hooks whose underlying value would *misdescribe* the wrapped behavior are intentionally **not** forwarded: `getCapabilities` is augmented with `isolation`/`savepoints` rather than passed through verbatim; `supports` (full-query push-down) is suppressed so the overlay always sees every row to merge; and `concurrencyMode` / `expectedLatencyMs` are derived rather than passed through verbatim: `concurrencyMode` is the weaker of the underlying and overlay modes, capped at `reentrant-reads` (the wrapper's own write path mutates shared overlay-merge state non-atomically, so it is never `fully-reentrant`), while `expectedLatencyMs` forwards the underlying's hint (defaulting to `0` when the underlying declares none).
|
|
137
|
+
**Transparent hook forwarding** — `IsolationModule` is a wrapper, so the engine/planner/lens machinery reaches *it* (the registered module) rather than the underlying. Optional `VirtualTableModule` hooks whose behavior is isolation-transparent are forwarded straight through to the underlying: `getMappingAdvertisements` (decomposition shape), `getBestAccessPlan` (index awareness), the `beginSchemaBatch` / `endSchemaBatch` APPLY SCHEMA batch hooks (single-substrate-commit batching of migration DDL), `assertCatalogObjectPersistable` (the pre-flight veto that lets a persisting module refuse a view / materialized view whose catalog entry it could not durably write — whether a definition is encodable is a property of the definition text, not of the overlay, and without the forward a wrapped store would silently keep dropping such views on reopen), and `getBackingHost` (the materialized-view backing-host capability — assigned in the constructor only when the underlying implements it, so method *presence* mirrors the underlying; backing writes are privileged and bypass the per-connection overlay entirely, making the underlying host's pending state the right surface). Hooks whose underlying value would *misdescribe* the wrapped behavior are intentionally **not** forwarded: `getCapabilities` is augmented with `isolation`/`savepoints` rather than passed through verbatim; `supports` (full-query push-down) is suppressed so the overlay always sees every row to merge; and `concurrencyMode` / `expectedLatencyMs` are derived rather than passed through verbatim: `concurrencyMode` is the weaker of the underlying and overlay modes, capped at `reentrant-reads` (the wrapper's own write path mutates shared overlay-merge state non-atomically, so it is never `fully-reentrant`), while `expectedLatencyMs` forwards the underlying's hint (defaulting to `0` when the underlying declares none).
|
|
110
138
|
|
|
111
139
|
**Atomic ALTER (issuer-faithful) + cross-connection poison** — DDL through Quereus is not transaction-scoped and the shared underlying base auto-commits its mutation immediately, so a half-applied ALTER cannot be rolled back. The blast radius is isolation-faithful — an ALTER never depends on another connection's uncommitted data:
|
|
112
140
|
|
|
113
141
|
- **Issuer's own overlay** — `IsolationModule.alterTable` dry-run **validates the issuing connection's own affected overlay's backfill** (per-row `NOT NULL` checks — for a new `ADD COLUMN ... NOT NULL` and for tightening an existing column with `ALTER COLUMN ... SET NOT NULL` — and the tombstone-present guard) *before* mutating the underlying. A rejection fires while the underlying, the schema catalog, and every overlay are still untouched, so for the issuer the ALTER either fails clean or fully applies — base and catalog never diverge. (The issuer staged both the data and the DDL, so rejecting up front is the least-surprising behavior.)
|
|
142
|
+
- **`ALTER TABLE ... ALTER COLUMN ... SET COLLATE` on a primary-key column** — the change re-keys the table, so two staged rows can land on one new key. A deletion marker sharing its new key with a staged live row is that row's before-image (delete `'A'`, insert `'a'`, then switch to `NOCASE`), so the marker is discarded and the ALTER succeeds; two staged **live** rows on one key are a real duplicate and are refused. A shape the re-keyed overlay cannot physically hold — a savepoint that could restore both a marker and its replacement at one key — is refused as retryable (`BUSY`), the same answer a plain non-isolated table gives. Every refusal is surfaced *before* the shared underlying is touched, so the transaction survives intact.
|
|
143
|
+
- **`ALTER TABLE ... ALTER PRIMARY KEY`** — the one change no overlay can follow: an overlay's staged rows (and its deletion markers) are keyed by the *old* primary key. A connection that has uncommitted changes staged for the table is therefore rejected with `UNSUPPORTED` *before* the underlying is touched — commit or roll back first, then retry. Another connection's overlay holding staged rows is poisoned; one holding nothing is swapped for an empty overlay under the new key. (The bundled `MemoryTableModule` rejects `ALTER PRIMARY KEY` outright, so this only arises under a store-backed underlying.)
|
|
114
144
|
- **Another connection's overlay** — the shared underlying and the catalog change regardless of any *other* connection's uncommitted state. A foreign overlay that *can* migrate is carried forward as usual (a staged `NULL` is backfilled when a usable literal `DEFAULT` exists); one that *cannot* (its staged row can't satisfy the new or newly-`NOT NULL` column) is left in place and marked **poisoned**. Its owning connection then raises a `CONSTRAINT` error the next time it reads (merged), writes, or commits that table, and recovers by rolling back (which discards the overlay and its poison). A committed-snapshot (`committed.<table>`) read bypasses the overlay and keeps working. A layer-invariant violation (e.g. a missing tombstone column, `INTERNAL`) still rethrows loud for everyone rather than poisoning.
|
|
115
145
|
|
|
116
146
|
**Configurable overlay module** — Use memory for fast transactions, or persistent storage for large transactions:
|
package/dist/src/flush.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flush.d.ts","sourceRoot":"","sources":["../../src/flush.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAKpF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,wBAAwB,CAC7C,eAAe,EAAE,YAAY,EAC7B,YAAY,EAAE,YAAY,EAC1B,eAAe,EAAE,MAAM,GACrB,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"flush.d.ts","sourceRoot":"","sources":["../../src/flush.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAKpF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAsB,wBAAwB,CAC7C,eAAe,EAAE,YAAY,EAC7B,YAAY,EAAE,YAAY,EAC1B,eAAe,EAAE,MAAM,GACrB,OAAO,CAAC,IAAI,CAAC,CAwEf;AAkBD;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,YAAY,EAAE,SAAS,EAAE,KAAK,EAAE,EAAE,EAAE,QAAQ,EAAE,EAAE,SAAS,EAAE,MAAM,GAAG,IAAI,CAOlH"}
|
package/dist/src/flush.js
CHANGED
|
@@ -47,6 +47,20 @@ export async function applyOverlayToUnderlying(underlyingTable, overlayTable, to
|
|
|
47
47
|
return;
|
|
48
48
|
// Deletes before inserts/updates (see doc comment above).
|
|
49
49
|
const ordered = [...overlayEntries].sort((a, b) => (a.isTombstone === b.isTombstone ? 0 : a.isTombstone ? -1 : 1));
|
|
50
|
+
// Resolve insert-vs-update for every live row BEFORE any write of this flush
|
|
51
|
+
// lands. `rowExistsInUnderlying` drives the underlying's PK index, and a
|
|
52
|
+
// module whose read path cannot serve an index-driven scan over its own
|
|
53
|
+
// uncommitted writes fails every probe after the first (lamina's
|
|
54
|
+
// staged-overlay wrapper refuses a compound-index scan once the episode holds
|
|
55
|
+
// staged drafts). Each PK appears at most once in the overlay, so no write in
|
|
56
|
+
// this flush can change another entry's existence answer — hoisting the
|
|
57
|
+
// probes is answer-preserving as well as one fewer read per written row.
|
|
58
|
+
const existsInUnderlying = new Map();
|
|
59
|
+
for (const entry of ordered) {
|
|
60
|
+
if (entry.isTombstone)
|
|
61
|
+
continue;
|
|
62
|
+
existsInUnderlying.set(entry, await rowExistsInUnderlying(underlyingTable, pkIndices, entry.pk));
|
|
63
|
+
}
|
|
50
64
|
for (const entry of ordered) {
|
|
51
65
|
if (entry.isTombstone) {
|
|
52
66
|
const result = await underlyingTable.update({
|
|
@@ -57,9 +71,8 @@ export async function applyOverlayToUnderlying(underlyingTable, overlayTable, to
|
|
|
57
71
|
assertFlushWriteOk(result, 'delete', entry.pk, tableName);
|
|
58
72
|
}
|
|
59
73
|
else {
|
|
60
|
-
// Insert vs update decided by
|
|
61
|
-
|
|
62
|
-
if (existsInUnderlying) {
|
|
74
|
+
// Insert vs update decided by the pre-write probe above.
|
|
75
|
+
if (existsInUnderlying.get(entry) === true) {
|
|
63
76
|
const result = await underlyingTable.update({
|
|
64
77
|
operation: 'update',
|
|
65
78
|
values: entry.dataRow,
|
package/dist/src/flush.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"flush.js","sourceRoot":"","sources":["../../src/flush.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AACnF,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,
|
|
1
|
+
{"version":3,"file":"flush.js","sourceRoot":"","sources":["../../src/flush.ts"],"names":[],"mappings":"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"}
|
|
@@ -54,6 +54,16 @@ export declare class IsolatedTable extends VirtualTable implements IsolatedTable
|
|
|
54
54
|
* which invalidates the entry.
|
|
55
55
|
*/
|
|
56
56
|
private pkCollationCache?;
|
|
57
|
+
/**
|
|
58
|
+
* Per-PK-column typed EQUALITY comparators for semantic-ordering members (TIMESPAN,
|
|
59
|
+
* JSON — see the engine's `hasSemanticOrdering`), `undefined` elsewhere. The
|
|
60
|
+
* underlying backends collapse semantically-equal PK spellings onto one row (the
|
|
61
|
+
* memory table's typed BTree; the store's `groupKey`-transformed key bytes), so
|
|
62
|
+
* shadowing and self-PK questions here must call 'PT1H' and 'PT60M' the same key
|
|
63
|
+
* too. Memoized against the schema object like {@link pkCollationCache}.
|
|
64
|
+
*/
|
|
65
|
+
private pkSemanticCache?;
|
|
66
|
+
private getPkSemanticComparators;
|
|
57
67
|
/**
|
|
58
68
|
* Returns the connection-scoped set of savepoint depths that pre-date the overlay.
|
|
59
69
|
* Stored in IsolationModule (keyed by db+schema+table) so all IsolatedTable instances
|
|
@@ -492,12 +502,14 @@ export declare class IsolatedTable extends VirtualTable implements IsolatedTable
|
|
|
492
502
|
/**
|
|
493
503
|
* True-match test shared by both merged-check phases so they compare identically:
|
|
494
504
|
* `candidate` matches `newRow` on every constrained column under that column's
|
|
495
|
-
*
|
|
505
|
+
* comparison function (NULLs never match), and — for a partial UNIQUE — the
|
|
496
506
|
* predicate holds for `candidate`.
|
|
497
507
|
*
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
* column
|
|
508
|
+
* `compares` carries one function per constrained column, resolved once in
|
|
509
|
+
* {@link findMergedUniqueConflict}: the type's `compare` for a semantic-ordering
|
|
510
|
+
* column, else the enforcement collation (the index's per-column COLLATE for an
|
|
511
|
+
* index-derived UNIQUE, else the declared column collation) — the latter enforces
|
|
512
|
+
* a UNIQUE over a collated column against merged rows through the isolation path
|
|
501
513
|
* (unique-constraint-honors-column-collation / store-index-derived-unique).
|
|
502
514
|
*/
|
|
503
515
|
private rowMatchesUniqueConstraint;
|
|
@@ -550,11 +562,13 @@ export declare class IsolatedTable extends VirtualTable implements IsolatedTable
|
|
|
550
562
|
/**
|
|
551
563
|
* Index DDL delegates to the module rather than driving the underlying and the overlay
|
|
552
564
|
* directly. The module owns the full protocol — validating against the issuing
|
|
553
|
-
* connection's effective rows, then
|
|
554
|
-
*
|
|
555
|
-
*
|
|
556
|
-
*
|
|
557
|
-
*
|
|
565
|
+
* connection's effective rows, then adopting the change IN PLACE into every affected
|
|
566
|
+
* overlay (narrowed to live rows, and with a foreign overlay's rejection poisoning it
|
|
567
|
+
* rather than losing its staged rows) — and a bare `underlying.createIndex()` +
|
|
568
|
+
* `overlay.createIndex()` pair silently skips both halves. The engine only ever reaches the
|
|
569
|
+
* module-level hook, so these instance methods exist for a module that wraps
|
|
570
|
+
* `IsolationModule` in turn; routing them through the module keeps that path on the same
|
|
571
|
+
* protocol.
|
|
558
572
|
*/
|
|
559
573
|
createIndex(indexInfo: IndexSchema, rows?: EffectiveRowSource): Promise<void>;
|
|
560
574
|
dropIndex(indexName: string): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"isolated-table.d.ts","sourceRoot":"","sources":["../../src/isolated-table.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAwC,QAAQ,EAAoB,YAAY,EAAE,GAAG,EAAY,gBAAgB,IAAI,WAAW,EAAE,UAAU,EAAE,gBAAgB,EAA0D,UAAU,EAAE,sBAAsB,EAAE,YAAY,EAA+C,MAAM,kBAAkB,CAAC;AAC5V,OAAO,EAAE,YAAY,
|
|
1
|
+
{"version":3,"file":"isolated-table.d.ts","sourceRoot":"","sources":["../../src/isolated-table.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { VirtualTable, compareSqlValues, compareSqlValuesFast, resolveCollationFunctions, BINARY_COLLATION, isUpdateOk, ConflictResolution, compilePredicate, QuereusError, StatusCode, resolveUniqueEnforcementCollations, uniqueEnforcementCollations, normalizeCollationName,
|
|
1
|
+
import { VirtualTable, compareSqlValues, compareSqlValuesFast, resolveCollationFunctions, BINARY_COLLATION, isUpdateOk, ConflictResolution, compilePredicate, QuereusError, StatusCode, resolveUniqueEnforcementCollations, uniqueEnforcementCollations, uniqueEnforcementComparators, normalizeCollationName, serializeKey, pkKeyCollationName, retargetFilterInfoIndex, PRIMARY_INDEX_NAME, coerceRowToSchema, IndexConstraintOp, decodeIdxStr, createTypedComparator, hasSemanticOrdering, semanticKeyTransform } from '@quereus/quereus';
|
|
2
2
|
import { IsolatedConnection } from './isolated-connection.js';
|
|
3
3
|
import { mergeStreams, createMergeEntry, createTombstone } from './merge-iterator.js';
|
|
4
4
|
import { makeFullScanFilterInfo, makePkPointLookupFilter, makeSecondaryIndexEqSeekFilter } from './filter-info.js';
|
|
@@ -55,6 +55,30 @@ export class IsolatedTable extends VirtualTable {
|
|
|
55
55
|
* which invalidates the entry.
|
|
56
56
|
*/
|
|
57
57
|
pkCollationCache;
|
|
58
|
+
/**
|
|
59
|
+
* Per-PK-column typed EQUALITY comparators for semantic-ordering members (TIMESPAN,
|
|
60
|
+
* JSON — see the engine's `hasSemanticOrdering`), `undefined` elsewhere. The
|
|
61
|
+
* underlying backends collapse semantically-equal PK spellings onto one row (the
|
|
62
|
+
* memory table's typed BTree; the store's `groupKey`-transformed key bytes), so
|
|
63
|
+
* shadowing and self-PK questions here must call 'PT1H' and 'PT60M' the same key
|
|
64
|
+
* too. Memoized against the schema object like {@link pkCollationCache}.
|
|
65
|
+
*/
|
|
66
|
+
pkSemanticCache;
|
|
67
|
+
getPkSemanticComparators() {
|
|
68
|
+
const schema = this.tableSchema;
|
|
69
|
+
if (!schema)
|
|
70
|
+
return [];
|
|
71
|
+
if (this.pkSemanticCache?.schema !== schema) {
|
|
72
|
+
this.pkSemanticCache = {
|
|
73
|
+
schema,
|
|
74
|
+
comparators: (schema.primaryKeyDefinition ?? []).map(pk => {
|
|
75
|
+
const logicalType = schema.columns[pk.index]?.logicalType;
|
|
76
|
+
return hasSemanticOrdering(logicalType) ? createTypedComparator(logicalType) : undefined;
|
|
77
|
+
}),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return this.pkSemanticCache.comparators;
|
|
81
|
+
}
|
|
58
82
|
/**
|
|
59
83
|
* Returns the connection-scoped set of savepoint depths that pre-date the overlay.
|
|
60
84
|
* Stored in IsolationModule (keyed by db+schema+table) so all IsolatedTable instances
|
|
@@ -386,7 +410,7 @@ export class IsolatedTable extends VirtualTable {
|
|
|
386
410
|
// getComparePK/keysEqual and agreeing with `db.registerCollation`.
|
|
387
411
|
//
|
|
388
412
|
// A PK column whose declared type can never hold text takes the identity
|
|
389
|
-
// normalizer regardless of its collation:
|
|
413
|
+
// normalizer regardless of its collation: the key serializer normalizes only
|
|
390
414
|
// string values, so the collation cannot affect how such a key buckets. Asking
|
|
391
415
|
// the resolver for it would reject `n integer collate mycoll` under a
|
|
392
416
|
// comparator-only collation, which the engine's own hash sites accept (they gate
|
|
@@ -398,6 +422,18 @@ export class IsolatedTable extends VirtualTable {
|
|
|
398
422
|
const column = this.tableSchema.columns[i];
|
|
399
423
|
return this.keyNormalizerResolver(pkKeyCollationName(column));
|
|
400
424
|
});
|
|
425
|
+
// Key-identity transforms for semantic-ordering PK members (TIMESPAN's total-seconds
|
|
426
|
+
// groupKey): the underlying backends key 'PT1H' and 'PT60M' as ONE row, so the
|
|
427
|
+
// modified-PK shadow set must bucket them identically or a staged rewrite fails to
|
|
428
|
+
// shadow the committed spelling and both rows surface. `!` on the serialized key is
|
|
429
|
+
// safe: PK columns are NOT NULL, so serializeKey never returns null; both the build
|
|
430
|
+
// and probe below use this one encoder so they stay consistent.
|
|
431
|
+
const pkTransforms = pkIndices.map(i => semanticKeyTransform(this.tableSchema.columns[i]?.logicalType));
|
|
432
|
+
const pkShadowKey = (row) => serializeKey(pkIndices.map((idx, i) => {
|
|
433
|
+
const v = row[idx];
|
|
434
|
+
const transform = pkTransforms[i];
|
|
435
|
+
return transform && v !== null ? transform(v) : v;
|
|
436
|
+
}), pkNormalizers);
|
|
401
437
|
// Step 1: one full overlay scan collects the modified PKs AND the in-window
|
|
402
438
|
// non-tombstone data rows. The window filter is applied here, unconditionally —
|
|
403
439
|
// whether the engine adds a residual Filter above the isolation scan depends on
|
|
@@ -411,9 +447,7 @@ export class IsolatedTable extends VirtualTable {
|
|
|
411
447
|
const modifiedPKs = new Set();
|
|
412
448
|
const overlayRows = [];
|
|
413
449
|
for await (const row of overlay.query(this.createFullScanFilterInfo())) {
|
|
414
|
-
|
|
415
|
-
// null here; both sides use the same encoder so they stay consistent.
|
|
416
|
-
modifiedPKs.add(serializeRowKey(row, pkIndices, pkNormalizers));
|
|
450
|
+
modifiedPKs.add(pkShadowKey(row));
|
|
417
451
|
if (row[tombstoneIndex] !== 1) {
|
|
418
452
|
const dataRow = row.slice(0, tombstoneIndex);
|
|
419
453
|
if (matchesWindow(dataRow)) {
|
|
@@ -433,8 +467,7 @@ export class IsolatedTable extends VirtualTable {
|
|
|
433
467
|
// Merge two sorted, disjoint streams
|
|
434
468
|
let oi = 0;
|
|
435
469
|
for await (const underlyingRow of this.underlyingTable.query(filterInfo)) {
|
|
436
|
-
|
|
437
|
-
if (modifiedPKs.has(serializeRowKey(underlyingRow, pkIndices, pkNormalizers))) {
|
|
470
|
+
if (modifiedPKs.has(pkShadowKey(underlyingRow))) {
|
|
438
471
|
continue; // Skip rows modified in overlay
|
|
439
472
|
}
|
|
440
473
|
// Yield any overlay rows that sort before this underlying row
|
|
@@ -827,11 +860,17 @@ export class IsolatedTable extends VirtualTable {
|
|
|
827
860
|
// the underlying row it shadows ('apple') as distinct keys; an ascending one
|
|
828
861
|
// walks a `primary key (k desc)` table against its scan order. Either way both
|
|
829
862
|
// rows surface in a scan instead of the overlay shadowing the underlying.
|
|
863
|
+
// A semantic-ordering member compares through its type's `compare` — both the
|
|
864
|
+
// memory table's typed BTree and the store's groupKey-transformed key bytes
|
|
865
|
+
// emit in that order, and its equality is what shadowing needs ('PT1H' ≡ 'PT60M').
|
|
830
866
|
const collations = this.getPkCollations();
|
|
831
867
|
const directions = this.getPkDirections();
|
|
868
|
+
const semantic = this.getPkSemanticComparators();
|
|
832
869
|
return (a, b) => {
|
|
833
870
|
for (let i = 0; i < a.length; i++) {
|
|
834
|
-
const cmp =
|
|
871
|
+
const cmp = semantic[i]
|
|
872
|
+
? semantic[i](a[i], b[i])
|
|
873
|
+
: compareSqlValuesFast(a[i], b[i], collations[i] ?? BINARY_COLLATION);
|
|
835
874
|
if (cmp !== 0)
|
|
836
875
|
return directions[i] ? -cmp : cmp;
|
|
837
876
|
}
|
|
@@ -847,9 +886,15 @@ export class IsolatedTable extends VirtualTable {
|
|
|
847
886
|
*/
|
|
848
887
|
buildDescriptorComparators(keyColumns) {
|
|
849
888
|
return keyColumns.map(kc => {
|
|
889
|
+
// A semantic-ordering key column merges by the type's `compare` — the order
|
|
890
|
+
// both backends emit such a column in (typed BTree / groupKey key bytes).
|
|
891
|
+
const logicalType = this.tableSchema?.columns[kc.columnIndex]?.logicalType;
|
|
850
892
|
const collation = kc.collation ? this.collationResolver(kc.collation) : BINARY_COLLATION;
|
|
893
|
+
const compare = hasSemanticOrdering(logicalType)
|
|
894
|
+
? createTypedComparator(logicalType, collation)
|
|
895
|
+
: (a, b) => compareSqlValuesFast(a, b, collation);
|
|
851
896
|
return (a, b) => {
|
|
852
|
-
const cmp =
|
|
897
|
+
const cmp = compare(a, b);
|
|
853
898
|
return kc.desc ? -cmp : cmp;
|
|
854
899
|
};
|
|
855
900
|
});
|
|
@@ -908,11 +953,7 @@ export class IsolatedTable extends VirtualTable {
|
|
|
908
953
|
* the existing one (bug-store-isolation-upsert-affinity-coerced-pk).
|
|
909
954
|
*/
|
|
910
955
|
coerceRow(row) {
|
|
911
|
-
|
|
912
|
-
if (row.length > cols.length) {
|
|
913
|
-
throw new QuereusError(`Too many values for ${this.schemaName}.${this.tableName}: expected ${cols.length}, got ${row.length}`, StatusCode.ERROR);
|
|
914
|
-
}
|
|
915
|
-
return row.map((v, i) => validateAndParse(v, cols[i].logicalType, cols[i].name));
|
|
956
|
+
return coerceRowToSchema(row, this.tableSchema.columns, `${this.schemaName}.${this.tableName}`);
|
|
916
957
|
}
|
|
917
958
|
// ==================== Write Operations ====================
|
|
918
959
|
/**
|
|
@@ -933,21 +974,22 @@ export class IsolatedTable extends VirtualTable {
|
|
|
933
974
|
this.setHasChanges(true);
|
|
934
975
|
const { operation, values, oldKeyValues } = args;
|
|
935
976
|
const tombstoneIndex = this.getTombstoneColumnIndex(overlay);
|
|
936
|
-
//
|
|
937
|
-
//
|
|
938
|
-
//
|
|
939
|
-
//
|
|
940
|
-
//
|
|
941
|
-
//
|
|
942
|
-
//
|
|
943
|
-
//
|
|
944
|
-
//
|
|
945
|
-
//
|
|
946
|
-
//
|
|
947
|
-
//
|
|
948
|
-
//
|
|
949
|
-
//
|
|
950
|
-
|
|
977
|
+
// The DML executor converts every row to declared column types at the top of
|
|
978
|
+
// the pipeline and says so via `preCoerced` — then `values` is already in
|
|
979
|
+
// declared form, must NOT be converted again (JSON's `parse` is not
|
|
980
|
+
// idempotent for a JSON-string scalar: `'"hello"'` parses to the native JS
|
|
981
|
+
// string `"hello"`, and re-parsing that bare string throws), and the flag is
|
|
982
|
+
// forwarded on every overlay data write below so the overlay's own memory
|
|
983
|
+
// module skips its pass too.
|
|
984
|
+
//
|
|
985
|
+
// A direct API caller that leaves the flag unset still hands us raw values:
|
|
986
|
+
// convert them here — conflict detection needs the declared form (probing the
|
|
987
|
+
// overlay/underlying by an un-coerced PK, e.g. TEXT '1' against a stored
|
|
988
|
+
// INTEGER 1, misses the existing row entirely —
|
|
989
|
+
// bug-store-isolation-upsert-affinity-coerced-pk) — but keep the OVERLAY
|
|
990
|
+
// write raw in that case (the overlay converts on its own write, and
|
|
991
|
+
// handing it the converted row without `preCoerced` would convert twice).
|
|
992
|
+
const coercedValues = values ? (args.preCoerced ? values : this.coerceRow(values)) : values;
|
|
951
993
|
// Resolve the effective PK-level action once so the wrapped overlay vtab
|
|
952
994
|
// agrees with the overlay's decision. Per-UC defaults are applied inside
|
|
953
995
|
// checkMergedUniqueConstraints, since each UC may declare its own action.
|
|
@@ -983,6 +1025,7 @@ export class IsolatedTable extends VirtualTable {
|
|
|
983
1025
|
values: overlayRow,
|
|
984
1026
|
oldKeyValues: pk,
|
|
985
1027
|
onConflict: effectiveOR,
|
|
1028
|
+
preCoerced: args.preCoerced,
|
|
986
1029
|
});
|
|
987
1030
|
const stripped = this.stripTombstoneFromResult(result, tombstoneIndex);
|
|
988
1031
|
return this.attachEvicted(stripped, evicted, tombstoneIndex);
|
|
@@ -1059,7 +1102,7 @@ export class IsolatedTable extends VirtualTable {
|
|
|
1059
1102
|
await this.insertTombstoneForPK(overlay, targetPK, tombstoneIndex);
|
|
1060
1103
|
// Reuse a tombstone already at newPK (a PK freed earlier in this txn)
|
|
1061
1104
|
// instead of colliding with it — see writeRelocatedRow.
|
|
1062
|
-
const result = await this.writeRelocatedRow(overlay, newPK, overlayRow, tombstoneIndex, effectiveOR);
|
|
1105
|
+
const result = await this.writeRelocatedRow(overlay, newPK, overlayRow, tombstoneIndex, effectiveOR, args.preCoerced);
|
|
1063
1106
|
const stripped = this.stripTombstoneFromResult(result, tombstoneIndex);
|
|
1064
1107
|
return this.attachEvicted(this.attachReplacedUnderlying(stripped, pkOutcome.replacedUnderlyingRow), evicted, tombstoneIndex);
|
|
1065
1108
|
}
|
|
@@ -1095,8 +1138,8 @@ export class IsolatedTable extends VirtualTable {
|
|
|
1095
1138
|
// A same-PK update has no overlay row at newPK (=== targetPK), so the
|
|
1096
1139
|
// helper's insert path matches the prior behavior.
|
|
1097
1140
|
const result = pkChanged
|
|
1098
|
-
? await this.writeRelocatedRow(overlay, newPK, overlayRow, tombstoneIndex, effectiveOR)
|
|
1099
|
-
: await overlay.update({ operation: 'insert', values: overlayRow, onConflict: effectiveOR });
|
|
1141
|
+
? await this.writeRelocatedRow(overlay, newPK, overlayRow, tombstoneIndex, effectiveOR, args.preCoerced)
|
|
1142
|
+
: await overlay.update({ operation: 'insert', values: overlayRow, onConflict: effectiveOR, preCoerced: args.preCoerced });
|
|
1100
1143
|
const stripped = this.stripTombstoneFromResult(result, tombstoneIndex);
|
|
1101
1144
|
return this.attachEvicted(this.attachReplacedUnderlying(stripped, replacedUnderlyingRow), evicted, tombstoneIndex);
|
|
1102
1145
|
}
|
|
@@ -1115,13 +1158,16 @@ export class IsolatedTable extends VirtualTable {
|
|
|
1115
1158
|
// (e.g. dml-executor auto-event path) know no actual row was removed.
|
|
1116
1159
|
return { status: 'ok' };
|
|
1117
1160
|
}
|
|
1118
|
-
// Convert to tombstone by updating the tombstone flag
|
|
1161
|
+
// Convert to tombstone by updating the tombstone flag. The row is sliced
|
|
1162
|
+
// from a row the overlay itself already wrote/coerced — mark preCoerced
|
|
1163
|
+
// so the overlay doesn't re-run (non-idempotent) JSON coercion on it.
|
|
1119
1164
|
const tombstoneRow = [...existingOverlayRow.slice(0, tombstoneIndex), 1];
|
|
1120
1165
|
await overlay.update({
|
|
1121
1166
|
operation: 'update',
|
|
1122
1167
|
values: tombstoneRow,
|
|
1123
1168
|
oldKeyValues: targetPK,
|
|
1124
1169
|
onConflict: args.onConflict,
|
|
1170
|
+
preCoerced: true,
|
|
1125
1171
|
});
|
|
1126
1172
|
// Return the pre-deletion row so dml-executor emits the auto change event.
|
|
1127
1173
|
const deletedRow = existingOverlayRow.slice(0, tombstoneIndex);
|
|
@@ -1138,10 +1184,14 @@ export class IsolatedTable extends VirtualTable {
|
|
|
1138
1184
|
tombstoneRow[colIdx] = targetPK[i];
|
|
1139
1185
|
});
|
|
1140
1186
|
tombstoneRow[tombstoneIndex] = 1; // Set tombstone flag
|
|
1187
|
+
// targetPK cells come from oldKeyValues (already coerced, read from the
|
|
1188
|
+
// source scan) — mark preCoerced so the overlay doesn't re-run (non-idempotent)
|
|
1189
|
+
// JSON coercion on them.
|
|
1141
1190
|
await overlay.update({
|
|
1142
1191
|
operation: 'insert',
|
|
1143
1192
|
values: tombstoneRow,
|
|
1144
1193
|
onConflict: args.onConflict,
|
|
1194
|
+
preCoerced: true,
|
|
1145
1195
|
});
|
|
1146
1196
|
// Return a minimal placeholder row (PK columns only) so dml-executor
|
|
1147
1197
|
// recognises the delete as successful and emits the auto change event.
|
|
@@ -1261,10 +1311,16 @@ export class IsolatedTable extends VirtualTable {
|
|
|
1261
1311
|
// store keys rows collation-aware (e.g. a NOCASE text PK), so a case-only PK
|
|
1262
1312
|
// rewrite ('apple' → 'APPLE') is the SAME logical key. A binary comparison here
|
|
1263
1313
|
// would mis-classify it as a PK relocation, then resolve the "new" key back to the
|
|
1264
|
-
// same physical underlying row and raise a false UNIQUE PK conflict.
|
|
1314
|
+
// same physical underlying row and raise a false UNIQUE PK conflict. A
|
|
1315
|
+
// semantic-ordering member compares through its type's `compare` for the same
|
|
1316
|
+
// reason: both backends key 'PT1H' and 'PT60M' as one row.
|
|
1265
1317
|
const collations = this.getPkCollations();
|
|
1318
|
+
const semantic = this.getPkSemanticComparators();
|
|
1266
1319
|
for (let i = 0; i < a.length; i++) {
|
|
1267
|
-
|
|
1320
|
+
const cmp = semantic[i]
|
|
1321
|
+
? semantic[i](a[i], b[i])
|
|
1322
|
+
: compareSqlValuesFast(a[i], b[i], collations[i] ?? BINARY_COLLATION);
|
|
1323
|
+
if (cmp !== 0)
|
|
1268
1324
|
return false;
|
|
1269
1325
|
}
|
|
1270
1326
|
return true;
|
|
@@ -1294,7 +1350,7 @@ export class IsolatedTable extends VirtualTable {
|
|
|
1294
1350
|
* with a non-tombstone overlay row should not happen; if it ever does, fall
|
|
1295
1351
|
* through to insert and let the overlay enforce the genuine conflict.
|
|
1296
1352
|
*/
|
|
1297
|
-
async writeRelocatedRow(overlay, newPK, overlayRow, tombstoneIndex, effectiveOR) {
|
|
1353
|
+
async writeRelocatedRow(overlay, newPK, overlayRow, tombstoneIndex, effectiveOR, preCoerced) {
|
|
1298
1354
|
const existingAtNewPK = await this.getOverlayRow(overlay, newPK);
|
|
1299
1355
|
if (existingAtNewPK && existingAtNewPK[tombstoneIndex] === 1) {
|
|
1300
1356
|
return overlay.update({
|
|
@@ -1302,12 +1358,14 @@ export class IsolatedTable extends VirtualTable {
|
|
|
1302
1358
|
values: overlayRow,
|
|
1303
1359
|
oldKeyValues: newPK,
|
|
1304
1360
|
onConflict: effectiveOR,
|
|
1361
|
+
preCoerced,
|
|
1305
1362
|
});
|
|
1306
1363
|
}
|
|
1307
1364
|
return overlay.update({
|
|
1308
1365
|
operation: 'insert',
|
|
1309
1366
|
values: overlayRow,
|
|
1310
1367
|
onConflict: effectiveOR,
|
|
1368
|
+
preCoerced,
|
|
1311
1369
|
});
|
|
1312
1370
|
}
|
|
1313
1371
|
async insertTombstoneForPK(overlay, pk, tombstoneIndex) {
|
|
@@ -1319,11 +1377,13 @@ export class IsolatedTable extends VirtualTable {
|
|
|
1319
1377
|
pkIndices.forEach((colIdx, i) => { tombstoneRow[colIdx] = pk[i]; });
|
|
1320
1378
|
tombstoneRow[tombstoneIndex] = 1;
|
|
1321
1379
|
const existing = await this.getOverlayRow(overlay, pk);
|
|
1380
|
+
// `pk` is already coerced (callers pass targetPK/coercedValues cells) — mark
|
|
1381
|
+
// preCoerced so the overlay doesn't re-run (non-idempotent) JSON coercion on it.
|
|
1322
1382
|
if (existing) {
|
|
1323
|
-
await overlay.update({ operation: 'update', values: tombstoneRow, oldKeyValues: pk });
|
|
1383
|
+
await overlay.update({ operation: 'update', values: tombstoneRow, oldKeyValues: pk, preCoerced: true });
|
|
1324
1384
|
}
|
|
1325
1385
|
else {
|
|
1326
|
-
await overlay.update({ operation: 'insert', values: tombstoneRow });
|
|
1386
|
+
await overlay.update({ operation: 'insert', values: tombstoneRow, preCoerced: true });
|
|
1327
1387
|
}
|
|
1328
1388
|
}
|
|
1329
1389
|
/**
|
|
@@ -1382,14 +1442,20 @@ export class IsolatedTable extends VirtualTable {
|
|
|
1382
1442
|
* WHICH row is named in that pre-violated case.
|
|
1383
1443
|
*/
|
|
1384
1444
|
async findMergedUniqueConflict(overlay, uc, predicate, newRow, selfPks, tombstoneIndex) {
|
|
1385
|
-
// One comparison
|
|
1386
|
-
//
|
|
1387
|
-
//
|
|
1445
|
+
// One comparison function per constrained column — the type's `compare` for a
|
|
1446
|
+
// semantic-ordering column ('PT1H' conflicts with 'PT60M', matching both
|
|
1447
|
+
// backends' typed enforcement), else the enforcement collation (the index's
|
|
1448
|
+
// per-column COLLATE for an index-derived UNIQUE, else the declared column
|
|
1449
|
+
// collation) through `compareSqlValuesFast`. Resolved once, above both
|
|
1450
|
+
// candidate scans; both phases compare identically. The construction is the
|
|
1451
|
+
// engine's `uniqueEnforcementComparators`, shared with the store and memory
|
|
1452
|
+
// re-validators so the three backends cannot drift.
|
|
1388
1453
|
const collations = resolveUniqueEnforcementCollations(this.tableSchema, uc, this.collationResolver);
|
|
1389
|
-
const
|
|
1454
|
+
const compares = uniqueEnforcementComparators(this.tableSchema.columns, uc.columns, collations);
|
|
1455
|
+
const overlayConflict = await this.findOverlayUniqueConflict(overlay, predicate, newRow, selfPks, tombstoneIndex, uc.columns, compares);
|
|
1390
1456
|
if (overlayConflict)
|
|
1391
1457
|
return overlayConflict;
|
|
1392
|
-
return this.findUnderlyingUniqueConflict(overlay, uc, predicate, newRow, selfPks, tombstoneIndex,
|
|
1458
|
+
return this.findUnderlyingUniqueConflict(overlay, uc, predicate, newRow, selfPks, tombstoneIndex, compares);
|
|
1393
1459
|
}
|
|
1394
1460
|
/**
|
|
1395
1461
|
* Phase 1 — scan the overlay (this connection's uncommitted write set, small and
|
|
@@ -1399,7 +1465,7 @@ export class IsolatedTable extends VirtualTable {
|
|
|
1399
1465
|
* what the merged view holds for that PK
|
|
1400
1466
|
* (isolation-merged-unique-stale-underlying-false-positive).
|
|
1401
1467
|
*/
|
|
1402
|
-
async findOverlayUniqueConflict(overlay, predicate, newRow, selfPks, tombstoneIndex, constrainedCols,
|
|
1468
|
+
async findOverlayUniqueConflict(overlay, predicate, newRow, selfPks, tombstoneIndex, constrainedCols, compares) {
|
|
1403
1469
|
if (!overlay.query)
|
|
1404
1470
|
return null;
|
|
1405
1471
|
const pkIndices = this.getPrimaryKeyIndices();
|
|
@@ -1410,7 +1476,7 @@ export class IsolatedTable extends VirtualTable {
|
|
|
1410
1476
|
if (selfPks.some(self => this.keysEqual(pk, self)))
|
|
1411
1477
|
continue;
|
|
1412
1478
|
const mergedRow = overlayRow.slice(0, tombstoneIndex);
|
|
1413
|
-
if (this.rowMatchesUniqueConstraint(mergedRow, newRow, constrainedCols,
|
|
1479
|
+
if (this.rowMatchesUniqueConstraint(mergedRow, newRow, constrainedCols, compares, predicate)) {
|
|
1414
1480
|
return { pk, row: mergedRow };
|
|
1415
1481
|
}
|
|
1416
1482
|
}
|
|
@@ -1424,7 +1490,7 @@ export class IsolatedTable extends VirtualTable {
|
|
|
1424
1490
|
* returns extra rows stays correct. `getOverlayRow` fires only for the candidates the
|
|
1425
1491
|
* seek actually returned, so it no longer costs one lookup per underlying row.
|
|
1426
1492
|
*/
|
|
1427
|
-
async findUnderlyingUniqueConflict(overlay, uc, predicate, newRow, selfPks, tombstoneIndex,
|
|
1493
|
+
async findUnderlyingUniqueConflict(overlay, uc, predicate, newRow, selfPks, tombstoneIndex, compares) {
|
|
1428
1494
|
if (!this.underlyingTable.query)
|
|
1429
1495
|
return null;
|
|
1430
1496
|
const pkIndices = this.getPrimaryKeyIndices();
|
|
@@ -1441,7 +1507,7 @@ export class IsolatedTable extends VirtualTable {
|
|
|
1441
1507
|
const overlayRow = await this.getOverlayRow(overlay, pk);
|
|
1442
1508
|
if (overlayRow)
|
|
1443
1509
|
continue;
|
|
1444
|
-
if (this.rowMatchesUniqueConstraint(underlyingRow, newRow, uc.columns,
|
|
1510
|
+
if (this.rowMatchesUniqueConstraint(underlyingRow, newRow, uc.columns, compares, predicate)) {
|
|
1445
1511
|
return { pk, row: underlyingRow };
|
|
1446
1512
|
}
|
|
1447
1513
|
}
|
|
@@ -1450,19 +1516,21 @@ export class IsolatedTable extends VirtualTable {
|
|
|
1450
1516
|
/**
|
|
1451
1517
|
* True-match test shared by both merged-check phases so they compare identically:
|
|
1452
1518
|
* `candidate` matches `newRow` on every constrained column under that column's
|
|
1453
|
-
*
|
|
1519
|
+
* comparison function (NULLs never match), and — for a partial UNIQUE — the
|
|
1454
1520
|
* predicate holds for `candidate`.
|
|
1455
1521
|
*
|
|
1456
|
-
*
|
|
1457
|
-
*
|
|
1458
|
-
* column
|
|
1522
|
+
* `compares` carries one function per constrained column, resolved once in
|
|
1523
|
+
* {@link findMergedUniqueConflict}: the type's `compare` for a semantic-ordering
|
|
1524
|
+
* column, else the enforcement collation (the index's per-column COLLATE for an
|
|
1525
|
+
* index-derived UNIQUE, else the declared column collation) — the latter enforces
|
|
1526
|
+
* a UNIQUE over a collated column against merged rows through the isolation path
|
|
1459
1527
|
* (unique-constraint-honors-column-collation / store-index-derived-unique).
|
|
1460
1528
|
*/
|
|
1461
|
-
rowMatchesUniqueConstraint(candidate, newRow, constrainedCols,
|
|
1529
|
+
rowMatchesUniqueConstraint(candidate, newRow, constrainedCols, compares, predicate) {
|
|
1462
1530
|
const matches = constrainedCols.every((idx, i) => {
|
|
1463
1531
|
if (newRow[idx] === null || candidate[idx] === null)
|
|
1464
1532
|
return false;
|
|
1465
|
-
return
|
|
1533
|
+
return compares[i](newRow[idx], candidate[idx]) === 0;
|
|
1466
1534
|
});
|
|
1467
1535
|
if (!matches)
|
|
1468
1536
|
return false;
|
|
@@ -1608,11 +1676,13 @@ export class IsolatedTable extends VirtualTable {
|
|
|
1608
1676
|
/**
|
|
1609
1677
|
* Index DDL delegates to the module rather than driving the underlying and the overlay
|
|
1610
1678
|
* directly. The module owns the full protocol — validating against the issuing
|
|
1611
|
-
* connection's effective rows, then
|
|
1612
|
-
*
|
|
1613
|
-
*
|
|
1614
|
-
*
|
|
1615
|
-
*
|
|
1679
|
+
* connection's effective rows, then adopting the change IN PLACE into every affected
|
|
1680
|
+
* overlay (narrowed to live rows, and with a foreign overlay's rejection poisoning it
|
|
1681
|
+
* rather than losing its staged rows) — and a bare `underlying.createIndex()` +
|
|
1682
|
+
* `overlay.createIndex()` pair silently skips both halves. The engine only ever reaches the
|
|
1683
|
+
* module-level hook, so these instance methods exist for a module that wraps
|
|
1684
|
+
* `IsolationModule` in turn; routing them through the module keeps that path on the same
|
|
1685
|
+
* protocol.
|
|
1616
1686
|
*/
|
|
1617
1687
|
async createIndex(indexInfo, rows) {
|
|
1618
1688
|
await this.isolationModule.createIndex(this.db, this.schemaName, this.tableName, indexInfo, rows);
|