@minnowdb/core 0.6.5 → 0.6.7

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.
Files changed (57) hide show
  1. package/dist/block-format/codecs.d.ts +0 -8
  2. package/dist/block-format/codecs.js +1 -1
  3. package/dist/engine/artifact-cache.d.ts +2 -1
  4. package/dist/engine/batch.d.ts +0 -1
  5. package/dist/engine/batch.js +1 -1
  6. package/dist/engine/buffered-writer.js +1 -12
  7. package/dist/engine/byte-estimates.d.ts +11 -0
  8. package/dist/engine/byte-estimates.js +25 -0
  9. package/dist/engine/database.js +58 -34
  10. package/dist/engine/fts.d.ts +0 -1
  11. package/dist/engine/fts.js +1 -1
  12. package/dist/engine/group-index.d.ts +2 -0
  13. package/dist/engine/group-index.js +4 -8
  14. package/dist/engine/join-index.d.ts +0 -1
  15. package/dist/engine/join-index.js +1 -15
  16. package/dist/engine/keyed-live.js +2 -36
  17. package/dist/engine/live-equal.d.ts +7 -0
  18. package/dist/engine/live-equal.js +42 -0
  19. package/dist/engine/optimizer.js +11 -16
  20. package/dist/engine/point-read.d.ts +1 -1
  21. package/dist/engine/point-read.js +3 -2
  22. package/dist/engine/query-cache.js +1 -12
  23. package/dist/engine/query.d.ts +20 -59
  24. package/dist/engine/query.js +286 -54
  25. package/dist/engine/result-wire.d.ts +2 -1
  26. package/dist/engine/schema.d.ts +15 -11
  27. package/dist/engine/schema.js +11 -19
  28. package/dist/engine/sort-keys.d.ts +2 -3
  29. package/dist/engine/sort-keys.js +1 -1
  30. package/dist/engine/sql-domains.d.ts +27 -2
  31. package/dist/engine/sql-domains.js +120 -6
  32. package/dist/engine/sql-json.d.ts +0 -2
  33. package/dist/engine/sql-json.js +1 -1
  34. package/dist/engine/sql-semantics.d.ts +2 -1
  35. package/dist/engine/typed-live.js +3 -41
  36. package/dist/engine/vector.d.ts +7 -7
  37. package/dist/engine/vector.js +11 -30
  38. package/dist/engine/write-block-planner.d.ts +2 -1
  39. package/dist/plan/model.d.ts +22 -0
  40. package/dist/plan/model.js +29 -1
  41. package/dist/storage/indexeddb.js +13 -23
  42. package/dist/storage/opfs/leader.d.ts +0 -4
  43. package/dist/storage/opfs/leader.js +5 -14
  44. package/dist/storage/opfs/snapshot-ledger.d.ts +3 -2
  45. package/dist/storage/toolkit/index.d.ts +1 -1
  46. package/dist/storage/toolkit/record-core.d.ts +0 -1
  47. package/dist/storage/toolkit/record-core.js +3 -19
  48. package/dist/storage/types.d.ts +4 -3
  49. package/dist/storage/types.js +13 -2
  50. package/dist/testing/block-store-conformance.js +28 -0
  51. package/dist/testing/opfs-shim.d.ts +2 -1
  52. package/dist/testing/simulator.js +3 -0
  53. package/dist/worker-protocol/index.d.ts +0 -32
  54. package/dist/worker-protocol/index.js +0 -40
  55. package/package.json +2 -1
  56. package/postgres-feature-profile.json +8 -3
  57. package/sql-feature-matrix.json +71 -11
@@ -16,14 +16,6 @@ export interface CompressionMemoryBound {
16
16
  /** Additional JavaScript-owned byte storage retained while producing the result. */
17
17
  readonly scratchBytes: number;
18
18
  }
19
- /**
20
- * The raw codec returns its input view unchanged in both directions: callers treat compressed
21
- * and decompressed payloads as read-only (encode copies the payload into the block envelope,
22
- * decode consumers only read or copy out), so a full-payload defensive copy per block would be
23
- * pure overhead on the default compression path. The returned view may share the caller's
24
- * buffer at a non-zero byte offset.
25
- */
26
- export declare const rawCodec: CompressionCodec;
27
19
  export declare function getCompressionMemoryBound(compression: Compression, inputLength: number): CompressionMemoryBound;
28
20
  export declare const gzipCodec: CompressionCodec;
29
21
  export declare function getCodec(id: Compression): CompressionCodec;
@@ -16,7 +16,7 @@ export class CompressionOutputLimitError extends RangeError {
16
16
  * pure overhead on the default compression path. The returned view may share the caller's
17
17
  * buffer at a non-zero byte offset.
18
18
  */
19
- export const rawCodec = {
19
+ const rawCodec = {
20
20
  id: "raw",
21
21
  async compress(bytes, maximumOutputLength) {
22
22
  if (maximumOutputLength !== undefined) {
@@ -1,5 +1,5 @@
1
1
  /** Snapshot of the byte-bounded artifact cache's lifetime counters. */
2
- export interface ArtifactCacheStats {
2
+ interface ArtifactCacheStats {
3
3
  limitBytes: number;
4
4
  usedBytes: number;
5
5
  entries: number;
@@ -26,3 +26,4 @@ export declare class ArtifactCache {
26
26
  clear(): void;
27
27
  stats(): ArtifactCacheStats;
28
28
  }
29
+ export {};
@@ -31,6 +31,5 @@ export interface ColumnarBatch {
31
31
  }
32
32
  /** What `insertBatch` and `upsertBatch` take: rows, or columns for a bulk load. */
33
33
  export type InsertBatchInput = readonly BatchRow[] | ColumnarBatch;
34
- export declare function isColumnarBatch(input: InsertBatchInput): input is ColumnarBatch;
35
34
  /** Pivots rows into the engine's columnar form; a columnar batch passes straight through. */
36
35
  export declare function toColumnarBatch(input: InsertBatchInput): ColumnarBatch;
@@ -6,7 +6,7 @@
6
6
  * This module deliberately has no imports: the main-thread client normalizes here too, and must
7
7
  * not pull the engine in behind it.
8
8
  */
9
- export function isColumnarBatch(input) {
9
+ function isColumnarBatch(input) {
10
10
  return !Array.isArray(input);
11
11
  }
12
12
  /** Pivots rows into the engine's columnar form; a columnar batch passes straight through. */
@@ -1,4 +1,5 @@
1
1
  import { copyDate } from "../date-value.js";
2
+ import { estimateRowBytes } from "./byte-estimates.js";
2
3
  /** Maximum accepted `add()` calls that have not completed. Callers must await for backpressure. */
3
4
  export const MAX_BUFFERED_WRITER_PENDING_ADDS = 64;
4
5
  /** Batches row-oriented writes by row count, estimated bytes, or age. */
@@ -169,15 +170,3 @@ function cloneRow(row) {
169
170
  value instanceof Date ? copyDate(value) : value,
170
171
  ]));
171
172
  }
172
- function estimateRowBytes(row) {
173
- let bytes = 0;
174
- for (const value of Object.values(row)) {
175
- if (typeof value === "string")
176
- bytes += 4 + value.length;
177
- else if (typeof value === "number" || value instanceof Date)
178
- bytes += 8;
179
- else
180
- bytes += 1;
181
- }
182
- return bytes;
183
- }
@@ -0,0 +1,11 @@
1
+ import type { ColumnarBatch } from "./batch.js";
2
+ /**
3
+ * The one modeled per-value size used for flush thresholds, cache accounting, and write
4
+ * metrics. One byte per UTF-16 code unit approximates the UTF-8 payload (exact for ASCII)
5
+ * without encoding the string just to measure it — this estimate feeds metrics and flush
6
+ * thresholds, not the physical format. Keeping a single implementation keeps those
7
+ * accountings from drifting apart.
8
+ */
9
+ export declare function estimateValuesBytes(values: readonly unknown[]): number;
10
+ export declare function estimateRowBytes(row: Readonly<Record<string, unknown>>): number;
11
+ export declare function estimateBatchBytes(input: ColumnarBatch): number;
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The one modeled per-value size used for flush thresholds, cache accounting, and write
3
+ * metrics. One byte per UTF-16 code unit approximates the UTF-8 payload (exact for ASCII)
4
+ * without encoding the string just to measure it — this estimate feeds metrics and flush
5
+ * thresholds, not the physical format. Keeping a single implementation keeps those
6
+ * accountings from drifting apart.
7
+ */
8
+ export function estimateValuesBytes(values) {
9
+ let bytes = 0;
10
+ for (const value of values) {
11
+ if (typeof value === "string")
12
+ bytes += 4 + value.length;
13
+ else if (typeof value === "number" || value instanceof Date)
14
+ bytes += 8;
15
+ else
16
+ bytes += 1;
17
+ }
18
+ return bytes;
19
+ }
20
+ export function estimateRowBytes(row) {
21
+ return estimateValuesBytes(Object.values(row));
22
+ }
23
+ export function estimateBatchBytes(input) {
24
+ return Object.values(input.columns).reduce((total, values) => total + estimateValuesBytes(values), 0);
25
+ }
@@ -1,5 +1,6 @@
1
1
  import { toColumnarBatch, } from "./batch.js";
2
2
  import { ArtifactCache } from "./artifact-cache.js";
3
+ import { estimateBatchBytes, estimateRowBytes, estimateValuesBytes } from "./byte-estimates.js";
3
4
  import { throwIfAborted } from "./cancellation.js";
4
5
  import { BufferedTableWriter } from "./buffered-writer.js";
5
6
  export { attachLifecycleFlush, BufferedTableWriter, MAX_BUFFERED_WRITER_PENDING_ADDS, } from "./buffered-writer.js";
@@ -14,13 +15,13 @@ import { cachedQueryTerms, FTS_TOKENIZER_VERSION, renderDocumentValue, tokenize
14
15
  import { boundedMaintenanceBatchItems, simpleDataTypes, floorWholeNumberProduct, BlockReadBatchTooLargeError, validateColumnDefault, validateEnumValues, secondaryIndexColumnIds, secondaryIndexDirections, secondaryUniqueKeyNamespace, CompactionJobConflictError, CompactionBacklogError, GarbageCollectionJobConflictError, MAX_LEVEL_ZERO_SEGMENTS, MAX_FTS_CANDIDATE_ROW_IDS, MAX_FTS_POSTING_TERM_CHARACTERS, MAX_FTS_POSTINGS_PER_CHUNK, MAX_FTS_POSTING_ROW_IDS_PER_CHUNK, MAX_INDEXED_STRING_CHARACTERS, MAX_CATALOG_NAME_CHARACTERS, MAX_MANIFEST_BLOCK_PRESENCE_IDS, MAX_MAINTENANCE_BATCH_ITEMS, MAX_STORAGE_BULK_READ_ITEMS, MAX_TRANSACTION_STAGE_BLOCKS, MAX_TRANSACTION_STAGE_BYTES, MAX_SNAPSHOT_SESSION_TTL_MS, MAX_POSTING_BUILD_TTL_MS, MAX_TEMP_OWNER_TTL_MS, MAX_SNAPSHOT_FRAME_BATCH_BYTES, MAX_SNAPSHOT_FRAME_BATCH_ITEMS, MAX_SNAPSHOT_METADATA_BATCH_BYTES, SNAPSHOT_FRAME_KINDS, validateStorageId, SnapshotManifestMissingError, SchemaConflictError, TableInUseError, TableRecordConflictError, TransactionRecordConflictError, UniqueKeyConflictError, UniqueIndexCoverageError, WriteConflictError, } from "../storage/types.js";
15
16
  import { decodeSnapshotFrameStream, encodeSnapshotFrameStreamFooter, encodeSnapshotFrameStreamHeader, extendSnapshotFrameStreamChecksum, snapshotFrameEnvelopeParts, snapshotFrameStreamHeaderIdentity, } from "../storage/snapshot.js";
16
17
  import { Snapshot, TransactionManager, } from "../transactions/index.js";
17
- import { applyWindowFunctions, bindPlanParameters, bindStatementParameters, DUAL_TABLE, dualTableRows, blockHasSubqueries, combineUnionResults, compileCheckExpression, compileQuery, hasAggregate, createRecursiveCteState, compileStatement, comparisonHolds, createPreparedColumnarQuery, evaluateJoinedRowExpression, evaluateRowExpression, externalizeQueryResult, expressionColumnNames, inferBlockSchema, inferResultColumnDomains, isDefaultInsertValue, referencedColumns, childExpressions, expandFtsColumns, expandNaturalJoins, expandSourceColumnAliases, expandViewSources, forEachBlockExpression, planContainsFts, planHasNaturalJoins, planHasSourceColumnAliases, planReadsTable, planReadsViews, planReadsBeyondSingleScan, projectResultColumns, queryResultNeedsExternalization, resolveStatementDatetimes, subqueryResolutionSteps, topLevelFtsMatchConjuncts, transparentProjectionSource, unknownColumnDomains, validateDefaultExpression, windowOutputDomain, windowOutputType, } from "./query.js";
18
+ import { applyWindowFunctions, bindPlanParameters, bindStatementParameters, DUAL_TABLE, dualTableRows, blockHasSubqueries, combineUnionResults, compileCheckExpression, compileQuery, hasAggregate, createRecursiveCteState, annotateAvgArgumentScales, compileStatement, comparisonHolds, createPreparedColumnarQuery, evaluateJoinedRowExpression, evaluateRowExpression, externalizeQueryResult, expressionColumnNames, inferBlockSchema, inferResultColumnDomains, isDefaultInsertValue, referencedColumns, childExpressions, expandFtsColumns, expandNaturalJoins, expandSourceColumnAliases, expandViewSources, forEachBlockExpression, planContainsFts, planHasNaturalJoins, planHasSourceColumnAliases, planReadsTable, planReadsViews, planReadsBeyondSingleScan, projectResultColumns, queryResultNeedsExternalization, resolveStatementDatetimes, subqueryResolutionSteps, topLevelFtsMatchConjuncts, transparentProjectionSource, unknownColumnDomains, validateDefaultExpression, windowOutputDomain, windowOutputType, } from "./query.js";
18
19
  import { copyQueryResult, planMemoKey, queryResultMemoKey, queryResultRetainedBytes, RESULT_MEMO_MAX_BYTES, } from "./query-cache.js";
19
20
  import { QueryMemoryBudgetError, QueryMemoryContext, DEFAULT_QUERY_MEMORY_BUDGET_BYTES, } from "./memory.js";
20
21
  import { LiveQueryLimitError, LiveQuerySet, MAX_LIVE_QUERY_SETS_PER_DATABASE, } from "./live.js";
21
22
  import { chooseJoinOrder, optimizePlan, qualifyCorrelatedReferences, renderPlan, } from "./optimizer.js";
22
23
  import { encodeSqlEqualityValue } from "./sql-semantics.js";
23
- import { externalSqlDomainValue, externalSqlTextValue, isDateDomainValue, isSqlDomainValue, normalizeSqlDomainValue, protectedSqlTextValue, } from "./sql-domains.js";
24
+ import { exactNumericAsNumber, externalSqlDomainValue, externalSqlTextValue, isDateDomainValue, isExactNumeric, isSqlDomainValue, normalizeSqlDomainValue, protectedSqlTextValue, } from "./sql-domains.js";
24
25
  import { toCatalog } from "./catalog.js";
25
26
  import { applyColumnSteps, assertColumnDroppable, compileGeneratedColumnExpression, declaredForeignKeys, isDestructiveStep, planMigration, } from "./schema.js";
26
27
  import { columnarTableFromRows, createColumnarTable, vectorValue, } from "./vector.js";
@@ -2666,6 +2667,9 @@ export class MinnowDatabase {
2666
2667
  // columnar forms of derived and windowed sources too, not just the result memo.
2667
2668
  options.memoize !== false, options.spillToStorage !== false, options.spillToStorage === true, options.spillPageRows, options.signal);
2668
2669
  throwIfAborted(options.signal);
2670
+ // After input preparation, because executing the nested blocks registered their
2671
+ // synthetic source schemas in typedSchemas — same reasoning as the domain inference.
2672
+ resolvedPlan = annotateAvgArgumentScales(resolvedPlan, typedSchemas);
2669
2673
  outputNeedsExternalization = queryResultNeedsExternalization(resolvedPlan, typedSchemas);
2670
2674
  outputColumnDomains = inferResultColumnDomains(resolvedPlan, typedSchemas);
2671
2675
  };
@@ -3602,12 +3606,15 @@ export class MinnowDatabase {
3602
3606
  if (!keyComponents.every((component) => equalityColumns.has(component.name))) {
3603
3607
  return undefined;
3604
3608
  }
3609
+ // A logical-domain projection is served too: the stored scalar is the tagged internal
3610
+ // value the ordinary executor's pipeline carries for a bare column reference, and both
3611
+ // paths cross the result boundary through the same externalizeQueryResult call, so
3612
+ // reporting the column's domain below makes the answers identical by construction.
3605
3613
  const projected = [];
3606
3614
  for (const item of shape.select) {
3607
3615
  const column = columnByName.get(item.column);
3608
- if (column === undefined || column.hidden === true || column.sqlDomain !== undefined) {
3616
+ if (column === undefined || column.hidden === true)
3609
3617
  return undefined;
3610
- }
3611
3618
  projected.push({ column, alias: item.alias });
3612
3619
  }
3613
3620
  const segments = await this.#visibleSegmentRecords(table, snapshot, visibility);
@@ -3749,11 +3756,16 @@ export class MinnowDatabase {
3749
3756
  if (columnVector === undefined)
3750
3757
  return undefined;
3751
3758
  const value = vectorValue(columnVector, slot);
3752
- // A stored plain-text value in the protected NUL namespace crosses the result
3753
- // boundary through the ordinary executor's wrapping rules; reproducing them here
3754
- // is not worth the risk, so the whole statement falls back.
3755
- if (typeof value === "string" && value.charCodeAt(0) === 0)
3756
- return undefined;
3759
+ if (typeof value === "string" && value.charCodeAt(0) === 0) {
3760
+ // A domain column's stored scalar is expected here: it carries the internal
3761
+ // tag and is stripped by the shared externalization boundary. Anything else in
3762
+ // the NUL namespace plain text crossing through the ordinary executor's
3763
+ // wrapping rules, or an untagged value where a tag is required — is not
3764
+ // provably reproduced, so the whole statement falls back.
3765
+ if (item.column.sqlDomain === undefined || !isSqlDomainValue(value)) {
3766
+ return undefined;
3767
+ }
3768
+ }
3757
3769
  row[item.alias] = value;
3758
3770
  }
3759
3771
  rows.push(row);
@@ -3762,7 +3774,7 @@ export class MinnowDatabase {
3762
3774
  }
3763
3775
  return {
3764
3776
  columns: shape.select.map((item) => item.alias),
3765
- columnDomains: shape.select.map(() => null),
3777
+ columnDomains: projected.map((item) => item.column.sqlDomain ?? null),
3766
3778
  rows,
3767
3779
  };
3768
3780
  }
@@ -7791,6 +7803,8 @@ export class MinnowDatabase {
7791
7803
  ...(sqlDomain === undefined ? {} : { sqlDomain }),
7792
7804
  })),
7793
7805
  ]));
7806
+ // Streamed plans scan real tables only, so the catalog schemas resolve every AVG argument.
7807
+ plan = annotateAvgArgumentScales(plan, typedSchemas);
7794
7808
  const columns = referencedColumns(plan, schemas);
7795
7809
  // Streaming must choose the scan/build orientation before creating its sliding base view.
7796
7810
  // Plain append histories have exact row counts in segment metadata; mutation histories keep
@@ -8846,6 +8860,9 @@ export class MinnowDatabase {
8846
8860
  throwIfAborted(signal);
8847
8861
  const inputs = await this.#prepareBlockInputs(block, snapshot, visibility, memory, realTables, typedSchemas, extraInputs, cacheResults, allowSpill, forceSpill, spillPageRows, signal);
8848
8862
  throwIfAborted(signal);
8863
+ // After input preparation, because executing this block's nested sources registered their
8864
+ // synthetic schemas in typedSchemas, which is what resolves a derived column's domain.
8865
+ block = annotateAvgArgumentScales(block, typedSchemas);
8849
8866
  const prepared = createPreparedColumnarQuery(block, inputs, memory.createChild(), ftsStats === undefined ? {} : { ftsStats });
8850
8867
  // The columnar preparation only knows vector kinds, so a plain domain column projects with a
8851
8868
  // null domain. Catalog-backed inference fills those in — at miss time, because executing the
@@ -15891,7 +15908,12 @@ function normalizeDomainBatch(table, input) {
15891
15908
  const values = input.columns[column.name];
15892
15909
  if (values === undefined)
15893
15910
  continue;
15894
- if (column.sqlDomain !== undefined || column.type === "datetime") {
15911
+ if (column.sqlDomain !== undefined ||
15912
+ column.type === "datetime" ||
15913
+ // A number column rewrites only when a tagged exact-NUMERIC constant actually reached it
15914
+ // (a SQL literal like 0.1/3 evaluated exactly), so plain bulk loads never copy here.
15915
+ (column.type === "number" &&
15916
+ values.some((value) => typeof value === "string" && isExactNumeric(value)))) {
15895
15917
  input.columns[column.name] = values.map((value) => normalizeColumnLogicalValue(column, value));
15896
15918
  }
15897
15919
  }
@@ -15905,6 +15927,18 @@ function normalizeColumnLogicalValue(column, value) {
15905
15927
  throw new TypeError("Invalid DATE value");
15906
15928
  return new Date(`${external}T00:00:00.000Z`);
15907
15929
  }
15930
+ if (column.type === "number" && typeof value === "string" && isExactNumeric(value)) {
15931
+ // PostgreSQL's assignment cast: an exact-NUMERIC value reaching a float column rounds to
15932
+ // the nearest Float64. An integer column keeps its exactness guarantee instead — a value
15933
+ // Float64 cannot hold exactly is rejected rather than silently rounded.
15934
+ const exact = exactNumericAsNumber(value);
15935
+ if (exact !== undefined)
15936
+ return exact;
15937
+ if (column.integer === true) {
15938
+ throw new TypeError(`${column.name} must be a safe integer`);
15939
+ }
15940
+ return Number(externalSqlDomainValue(value));
15941
+ }
15908
15942
  return value;
15909
15943
  }
15910
15944
  function selectBatchRows(input, indexes) {
@@ -15939,14 +15973,25 @@ function normalizeUpsertConflictWhere(table, predicate) {
15939
15973
  return { column, operator: predicate.operator, value };
15940
15974
  }
15941
15975
  function normalizeDomainUpdate(table, input) {
15942
- if (!table.columns.some((column) => column.sqlDomain !== undefined || column.type === "datetime"))
15976
+ const numberColumnNeedsCast = (column, values) => column.type === "number" &&
15977
+ values.some((value) => typeof value === "string" && isExactNumeric(value));
15978
+ if (!table.columns.some((column) => column.sqlDomain !== undefined || column.type === "datetime") &&
15979
+ !Object.entries(input.changes).some(([name, values]) => {
15980
+ const column = table.columns.find((candidate) => candidate.name === name);
15981
+ return column !== undefined && numberColumnNeedsCast(column, values);
15982
+ })) {
15943
15983
  return input;
15984
+ }
15944
15985
  let changed = false;
15945
15986
  const changes = { ...input.changes };
15946
15987
  for (const [name, values] of Object.entries(input.changes)) {
15947
15988
  const column = table.columns.find((candidate) => candidate.name === name);
15948
- if (column === undefined || (column.sqlDomain === undefined && column.type !== "datetime"))
15989
+ if (column === undefined ||
15990
+ (column.sqlDomain === undefined &&
15991
+ column.type !== "datetime" &&
15992
+ !numberColumnNeedsCast(column, values))) {
15949
15993
  continue;
15994
+ }
15950
15995
  changed = true;
15951
15996
  changes[name] = values.map((value) => normalizeColumnLogicalValue(column, value));
15952
15997
  }
@@ -18908,12 +18953,6 @@ function compactTableSkipped(tableName, skipReason, sourceSegments, sourceBlockI
18908
18953
  metrics: null,
18909
18954
  };
18910
18955
  }
18911
- function estimateRowBytes(row) {
18912
- return estimateValuesBytes(Object.values(row));
18913
- }
18914
- function estimateBatchBytes(input) {
18915
- return Object.values(input.columns).reduce((total, values) => total + estimateValuesBytes(values), 0);
18916
- }
18917
18956
  function writeColumnValues(type, values) {
18918
18957
  const cached = type === "string" ? validatedStringByteLengths.get(values) : undefined;
18919
18958
  return {
@@ -18941,21 +18980,6 @@ function maximumWriteBlockStoredBytes(column, start, end, compression) {
18941
18980
  : physicalBytes;
18942
18981
  return Math.min(MAX_STORED_BLOCK_BYTE_LENGTH, BLOCK_HEADER_LENGTH + MAX_BLOCK_METADATA_BYTE_LENGTH + storedPayloadBytes);
18943
18982
  }
18944
- function estimateValuesBytes(values) {
18945
- let bytes = 0;
18946
- for (const value of values) {
18947
- // One byte per UTF-16 code unit approximates the UTF-8 payload (exact for ASCII) without
18948
- // encoding the string just to measure it — this estimate feeds metrics and flush
18949
- // thresholds, not the physical format.
18950
- if (typeof value === "string")
18951
- bytes += 4 + value.length;
18952
- else if (typeof value === "number" || value instanceof Date)
18953
- bytes += 8;
18954
- else
18955
- bytes += 1;
18956
- }
18957
- return bytes;
18958
- }
18959
18983
  function cacheableQueryInput(sql, params) {
18960
18984
  let characters = sql.length;
18961
18985
  if (characters > MAX_CACHEABLE_TEXT_CHARACTERS)
@@ -27,7 +27,6 @@ export declare function tokenize(text: string): string[];
27
27
  * query matches nothing.
28
28
  */
29
29
  export declare function tokenizeQuery(query: string): FtsQueryTerm[];
30
- export declare function termMatches(token: string, term: FtsQueryTerm): boolean;
31
30
  /** Tokenized query terms with the same bounded caching scheme as the LIKE pattern cache. */
32
31
  export declare function cachedQueryTerms(query: string): FtsQueryTerm[];
33
32
  /** Compile-time validation shared by the SQL parser and the DSL expression builder. */
@@ -110,7 +110,7 @@ export function tokenizeQuery(query) {
110
110
  }
111
111
  return terms;
112
112
  }
113
- export function termMatches(token, term) {
113
+ function termMatches(token, term) {
114
114
  return term.prefix ? token.startsWith(term.term) : token === term.term;
115
115
  }
116
116
  /**
@@ -29,3 +29,5 @@ export declare function hashScratch(length: number): number;
29
29
  export declare function equalsScratch(arena: Uint8Array, offset: number, length: number): boolean;
30
30
  /** Copies the scratch arena's first `length` bytes into owned storage. Exported for the join index. */
31
31
  export declare function copyScratchKey(length: number): Uint8Array;
32
+ export declare function safeDouble(value: number, label: string): number;
33
+ export declare function safeProduct(left: number, right: number, label: string): number;
@@ -49,7 +49,7 @@ export class ByteGroupIndex {
49
49
  return this.get([]);
50
50
  }
51
51
  getOne(key) {
52
- const length = encodeSingleGroupKey(key);
52
+ const length = encodeSingleScalarKey(key);
53
53
  const index = this.#find(length, hashScratch(length));
54
54
  return index < 0 ? undefined : this.#values[index];
55
55
  }
@@ -68,7 +68,7 @@ export class ByteGroupIndex {
68
68
  return this.#getOrInsertScratch(length, create);
69
69
  }
70
70
  getOrInsertOne(key, create) {
71
- const length = encodeSingleGroupKey(key);
71
+ const length = encodeSingleScalarKey(key);
72
72
  return this.#getOrInsertScratch(length, create);
73
73
  }
74
74
  /**
@@ -231,10 +231,6 @@ export function encodeSingleScalarKey(key) {
231
231
  reclaimScratch();
232
232
  return writeGroupKey(key, 0);
233
233
  }
234
- function encodeSingleGroupKey(key) {
235
- reclaimScratch();
236
- return writeGroupKey(key, 0);
237
- }
238
234
  function writeGroupKey(key, offset) {
239
235
  if (key === null) {
240
236
  ensureScratchCapacity(offset + 1);
@@ -341,13 +337,13 @@ export function equalsScratch(arena, offset, length) {
341
337
  export function copyScratchKey(length) {
342
338
  return scratch.slice(0, length);
343
339
  }
344
- function safeDouble(value, label) {
340
+ export function safeDouble(value, label) {
345
341
  const doubled = value * 2;
346
342
  if (!Number.isSafeInteger(doubled))
347
343
  throw new RangeError(`${label} exceeds the safe integer range`);
348
344
  return doubled;
349
345
  }
350
- function safeProduct(left, right, label) {
346
+ export function safeProduct(left, right, label) {
351
347
  const product = left * right;
352
348
  if (!Number.isSafeInteger(product))
353
349
  throw new RangeError(`${label} exceeds the safe integer range`);
@@ -1,5 +1,4 @@
1
1
  import { QueryMemoryContext } from "./memory.js";
2
- export type JoinIndexKey = boolean | number | string | Date;
3
2
  /** Collision-checked scalar-key hash index with typed duplicate row chains. */
4
3
  export declare class ByteJoinIndex {
5
4
  #private;
@@ -1,5 +1,5 @@
1
1
  import { dateMilliseconds } from "../date-value.js";
2
- import { copyScratchKey, encodeSingleScalarKey, equalsScratch, hashScratch, } from "./group-index.js";
2
+ import { copyScratchKey, encodeSingleScalarKey, equalsScratch, hashScratch, safeDouble, safeProduct, } from "./group-index.js";
3
3
  const INITIAL_ENTRY_CAPACITY = 4;
4
4
  const INITIAL_BUCKET_CAPACITY = 8;
5
5
  const INITIAL_KEY_CAPACITY = 32;
@@ -201,17 +201,3 @@ function encodeJoinKey(value) {
201
201
  }
202
202
  throw new TypeError("Join keys must be SQL scalar values");
203
203
  }
204
- function safeDouble(value, label) {
205
- const doubled = value * 2;
206
- if (!Number.isSafeInteger(doubled)) {
207
- throw new RangeError(`${label} exceeds the safe integer range`);
208
- }
209
- return doubled;
210
- }
211
- function safeProduct(left, right, label) {
212
- const product = left * right;
213
- if (!Number.isSafeInteger(product)) {
214
- throw new RangeError(`${label} exceeds the safe integer range`);
215
- }
216
- return product;
217
- }
@@ -1,39 +1,5 @@
1
1
  import { dateMilliseconds } from "../date-value.js";
2
- function sameValue(left, right) {
3
- if (Object.is(left, right))
4
- return true;
5
- if (left instanceof Date || right instanceof Date) {
6
- return (left instanceof Date &&
7
- right instanceof Date &&
8
- Object.is(dateMilliseconds(left), dateMilliseconds(right)));
9
- }
10
- if (Array.isArray(left) || Array.isArray(right)) {
11
- if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length)
12
- return false;
13
- for (let index = 0; index < left.length; index += 1) {
14
- if (!sameValue(left[index], right[index]))
15
- return false;
16
- }
17
- return true;
18
- }
19
- if (typeof left !== "object" || left === null || typeof right !== "object" || right === null) {
20
- return false;
21
- }
22
- const leftRecord = left;
23
- const rightRecord = right;
24
- const leftKeys = Object.keys(leftRecord);
25
- const rightKeys = Object.keys(rightRecord);
26
- if (leftKeys.length !== rightKeys.length)
27
- return false;
28
- for (let index = 0; index < leftKeys.length; index += 1) {
29
- const key = leftKeys[index];
30
- if (key === undefined || key !== rightKeys[index])
31
- return false;
32
- if (!sameValue(leftRecord[key], rightRecord[key]))
33
- return false;
34
- }
35
- return true;
36
- }
2
+ import { sameLiveValue } from "./live-equal.js";
37
3
  function keyToken(value, name) {
38
4
  if (typeof value === "string")
39
5
  return `s:${String(value.length)}:${value}`;
@@ -84,7 +50,7 @@ function diffRows(previousRows, rows, key) {
84
50
  changes.push({ type: "insert", row: next.row, index: next.index });
85
51
  continue;
86
52
  }
87
- if (!sameValue(old.row, next.row)) {
53
+ if (!sameLiveValue(old.row, next.row)) {
88
54
  changes.push({ type: "update", row: next.row, previous: old.row, index: next.index });
89
55
  }
90
56
  if (old.index !== next.index) {
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Structural equality over live result values: dates by instant, arrays by element, objects by
3
+ * own keys in insertion order. Shared by the typed live store's exact suppression and the keyed
4
+ * live window's diffing; internal on purpose — live-api re-exports its modules wholesale, and
5
+ * this helper is not part of the live API.
6
+ */
7
+ export declare function sameLiveValue(left: unknown, right: unknown): boolean;
@@ -0,0 +1,42 @@
1
+ import { dateMilliseconds } from "../date-value.js";
2
+ /**
3
+ * Structural equality over live result values: dates by instant, arrays by element, objects by
4
+ * own keys in insertion order. Shared by the typed live store's exact suppression and the keyed
5
+ * live window's diffing; internal on purpose — live-api re-exports its modules wholesale, and
6
+ * this helper is not part of the live API.
7
+ */
8
+ export function sameLiveValue(left, right) {
9
+ if (Object.is(left, right))
10
+ return true;
11
+ if (left instanceof Date || right instanceof Date) {
12
+ return (left instanceof Date &&
13
+ right instanceof Date &&
14
+ Object.is(dateMilliseconds(left), dateMilliseconds(right)));
15
+ }
16
+ if (Array.isArray(left) || Array.isArray(right)) {
17
+ if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length)
18
+ return false;
19
+ for (let index = 0; index < left.length; index += 1) {
20
+ if (!sameLiveValue(left[index], right[index]))
21
+ return false;
22
+ }
23
+ return true;
24
+ }
25
+ if (typeof left !== "object" || left === null || typeof right !== "object" || right === null) {
26
+ return false;
27
+ }
28
+ const leftRecord = left;
29
+ const rightRecord = right;
30
+ const leftKeys = Object.keys(leftRecord);
31
+ const rightKeys = Object.keys(rightRecord);
32
+ if (leftKeys.length !== rightKeys.length)
33
+ return false;
34
+ for (let index = 0; index < leftKeys.length; index += 1) {
35
+ const key = leftKeys[index];
36
+ if (key === undefined || key !== rightKeys[index])
37
+ return false;
38
+ if (!sameLiveValue(leftRecord[key], rightRecord[key]))
39
+ return false;
40
+ }
41
+ return true;
42
+ }
@@ -1,6 +1,7 @@
1
1
  import { dateIsoString, dateMilliseconds } from "../date-value.js";
2
+ import { crossJoinPlan } from "../plan/model.js";
2
3
  import { blockHasSubqueries, childExpressions, DUAL_TABLE, expressionAliases, forEachBlockExpression, forEachNestedBlock, hasAggregate, isAggregateCall, isScalarFunctionName, mapBlockExpressions, mapChildExpressions, parseQuantified, scalarFunctionNames, scalarFunctionValue, splitCondition, volatileScalarFunctionNames, } from "./query.js";
3
- import { isSqlDomainValue } from "./sql-domains.js";
4
+ import { concatenatedSqlValue, isSqlDomainValue } from "./sql-domains.js";
4
5
  /**
5
6
  * Deterministic plan-to-plan rewrites over the shared compiled representation. Every rule
6
7
  * preserves result semantics exactly; rules that cannot prove safety leave the plan unchanged.
@@ -1776,20 +1777,7 @@ function decorrelateExistsWithProbes(block, exists, scope, nextAlias) {
1776
1777
  reference: `${probesAlias}.${correlationProbeAlias(index)}`,
1777
1778
  }));
1778
1779
  replacePlanReferences(inner, new Map(references.map((reference, index) => [reference, probeReferences[index] ?? null])));
1779
- inner.joins.push({
1780
- table: probesAlias,
1781
- alias: probesAlias,
1782
- derived: probes,
1783
- kind: "inner",
1784
- left: { kind: "literal", value: null },
1785
- right: { kind: "literal", value: null },
1786
- on: {
1787
- kind: "condition",
1788
- operator: "=",
1789
- left: { kind: "literal", value: 1 },
1790
- right: { kind: "literal", value: 1 },
1791
- },
1792
- });
1780
+ inner.joins.push(crossJoinPlan({ table: probesAlias, alias: probesAlias, derived: probes }));
1793
1781
  const flagsAlias = nextAlias();
1794
1782
  const flags = {
1795
1783
  sql: "(probe-lifted correlated exists flags)",
@@ -2244,7 +2232,14 @@ function foldBinary(operator, leftValue, rightValue) {
2244
2232
  return null;
2245
2233
  if (operator === "||") {
2246
2234
  if (typeof leftValue === "string" && typeof rightValue === "string") {
2247
- return leftValue + rightValue;
2235
+ // Same helper the executors use, so folding cannot concatenate internal domain
2236
+ // encodings. A refused shape stays unfolded and raises the same error at execution.
2237
+ try {
2238
+ return concatenatedSqlValue(leftValue, rightValue);
2239
+ }
2240
+ catch {
2241
+ return undefined;
2242
+ }
2248
2243
  }
2249
2244
  return undefined;
2250
2245
  }
@@ -1,5 +1,5 @@
1
1
  import type { CompiledQuery, QueryValue } from "../plan/model.js";
2
- export type PointReadValue = boolean | number | string | Date;
2
+ type PointReadValue = boolean | number | string | Date;
3
3
  export interface PointReadEquality {
4
4
  column: string;
5
5
  value: PointReadValue;
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Shape analysis for the keyed point-read fast path: a single-table conjunction of
3
- * column-equals-literal predicates that covers the table's unique key, projecting plain
4
- * columns. Such a statement addresses at most one row, so execution can skip parameter
3
+ * column-equals-literal predicates that covers the table's unique key, projecting bare
4
+ * columns plain or logical-domain typed, since domain scalars externalize through the
5
+ * same result boundary. Such a statement addresses at most one row, so execution can skip parameter
5
6
  * binding, plan cloning, streamed-view construction, and the vector pipeline entirely and
6
7
  * answer from cached decoded blocks. Anything this module cannot prove eligible falls back to
7
8
  * the ordinary executor, which stays the authority on errors and semantics.
@@ -1,4 +1,5 @@
1
1
  import { copyDate, dateMilliseconds } from "../date-value.js";
2
+ import { estimateValuesBytes } from "./byte-estimates.js";
2
3
  import { copyQueryResultExternalization } from "./query.js";
3
4
  import { defineSqlResultProperty } from "./sql-semantics.js";
4
5
  /** Modest per-entry cap so one giant result cannot thrash the shared artifact cache. */
@@ -60,18 +61,6 @@ export function queryResultRetainedBytes(result) {
60
61
  bytes += estimateValuesBytes(Object.values(row));
61
62
  return bytes;
62
63
  }
63
- function estimateValuesBytes(values) {
64
- let bytes = 0;
65
- for (const value of values) {
66
- if (typeof value === "string")
67
- bytes += 4 + value.length;
68
- else if (typeof value === "number" || value instanceof Date)
69
- bytes += 8;
70
- else
71
- bytes += 1;
72
- }
73
- return bytes;
74
- }
75
64
  function encodeParameter(value) {
76
65
  if (value === null)
77
66
  return [0];