@minnowdb/core 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (270) hide show
  1. package/README.md +23 -32
  2. package/dist/block-format/block.d.ts +32 -13
  3. package/dist/block-format/block.js +179 -55
  4. package/dist/block-format/checksum.d.ts +2 -1
  5. package/dist/block-format/checksum.js +5 -2
  6. package/dist/block-format/codecs.d.ts +7 -2
  7. package/dist/block-format/codecs.js +53 -12
  8. package/dist/block-format/column.d.ts +3 -2
  9. package/dist/block-format/column.js +96 -30
  10. package/dist/block-format/index.d.ts +2 -2
  11. package/dist/block-format/index.js +2 -2
  12. package/dist/block-format/physical.d.ts +8 -2
  13. package/dist/block-format/physical.js +14 -6
  14. package/dist/block-format/types.d.ts +7 -3
  15. package/dist/block-format/types.js +0 -1
  16. package/dist/block-format/unicode.d.ts +11 -0
  17. package/dist/block-format/unicode.js +46 -0
  18. package/dist/date-value.d.ts +15 -0
  19. package/dist/date-value.js +64 -0
  20. package/dist/engine/artifact-cache.d.ts +2 -1
  21. package/dist/engine/artifact-cache.js +5 -1
  22. package/dist/engine/batch.d.ts +11 -3
  23. package/dist/engine/batch.js +17 -7
  24. package/dist/engine/buffered-writer.d.ts +2 -1
  25. package/dist/engine/buffered-writer.js +34 -9
  26. package/dist/engine/cache-limits.d.ts +25 -0
  27. package/dist/engine/cache-limits.js +25 -0
  28. package/dist/engine/catalog.d.ts +24 -5
  29. package/dist/engine/catalog.js +34 -7
  30. package/dist/engine/client.d.ts +29 -7
  31. package/dist/engine/client.js +216 -40
  32. package/dist/engine/database.d.ts +224 -39
  33. package/dist/engine/database.js +7475 -1539
  34. package/dist/engine/defaults.d.ts +4 -8
  35. package/dist/engine/defaults.js +46 -22
  36. package/dist/engine/errors.d.ts +19 -1
  37. package/dist/engine/errors.js +32 -2
  38. package/dist/engine/fts.d.ts +0 -6
  39. package/dist/engine/fts.js +41 -14
  40. package/dist/engine/group-index.d.ts +0 -1
  41. package/dist/engine/group-index.js +6 -11
  42. package/dist/engine/index.d.ts +16 -10
  43. package/dist/engine/index.js +12 -9
  44. package/dist/engine/join-index.d.ts +0 -1
  45. package/dist/engine/join-index.js +2 -2
  46. package/dist/engine/keyed-live.d.ts +57 -0
  47. package/dist/engine/keyed-live.js +226 -0
  48. package/dist/engine/live-api.d.ts +4 -0
  49. package/dist/engine/live-api.js +4 -0
  50. package/dist/engine/live.d.ts +43 -26
  51. package/dist/engine/live.js +504 -126
  52. package/dist/engine/memory.d.ts +2 -1
  53. package/dist/engine/memory.js +3 -2
  54. package/dist/engine/optimizer.d.ts +0 -1
  55. package/dist/engine/optimizer.js +262 -27
  56. package/dist/engine/query-api.d.ts +3 -0
  57. package/dist/engine/query-api.js +3 -0
  58. package/dist/engine/query-cache.d.ts +1 -2
  59. package/dist/engine/query-cache.js +7 -5
  60. package/dist/engine/query.d.ts +134 -29
  61. package/dist/engine/query.js +1668 -372
  62. package/dist/engine/result-wire.d.ts +0 -1
  63. package/dist/engine/result-wire.js +2 -2
  64. package/dist/engine/schema-wire.d.ts +6 -3
  65. package/dist/engine/schema-wire.js +40 -34
  66. package/dist/engine/schema.d.ts +156 -80
  67. package/dist/engine/schema.js +570 -108
  68. package/dist/engine/sort-keys.d.ts +4 -4
  69. package/dist/engine/sort-keys.js +44 -29
  70. package/dist/engine/sql-domains.d.ts +31 -0
  71. package/dist/engine/sql-domains.js +585 -0
  72. package/dist/engine/sql-driver.d.ts +19 -0
  73. package/dist/engine/sql-driver.js +1 -0
  74. package/dist/engine/sql-json.d.ts +7 -9
  75. package/dist/engine/sql-json.js +75 -15
  76. package/dist/engine/sql-semantics.d.ts +8 -3
  77. package/dist/engine/sql-semantics.js +458 -30
  78. package/dist/engine/typed-live.d.ts +67 -0
  79. package/dist/engine/typed-live.js +349 -0
  80. package/dist/engine/vector.d.ts +12 -2
  81. package/dist/engine/vector.js +635 -119
  82. package/dist/engine/worker-host.d.ts +9 -2
  83. package/dist/engine/worker-host.js +620 -152
  84. package/dist/engine/worker.d.ts +0 -1
  85. package/dist/engine/worker.js +3 -3
  86. package/dist/engine/write-block-planner.d.ts +18 -0
  87. package/dist/engine/write-block-planner.js +112 -0
  88. package/dist/index.d.ts +0 -1
  89. package/dist/index.js +0 -1
  90. package/dist/plan/index.d.ts +3 -4
  91. package/dist/plan/index.js +3 -4
  92. package/dist/storage/index.d.ts +1 -1
  93. package/dist/storage/index.js +1 -1
  94. package/dist/storage/indexeddb.d.ts +99 -51
  95. package/dist/storage/indexeddb.js +13448 -2660
  96. package/dist/storage/memory.d.ts +64 -57
  97. package/dist/storage/memory.js +1042 -128
  98. package/dist/storage/opfs/files.d.ts +18 -7
  99. package/dist/storage/opfs/files.js +115 -28
  100. package/dist/storage/opfs/index.d.ts +1 -1
  101. package/dist/storage/opfs/index.js +1 -1
  102. package/dist/storage/opfs/leader.d.ts +409 -75
  103. package/dist/storage/opfs/leader.js +4621 -650
  104. package/dist/storage/opfs/rpc.d.ts +15 -3
  105. package/dist/storage/opfs/rpc.js +224 -2
  106. package/dist/storage/opfs/snapshot-ledger.d.ts +40 -0
  107. package/dist/storage/opfs/snapshot-ledger.js +281 -0
  108. package/dist/storage/opfs/store.d.ts +33 -99
  109. package/dist/storage/opfs/store.js +453 -364
  110. package/dist/storage/persistence.d.ts +37 -0
  111. package/dist/storage/persistence.js +78 -0
  112. package/dist/storage/snapshot-stream.d.ts +24 -0
  113. package/dist/storage/snapshot-stream.js +897 -0
  114. package/dist/storage/snapshot.d.ts +9 -85
  115. package/dist/storage/snapshot.js +33 -265
  116. package/dist/storage/toolkit/extents.d.ts +41 -7
  117. package/dist/storage/toolkit/extents.js +402 -39
  118. package/dist/storage/toolkit/index.d.ts +4 -5
  119. package/dist/storage/toolkit/index.js +28 -4
  120. package/dist/storage/toolkit/record-core.d.ts +182 -55
  121. package/dist/storage/toolkit/record-core.js +6158 -1331
  122. package/dist/storage/toolkit/sync-file.d.ts +10 -1
  123. package/dist/storage/toolkit/sync-file.js +50 -2
  124. package/dist/storage/toolkit/wal.d.ts +23 -5
  125. package/dist/storage/toolkit/wal.js +89 -27
  126. package/dist/storage/toolkit/wire.d.ts +15 -2
  127. package/dist/storage/toolkit/wire.js +199 -10
  128. package/dist/storage/types.d.ts +1439 -204
  129. package/dist/storage/types.js +1745 -141
  130. package/dist/testing/block-store-conformance.d.ts +0 -1
  131. package/dist/testing/block-store-conformance.js +1009 -119
  132. package/dist/testing/index.d.ts +46 -26
  133. package/dist/testing/index.js +112 -57
  134. package/dist/testing/opfs-shim.d.ts +13 -3
  135. package/dist/testing/opfs-shim.js +40 -6
  136. package/dist/testing/simulator.d.ts +97 -0
  137. package/dist/testing/simulator.js +591 -0
  138. package/dist/testing/sqllogictest.d.ts +89 -0
  139. package/dist/testing/sqllogictest.js +434 -0
  140. package/dist/transactions/index.d.ts +92 -16
  141. package/dist/transactions/index.js +1074 -236
  142. package/dist/worker-protocol/index.d.ts +3 -2
  143. package/dist/worker-protocol/index.js +2 -5
  144. package/package.json +53 -3
  145. package/postgres-feature-profile.json +223 -0
  146. package/sql-feature-matrix.json +172 -252
  147. package/dist/block-format/block.d.ts.map +0 -1
  148. package/dist/block-format/block.js.map +0 -1
  149. package/dist/block-format/checksum.d.ts.map +0 -1
  150. package/dist/block-format/checksum.js.map +0 -1
  151. package/dist/block-format/codecs.d.ts.map +0 -1
  152. package/dist/block-format/codecs.js.map +0 -1
  153. package/dist/block-format/column.d.ts.map +0 -1
  154. package/dist/block-format/column.js.map +0 -1
  155. package/dist/block-format/index.d.ts.map +0 -1
  156. package/dist/block-format/index.js.map +0 -1
  157. package/dist/block-format/physical.d.ts.map +0 -1
  158. package/dist/block-format/physical.js.map +0 -1
  159. package/dist/block-format/types.d.ts.map +0 -1
  160. package/dist/block-format/types.js.map +0 -1
  161. package/dist/engine/artifact-cache.d.ts.map +0 -1
  162. package/dist/engine/artifact-cache.js.map +0 -1
  163. package/dist/engine/batch.d.ts.map +0 -1
  164. package/dist/engine/batch.js.map +0 -1
  165. package/dist/engine/buffered-writer.d.ts.map +0 -1
  166. package/dist/engine/buffered-writer.js.map +0 -1
  167. package/dist/engine/catalog.d.ts.map +0 -1
  168. package/dist/engine/catalog.js.map +0 -1
  169. package/dist/engine/client.d.ts.map +0 -1
  170. package/dist/engine/client.js.map +0 -1
  171. package/dist/engine/coordinator.d.ts +0 -17
  172. package/dist/engine/coordinator.d.ts.map +0 -1
  173. package/dist/engine/coordinator.js +0 -60
  174. package/dist/engine/coordinator.js.map +0 -1
  175. package/dist/engine/database.d.ts.map +0 -1
  176. package/dist/engine/database.js.map +0 -1
  177. package/dist/engine/defaults.d.ts.map +0 -1
  178. package/dist/engine/defaults.js.map +0 -1
  179. package/dist/engine/errors.d.ts.map +0 -1
  180. package/dist/engine/errors.js.map +0 -1
  181. package/dist/engine/fts.d.ts.map +0 -1
  182. package/dist/engine/fts.js.map +0 -1
  183. package/dist/engine/group-index.d.ts.map +0 -1
  184. package/dist/engine/group-index.js.map +0 -1
  185. package/dist/engine/index.d.ts.map +0 -1
  186. package/dist/engine/index.js.map +0 -1
  187. package/dist/engine/join-index.d.ts.map +0 -1
  188. package/dist/engine/join-index.js.map +0 -1
  189. package/dist/engine/live.d.ts.map +0 -1
  190. package/dist/engine/live.js.map +0 -1
  191. package/dist/engine/memory.d.ts.map +0 -1
  192. package/dist/engine/memory.js.map +0 -1
  193. package/dist/engine/optimizer.d.ts.map +0 -1
  194. package/dist/engine/optimizer.js.map +0 -1
  195. package/dist/engine/query-cache.d.ts.map +0 -1
  196. package/dist/engine/query-cache.js.map +0 -1
  197. package/dist/engine/query.d.ts.map +0 -1
  198. package/dist/engine/query.js.map +0 -1
  199. package/dist/engine/result-wire.d.ts.map +0 -1
  200. package/dist/engine/result-wire.js.map +0 -1
  201. package/dist/engine/schema-wire.d.ts.map +0 -1
  202. package/dist/engine/schema-wire.js.map +0 -1
  203. package/dist/engine/schema.d.ts.map +0 -1
  204. package/dist/engine/schema.js.map +0 -1
  205. package/dist/engine/sort-keys.d.ts.map +0 -1
  206. package/dist/engine/sort-keys.js.map +0 -1
  207. package/dist/engine/sql-json.d.ts.map +0 -1
  208. package/dist/engine/sql-json.js.map +0 -1
  209. package/dist/engine/sql-semantics.d.ts.map +0 -1
  210. package/dist/engine/sql-semantics.js.map +0 -1
  211. package/dist/engine/vector.d.ts.map +0 -1
  212. package/dist/engine/vector.js.map +0 -1
  213. package/dist/engine/worker-host.d.ts.map +0 -1
  214. package/dist/engine/worker-host.js.map +0 -1
  215. package/dist/engine/worker.d.ts.map +0 -1
  216. package/dist/engine/worker.js.map +0 -1
  217. package/dist/index.d.ts.map +0 -1
  218. package/dist/index.js.map +0 -1
  219. package/dist/plan/index.d.ts.map +0 -1
  220. package/dist/plan/index.js.map +0 -1
  221. package/dist/storage/fixture-shape.d.ts +0 -42
  222. package/dist/storage/fixture-shape.d.ts.map +0 -1
  223. package/dist/storage/fixture-shape.js +0 -146
  224. package/dist/storage/fixture-shape.js.map +0 -1
  225. package/dist/storage/index.d.ts.map +0 -1
  226. package/dist/storage/index.js.map +0 -1
  227. package/dist/storage/indexeddb.d.ts.map +0 -1
  228. package/dist/storage/indexeddb.js.map +0 -1
  229. package/dist/storage/memory.d.ts.map +0 -1
  230. package/dist/storage/memory.js.map +0 -1
  231. package/dist/storage/opfs/files.d.ts.map +0 -1
  232. package/dist/storage/opfs/files.js.map +0 -1
  233. package/dist/storage/opfs/index.d.ts.map +0 -1
  234. package/dist/storage/opfs/index.js.map +0 -1
  235. package/dist/storage/opfs/leader.d.ts.map +0 -1
  236. package/dist/storage/opfs/leader.js.map +0 -1
  237. package/dist/storage/opfs/rpc.d.ts.map +0 -1
  238. package/dist/storage/opfs/rpc.js.map +0 -1
  239. package/dist/storage/opfs/store.d.ts.map +0 -1
  240. package/dist/storage/opfs/store.js.map +0 -1
  241. package/dist/storage/snapshot.d.ts.map +0 -1
  242. package/dist/storage/snapshot.js.map +0 -1
  243. package/dist/storage/toolkit/extents.d.ts.map +0 -1
  244. package/dist/storage/toolkit/extents.js.map +0 -1
  245. package/dist/storage/toolkit/index.d.ts.map +0 -1
  246. package/dist/storage/toolkit/index.js.map +0 -1
  247. package/dist/storage/toolkit/record-core.d.ts.map +0 -1
  248. package/dist/storage/toolkit/record-core.js.map +0 -1
  249. package/dist/storage/toolkit/sync-file.d.ts.map +0 -1
  250. package/dist/storage/toolkit/sync-file.js.map +0 -1
  251. package/dist/storage/toolkit/wal.d.ts.map +0 -1
  252. package/dist/storage/toolkit/wal.js.map +0 -1
  253. package/dist/storage/toolkit/wire.d.ts.map +0 -1
  254. package/dist/storage/toolkit/wire.js.map +0 -1
  255. package/dist/storage/types.d.ts.map +0 -1
  256. package/dist/storage/types.js.map +0 -1
  257. package/dist/testing/block-store-conformance.d.ts.map +0 -1
  258. package/dist/testing/block-store-conformance.js.map +0 -1
  259. package/dist/testing/index.d.ts.map +0 -1
  260. package/dist/testing/index.js.map +0 -1
  261. package/dist/testing/opfs-shim.d.ts.map +0 -1
  262. package/dist/testing/opfs-shim.js.map +0 -1
  263. package/dist/testing/seeds.d.ts +0 -11
  264. package/dist/testing/seeds.d.ts.map +0 -1
  265. package/dist/testing/seeds.js +0 -50
  266. package/dist/testing/seeds.js.map +0 -1
  267. package/dist/transactions/index.d.ts.map +0 -1
  268. package/dist/transactions/index.js.map +0 -1
  269. package/dist/worker-protocol/index.d.ts.map +0 -1
  270. package/dist/worker-protocol/index.js.map +0 -1
@@ -1,9 +1,13 @@
1
- import { cachedListMembership, distinctFromComparison, explicitNullOrder, isScalarFunctionName, likeMatches, orderOutputName, parseQuantified, quantifiedComparison, scalarFunctionValue, } from "./query.js";
1
+ import { dateMilliseconds } from "../date-value.js";
2
+ import { MAX_TEMP_RUN_BATCH_BYTES, MAX_TEMP_RUN_PAGE_BYTES, MAX_TEMP_RUN_PAGES_PER_BATCH, } from "../storage/types.js";
3
+ import { cachedListMembership, childExpressions, distinctFromComparison, nullOrder, isScalarFunctionName, likeMatches, orderOutputName, parseQuantified, quantifiedComparison, scalarFunctionValue, } from "./query.js";
4
+ import { jsonValueOf } from "./sql-json.js";
2
5
  import { bm25DocumentScore, cachedQueryTerms, FtsStatsAccumulator, fullTermsMask, renderDocumentValue, termFrequencies, termsMask, tokenize, } from "./fts.js";
3
6
  import { ByteGroupIndex } from "./group-index.js";
4
7
  import { ByteJoinIndex } from "./join-index.js";
5
8
  import { QueryMemoryBudgetError, QueryMemoryContext, } from "./memory.js";
6
- import { compareSqlStrings, defineSqlResultProperty } from "./sql-semantics.js";
9
+ import { compareSqlStrings, compileSimilarPattern, defineSqlResultProperty, } from "./sql-semantics.js";
10
+ import { exactNumericBinary, exactNumericCompare, collatedDomainCompare, enumDomainCompare, externalSqlDomainValue, isExactNumeric, protectedSqlTextValue, } from "./sql-domains.js";
7
11
  import { buildSortKeyColumn, sortKeyIndexes } from "./sort-keys.js";
8
12
  const DEFAULT_BATCH_ROWS = 2_048;
9
13
  /** Above this, locating each IN member separately costs more than scanning between them. */
@@ -25,7 +29,17 @@ const SPILL_PAGE_MAGIC = 0x5350494c;
25
29
  const SPILL_PAGE_HEADER_BYTES = 8;
26
30
  const vectorTextEncoder = new TextEncoder();
27
31
  /** Pages per batched spill write: bounds the transient encoded copies a batch holds. */
28
- const SPILL_WRITE_BATCH_PAGES = 8;
32
+ const SPILL_WRITE_BATCH_PAGES = Math.min(8, MAX_TEMP_RUN_PAGES_PER_BATCH);
33
+ function boundedSpillPageRows(value) {
34
+ const rows = value ?? DEFAULT_BATCH_ROWS;
35
+ if (!Number.isSafeInteger(rows) || rows <= 0) {
36
+ throw new RangeError("Query spill page rows must be a positive whole number");
37
+ }
38
+ // Page rows are a tuning target, not a request to buffer an unbounded number of rows. Larger
39
+ // pages do not improve the block-store round trip because byte-bounded batches provide that
40
+ // amortization independently.
41
+ return Math.min(rows, DEFAULT_BATCH_ROWS);
42
+ }
29
43
  /** Writes already-materialized pages through the batch method when the store offers one. */
30
44
  async function writeSpillPages(store, pages) {
31
45
  const batched = store.putPages?.bind(store);
@@ -35,9 +49,43 @@ async function writeSpillPages(store, pages) {
35
49
  }
36
50
  return;
37
51
  }
38
- for (let start = 0; start < pages.length; start += SPILL_WRITE_BATCH_PAGES) {
39
- await batched(pages.slice(start, start + SPILL_WRITE_BATCH_PAGES));
52
+ let batch = [];
53
+ let batchBytes = 0;
54
+ for (const page of pages) {
55
+ if (page.bytes.byteLength > MAX_TEMP_RUN_PAGE_BYTES) {
56
+ throw new RangeError("Query spill page exceeds the storage limit");
57
+ }
58
+ if (batch.length > 0 &&
59
+ (batch.length === SPILL_WRITE_BATCH_PAGES ||
60
+ batchBytes + page.bytes.byteLength > MAX_TEMP_RUN_BATCH_BYTES)) {
61
+ await batched(batch);
62
+ batch = [];
63
+ batchBytes = 0;
64
+ }
65
+ batch.push(page);
66
+ batchBytes += page.bytes.byteLength;
40
67
  }
68
+ if (batch.length > 0)
69
+ await batched(batch);
70
+ }
71
+ async function writeSpillRowPages(store, ownerId, runId, startPageIndex, columns, rows, pageRows) {
72
+ let pending = [];
73
+ let pendingBytes = 0;
74
+ let pageCount = 0;
75
+ for (const bytes of encodeSpillRowPages(columns, rows, pageRows)) {
76
+ if (pending.length > 0 &&
77
+ (pending.length === SPILL_WRITE_BATCH_PAGES ||
78
+ pendingBytes + bytes.byteLength > MAX_TEMP_RUN_BATCH_BYTES)) {
79
+ await writeSpillPages(store, pending);
80
+ pending = [];
81
+ pendingBytes = 0;
82
+ }
83
+ pending.push({ ownerId, runId, pageIndex: startPageIndex + pageCount, bytes });
84
+ pendingBytes += bytes.byteLength;
85
+ pageCount += 1;
86
+ }
87
+ await writeSpillPages(store, pending);
88
+ return pageCount;
41
89
  }
42
90
  export function createColumnarTable(name, columns, uniqueKey) {
43
91
  const first = columns.values().next().value;
@@ -59,7 +107,7 @@ export function createColumnarTable(name, columns, uniqueKey) {
59
107
  ...(uniqueKey === undefined ? {} : { uniqueKey }),
60
108
  };
61
109
  }
62
- export function columnarTableFromRows(name, rows, projectedColumnNames) {
110
+ export function columnarTableFromRows(name, rows, projectedColumnNames, protectText = true) {
63
111
  const columnNameSet = new Set(projectedColumnNames);
64
112
  if (projectedColumnNames === undefined) {
65
113
  for (const row of rows) {
@@ -72,7 +120,10 @@ export function columnarTableFromRows(name, rows, projectedColumnNames) {
72
120
  return { name, rowCount: rows.length, columns: new Map() };
73
121
  const columns = new Map();
74
122
  for (const columnName of columnNames) {
75
- const values = rows.map((row) => row[columnName] ?? null);
123
+ const values = rows.map((row) => {
124
+ const value = row[columnName] ?? null;
125
+ return protectText && typeof value === "string" ? protectedSqlTextValue(value) : value;
126
+ });
76
127
  columns.set(columnName, { type: inferVectorType(values), values });
77
128
  }
78
129
  return createColumnarTable(name, columns);
@@ -105,7 +156,7 @@ export function prepareVectorQuery(plan, inputTables, options = {}) {
105
156
  async executeAsync(executionOptions = {}) {
106
157
  if (closed)
107
158
  throw new Error("Prepared vector query is closed");
108
- const canSpillSort = bound.orderBy.length > 0 && !bound.grouped;
159
+ const canSpillSort = bound.orderBy.length > 0 && !bound.grouped && !bound.sourceOrdered;
109
160
  // An unordered grouped plan spills too: the empty ordering makes the pairwise merge a
110
161
  // stable concatenation, and partition-wise accumulation bounds peak group state.
111
162
  const canSpillHash = bound.grouped && bound.groupBy.length > 0;
@@ -130,6 +181,23 @@ export function prepareVectorQuery(plan, inputTables, options = {}) {
130
181
  executionMemory.close();
131
182
  }
132
183
  },
184
+ async executeBatches(executionOptions, consume) {
185
+ if (closed)
186
+ throw new Error("Prepared vector query is closed");
187
+ if (bound.grouped || bound.orderBy.length > 0 || bound.joins.length > 0) {
188
+ throw new TypeError("Native query batching requires an ungrouped, unordered, single-source plan");
189
+ }
190
+ if (!Number.isSafeInteger(executionOptions.batchRows) || executionOptions.batchRows <= 0) {
191
+ throw new RangeError("Query batch rows must be a positive whole number");
192
+ }
193
+ const executionMemory = retainedMemory.createChild();
194
+ try {
195
+ return await executeBoundPlanBatches(bound, executionMemory, executionOptions, consume);
196
+ }
197
+ finally {
198
+ executionMemory.close();
199
+ }
200
+ },
133
201
  close() {
134
202
  if (closed)
135
203
  return;
@@ -193,7 +261,7 @@ function createVector(input) {
193
261
  continue;
194
262
  const numericValue = input.type === "datetime"
195
263
  ? value instanceof Date
196
- ? value.getTime()
264
+ ? dateMilliseconds(value)
197
265
  : Number.NaN
198
266
  : typeof value === "number"
199
267
  ? value
@@ -255,7 +323,7 @@ export function vectorValue(vector, rowIndex) {
255
323
  return code === NULL_STRING_CODE ? null : (vector.dictionary[code] ?? null);
256
324
  }
257
325
  function columnarTablePayloadBytes(table) {
258
- let total = 0;
326
+ let total = table.scanOrder?.byteLength ?? 0;
259
327
  for (const vector of table.columns.values()) {
260
328
  total = safeMemorySum(total, vector.validity.byteLength, "Column vector payload");
261
329
  if (vector.kind === "string") {
@@ -315,7 +383,178 @@ function disjunctiveNormalForm(predicate) {
315
383
  };
316
384
  return visit(predicate.left) && branches.length > 1 ? branches : undefined;
317
385
  }
386
+ /**
387
+ * Gives a pure comma/CROSS-join group a connected physical order once schemas can resolve bare
388
+ * column names. SQLLogicTest deliberately permutes FROM lists; preserving that written order can
389
+ * otherwise materialize several disconnected Cartesian products before their WHERE equalities
390
+ * become applicable. The streamed base stays fixed; explicit/outer joins and wildcard projection
391
+ * keep their complete written order.
392
+ *
393
+ * One equality that connects each new source becomes the inner hash-join key. The remaining
394
+ * conditions stay ordinary predicates, so null and residual-filter semantics do not change.
395
+ */
396
+ function orderCartesianJoins(plan, tables) {
397
+ if (plan.joins.length < 2 ||
398
+ plan.select.some((item) => item.expression.kind === "wildcard") ||
399
+ plan.joins.some((join) => !isCartesianJoin(join))) {
400
+ return plan;
401
+ }
402
+ const entries = [
403
+ { source: plan.base, originalIndex: 0 },
404
+ ...plan.joins.map((join, index) => ({
405
+ source: tableSourceOf(join),
406
+ originalIndex: index + 1,
407
+ })),
408
+ ];
409
+ const sourceTables = entries.map(({ source }) => tables.get(source.table));
410
+ if (sourceTables.some((table) => table === undefined))
411
+ return plan;
412
+ const predicateSources = plan.predicates.map((predicate) => ({
413
+ predicate,
414
+ sources: rawPredicateSources(predicate, entries, sourceTables),
415
+ }));
416
+ // The preparation layer may stream the written base table through a resident window. Keep
417
+ // that source fixed and order only the fully materialized join sides around it.
418
+ const remaining = new Set(entries.slice(1).map(({ originalIndex }) => originalIndex));
419
+ const ordered = [0];
420
+ while (remaining.size > 0) {
421
+ let best;
422
+ let bestScore = Number.NEGATIVE_INFINITY;
423
+ for (const candidate of remaining) {
424
+ const score = cartesianSourceScore(candidate, ordered, predicateSources);
425
+ if (score > bestScore || (score === bestScore && (best === undefined || candidate < best))) {
426
+ best = candidate;
427
+ bestScore = score;
428
+ }
429
+ }
430
+ if (best === undefined)
431
+ return plan;
432
+ ordered.push(best);
433
+ remaining.delete(best);
434
+ }
435
+ const predicates = [...plan.predicates];
436
+ const available = new Set([ordered[0] ?? 0]);
437
+ const joins = [];
438
+ for (const sourceIndex of ordered.slice(1)) {
439
+ const source = entries[sourceIndex]?.source;
440
+ if (source === undefined)
441
+ return plan;
442
+ const keyIndex = predicates.findIndex((predicate) => predicateJoinsSource(predicate, sourceIndex, available, entries, sourceTables));
443
+ const key = keyIndex < 0 ? undefined : predicates.splice(keyIndex, 1)[0];
444
+ joins.push(key === undefined ? cartesianJoin(source) : keyedInnerJoin(source, key));
445
+ available.add(sourceIndex);
446
+ }
447
+ return { ...plan, base: plan.base, joins, predicates };
448
+ }
449
+ function isCartesianJoin(join) {
450
+ const condition = join.on;
451
+ return (join.kind === "inner" &&
452
+ condition?.kind === "condition" &&
453
+ condition.operator === "=" &&
454
+ condition.left.kind === "literal" &&
455
+ condition.left.value === 1 &&
456
+ condition.right.kind === "literal" &&
457
+ condition.right.value === 1);
458
+ }
459
+ function tableSourceOf(join) {
460
+ const { kind, left, right, on, full, natural, ...source } = join;
461
+ void kind;
462
+ void left;
463
+ void right;
464
+ void on;
465
+ void full;
466
+ void natural;
467
+ return source;
468
+ }
469
+ function cartesianJoin(source) {
470
+ return {
471
+ ...source,
472
+ kind: "inner",
473
+ left: { kind: "literal", value: null },
474
+ right: { kind: "literal", value: null },
475
+ on: {
476
+ kind: "condition",
477
+ operator: "=",
478
+ left: { kind: "literal", value: 1 },
479
+ right: { kind: "literal", value: 1 },
480
+ },
481
+ };
482
+ }
483
+ function keyedInnerJoin(source, predicate) {
484
+ return {
485
+ ...source,
486
+ kind: "inner",
487
+ left: predicate.left,
488
+ right: predicate.right,
489
+ };
490
+ }
491
+ function cartesianSourceScore(candidate, ordered, predicates) {
492
+ const available = new Set(ordered);
493
+ let local = 0;
494
+ let connected = 0;
495
+ let equality = 0;
496
+ for (const entry of predicates) {
497
+ const sources = entry.sources;
498
+ if (sources?.has(candidate) !== true)
499
+ continue;
500
+ if (sources.size === 1) {
501
+ local++;
502
+ continue;
503
+ }
504
+ if ([...sources].every((source) => source === candidate || available.has(source))) {
505
+ connected++;
506
+ if (entry.predicate.operator === "=")
507
+ equality++;
508
+ }
509
+ }
510
+ // A usable equality dominates a general connection, which dominates a local filter; original
511
+ // order breaks ties. The empty-order branch keeps the helper correct if base selection is ever
512
+ // made schema-aware too, though the streaming executor currently fixes that source.
513
+ return ordered.length === 0 ? local : equality * 100 + connected * 10 + local;
514
+ }
515
+ function rawPredicateSources(predicate, entries, tables) {
516
+ const sources = new Set();
517
+ return rawExpressionSources(predicate.left, entries, tables, sources) &&
518
+ rawExpressionSources(predicate.right, entries, tables, sources)
519
+ ? sources
520
+ : undefined;
521
+ }
522
+ function rawExpressionSources(expression, entries, tables, into) {
523
+ if (expression.kind === "subquery" || expression.kind === "exists")
524
+ return false;
525
+ if (expression.kind === "column") {
526
+ const parts = expression.reference.split(".");
527
+ let matches;
528
+ if (parts.length === 2) {
529
+ const match = entries.findIndex(({ source }) => source.alias === parts[0]);
530
+ matches = match < 0 || tables[match]?.columns.has(parts[1] ?? "") !== true ? [] : [match];
531
+ }
532
+ else {
533
+ const column = parts[0] ?? "";
534
+ matches = tables.flatMap((table, index) => table?.columns.has(column) === true ? [index] : []);
535
+ }
536
+ if (matches.length !== 1)
537
+ return false;
538
+ into.add(matches[0] ?? -1);
539
+ return true;
540
+ }
541
+ return childExpressions(expression).every((child) => rawExpressionSources(child, entries, tables, into));
542
+ }
543
+ function predicateJoinsSource(predicate, candidate, available, entries, tables) {
544
+ if (predicate.operator !== "=")
545
+ return false;
546
+ const left = new Set();
547
+ const right = new Set();
548
+ if (!rawExpressionSources(predicate.left, entries, tables, left) ||
549
+ !rawExpressionSources(predicate.right, entries, tables, right)) {
550
+ return false;
551
+ }
552
+ const candidateOnly = (sources) => sources.size === 1 && sources.has(candidate);
553
+ const availableOnly = (sources) => sources.size > 0 && [...sources].every((source) => available.has(source));
554
+ return ((candidateOnly(left) && availableOnly(right)) || (candidateOnly(right) && availableOnly(left)));
555
+ }
318
556
  function bindPlan(plan, tables, memory, ftsStats) {
557
+ plan = orderCartesianJoins(plan, tables);
319
558
  const sources = [plan.base, ...plan.joins];
320
559
  const sourceTables = sources.map((source) => {
321
560
  const table = tables.get(source.table);
@@ -422,6 +661,7 @@ function bindPlan(plan, tables, memory, ftsStats) {
422
661
  ...(nulls === undefined ? {} : { nulls }),
423
662
  }));
424
663
  const grouped = groupBy.length > 0 || aggregateSpecs.length > 0;
664
+ const sourceOrdered = sourceTables[0]?.orderedForPlan === true && standardJoins.length === 0 && !grouped;
425
665
  // A single bare string-column GROUP BY can group on dictionary codes: identical values share a
426
666
  // code within one vector, so a code-indexed slot table replaces per-row hashing. Streamed
427
667
  // vectors swap dictionaries per window; the accumulator remaps its slot table by value on
@@ -440,9 +680,11 @@ function bindPlan(plan, tables, memory, ftsStats) {
440
680
  groupBy,
441
681
  groupIndexBySignature,
442
682
  aggregates: aggregateSpecs,
683
+ hasListAggregate: aggregateSpecs.some((aggregate) => aggregate.name === "JSON_ARRAYAGG" || aggregate.name === "STRING_AGG"),
443
684
  select,
444
685
  orderBy,
445
686
  grouped,
687
+ sourceOrdered,
446
688
  ...(codeGrouping === undefined ? {} : { codeGrouping }),
447
689
  wildcard: plan.select[0]?.expression.kind === "wildcard",
448
690
  ...(plan.limit === undefined ? {} : { limit: plan.limit }),
@@ -513,7 +755,11 @@ function bindExpression(expression, sources, aggregateSpecs, aggregateIndexes, m
513
755
  };
514
756
  }
515
757
  if (expression.kind === "literal" || expression.kind === "wildcard") {
516
- return { ...expression, signature };
758
+ return expression.kind === "literal" &&
759
+ typeof expression.value === "string" &&
760
+ expression.internalSqlValue !== true
761
+ ? { ...expression, value: protectedSqlTextValue(expression.value), signature }
762
+ : { ...expression, signature };
517
763
  }
518
764
  if (expression.kind === "column") {
519
765
  const parts = expression.reference.split(".");
@@ -599,12 +845,26 @@ function bindExpression(expression, sources, aggregateSpecs, aggregateIndexes, m
599
845
  argument.vector.kind === "datetime"
600
846
  ? { source: argument.source, vector: argument.vector }
601
847
  : undefined;
602
- const rawNumber = argument.kind === "column" && argument.vector.kind === "number"
848
+ const rawNumber = expression.name !== "JSON_ARRAYAGG" &&
849
+ expression.name !== "STRING_AGG" &&
850
+ argument.kind === "column" &&
851
+ argument.vector.kind === "number"
603
852
  ? { source: argument.source, vector: argument.vector }
604
853
  : undefined;
605
854
  aggregateSpecs.push({
606
855
  name: expression.name,
607
856
  argument,
857
+ ...(expression.name === "STRING_AGG"
858
+ ? { delimiter: required(arguments_[1], "STRING_AGG delimiter is missing") }
859
+ : {}),
860
+ ...(expression.aggregateOrderBy === undefined
861
+ ? {}
862
+ : {
863
+ orderBy: expression.aggregateOrderBy.map((order) => ({
864
+ ...order,
865
+ expression: bindExpression(order.expression, sources, aggregateSpecs, aggregateIndexes, memory, ftsBySignature, ftsStats),
866
+ })),
867
+ }),
608
868
  // The signature this slot was keyed by is the JSON of the compiled call, which carries the
609
869
  // flag — so COUNT(x) and COUNT(DISTINCT x) in one select land in separate slots.
610
870
  ...(expression.distinct === true ? { distinct: true } : {}),
@@ -674,7 +934,8 @@ function ensureFtsDictionaryCache(expression, columnIndex, vector, scoring) {
674
934
  const tokenCount = withScores ? new Uint32Array(vector.dictionary.length) : undefined;
675
935
  const termTf = withScores ? new Uint32Array(vector.dictionary.length * termCount) : undefined;
676
936
  for (let code = 0; code < vector.dictionary.length; code += 1) {
677
- const tokens = tokenize(vector.dictionary[code] ?? "");
937
+ const rendered = externalSqlDomainValue(vector.dictionary[code] ?? "");
938
+ const tokens = tokenize(typeof rendered === "string" ? rendered : "");
678
939
  masks[code] = termsMask(tokens, expression.terms);
679
940
  if (tokenCount !== undefined)
680
941
  tokenCount[code] = tokens.length;
@@ -922,8 +1183,10 @@ function runScanBatch(plan, start, length, groups, output, memory) {
922
1183
  const scan = sourceRows[plan.scanSource];
923
1184
  if (scan === undefined)
924
1185
  return false;
925
- for (let index = 0; index < length; index += 1)
926
- scan[index] = start + index;
1186
+ const order = plan.sourceTables[plan.scanSource]?.scanOrder;
1187
+ for (let index = 0; index < length; index += 1) {
1188
+ scan[index] = order?.[start + index] ?? start + index;
1189
+ }
927
1190
  const batch = { length, rowsBySource: sourceRows, memory: batchMemory };
928
1191
  return consumeJoinedBatches(plan, batch, 0, groups, output, memory);
929
1192
  }
@@ -938,7 +1201,7 @@ function executeBoundPlan(plan, memory) {
938
1201
  const groups = new GroupAccumulator(plan, memory);
939
1202
  const output = new ResultSink(plan, memory, true);
940
1203
  const scanRows = plan.sourceTables[plan.scanSource]?.rowCount ?? 0;
941
- const narrowed = ascendingScanRange(plan, 0, scanRows);
1204
+ const narrowed = plan.sourceOrdered ? undefined : ascendingScanRange(plan, 0, scanRows);
942
1205
  const ranges = narrowed?.ranges ?? [
943
1206
  { begin: narrowed?.begin ?? 0, end: narrowed?.end ?? scanRows },
944
1207
  ];
@@ -972,7 +1235,7 @@ async function executeBoundPlanAsync(plan, memory, options) {
972
1235
  // window cannot answer instead of stepping through them. `windowEnd` is where the loader
973
1236
  // stopped, which is the block boundary — exactly the span the ordering check covers.
974
1237
  const windowEnd = typeof residentEnd === "number" && residentEnd > start ? residentEnd : start + length;
975
- const narrowed = ascendingScanRange(plan, start, windowEnd);
1238
+ const narrowed = plan.sourceOrdered ? undefined : ascendingScanRange(plan, start, windowEnd);
976
1239
  if (narrowed === undefined) {
977
1240
  if (runScanBatch(plan, start, length, groups, output, memory))
978
1241
  break;
@@ -1002,15 +1265,61 @@ async function executeBoundPlanAsync(plan, memory, options) {
1002
1265
  const rows = plan.grouped ? finishGroups(plan, groups.values(), memory) : output.finish();
1003
1266
  return finishResult(plan, rows, memory);
1004
1267
  }
1268
+ /**
1269
+ * Pull-driven scan execution for cursor-safe plans. A result page owns a child memory context
1270
+ * that closes after the consumer settles, so both modeled memory and live row objects are
1271
+ * bounded by one page. OFFSET/LIMIT are applied once across the whole scan, never per page.
1272
+ */
1273
+ async function executeBoundPlanBatches(plan, memory, options, consume) {
1274
+ const columns = plan.wildcard ? wildcardColumnNames(plan) : plan.select.map((item) => item.alias);
1275
+ const { limit, offset, ...unbounded } = plan;
1276
+ const scanPlan = unbounded;
1277
+ let skipped = 0;
1278
+ let emitted = 0;
1279
+ const scanRows = scanPlan.sourceTables[scanPlan.scanSource]?.rowCount ?? 0;
1280
+ const step = Math.min(DEFAULT_BATCH_ROWS, options.batchRows);
1281
+ for (let start = 0; start < scanRows && (limit === undefined || emitted < limit);) {
1282
+ options.signal?.throwIfAborted();
1283
+ let length = Math.min(step, scanRows - start);
1284
+ const loaded = options.loadScanWindow?.(start, length);
1285
+ const residentEnd = typeof loaded === "number" || loaded === undefined ? loaded : await loaded;
1286
+ options.signal?.throwIfAborted();
1287
+ if (typeof residentEnd === "number" && residentEnd > start) {
1288
+ length = Math.min(length, residentEnd - start);
1289
+ }
1290
+ const pageMemory = memory.createChild();
1291
+ try {
1292
+ const groups = new GroupAccumulator(scanPlan, pageMemory);
1293
+ const output = new ResultSink(scanPlan, pageMemory, options.loadScanWindow === undefined);
1294
+ runScanBatch(scanPlan, start, length, groups, output, pageMemory);
1295
+ let rows = output.finish();
1296
+ const remainingOffset = Math.max(0, (offset ?? 0) - skipped);
1297
+ if (remainingOffset > 0) {
1298
+ const remove = Math.min(remainingOffset, rows.length);
1299
+ rows = rows.slice(remove);
1300
+ skipped += remove;
1301
+ }
1302
+ if (limit !== undefined && rows.length > limit - emitted) {
1303
+ rows = rows.slice(0, limit - emitted);
1304
+ }
1305
+ if (rows.length > 0) {
1306
+ emitted += rows.length;
1307
+ await consume({ columns: [...columns], rows });
1308
+ }
1309
+ }
1310
+ finally {
1311
+ pageMemory.close();
1312
+ }
1313
+ start += length;
1314
+ }
1315
+ return columns;
1316
+ }
1005
1317
  function createSpillOwnerId() {
1006
1318
  return `query-${globalThis.crypto.randomUUID()}`;
1007
1319
  }
1008
1320
  async function executeBoundPlanWithSortSpill(plan, memory, options) {
1009
1321
  const store = required(options.spillStore, "Query spill store is missing");
1010
- const pageRows = options.spillPageRows ?? DEFAULT_BATCH_ROWS;
1011
- if (!Number.isSafeInteger(pageRows) || pageRows <= 0) {
1012
- throw new RangeError("Query spill page rows must be a positive whole number");
1013
- }
1322
+ const pageRows = boundedSpillPageRows(options.spillPageRows);
1014
1323
  const columns = plan.wildcard ? wildcardColumnNames(plan) : plan.select.map((item) => item.alias);
1015
1324
  const ownerId = createSpillOwnerId();
1016
1325
  const runs = [];
@@ -1051,8 +1360,8 @@ async function executeBoundPlanWithSortSpill(plan, memory, options) {
1051
1360
  ordering.release();
1052
1361
  }
1053
1362
  const runId = `run-${String(runSequence++)}`;
1054
- await store.putPage(ownerId, runId, 0, encodeSpillRows(columns, rows));
1055
- runs.push({ id: runId, pageCount: 1 });
1363
+ const pageCount = await writeSpillRowPages(store, ownerId, runId, 0, columns, rows, pageRows);
1364
+ runs.push({ id: runId, pageCount });
1056
1365
  }
1057
1366
  finally {
1058
1367
  outputMemory.close();
@@ -1107,10 +1416,7 @@ async function executeBoundPlanWithSortSpill(plan, memory, options) {
1107
1416
  }
1108
1417
  async function executeBoundPlanWithHashSpill(plan, memory, options) {
1109
1418
  const store = required(options.spillStore, "Query spill store is missing");
1110
- const pageRows = options.spillPageRows ?? DEFAULT_BATCH_ROWS;
1111
- if (!Number.isSafeInteger(pageRows) || pageRows <= 0) {
1112
- throw new RangeError("Query spill page rows must be a positive whole number");
1113
- }
1419
+ const pageRows = boundedSpillPageRows(options.spillPageRows);
1114
1420
  const partitionCount = 64;
1115
1421
  const columns = plan.select.map((item) => item.alias);
1116
1422
  const groupColumnNames = plan.groupBy.map((_, index) => `g${String(index)}`);
@@ -1161,8 +1467,18 @@ async function executeBoundPlanWithHashSpill(plan, memory, options) {
1161
1467
  const raw = spec.argument.kind === "wildcard"
1162
1468
  ? 1
1163
1469
  : evaluateBatchExpression(plan, spec.argument, batch, row);
1164
- spillRow[`a${String(index)}`] =
1165
- raw === null || raw === undefined ? null : asQueryValue(raw);
1470
+ if (spec.name === "STRING_AGG" && raw !== null && raw !== undefined) {
1471
+ const delimiter = required(spec.delimiter, "STRING_AGG delimiter is missing");
1472
+ spillRow[`a${String(index)}`] = JSON.stringify([
1473
+ raw,
1474
+ evaluateBatchExpression(plan, delimiter, batch, row),
1475
+ (spec.orderBy ?? []).map((order) => encodeSpillValue(asQueryValue(evaluateBatchExpression(plan, order.expression, batch, row)))),
1476
+ ]);
1477
+ }
1478
+ else {
1479
+ spillRow[`a${String(index)}`] =
1480
+ raw === null || raw === undefined ? null : asQueryValue(raw);
1481
+ }
1166
1482
  }
1167
1483
  batchMemory.tally(queryRowPayloadBytes(spillRow), "Hash spill value row");
1168
1484
  const partition = hashQueryValues(groupValues) & (partitionCount - 1);
@@ -1172,15 +1488,26 @@ async function executeBoundPlanWithHashSpill(plan, memory, options) {
1172
1488
  }
1173
1489
  });
1174
1490
  const flush = [];
1491
+ let flushBytes = 0;
1175
1492
  for (const [partition, rows] of partitionBuffers) {
1176
- const pageIndex = partitionPages[partition] ?? 0;
1177
- flush.push({
1178
- ownerId,
1179
- runId: `partition-${String(partition)}`,
1180
- pageIndex,
1181
- bytes: encodeSpillRows(spillColumns, rows),
1182
- });
1183
- partitionPages[partition] = pageIndex + 1;
1493
+ for (const bytes of encodeSpillRowPages(spillColumns, rows, pageRows)) {
1494
+ if (flush.length > 0 &&
1495
+ (flush.length === SPILL_WRITE_BATCH_PAGES ||
1496
+ flushBytes + bytes.byteLength > MAX_TEMP_RUN_BATCH_BYTES)) {
1497
+ await writeSpillPages(store, flush);
1498
+ flush.length = 0;
1499
+ flushBytes = 0;
1500
+ }
1501
+ const pageIndex = partitionPages[partition] ?? 0;
1502
+ flush.push({
1503
+ ownerId,
1504
+ runId: `partition-${String(partition)}`,
1505
+ pageIndex,
1506
+ bytes,
1507
+ });
1508
+ flushBytes += bytes.byteLength;
1509
+ partitionPages[partition] = pageIndex + 1;
1510
+ }
1184
1511
  }
1185
1512
  await writeSpillPages(store, flush);
1186
1513
  }
@@ -1227,23 +1554,8 @@ async function executeBoundPlanWithHashSpill(plan, memory, options) {
1227
1554
  }
1228
1555
  }
1229
1556
  const runId = `group-${String(runSequence++)}`;
1230
- let outputPage = 0;
1231
- let pending = [];
1232
- for (let start = 0; start < rows.length; start += pageRows) {
1233
- pending.push({
1234
- ownerId,
1235
- runId,
1236
- pageIndex: outputPage,
1237
- bytes: encodeSpillRows(columns, rows.slice(start, start + pageRows)),
1238
- });
1239
- outputPage += 1;
1240
- if (pending.length >= SPILL_WRITE_BATCH_PAGES) {
1241
- await writeSpillPages(store, pending);
1242
- pending = [];
1243
- }
1244
- }
1245
- await writeSpillPages(store, pending);
1246
- runs.push({ id: runId, pageCount: outputPage });
1557
+ const pageCount = await writeSpillRowPages(store, ownerId, runId, 0, columns, rows, pageRows);
1558
+ runs.push({ id: runId, pageCount });
1247
1559
  }
1248
1560
  finally {
1249
1561
  partitionMemory.close();
@@ -1379,11 +1691,10 @@ async function mergeSpillRuns(store, ownerId, left, right, outputId, columns, or
1379
1691
  const flush = async () => {
1380
1692
  if (outputPage.length === 0)
1381
1693
  return;
1382
- await store.putPage(ownerId, outputId, pageIndex, encodeSpillRows(columns, outputPage));
1694
+ pageIndex += await writeSpillRowPages(store, ownerId, outputId, pageIndex, columns, outputPage, pageRows);
1383
1695
  outputPage = [];
1384
1696
  outputMemory.close();
1385
1697
  outputMemory = mergeMemory.createChild();
1386
- pageIndex += 1;
1387
1698
  };
1388
1699
  try {
1389
1700
  let leftRow = await leftReader.next();
@@ -1446,7 +1757,7 @@ function createSpillRunReader(store, ownerId, run, columns, memory) {
1446
1757
  }
1447
1758
  function compareOrderedRows(left, right, orderBy) {
1448
1759
  for (const order of orderBy) {
1449
- const placed = explicitNullOrder(left[order.outputName], right[order.outputName], order.nulls);
1760
+ const placed = nullOrder(left[order.outputName], right[order.outputName], order.nulls, order.direction);
1450
1761
  if (placed !== undefined && placed !== 0)
1451
1762
  return placed;
1452
1763
  const comparison = compareValues(left[order.outputName], right[order.outputName]);
@@ -1455,19 +1766,63 @@ function compareOrderedRows(left, right, orderBy) {
1455
1766
  }
1456
1767
  return 0;
1457
1768
  }
1458
- function encodeSpillRows(columns, rows) {
1459
- const encoded = rows.map((row) => columns.map((column) => encodeSpillValue(row[column] ?? null)));
1460
- const payload = vectorTextEncoder.encode(JSON.stringify(encoded));
1461
- const modeledBytes = rows.reduce((total, row) => safeMemorySum(total, queryRowPayloadBytes(row), "Spill page rows"), 0);
1462
- if (modeledBytes > 0xffffffff)
1463
- throw new RangeError("Spill page modeled bytes exceed uint32");
1464
- const bytes = new Uint8Array(SPILL_PAGE_HEADER_BYTES + payload.byteLength);
1769
+ function encodeSpillRow(columns, row) {
1770
+ return {
1771
+ bytes: vectorTextEncoder.encode(JSON.stringify(columns.map((column) => encodeSpillValue(row[column] ?? null)))),
1772
+ modeledBytes: queryRowPayloadBytes(row),
1773
+ };
1774
+ }
1775
+ function buildSpillPage(rows, byteLength, modeledBytes) {
1776
+ const bytes = new Uint8Array(byteLength);
1465
1777
  const header = new DataView(bytes.buffer);
1466
1778
  header.setUint32(0, SPILL_PAGE_MAGIC, true);
1467
1779
  header.setUint32(4, modeledBytes, true);
1468
- bytes.set(payload, SPILL_PAGE_HEADER_BYTES);
1780
+ let offset = SPILL_PAGE_HEADER_BYTES;
1781
+ bytes[offset++] = 0x5b;
1782
+ for (let index = 0; index < rows.length; index += 1) {
1783
+ if (index > 0)
1784
+ bytes[offset++] = 0x2c;
1785
+ const row = required(rows[index], "Encoded spill row is missing");
1786
+ bytes.set(row.bytes, offset);
1787
+ offset += row.bytes.byteLength;
1788
+ }
1789
+ bytes[offset] = 0x5d;
1469
1790
  return bytes;
1470
1791
  }
1792
+ /** Encodes pages incrementally and splits on both the requested row target and the hard byte cap. */
1793
+ function* encodeSpillRowPages(columns, rows, pageRows) {
1794
+ let pending = [];
1795
+ let byteLength = SPILL_PAGE_HEADER_BYTES + 2;
1796
+ let modeledBytes = 0;
1797
+ const flush = () => {
1798
+ const page = buildSpillPage(pending, byteLength, modeledBytes);
1799
+ pending = [];
1800
+ byteLength = SPILL_PAGE_HEADER_BYTES + 2;
1801
+ modeledBytes = 0;
1802
+ return page;
1803
+ };
1804
+ for (const row of rows) {
1805
+ const encoded = encodeSpillRow(columns, row);
1806
+ const separatorBytes = pending.length === 0 ? 0 : 1;
1807
+ const nextByteLength = byteLength + separatorBytes + encoded.bytes.byteLength;
1808
+ const nextModeledBytes = modeledBytes + encoded.modeledBytes;
1809
+ if (pending.length > 0 &&
1810
+ (pending.length === pageRows ||
1811
+ nextByteLength > MAX_TEMP_RUN_PAGE_BYTES ||
1812
+ nextModeledBytes > 0xffffffff)) {
1813
+ yield flush();
1814
+ }
1815
+ const addedSeparatorBytes = pending.length === 0 ? 0 : 1;
1816
+ byteLength += addedSeparatorBytes + encoded.bytes.byteLength;
1817
+ modeledBytes += encoded.modeledBytes;
1818
+ if (byteLength > MAX_TEMP_RUN_PAGE_BYTES || modeledBytes > 0xffffffff) {
1819
+ throw new RangeError("One query result row exceeds the spill page storage limit");
1820
+ }
1821
+ pending.push(encoded);
1822
+ }
1823
+ if (pending.length > 0)
1824
+ yield flush();
1825
+ }
1471
1826
  function decodeSpillRows(columns, bytes) {
1472
1827
  spillRowsModeledBytes(bytes);
1473
1828
  const value = JSON.parse(new TextDecoder().decode(bytes.subarray(SPILL_PAGE_HEADER_BYTES)));
@@ -1498,7 +1853,7 @@ function encodeSpillValue(value) {
1498
1853
  return [2, Object.is(value, -0) ? "-0" : String(value)];
1499
1854
  if (typeof value === "string")
1500
1855
  return [3, value];
1501
- return [4, value.getTime()];
1856
+ return [4, dateMilliseconds(value)];
1502
1857
  }
1503
1858
  function decodeSpillValue(value) {
1504
1859
  if (!Array.isArray(value))
@@ -1533,7 +1888,7 @@ function executeMetadataCount(plan, memory) {
1533
1888
  }
1534
1889
  function finishResult(plan, inputRows, memory) {
1535
1890
  const rows = inputRows;
1536
- if (plan.orderBy.length > 0) {
1891
+ if (plan.orderBy.length > 0 && !plan.sourceOrdered) {
1537
1892
  const ordering = memory.reserve(safeMemoryProduct(rows.length, Uint32Array.BYTES_PER_ELEMENT * 2 +
1538
1893
  Uint8Array.BYTES_PER_ELEMENT +
1539
1894
  // Per extracted sort key: one slot, plus the null mask a numeric column adds beside
@@ -2110,7 +2465,7 @@ function consumeJoinedBatches(plan, batch, joinIndex, groups, output, memory, pr
2110
2465
  let working = batch;
2111
2466
  let complete = prefiltered;
2112
2467
  let owned;
2113
- if (joinIndex === 0 && !prefiltered) {
2468
+ if (!prefiltered) {
2114
2469
  const filtered = prefilterBatch(plan, batch, join);
2115
2470
  if (filtered !== undefined) {
2116
2471
  complete = filtered.complete;
@@ -2144,7 +2499,7 @@ function consumeJoinedBatches(plan, batch, joinIndex, groups, output, memory, pr
2144
2499
  function reachedEarlyLimit(plan, outputRows) {
2145
2500
  // Early termination must still produce the rows the trailing OFFSET will discard.
2146
2501
  return (!plan.grouped &&
2147
- plan.orderBy.length === 0 &&
2502
+ (plan.orderBy.length === 0 || plan.sourceOrdered) &&
2148
2503
  plan.limit !== undefined &&
2149
2504
  outputRows >= plan.limit + (plan.offset ?? 0));
2150
2505
  }
@@ -2186,11 +2541,18 @@ function* joinBatches(plan, input, join, memory) {
2186
2541
  const probeKey = evaluateBatchExpression(plan, join.probe, input, row);
2187
2542
  let buildRow = probeKey === null ? -1 : join.lookup.firstRow(probeKey);
2188
2543
  if (buildRow < 0) {
2189
- if (join.kind === "left") {
2544
+ if (join.kind === "left" || join.kind === "anti") {
2190
2545
  appendJoinedRow(ensureOutput(), outputLength, input, row, join.buildSource, -1);
2191
2546
  outputLength += 1;
2192
2547
  }
2193
2548
  }
2549
+ else if (join.kind === "semi") {
2550
+ appendJoinedRow(ensureOutput(), outputLength, input, row, join.buildSource, buildRow);
2551
+ outputLength += 1;
2552
+ }
2553
+ else if (join.kind === "anti") {
2554
+ // A match excludes the probe row.
2555
+ }
2194
2556
  else {
2195
2557
  while (buildRow >= 0) {
2196
2558
  appendJoinedRow(ensureOutput(), outputLength, input, row, join.buildSource, buildRow);
@@ -2254,12 +2616,16 @@ function* loopJoinBatches(plan, input, join, loop, memory) {
2254
2616
  if (!holds)
2255
2617
  continue;
2256
2618
  matched = true;
2257
- appendJoinedRow(ensureOutput(), outputLength, input, row, join.buildSource, buildRow);
2258
- outputLength += 1;
2259
- if (outputLength === DEFAULT_BATCH_ROWS)
2260
- yield* emit();
2619
+ if (join.kind !== "anti") {
2620
+ appendJoinedRow(ensureOutput(), outputLength, input, row, join.buildSource, buildRow);
2621
+ outputLength += 1;
2622
+ if (outputLength === DEFAULT_BATCH_ROWS)
2623
+ yield* emit();
2624
+ }
2625
+ if (join.kind === "semi" || join.kind === "anti")
2626
+ break;
2261
2627
  }
2262
- if (!matched && join.kind === "left") {
2628
+ if (!matched && (join.kind === "left" || join.kind === "anti")) {
2263
2629
  appendJoinedRow(ensureOutput(), outputLength, input, row, join.buildSource, -1);
2264
2630
  outputLength += 1;
2265
2631
  if (outputLength === DEFAULT_BATCH_ROWS)
@@ -2309,10 +2675,12 @@ function joinUniqueBatch(plan, input, join, memory) {
2309
2675
  cache[code] = buildRow;
2310
2676
  }
2311
2677
  }
2312
- if (buildRow < 0 && join.kind === "inner")
2678
+ if ((buildRow < 0 && (join.kind === "inner" || join.kind === "semi")) ||
2679
+ (buildRow >= 0 && join.kind === "anti")) {
2313
2680
  continue;
2681
+ }
2314
2682
  selectedRows[outputLength] = row;
2315
- buildRows[outputLength] = buildRow;
2683
+ buildRows[outputLength] = join.kind === "anti" ? -1 : buildRow;
2316
2684
  outputLength += 1;
2317
2685
  }
2318
2686
  }
@@ -2320,10 +2688,12 @@ function joinUniqueBatch(plan, input, join, memory) {
2320
2688
  for (let row = 0; row < input.length; row += 1) {
2321
2689
  const probeKey = evaluateBatchExpression(plan, join.probe, input, row);
2322
2690
  const buildRow = probeKey === null ? -1 : join.lookup.firstRow(probeKey);
2323
- if (buildRow < 0 && join.kind === "inner")
2691
+ if ((buildRow < 0 && (join.kind === "inner" || join.kind === "semi")) ||
2692
+ (buildRow >= 0 && join.kind === "anti")) {
2324
2693
  continue;
2694
+ }
2325
2695
  selectedRows[outputLength] = row;
2326
- buildRows[outputLength] = buildRow;
2696
+ buildRows[outputLength] = join.kind === "anti" ? -1 : buildRow;
2327
2697
  outputLength += 1;
2328
2698
  }
2329
2699
  }
@@ -2415,6 +2785,7 @@ class ResultSink {
2415
2785
  const capacity = (plan.limit ?? 0) + (plan.offset ?? 0);
2416
2786
  const scanRows = plan.sourceTables[plan.scanSource]?.rowCount ?? 0;
2417
2787
  const bounded = !plan.grouped &&
2788
+ !plan.sourceOrdered &&
2418
2789
  plan.orderBy.length > 0 &&
2419
2790
  plan.limit !== undefined &&
2420
2791
  (plan.joins.length > 0 || capacity < scanRows * BOUNDED_SINK_MAX_SHARE);
@@ -2625,7 +2996,7 @@ class ResultSink {
2625
2996
  : typeof first === "number"
2626
2997
  ? first
2627
2998
  : first instanceof Date
2628
- ? first.getTime()
2999
+ ? dateMilliseconds(first)
2629
3000
  : undefined;
2630
3001
  }
2631
3002
  /**
@@ -2692,7 +3063,7 @@ class ResultSink {
2692
3063
  const orderBy = this.#plan.orderBy;
2693
3064
  for (let index = 0; index < orderBy.length; index += 1) {
2694
3065
  const order = required(orderBy[index], "Order term is missing");
2695
- const placed = explicitNullOrder(left[index], right[index], order.nulls);
3066
+ const placed = nullOrder(left[index], right[index], order.nulls, order.direction);
2696
3067
  if (placed !== undefined && placed !== 0)
2697
3068
  return placed;
2698
3069
  const comparison = compareValues(left[index], right[index]);
@@ -2820,7 +3191,7 @@ class GroupAccumulator {
2820
3191
  * COUNT/SUM/AVG aggregates: one pass with unboxed reads and no per-row dispatch. Returns false
2821
3192
  * when the plan shape needs the generic per-row path.
2822
3193
  */
2823
- consumeFast(batch, passes, hasPredicates) {
3194
+ consumeFast(batch) {
2824
3195
  const plan = this.#plan;
2825
3196
  const codeGrouping = plan.codeGrouping;
2826
3197
  const globalGroup = plan.groupBy.length === 0;
@@ -2831,7 +3202,7 @@ class GroupAccumulator {
2831
3202
  if (specs === undefined)
2832
3203
  return false;
2833
3204
  // The purest shape — global COUNT(*) with no predicates — needs no row loop at all.
2834
- if (globalGroup && !hasPredicates && specs.every((spec) => spec.kind === "star")) {
3205
+ if (globalGroup && specs.every((spec) => spec.kind === "star")) {
2835
3206
  const state = required(this.#index.getEmpty(), "Grouped query state is missing");
2836
3207
  for (let index = 0; index < specs.length; index += 1) {
2837
3208
  state.counts[index] = (state.counts[index] ?? 0) + batch.length;
@@ -2881,8 +3252,6 @@ class GroupAccumulator {
2881
3252
  slots: groupVector.codes.length,
2882
3253
  };
2883
3254
  for (let row = 0; row < batch.length; row += 1) {
2884
- if (hasPredicates && !passes(row))
2885
- continue;
2886
3255
  let state = globalState;
2887
3256
  if (state === undefined && grouping !== undefined && states !== undefined) {
2888
3257
  const sourceRow = groupRows?.[row] ?? -1;
@@ -3101,7 +3470,7 @@ function consumeBatch(plan, batch, groups, output, memory, prefiltered = false)
3101
3470
  }
3102
3471
  return;
3103
3472
  }
3104
- if (plan.grouped && groups.consumeFast(batch, () => true, false))
3473
+ if (plan.grouped && groups.consumeFast(batch))
3105
3474
  return;
3106
3475
  if (!plan.grouped && output.tryAddBatch(batch))
3107
3476
  return;
@@ -3133,6 +3502,9 @@ function createGroupState(groupValues, plan, memory) {
3133
3502
  payloadBytes = safeMemorySum(payloadBytes, queryValuePayloadBytes(value), "Group state");
3134
3503
  }
3135
3504
  payloadBytes = safeMemorySum(payloadBytes, safeMemoryProduct(plan.aggregates.length, AGGREGATE_ACCUMULATOR_BYTES, "Aggregate accumulator state"), "Group state");
3505
+ if (plan.hasListAggregate) {
3506
+ payloadBytes = safeMemorySum(payloadBytes, safeMemoryProduct(plan.aggregates.length, QUERY_REFERENCE_BYTES, "JSON aggregate list slots"), "Group state");
3507
+ }
3136
3508
  // tally, not reserve: a group state lives until the context closes and is never released
3137
3509
  // on its own, so a per-group QueryMemoryReservation object — retained in the context's Set
3138
3510
  // for the whole query — is pure overhead at one per distinct group.
@@ -3162,6 +3534,9 @@ function createGroupState(groupValues, plan, memory) {
3162
3534
  counts: new Float64Array(plan.aggregates.length),
3163
3535
  sums: new Float64Array(plan.aggregates.length),
3164
3536
  values: new Array(plan.aggregates.length),
3537
+ ...(plan.hasListAggregate
3538
+ ? { lists: new Array(plan.aggregates.length) }
3539
+ : {}),
3165
3540
  valueReservations: new Array(plan.aggregates.length),
3166
3541
  valueReservationBytes: new Float64Array(plan.aggregates.length),
3167
3542
  distincts,
@@ -3174,7 +3549,7 @@ function createGroupState(groupValues, plan, memory) {
3174
3549
  * string a row genuinely holds.
3175
3550
  */
3176
3551
  function distinctKey(value) {
3177
- return value instanceof Date ? `d${String(value.getTime())}` : value;
3552
+ return value instanceof Date ? `\0d${String(dateMilliseconds(value))}` : value;
3178
3553
  }
3179
3554
  /**
3180
3555
  * Whether this value is the first of its kind for the slot, folding it into the set when it is.
@@ -3236,7 +3611,11 @@ function updateAggregates(plan, state, batch, row, memory) {
3236
3611
  const value = spec.argument.kind === "wildcard"
3237
3612
  ? 1
3238
3613
  : evaluateBatchExpression(plan, spec.argument, batch, row);
3239
- applyAggregateValue(spec, state, index, value, memory);
3614
+ const delimiter = spec.delimiter === undefined
3615
+ ? undefined
3616
+ : evaluateBatchExpression(plan, spec.delimiter, batch, row);
3617
+ const orderValues = (spec.orderBy ?? []).map((order) => asQueryValue(evaluateBatchExpression(plan, order.expression, batch, row)));
3618
+ applyAggregateValue(spec, state, index, value, memory, delimiter, orderValues);
3240
3619
  }
3241
3620
  }
3242
3621
  /** Reads a float64 slot (number value or raw epoch milliseconds) unboxed, or null when invalid. */
@@ -3258,10 +3637,63 @@ function rawFloat64Value(vector, rowIndex) {
3258
3637
  function updateAggregatesFromValues(plan, state, values, memory) {
3259
3638
  for (let index = 0; index < plan.aggregates.length; index += 1) {
3260
3639
  const spec = required(plan.aggregates[index], "Aggregate specification is missing");
3261
- applyAggregateValue(spec, state, index, values[index], memory);
3640
+ const value = values[index];
3641
+ if (spec.name === "STRING_AGG" && typeof value === "string") {
3642
+ const decoded = JSON.parse(value);
3643
+ if (!Array.isArray(decoded) || decoded.length !== 3) {
3644
+ throw new Error("Spilled STRING_AGG input is invalid");
3645
+ }
3646
+ const encodedOrder = decoded[2];
3647
+ if (!Array.isArray(encodedOrder))
3648
+ throw new Error("Spilled STRING_AGG order is invalid");
3649
+ applyAggregateValue(spec, state, index, decoded[0], memory, decoded[1], encodedOrder.map(decodeSpillValue));
3650
+ }
3651
+ else {
3652
+ applyAggregateValue(spec, state, index, value, memory);
3653
+ }
3262
3654
  }
3263
3655
  }
3264
- function applyAggregateValue(spec, state, index, value, memory) {
3656
+ function applyAggregateValue(spec, state, index, value, memory, delimiter, orderValues = []) {
3657
+ if (spec.name === "JSON_ARRAYAGG") {
3658
+ const member = asQueryValue(value ?? null);
3659
+ if (spec.distinct === true && !firstOfItsKind(state, index, member, memory))
3660
+ return;
3661
+ state.counts[index] = (state.counts[index] ?? 0) + 1;
3662
+ const lists = required(state.lists, "JSON aggregate list state is missing");
3663
+ let list = lists[index];
3664
+ if (list === undefined) {
3665
+ list = [];
3666
+ lists[index] = list;
3667
+ }
3668
+ list.push(member);
3669
+ memory.tally(safeMemorySum(QUERY_REFERENCE_BYTES, queryValuePayloadBytes(member), "JSON_ARRAYAGG member"), "JSON_ARRAYAGG member");
3670
+ return;
3671
+ }
3672
+ if (spec.name === "STRING_AGG") {
3673
+ if (value === null || value === undefined)
3674
+ return;
3675
+ value = externalSqlDomainValue(value);
3676
+ delimiter = externalSqlDomainValue(delimiter);
3677
+ if (typeof value !== "string")
3678
+ throw new TypeError("STRING_AGG value must be a string");
3679
+ if (delimiter !== null && delimiter !== undefined && typeof delimiter !== "string") {
3680
+ throw new TypeError("STRING_AGG delimiter must be a string");
3681
+ }
3682
+ const distinctMember = JSON.stringify([value, delimiter ?? ""]);
3683
+ if (spec.distinct === true && !firstOfItsKind(state, index, distinctMember, memory))
3684
+ return;
3685
+ const member = JSON.stringify([value, delimiter ?? "", orderValues.map(encodeSpillValue)]);
3686
+ state.counts[index] = (state.counts[index] ?? 0) + 1;
3687
+ const lists = required(state.lists, "STRING_AGG list state is missing");
3688
+ let list = lists[index];
3689
+ if (list === undefined) {
3690
+ list = [];
3691
+ lists[index] = list;
3692
+ }
3693
+ list.push(member);
3694
+ memory.tally(safeMemorySum(QUERY_REFERENCE_BYTES, queryValuePayloadBytes(member), "STRING_AGG member"), "STRING_AGG member");
3695
+ return;
3696
+ }
3265
3697
  if (value === null || value === undefined)
3266
3698
  return;
3267
3699
  // One gate for every path that accumulates: the generic per-row path, the raw datetime path,
@@ -3270,7 +3702,16 @@ function applyAggregateValue(spec, state, index, value, memory) {
3270
3702
  return;
3271
3703
  state.counts[index] = (state.counts[index] ?? 0) + 1;
3272
3704
  if (spec.name === "SUM" || spec.name === "AVG") {
3273
- state.sums[index] = (state.sums[index] ?? 0) + numeric(value);
3705
+ if (isExactNumeric(value)) {
3706
+ const current = state.values[index];
3707
+ const sum = current === undefined ? value : exactNumericBinary("+", current, value);
3708
+ if (sum === null || sum === undefined)
3709
+ throw new Error("Exact NUMERIC sum disappeared");
3710
+ replaceAggregateValue(state, index, sum, `${spec.name} aggregate value`, memory);
3711
+ }
3712
+ else {
3713
+ state.sums[index] = (state.sums[index] ?? 0) + numeric(value);
3714
+ }
3274
3715
  }
3275
3716
  else if (spec.name === "MIN" &&
3276
3717
  (state.values[index] === undefined || compareValues(value, state.values[index]) < 0)) {
@@ -3364,12 +3805,55 @@ function evaluateFinalExpression(plan, expression, group) {
3364
3805
  return count;
3365
3806
  if (count === 0)
3366
3807
  return null;
3367
- const sum = group.sums[aggregateIndex] ?? 0;
3368
- if (expression.name === "SUM")
3369
- return sum;
3370
- if (expression.name === "AVG")
3371
- return sum / count;
3808
+ if (expression.name === "JSON_ARRAYAGG") {
3809
+ return JSON.stringify((required(group.lists, "JSON aggregate list state is missing")[aggregateIndex] ?? []).map(jsonValueOf));
3810
+ }
3811
+ if (expression.name === "STRING_AGG") {
3812
+ const members = required(group.lists, "STRING_AGG list state is missing")[aggregateIndex] ?? [];
3813
+ const decoded = members.map((encoded) => {
3814
+ if (typeof encoded !== "string")
3815
+ throw new Error("STRING_AGG member is invalid");
3816
+ const pair = JSON.parse(encoded);
3817
+ if (!Array.isArray(pair) ||
3818
+ typeof pair[0] !== "string" ||
3819
+ typeof pair[1] !== "string" ||
3820
+ !Array.isArray(pair[2])) {
3821
+ throw new Error("STRING_AGG member is invalid");
3822
+ }
3823
+ return {
3824
+ value: pair[0],
3825
+ delimiter: pair[1],
3826
+ order: pair[2].map(decodeSpillValue),
3827
+ };
3828
+ });
3829
+ const orderBy = plan.aggregates[aggregateIndex]?.orderBy ?? [];
3830
+ if (orderBy.length > 0) {
3831
+ decoded.sort((left, right) => {
3832
+ for (const [index, order] of orderBy.entries()) {
3833
+ const a = left.order[index];
3834
+ const b = right.order[index];
3835
+ const placed = nullOrder(a, b, order.nulls, order.direction);
3836
+ if (placed !== undefined && placed !== 0)
3837
+ return placed;
3838
+ const compared = compareValues(a, b);
3839
+ if (compared !== 0)
3840
+ return order.direction === "desc" ? -compared : compared;
3841
+ }
3842
+ return 0;
3843
+ });
3844
+ }
3845
+ return protectedSqlTextValue(decoded
3846
+ .map((member, index) => (index === 0 ? member.value : member.delimiter + member.value))
3847
+ .join(""));
3848
+ }
3372
3849
  const value = group.values[aggregateIndex] ?? null;
3850
+ if (expression.name === "SUM")
3851
+ return isExactNumeric(value) ? value : (group.sums[aggregateIndex] ?? 0);
3852
+ if (expression.name === "AVG") {
3853
+ if (isExactNumeric(value))
3854
+ return exactNumericBinary("/", value, count);
3855
+ return (group.sums[aggregateIndex] ?? 0) / count;
3856
+ }
3373
3857
  // Raw-millisecond datetime extremes re-box into a Date only here, once per surviving group.
3374
3858
  if (typeof value === "number" && plan.aggregates[aggregateIndex]?.rawDatetime !== undefined) {
3375
3859
  return new Date(value);
@@ -3460,7 +3944,7 @@ function detectPrimitiveComparison(predicate) {
3460
3944
  return { source: column.source, vector, operator, value };
3461
3945
  }
3462
3946
  if (vector.kind === "datetime" && value instanceof Date) {
3463
- return { source: column.source, vector, operator, value: value.getTime() };
3947
+ return { source: column.source, vector, operator, value: dateMilliseconds(value) };
3464
3948
  }
3465
3949
  if (vector.kind === "boolean" &&
3466
3950
  typeof value === "boolean" &&
@@ -3508,7 +3992,7 @@ function detectPrimitiveInList(predicate) {
3508
3992
  else {
3509
3993
  if (!(value instanceof Date))
3510
3994
  return undefined;
3511
- members.add(value.getTime());
3995
+ members.add(dateMilliseconds(value));
3512
3996
  }
3513
3997
  }
3514
3998
  if (members.size === 0)
@@ -3598,19 +4082,24 @@ function detectDictionaryLike(predicate) {
3598
4082
  */
3599
4083
  const dictionaryLikeCache = new WeakMap();
3600
4084
  function dictionaryLikeMatches(dictionary, pattern, caseInsensitive, escape) {
4085
+ const externalPattern = externalSqlDomainValue(pattern);
4086
+ if (typeof externalPattern !== "string")
4087
+ throw new TypeError("LIKE requires string operands");
3601
4088
  let patterns = dictionaryLikeCache.get(dictionary);
3602
4089
  if (patterns === undefined) {
3603
4090
  patterns = new Map();
3604
4091
  dictionaryLikeCache.set(dictionary, patterns);
3605
4092
  }
3606
- const key = `${caseInsensitive ? "i" : "s"}${escape ?? ""} ${pattern}`;
4093
+ const key = `${caseInsensitive ? "i" : "s"}${escape ?? ""} ${externalPattern}`;
3607
4094
  let matches = patterns.get(key);
3608
4095
  if (matches === undefined) {
3609
4096
  matches = new Uint8Array(dictionary.length);
3610
4097
  for (let index = 0; index < dictionary.length; index += 1) {
3611
- matches[index] = likeMatches(pattern, dictionary[index] ?? "", caseInsensitive, escape)
3612
- ? 1
3613
- : 0;
4098
+ const value = externalSqlDomainValue(dictionary[index] ?? "");
4099
+ matches[index] =
4100
+ typeof value === "string" && likeMatches(externalPattern, value, caseInsensitive, escape)
4101
+ ? 1
4102
+ : 0;
3614
4103
  }
3615
4104
  if (patterns.size >= 32)
3616
4105
  patterns.clear();
@@ -3709,17 +4198,24 @@ function booleanTruth(expression, evaluateValue) {
3709
4198
  if (operator === "LIKE" ||
3710
4199
  operator === "NOT LIKE" ||
3711
4200
  operator === "ILIKE" ||
3712
- operator === "NOT ILIKE") {
3713
- const value = evaluateValue(expression.left);
3714
- const pattern = evaluateValue(expression.right);
4201
+ operator === "NOT ILIKE" ||
4202
+ operator === "SIMILAR TO" ||
4203
+ operator === "NOT SIMILAR TO") {
4204
+ const value = externalSqlDomainValue(evaluateValue(expression.left));
4205
+ const pattern = externalSqlDomainValue(evaluateValue(expression.right));
3715
4206
  if (value === null || value === undefined || pattern === null || pattern === undefined) {
3716
4207
  return null;
3717
4208
  }
3718
4209
  if (typeof value !== "string" || typeof pattern !== "string") {
3719
4210
  throw new TypeError("LIKE requires string operands");
3720
4211
  }
3721
- const matched = likeMatches(pattern, value, operator === "ILIKE" || operator === "NOT ILIKE", expression.escape);
3722
- return operator === "LIKE" || operator === "ILIKE" ? matched : !matched;
4212
+ const similar = operator === "SIMILAR TO" || operator === "NOT SIMILAR TO";
4213
+ const matched = similar
4214
+ ? compileSimilarPattern(pattern, expression.escape ?? "\\").test(value)
4215
+ : likeMatches(pattern, value, operator === "ILIKE" || operator === "NOT ILIKE", expression.escape);
4216
+ return operator === "LIKE" || operator === "ILIKE" || operator === "SIMILAR TO"
4217
+ ? matched
4218
+ : !matched;
3723
4219
  }
3724
4220
  if (operator === "IS DISTINCT FROM" || operator === "IS NOT DISTINCT FROM") {
3725
4221
  const distinct = distinctFromComparison(evaluateValue(expression.left), evaluateValue(expression.right));
@@ -3732,9 +4228,9 @@ function booleanTruth(expression, evaluateValue) {
3732
4228
  const a = comparable(left);
3733
4229
  const b = comparable(right);
3734
4230
  if (operator === "=")
3735
- return a === b;
4231
+ return compareValues(a, b) === 0;
3736
4232
  if (operator === "!=" || operator === "<>")
3737
- return a !== b;
4233
+ return compareValues(a, b) !== 0;
3738
4234
  const comparison = compareValues(a, b);
3739
4235
  if (operator === ">")
3740
4236
  return comparison > 0;
@@ -3933,8 +4429,11 @@ function binaryValue(operator, left, right) {
3933
4429
  if (typeof left !== "string" || typeof right !== "string") {
3934
4430
  throw new TypeError("|| requires string operands");
3935
4431
  }
3936
- return left + right;
4432
+ return protectedSqlTextValue(String(externalSqlDomainValue(left)) + String(externalSqlDomainValue(right)));
3937
4433
  }
4434
+ const exact = exactNumericBinary(operator, left, right);
4435
+ if (exact !== undefined)
4436
+ return exact;
3938
4437
  const a = numeric(left);
3939
4438
  const b = numeric(right);
3940
4439
  if (operator === "+")
@@ -3963,7 +4462,11 @@ function comparisonValue(operator, leftValue, rightValue) {
3963
4462
  if (operator === "LIKE" ||
3964
4463
  operator === "NOT LIKE" ||
3965
4464
  operator === "ILIKE" ||
3966
- operator === "NOT ILIKE") {
4465
+ operator === "NOT ILIKE" ||
4466
+ operator === "SIMILAR TO" ||
4467
+ operator === "NOT SIMILAR TO") {
4468
+ leftValue = externalSqlDomainValue(leftValue);
4469
+ rightValue = externalSqlDomainValue(rightValue);
3967
4470
  if (leftValue === null ||
3968
4471
  leftValue === undefined ||
3969
4472
  rightValue === null ||
@@ -3973,8 +4476,13 @@ function comparisonValue(operator, leftValue, rightValue) {
3973
4476
  if (typeof leftValue !== "string" || typeof rightValue !== "string") {
3974
4477
  throw new TypeError("LIKE requires string operands");
3975
4478
  }
3976
- const matched = likeMatches(rightValue, leftValue, operator === "ILIKE" || operator === "NOT ILIKE");
3977
- return operator === "LIKE" || operator === "ILIKE" ? matched : !matched;
4479
+ const similar = operator === "SIMILAR TO" || operator === "NOT SIMILAR TO";
4480
+ const matched = similar
4481
+ ? compileSimilarPattern(rightValue).test(leftValue)
4482
+ : likeMatches(rightValue, leftValue, operator === "ILIKE" || operator === "NOT ILIKE");
4483
+ return operator === "LIKE" || operator === "ILIKE" || operator === "SIMILAR TO"
4484
+ ? matched
4485
+ : !matched;
3978
4486
  }
3979
4487
  if (leftValue === null ||
3980
4488
  leftValue === undefined ||
@@ -3985,9 +4493,9 @@ function comparisonValue(operator, leftValue, rightValue) {
3985
4493
  const left = comparable(leftValue);
3986
4494
  const right = comparable(rightValue);
3987
4495
  if (operator === "=")
3988
- return left === right;
4496
+ return compareValues(left, right) === 0;
3989
4497
  if (operator === "!=" || operator === "<>")
3990
- return left !== right;
4498
+ return compareValues(left, right) !== 0;
3991
4499
  const comparison = compareValues(left, right);
3992
4500
  if (operator === ">")
3993
4501
  return comparison > 0;
@@ -3998,7 +4506,7 @@ function comparisonValue(operator, leftValue, rightValue) {
3998
4506
  return comparison <= 0;
3999
4507
  }
4000
4508
  function comparable(value) {
4001
- return value instanceof Date ? value.getTime() : value;
4509
+ return value instanceof Date ? dateMilliseconds(value) : value;
4002
4510
  }
4003
4511
  function groupKey(value) {
4004
4512
  const comparableValue = comparable(value);
@@ -4039,8 +4547,17 @@ function stableSortRows(rows, orderBy) {
4039
4547
  }
4040
4548
  }
4041
4549
  function compareValues(left, right) {
4042
- const a = left instanceof Date ? left.getTime() : left;
4043
- const b = right instanceof Date ? right.getTime() : right;
4550
+ const collated = collatedDomainCompare(left, right);
4551
+ if (collated !== undefined)
4552
+ return collated;
4553
+ const enumOrder = enumDomainCompare(left, right);
4554
+ if (enumOrder !== undefined)
4555
+ return enumOrder;
4556
+ const exact = exactNumericCompare(left, right);
4557
+ if (exact !== undefined)
4558
+ return exact;
4559
+ const a = left instanceof Date ? dateMilliseconds(left) : left;
4560
+ const b = right instanceof Date ? dateMilliseconds(right) : right;
4044
4561
  if (a === b)
4045
4562
  return 0;
4046
4563
  if (a === null || a === undefined)
@@ -4120,4 +4637,3 @@ function safeMemoryProduct(left, right, label) {
4120
4637
  }
4121
4638
  return product;
4122
4639
  }
4123
- //# sourceMappingURL=vector.js.map