@quereus/isolation 4.3.2 → 4.4.1
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 +5 -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 +16 -4
- package/dist/src/isolated-table.d.ts.map +1 -1
- package/dist/src/isolated-table.js +121 -53
- package/dist/src/isolated-table.js.map +1 -1
- package/dist/src/isolation-module.d.ts +341 -110
- package/dist/src/isolation-module.d.ts.map +1 -1
- package/dist/src/isolation-module.js +645 -274
- package/dist/src/isolation-module.js.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 +3 -3
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
|
|
@@ -106,7 +110,7 @@ read-your-own-writes** than to snapshot isolation:
|
|
|
106
110
|
|
|
107
111
|
**Lazy overlay creation** — No memory overhead until the first write in a transaction.
|
|
108
112
|
|
|
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).
|
|
113
|
+
**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
114
|
|
|
111
115
|
**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
116
|
|
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;
|
|
@@ -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;
|