@quereus/isolation 4.4.0 → 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 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
 
@@ -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,CA2Df;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"}
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 whether the row already exists underlying.
61
- const existsInUnderlying = await rowExistsInUnderlying(underlyingTable, pkIndices, entry.pk);
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,
@@ -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,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAE1D;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC7C,eAA6B,EAC7B,YAA0B,EAC1B,eAAuB;IAEvB,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC;QACzB,sEAAsE;QACtE,sEAAsE;QACtE,MAAM,eAAe,CAAC,KAAK,EAAE,EAAE,CAAC;QAChC,OAAO;IACR,CAAC;IAED,MAAM,gBAAgB,GAAG,eAAe,CAAC,WAAW,CAAC;IACrD,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACvB,MAAM,IAAI,YAAY,CAAC,gCAAgC,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAC;IAC/E,CAAC;IACD,MAAM,SAAS,GAAG,gBAAgB,CAAC,oBAAoB,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAClF,MAAM,SAAS,GAAG,gBAAgB,CAAC,IAAI,CAAC;IAExC,qCAAqC;IACrC,MAAM,cAAc,GAAG,MAAM,qBAAqB,CAAC,YAAY,EAAE,eAAe,EAAE,SAAS,CAAC,CAAC;IAE7F,+EAA+E;IAC/E,gFAAgF;IAChF,qDAAqD;IACrD,MAAM,eAAe,CAAC,KAAK,EAAE,EAAE,CAAC;IAChC,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAExC,0DAA0D;IAC1D,MAAM,OAAO,GAAG,CAAC,GAAG,cAAc,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CACjD,CAAC,CAAC,CAAC,WAAW,KAAK,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IAEjE,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,KAAK,CAAC,WAAW,EAAE,CAAC;YACvB,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,MAAM,CAAC;gBAC3C,SAAS,EAAE,QAAQ;gBACnB,MAAM,EAAE,SAAS;gBACjB,YAAY,EAAE,KAAK,CAAC,EAAE;aACtB,CAAC,CAAC;YACH,kBAAkB,CAAC,MAAM,EAAE,QAAQ,EAAE,KAAK,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;QAC3D,CAAC;aAAM,CAAC;YACP,yEAAyE;YACzE,MAAM,kBAAkB,GAAG,MAAM,qBAAqB,CAAC,eAAe,EAAE,SAAS,EAAE,KAAK,CAAC,EAAE,CAAC,CAAC;YAC7F,IAAI,kBAAkB,EAAE,CAAC;gBACxB,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,MAAM,CAAC;oBAC3C,SAAS,EAAE,QAAQ;oBACnB,MAAM,EAAE,KAAK,CAAC,OAAO;oBACrB,YAAY,EAAE,KAAK,CAAC,EAAE;oBACtB,UAAU,EAAE,IAAI;oBAChB,YAAY,EAAE,IAAI;iBAClB,CAAC,CAAC;gBACH,kBAAkB,CAAC,MAAM,EAAE,QAAQ,EAAE,KAAK,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;YAC3D,CAAC;iBAAM,CAAC;gBACP,MAAM,MAAM,GAAG,MAAM,eAAe,CAAC,MAAM,CAAC;oBAC3C,SAAS,EAAE,QAAQ;oBACnB,MAAM,EAAE,KAAK,CAAC,OAAO;oBACrB,UAAU,EAAE,IAAI;oBAChB,YAAY,EAAE,IAAI;iBAClB,CAAC,CAAC;gBACH,kBAAkB,CAAC,MAAM,EAAE,QAAQ,EAAE,KAAK,CAAC,EAAE,EAAE,SAAS,CAAC,CAAC;YAC3D,CAAC;QACF,CAAC;IACF,CAAC;AACF,CAAC;AAED;;;GAGG;AACH,KAAK,UAAU,qBAAqB,CACnC,eAA6B,EAC7B,SAAmB,EACnB,EAAc;IAEd,IAAI,CAAC,eAAe,CAAC,KAAK;QAAE,OAAO,KAAK,CAAC;IACzC,IAAI,KAAK,EAAE,MAAM,IAAI,IAAI,eAAe,CAAC,KAAK,CAAC,uBAAuB,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC;QACxF,OAAO,IAAI,CAAC;IACb,CAAC;IACD,OAAO,KAAK,CAAC;AACd,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAAoB,EAAE,SAAgB,EAAE,EAAc,EAAE,SAAiB;IAC3G,IAAI,qBAAqB,CAAC,MAAM,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,YAAY,CACrB,mBAAmB,SAAS,QAAQ,SAAS,UAAU,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,MAAM,CAAC,UAAU,gBAAgB,MAAM,CAAC,OAAO,IAAI,YAAY,sIAAsI,EACrR,UAAU,CAAC,QAAQ,CACnB,CAAC;IACH,CAAC;AACF,CAAC"}
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
- * enforcement collation (NULLs never match), and — for a partial UNIQUE — the
505
+ * comparison function (NULLs never match), and — for a partial UNIQUE — the
496
506
  * predicate holds for `candidate`.
497
507
  *
498
- * The per-column collation (the index's per-column COLLATE for an index-derived
499
- * UNIQUE, else the declared column collation) enforces a UNIQUE over a collated
500
- * column against merged rows through the isolation path
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":"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"}
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, serializeRowKey, pkKeyCollationName, retargetFilterInfoIndex, PRIMARY_INDEX_NAME, validateAndParse, IndexConstraintOp, decodeIdxStr } from '@quereus/quereus';
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: `serializeRowKey` normalizes only
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
- // `!` is safe: PK columns are NOT NULL, so serializeRowKey never returns
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
- // `!` is safe for the same reason as the build loop above (PK NOT NULL).
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 = compareSqlValuesFast(a[i], b[i], collations[i] ?? BINARY_COLLATION);
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 = compareSqlValuesFast(a, b, collation);
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
- const cols = this.tableSchema.columns;
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
- // Coerced only for PK extraction / merged-view conflict detection below — NOT
937
- // for the overlay write. The overlay (a memory-module table) always re-coerces
938
- // every cell on its own insert/update unconditionally, so writing the coerced
939
- // row through would coerce twice. That is a no-op for most logical types, but
940
- // JSON's `parse` is not idempotent for a JSON-string scalar (`'"hello"'` parses
941
- // to the native JS string `"hello"`; re-parsing that bare string as JSON throws,
942
- // since it lacks its own quotes) — double coercion would break any JSON column.
943
- // Detection instead needs the coerced form: probing the overlay/underlying by an
944
- // un-coerced PK (e.g. TEXT '1' against a stored INTEGER 1) misses the existing
945
- // row entirely (bug-store-isolation-upsert-affinity-coerced-pk).
946
- // NOTE: this coerces the full row on every isolation-layer write, and the overlay
947
- // coerces it again on its own insert/update — two validateAndParse passes per write
948
- // (two JSON.parse for JSON columns). Negligible now; if isolation-write throughput or
949
- // large-JSON rows ever show as hot, thread the coerced row through as pre-coerced.
950
- const coercedValues = values ? this.coerceRow(values) : values;
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
- if (compareSqlValuesFast(a[i], b[i], collations[i] ?? BINARY_COLLATION) !== 0)
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 collation per constrained column — the index's per-column
1386
- // COLLATE for an index-derived UNIQUE, else the declared column collation.
1387
- // Resolved once, above both candidate scans; both phases compare identically.
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 overlayConflict = await this.findOverlayUniqueConflict(overlay, predicate, newRow, selfPks, tombstoneIndex, uc.columns, collations);
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, collations);
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, collations) {
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, collations, predicate)) {
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, collations) {
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, collations, predicate)) {
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
- * enforcement collation (NULLs never match), and — for a partial UNIQUE — the
1519
+ * comparison function (NULLs never match), and — for a partial UNIQUE — the
1454
1520
  * predicate holds for `candidate`.
1455
1521
  *
1456
- * The per-column collation (the index's per-column COLLATE for an index-derived
1457
- * UNIQUE, else the declared column collation) enforces a UNIQUE over a collated
1458
- * column against merged rows through the isolation path
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, collations, predicate) {
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 compareSqlValuesFast(newRow[idx], candidate[idx], collations[i]) === 0;
1533
+ return compares[i](newRow[idx], candidate[idx]) === 0;
1466
1534
  });
1467
1535
  if (!matches)
1468
1536
  return false;