@minnowdb/core 0.0.1 → 0.1.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 (218) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +27 -1
  3. package/dist/block-format/block.d.ts +46 -0
  4. package/dist/block-format/block.d.ts.map +1 -0
  5. package/dist/block-format/block.js +244 -0
  6. package/dist/block-format/block.js.map +1 -0
  7. package/dist/block-format/checksum.d.ts +7 -0
  8. package/dist/block-format/checksum.d.ts.map +1 -0
  9. package/dist/block-format/checksum.js +56 -0
  10. package/dist/block-format/checksum.js.map +1 -0
  11. package/dist/block-format/codecs.d.ts +24 -0
  12. package/dist/block-format/codecs.d.ts.map +1 -0
  13. package/dist/block-format/codecs.js +102 -0
  14. package/dist/block-format/codecs.js.map +1 -0
  15. package/dist/block-format/column.d.ts +8 -0
  16. package/dist/block-format/column.d.ts.map +1 -0
  17. package/dist/block-format/column.js +166 -0
  18. package/dist/block-format/column.js.map +1 -0
  19. package/dist/block-format/index.d.ts +7 -0
  20. package/dist/block-format/index.d.ts.map +1 -0
  21. package/dist/block-format/index.js +7 -0
  22. package/dist/block-format/index.js.map +1 -0
  23. package/dist/block-format/physical.d.ts +20 -0
  24. package/dist/block-format/physical.d.ts.map +1 -0
  25. package/dist/block-format/physical.js +439 -0
  26. package/dist/block-format/physical.js.map +1 -0
  27. package/dist/block-format/types.d.ts +93 -0
  28. package/dist/block-format/types.d.ts.map +1 -0
  29. package/dist/block-format/types.js +2 -0
  30. package/dist/block-format/types.js.map +1 -0
  31. package/dist/engine/artifact-cache.d.ts +22 -0
  32. package/dist/engine/artifact-cache.d.ts.map +1 -0
  33. package/dist/engine/artifact-cache.js +68 -0
  34. package/dist/engine/artifact-cache.js.map +1 -0
  35. package/dist/engine/batch.d.ts +28 -0
  36. package/dist/engine/batch.d.ts.map +1 -0
  37. package/dist/engine/batch.js +38 -0
  38. package/dist/engine/batch.js.map +1 -0
  39. package/dist/engine/buffered-writer.d.ts +43 -0
  40. package/dist/engine/buffered-writer.d.ts.map +1 -0
  41. package/dist/engine/buffered-writer.js +158 -0
  42. package/dist/engine/buffered-writer.js.map +1 -0
  43. package/dist/engine/catalog.d.ts +82 -0
  44. package/dist/engine/catalog.d.ts.map +1 -0
  45. package/dist/engine/catalog.js +47 -0
  46. package/dist/engine/catalog.js.map +1 -0
  47. package/dist/engine/client.d.ts +211 -0
  48. package/dist/engine/client.d.ts.map +1 -0
  49. package/dist/engine/client.js +538 -0
  50. package/dist/engine/client.js.map +1 -0
  51. package/dist/engine/coordinator.d.ts +17 -0
  52. package/dist/engine/coordinator.d.ts.map +1 -0
  53. package/dist/engine/coordinator.js +60 -0
  54. package/dist/engine/coordinator.js.map +1 -0
  55. package/dist/engine/database.d.ts +661 -0
  56. package/dist/engine/database.d.ts.map +1 -0
  57. package/dist/engine/database.js +10313 -0
  58. package/dist/engine/database.js.map +1 -0
  59. package/dist/engine/defaults.d.ts +33 -0
  60. package/dist/engine/defaults.d.ts.map +1 -0
  61. package/dist/engine/defaults.js +87 -0
  62. package/dist/engine/defaults.js.map +1 -0
  63. package/dist/engine/dsl/db.d.ts +137 -0
  64. package/dist/engine/dsl/db.d.ts.map +1 -0
  65. package/dist/engine/dsl/db.js +161 -0
  66. package/dist/engine/dsl/db.js.map +1 -0
  67. package/dist/engine/dsl/expression.d.ts +172 -0
  68. package/dist/engine/dsl/expression.d.ts.map +1 -0
  69. package/dist/engine/dsl/expression.js +397 -0
  70. package/dist/engine/dsl/expression.js.map +1 -0
  71. package/dist/engine/dsl/index.d.ts +8 -0
  72. package/dist/engine/dsl/index.d.ts.map +1 -0
  73. package/dist/engine/dsl/index.js +7 -0
  74. package/dist/engine/dsl/index.js.map +1 -0
  75. package/dist/engine/dsl/live-query.d.ts +45 -0
  76. package/dist/engine/dsl/live-query.d.ts.map +1 -0
  77. package/dist/engine/dsl/live-query.js +116 -0
  78. package/dist/engine/dsl/live-query.js.map +1 -0
  79. package/dist/engine/dsl/mutations.d.ts +123 -0
  80. package/dist/engine/dsl/mutations.d.ts.map +1 -0
  81. package/dist/engine/dsl/mutations.js +241 -0
  82. package/dist/engine/dsl/mutations.js.map +1 -0
  83. package/dist/engine/dsl/select-query-builder.d.ts +148 -0
  84. package/dist/engine/dsl/select-query-builder.d.ts.map +1 -0
  85. package/dist/engine/dsl/select-query-builder.js +377 -0
  86. package/dist/engine/dsl/select-query-builder.js.map +1 -0
  87. package/dist/engine/dsl/sql-tag.d.ts +46 -0
  88. package/dist/engine/dsl/sql-tag.d.ts.map +1 -0
  89. package/dist/engine/dsl/sql-tag.js +85 -0
  90. package/dist/engine/dsl/sql-tag.js.map +1 -0
  91. package/dist/engine/dsl/types.d.ts +133 -0
  92. package/dist/engine/dsl/types.d.ts.map +1 -0
  93. package/dist/engine/dsl/types.js +9 -0
  94. package/dist/engine/dsl/types.js.map +1 -0
  95. package/dist/engine/errors.d.ts +55 -0
  96. package/dist/engine/errors.d.ts.map +1 -0
  97. package/dist/engine/errors.js +81 -0
  98. package/dist/engine/errors.js.map +1 -0
  99. package/dist/engine/fts.d.ts +93 -0
  100. package/dist/engine/fts.d.ts.map +1 -0
  101. package/dist/engine/fts.js +273 -0
  102. package/dist/engine/fts.js.map +1 -0
  103. package/dist/engine/group-index.d.ts +32 -0
  104. package/dist/engine/group-index.d.ts.map +1 -0
  105. package/dist/engine/group-index.js +366 -0
  106. package/dist/engine/group-index.js.map +1 -0
  107. package/dist/engine/index.d.ts +13 -0
  108. package/dist/engine/index.d.ts.map +1 -0
  109. package/dist/engine/index.js +12 -0
  110. package/dist/engine/index.js.map +1 -0
  111. package/dist/engine/join-index.d.ts +12 -0
  112. package/dist/engine/join-index.d.ts.map +1 -0
  113. package/dist/engine/join-index.js +217 -0
  114. package/dist/engine/join-index.js.map +1 -0
  115. package/dist/engine/live.d.ts +86 -0
  116. package/dist/engine/live.d.ts.map +1 -0
  117. package/dist/engine/live.js +289 -0
  118. package/dist/engine/live.js.map +1 -0
  119. package/dist/engine/memory.d.ts +37 -0
  120. package/dist/engine/memory.d.ts.map +1 -0
  121. package/dist/engine/memory.js +119 -0
  122. package/dist/engine/memory.js.map +1 -0
  123. package/dist/engine/optimizer.d.ts +20 -0
  124. package/dist/engine/optimizer.d.ts.map +1 -0
  125. package/dist/engine/optimizer.js +1170 -0
  126. package/dist/engine/optimizer.js.map +1 -0
  127. package/dist/engine/query-cache.d.ts +10 -0
  128. package/dist/engine/query-cache.d.ts.map +1 -0
  129. package/dist/engine/query-cache.js +58 -0
  130. package/dist/engine/query-cache.js.map +1 -0
  131. package/dist/engine/query.d.ts +844 -0
  132. package/dist/engine/query.d.ts.map +1 -0
  133. package/dist/engine/query.js +7450 -0
  134. package/dist/engine/query.js.map +1 -0
  135. package/dist/engine/schema-wire.d.ts +92 -0
  136. package/dist/engine/schema-wire.d.ts.map +1 -0
  137. package/dist/engine/schema-wire.js +115 -0
  138. package/dist/engine/schema-wire.js.map +1 -0
  139. package/dist/engine/schema.d.ts +394 -0
  140. package/dist/engine/schema.d.ts.map +1 -0
  141. package/dist/engine/schema.js +709 -0
  142. package/dist/engine/schema.js.map +1 -0
  143. package/dist/engine/sort-keys.d.ts +36 -0
  144. package/dist/engine/sort-keys.d.ts.map +1 -0
  145. package/dist/engine/sort-keys.js +60 -0
  146. package/dist/engine/sort-keys.js.map +1 -0
  147. package/dist/engine/sql-json.d.ts +30 -0
  148. package/dist/engine/sql-json.d.ts.map +1 -0
  149. package/dist/engine/sql-json.js +120 -0
  150. package/dist/engine/sql-json.js.map +1 -0
  151. package/dist/engine/sql-semantics.d.ts +31 -0
  152. package/dist/engine/sql-semantics.d.ts.map +1 -0
  153. package/dist/engine/sql-semantics.js +121 -0
  154. package/dist/engine/sql-semantics.js.map +1 -0
  155. package/dist/engine/vector.d.ts +87 -0
  156. package/dist/engine/vector.d.ts.map +1 -0
  157. package/dist/engine/vector.js +4227 -0
  158. package/dist/engine/vector.js.map +1 -0
  159. package/dist/engine/worker-host.d.ts +48 -0
  160. package/dist/engine/worker-host.d.ts.map +1 -0
  161. package/dist/engine/worker-host.js +501 -0
  162. package/dist/engine/worker-host.js.map +1 -0
  163. package/dist/engine/worker.d.ts +2 -0
  164. package/dist/engine/worker.d.ts.map +1 -0
  165. package/dist/engine/worker.js +14 -0
  166. package/dist/engine/worker.js.map +1 -0
  167. package/dist/index.d.ts +2 -0
  168. package/dist/index.d.ts.map +1 -0
  169. package/dist/index.js +2 -0
  170. package/dist/index.js.map +1 -0
  171. package/dist/plan/index.d.ts +17 -0
  172. package/dist/plan/index.d.ts.map +1 -0
  173. package/dist/plan/index.js +17 -0
  174. package/dist/plan/index.js.map +1 -0
  175. package/dist/storage/fixture-shape.d.ts +42 -0
  176. package/dist/storage/fixture-shape.d.ts.map +1 -0
  177. package/dist/storage/fixture-shape.js +146 -0
  178. package/dist/storage/fixture-shape.js.map +1 -0
  179. package/dist/storage/index.d.ts +5 -0
  180. package/dist/storage/index.d.ts.map +1 -0
  181. package/dist/storage/index.js +5 -0
  182. package/dist/storage/index.js.map +1 -0
  183. package/dist/storage/indexeddb.d.ts +139 -0
  184. package/dist/storage/indexeddb.d.ts.map +1 -0
  185. package/dist/storage/indexeddb.js +3161 -0
  186. package/dist/storage/indexeddb.js.map +1 -0
  187. package/dist/storage/memory.d.ts +121 -0
  188. package/dist/storage/memory.d.ts.map +1 -0
  189. package/dist/storage/memory.js +1455 -0
  190. package/dist/storage/memory.js.map +1 -0
  191. package/dist/storage/snapshot.d.ts +108 -0
  192. package/dist/storage/snapshot.d.ts.map +1 -0
  193. package/dist/storage/snapshot.js +276 -0
  194. package/dist/storage/snapshot.js.map +1 -0
  195. package/dist/storage/types.d.ts +895 -0
  196. package/dist/storage/types.d.ts.map +1 -0
  197. package/dist/storage/types.js +1549 -0
  198. package/dist/storage/types.js.map +1 -0
  199. package/dist/testing/index.d.ts +76 -0
  200. package/dist/testing/index.d.ts.map +1 -0
  201. package/dist/testing/index.js +233 -0
  202. package/dist/testing/index.js.map +1 -0
  203. package/dist/testing/seeds.d.ts +11 -0
  204. package/dist/testing/seeds.d.ts.map +1 -0
  205. package/dist/testing/seeds.js +50 -0
  206. package/dist/testing/seeds.js.map +1 -0
  207. package/dist/transactions/index.d.ts +148 -0
  208. package/dist/transactions/index.d.ts.map +1 -0
  209. package/dist/transactions/index.js +662 -0
  210. package/dist/transactions/index.js.map +1 -0
  211. package/dist/worker-protocol/index.d.ts +84 -0
  212. package/dist/worker-protocol/index.d.ts.map +1 -0
  213. package/dist/worker-protocol/index.js +117 -0
  214. package/dist/worker-protocol/index.js.map +1 -0
  215. package/package.json +66 -16
  216. package/sql-feature-matrix.json +1275 -0
  217. package/index.d.ts +0 -1
  218. package/index.js +0 -1
@@ -0,0 +1,4227 @@
1
+ import { cachedListMembership, distinctFromComparison, explicitNullOrder, isScalarFunctionName, likeMatches, orderOutputName, parseQuantified, quantifiedComparison, scalarFunctionValue, } from "./query.js";
2
+ import { bm25DocumentScore, cachedQueryTerms, FtsStatsAccumulator, fullTermsMask, renderDocumentValue, termFrequencies, termsMask, tokenize, } from "./fts.js";
3
+ import { ByteGroupIndex } from "./group-index.js";
4
+ import { ByteJoinIndex } from "./join-index.js";
5
+ import { QueryMemoryBudgetError, QueryMemoryContext, } from "./memory.js";
6
+ import { compareSqlStrings, defineSqlResultProperty } from "./sql-semantics.js";
7
+ import { buildSortKeyColumn } from "./sort-keys.js";
8
+ const DEFAULT_BATCH_ROWS = 2_048;
9
+ /** Above this, locating each IN member separately costs more than scanning between them. */
10
+ const MAX_SPLIT_LIST_MEMBERS = 64;
11
+ /**
12
+ * Upper bound on the direct-address slot space for compound dictionary-code grouping: 65,536
13
+ * slots reserve 512 KiB of references, small against the default budget while covering typical
14
+ * categorical column combinations.
15
+ */
16
+ const MULTI_CODE_GROUP_SLOT_CAP = 65_536;
17
+ const HASH_SPILL_SCAN_CHUNK_ROWS = 512;
18
+ const NULL_STRING_CODE = 0xffffffff;
19
+ const QUERY_REFERENCE_BYTES = 8;
20
+ const QUERY_VALUE_TAG_BYTES = 1;
21
+ const AGGREGATE_ACCUMULATOR_BYTES = 24;
22
+ /** Modeled sparse-map bookkeeping for one packed compound dictionary-code group. */
23
+ const PACKED_GROUP_ENTRY_BYTES = 24;
24
+ const SPILL_PAGE_MAGIC = 0x5350494c;
25
+ const SPILL_PAGE_HEADER_BYTES = 8;
26
+ const vectorTextEncoder = new TextEncoder();
27
+ export function createColumnarTable(name, columns, uniqueKey) {
28
+ const first = columns.values().next().value;
29
+ const rowCount = first?.values.length ?? 0;
30
+ const vectors = new Map();
31
+ for (const [columnName, column] of columns) {
32
+ if (column.values.length !== rowCount) {
33
+ throw new Error(`Column row count mismatch: ${name}.${columnName}`);
34
+ }
35
+ vectors.set(columnName, createVector(column));
36
+ }
37
+ if (uniqueKey !== undefined && !vectors.has(uniqueKey)) {
38
+ throw new Error(`Unique-key vector is missing: ${name}.${uniqueKey}`);
39
+ }
40
+ return {
41
+ name,
42
+ rowCount,
43
+ columns: vectors,
44
+ ...(uniqueKey === undefined ? {} : { uniqueKey }),
45
+ };
46
+ }
47
+ export function columnarTableFromRows(name, rows, projectedColumnNames) {
48
+ const columnNameSet = new Set(projectedColumnNames);
49
+ if (projectedColumnNames === undefined) {
50
+ for (const row of rows) {
51
+ for (const columnName of Object.keys(row))
52
+ columnNameSet.add(columnName);
53
+ }
54
+ }
55
+ const columnNames = [...columnNameSet];
56
+ if (columnNames.length === 0)
57
+ return { name, rowCount: rows.length, columns: new Map() };
58
+ const columns = new Map();
59
+ for (const columnName of columnNames) {
60
+ const values = rows.map((row) => row[columnName] ?? null);
61
+ columns.set(columnName, { type: inferVectorType(values), values });
62
+ }
63
+ return createColumnarTable(name, columns);
64
+ }
65
+ export function prepareVectorQuery(plan, inputTables, options = {}) {
66
+ const rootMemory = options.memoryContext ?? new QueryMemoryContext();
67
+ const ownsRootMemory = options.memoryContext === undefined;
68
+ const retainedMemory = rootMemory.createChild();
69
+ try {
70
+ for (const table of new Set(inputTables.values())) {
71
+ retainedMemory.reserve(columnarTablePayloadBytes(table), `Columnar table ${table.name}`);
72
+ }
73
+ const bound = bindPlan(plan, inputTables, retainedMemory, options.ftsStats);
74
+ let closed = false;
75
+ return {
76
+ get memoryUsage() {
77
+ return rootMemory.usage;
78
+ },
79
+ execute() {
80
+ if (closed)
81
+ throw new Error("Prepared vector query is closed");
82
+ const executionMemory = retainedMemory.createChild();
83
+ try {
84
+ return executeBoundPlan(bound, executionMemory);
85
+ }
86
+ finally {
87
+ executionMemory.close();
88
+ }
89
+ },
90
+ async executeAsync(executionOptions = {}) {
91
+ if (closed)
92
+ throw new Error("Prepared vector query is closed");
93
+ const canSpillSort = bound.orderBy.length > 0 && !bound.grouped;
94
+ // An unordered grouped plan spills too: the empty ordering makes the pairwise merge a
95
+ // stable concatenation, and partition-wise accumulation bounds peak group state.
96
+ const canSpillHash = bound.grouped && bound.groupBy.length > 0;
97
+ if (executionOptions.spillStore === undefined || (!canSpillSort && !canSpillHash)) {
98
+ if (executionOptions.loadScanWindow === undefined)
99
+ return this.execute();
100
+ const executionMemory = retainedMemory.createChild();
101
+ try {
102
+ return await executeBoundPlanAsync(bound, executionMemory, executionOptions);
103
+ }
104
+ finally {
105
+ executionMemory.close();
106
+ }
107
+ }
108
+ const executionMemory = retainedMemory.createChild();
109
+ try {
110
+ return canSpillHash
111
+ ? await executeBoundPlanWithHashSpill(bound, executionMemory, executionOptions)
112
+ : await executeBoundPlanWithSortSpill(bound, executionMemory, executionOptions);
113
+ }
114
+ finally {
115
+ executionMemory.close();
116
+ }
117
+ },
118
+ close() {
119
+ if (closed)
120
+ return;
121
+ closed = true;
122
+ retainedMemory.close();
123
+ if (ownsRootMemory)
124
+ rootMemory.close();
125
+ },
126
+ };
127
+ }
128
+ catch (error) {
129
+ retainedMemory.close();
130
+ if (ownsRootMemory)
131
+ rootMemory.close();
132
+ throw error;
133
+ }
134
+ }
135
+ function createVector(input) {
136
+ validateVectorType(input.type);
137
+ const length = input.values.length;
138
+ const validity = new Uint8Array(Math.ceil(length / 8));
139
+ if (input.type === "string") {
140
+ const dictionary = [];
141
+ const dictionaryIndex = new Map();
142
+ const codes = new Uint32Array(length);
143
+ codes.fill(NULL_STRING_CODE);
144
+ for (let index = 0; index < length; index += 1) {
145
+ const value = input.values[index];
146
+ if (value === null)
147
+ continue;
148
+ if (typeof value !== "string")
149
+ throw vectorTypeError(input.type, value);
150
+ setValid(validity, index);
151
+ let code = dictionaryIndex.get(value);
152
+ if (code === undefined) {
153
+ code = dictionary.length;
154
+ dictionary.push(value);
155
+ dictionaryIndex.set(value, code);
156
+ }
157
+ codes[index] = code;
158
+ }
159
+ return { kind: "string", length, validity, codes, dictionary };
160
+ }
161
+ if (input.type === "boolean") {
162
+ const values = new Uint8Array(length);
163
+ for (let index = 0; index < length; index += 1) {
164
+ const value = input.values[index];
165
+ if (value === null)
166
+ continue;
167
+ if (typeof value !== "boolean")
168
+ throw vectorTypeError(input.type, value);
169
+ setValid(validity, index);
170
+ values[index] = value ? 1 : 0;
171
+ }
172
+ return { kind: "boolean", length, validity, values };
173
+ }
174
+ const values = new Float64Array(length);
175
+ for (let index = 0; index < length; index += 1) {
176
+ const value = input.values[index];
177
+ if (value === null)
178
+ continue;
179
+ const numericValue = input.type === "datetime"
180
+ ? value instanceof Date
181
+ ? value.getTime()
182
+ : Number.NaN
183
+ : typeof value === "number"
184
+ ? value
185
+ : Number.NaN;
186
+ if (!Number.isFinite(numericValue))
187
+ throw vectorTypeError(input.type, value);
188
+ setValid(validity, index);
189
+ values[index] = numericValue;
190
+ }
191
+ return input.type === "datetime"
192
+ ? { kind: "datetime", length, validity, values }
193
+ : { kind: "number", length, validity, values };
194
+ }
195
+ function validateVectorType(type) {
196
+ if (type !== "boolean" && type !== "number" && type !== "string" && type !== "datetime") {
197
+ throw new TypeError(`Unsupported vector type: ${String(type)}`);
198
+ }
199
+ }
200
+ function vectorTypeError(type, value) {
201
+ return new TypeError(`Invalid ${type} vector value: ${String(value)}`);
202
+ }
203
+ function inferVectorType(values) {
204
+ const value = values.find((candidate) => candidate !== null);
205
+ if (typeof value === "boolean")
206
+ return "boolean";
207
+ if (typeof value === "number")
208
+ return "number";
209
+ if (value instanceof Date)
210
+ return "datetime";
211
+ return "string";
212
+ }
213
+ function setValid(bitmap, index) {
214
+ const byte = index >>> 3;
215
+ bitmap[byte] = (bitmap[byte] ?? 0) | (1 << (index & 7));
216
+ }
217
+ function isValid(bitmap, index) {
218
+ return ((bitmap[index >>> 3] ?? 0) & (1 << (index & 7))) !== 0;
219
+ }
220
+ export function vectorValue(vector, rowIndex) {
221
+ if (rowIndex < 0 || rowIndex >= vector.length)
222
+ return null;
223
+ const window = vector.window;
224
+ let slot = rowIndex;
225
+ if (window !== undefined) {
226
+ slot = rowIndex - window.start;
227
+ if (slot < 0 || slot >= window.length) {
228
+ throw new RangeError(`Streamed vector row ${String(rowIndex)} is outside the resident window ${String(window.start)}..${String(window.start + window.length)}`);
229
+ }
230
+ }
231
+ if (!isValid(vector.validity, slot))
232
+ return null;
233
+ if (vector.kind === "boolean")
234
+ return vector.values[slot] === 1;
235
+ if (vector.kind === "number")
236
+ return vector.values[slot] ?? 0;
237
+ if (vector.kind === "datetime")
238
+ return new Date(vector.values[slot] ?? 0);
239
+ const code = vector.codes[slot] ?? NULL_STRING_CODE;
240
+ return code === NULL_STRING_CODE ? null : (vector.dictionary[code] ?? null);
241
+ }
242
+ function columnarTablePayloadBytes(table) {
243
+ let total = 0;
244
+ for (const vector of table.columns.values()) {
245
+ total = safeMemorySum(total, vector.validity.byteLength, "Column vector payload");
246
+ if (vector.kind === "string") {
247
+ total = safeMemorySum(total, vector.codes.byteLength, "Column vector payload");
248
+ // One byte per UTF-16 code unit, matching queryValuePayloadBytes: exact for Latin-1
249
+ // and O(1) per entry instead of a throwaway UTF-8 encode of the whole dictionary.
250
+ for (const value of vector.dictionary) {
251
+ total = safeMemorySum(total, value.length, "String dictionary payload");
252
+ }
253
+ }
254
+ else {
255
+ total = safeMemorySum(total, vector.values.byteLength, "Column vector payload");
256
+ }
257
+ }
258
+ return total;
259
+ }
260
+ /** One conjunction of a disjunction: plain conditions only, or undefined if any leaf is not. */
261
+ function conjunctionLeaves(expression, output) {
262
+ if (expression.kind === "logical" && expression.operator === "and") {
263
+ return (conjunctionLeaves(expression.left, output) && conjunctionLeaves(expression.right, output));
264
+ }
265
+ if (expression.kind !== "condition")
266
+ return false;
267
+ // A quantified comparison or a subquery leaf carries evaluation rules of its own; leaving it
268
+ // to the generic evaluator is always correct, just slower.
269
+ if (expression.left.kind === "subquery" || expression.right.kind === "subquery")
270
+ return false;
271
+ output.push({
272
+ left: expression.left,
273
+ operator: expression.operator,
274
+ right: expression.right,
275
+ ...(expression.escape === undefined ? {} : { escape: expression.escape }),
276
+ });
277
+ return true;
278
+ }
279
+ /**
280
+ * Reads a predicate as an OR of AND-groups. Only a bare boolean expression -- one the parser
281
+ * wrapped in IS TRUE -- can be a disjunction, and every leaf must be a plain condition, so the
282
+ * branches evaluate under the same three-valued rules the whole expression did: a branch is
283
+ * taken only where it is true, and false and unknown are both simply not taken.
284
+ */
285
+ function disjunctiveNormalForm(predicate) {
286
+ if (predicate.operator !== "IS TRUE")
287
+ return undefined;
288
+ if (predicate.left.kind !== "logical" || predicate.left.operator !== "or")
289
+ return undefined;
290
+ const branches = [];
291
+ const visit = (node) => {
292
+ if (node.kind === "logical" && node.operator === "or") {
293
+ return visit(node.left) && visit(node.right);
294
+ }
295
+ const group = [];
296
+ if (!conjunctionLeaves(node, group))
297
+ return false;
298
+ branches.push(group);
299
+ return true;
300
+ };
301
+ return visit(predicate.left) && branches.length > 1 ? branches : undefined;
302
+ }
303
+ function bindPlan(plan, tables, memory, ftsStats) {
304
+ const sources = [plan.base, ...plan.joins];
305
+ const sourceTables = sources.map((source) => {
306
+ const table = tables.get(source.table);
307
+ if (table === undefined)
308
+ throw new TypeError(`Unknown table: ${source.table}`);
309
+ return table;
310
+ });
311
+ const sourceAliases = sources.map((source) => source.alias);
312
+ if (new Set(sourceAliases).size !== sourceAliases.length)
313
+ throw new TypeError("Table aliases must be unique");
314
+ const aggregateSpecs = [];
315
+ const aggregateIndexes = new Map();
316
+ const ftsBySignature = new Map();
317
+ const bind = (expression) => bindExpression(expression, sources.map((source, index) => ({ alias: source.alias, table: sourceTables[index] })), aggregateSpecs, aggregateIndexes, memory, ftsBySignature, ftsStats);
318
+ const select = plan.select.map((item) => ({
319
+ expression: bind(item.expression),
320
+ alias: item.alias,
321
+ }));
322
+ const groupBy = plan.groupBy.map(bind);
323
+ const groupIndexBySignature = new Map(groupBy.map((expression, index) => [expression.signature, index]));
324
+ const bindPredicate = (predicate) => {
325
+ const bound = {
326
+ left: bind(predicate.left),
327
+ operator: predicate.operator,
328
+ right: bind(predicate.right),
329
+ ...(predicate.escape === undefined ? {} : { escape: predicate.escape }),
330
+ };
331
+ const dictionaryEquality = detectDictionaryEquality(bound);
332
+ if (dictionaryEquality !== undefined)
333
+ return { ...bound, dictionaryEquality };
334
+ const dictionaryLike = detectDictionaryLike(bound);
335
+ if (dictionaryLike !== undefined)
336
+ return { ...bound, dictionaryLike };
337
+ const primitive = detectPrimitiveComparison(bound);
338
+ if (primitive !== undefined)
339
+ return { ...bound, primitive };
340
+ const primitiveIn = detectPrimitiveInList(bound);
341
+ return primitiveIn === undefined ? bound : { ...bound, primitiveIn };
342
+ };
343
+ const predicates = plan.predicates.map((predicate) => {
344
+ const bound = bindPredicate(predicate);
345
+ if (bound.primitive !== undefined || bound.primitiveIn !== undefined)
346
+ return bound;
347
+ if (bound.dictionaryEquality !== undefined || bound.dictionaryLike !== undefined)
348
+ return bound;
349
+ const branches = disjunctiveNormalForm(predicate);
350
+ if (branches === undefined)
351
+ return bound;
352
+ return {
353
+ ...bound,
354
+ disjunction: { branches: branches.map((group) => group.map(bindPredicate)) },
355
+ };
356
+ });
357
+ const having = plan.having.map((predicate) => ({
358
+ left: bind(predicate.left),
359
+ operator: predicate.operator,
360
+ right: bind(predicate.right),
361
+ }));
362
+ const standardJoins = plan.joins.map((join, joinIndex) => {
363
+ const source = joinIndex + 1;
364
+ if (join.on !== undefined) {
365
+ const condition = bind(join.on);
366
+ if ([...expressionSources(condition)].some((used) => used > source)) {
367
+ throw new TypeError(`JOIN condition for ${join.alias} references a later table`);
368
+ }
369
+ const table = required(sourceTables[source], `JOIN table is missing: ${join.table}`);
370
+ const placeholder = { kind: "literal", value: null, signature: "" };
371
+ return {
372
+ kind: join.kind,
373
+ buildSource: source,
374
+ probe: placeholder,
375
+ build: placeholder,
376
+ lookup: { unique: false, firstRow: () => -1, nextRow: () => -1 },
377
+ loop: { condition, rowCount: table.rowCount },
378
+ };
379
+ }
380
+ const left = bind(join.left);
381
+ const right = bind(join.right);
382
+ const rightUsesBuild = expressionSources(right).has(source);
383
+ const leftUsesBuild = expressionSources(left).has(source);
384
+ if (leftUsesBuild === rightUsesBuild) {
385
+ throw new TypeError(`JOIN condition must reference the new alias ${join.alias} on one side`);
386
+ }
387
+ const build = rightUsesBuild ? right : left;
388
+ const probe = rightUsesBuild ? left : right;
389
+ const buildSources = expressionSources(build);
390
+ if (buildSources.size !== 1 || !buildSources.has(source)) {
391
+ throw new TypeError(`JOIN build expression for ${join.alias} must reference only that table`);
392
+ }
393
+ if ([...expressionSources(probe)].some((probeSource) => probeSource >= source)) {
394
+ throw new TypeError(`JOIN probe expression for ${join.alias} references a later table`);
395
+ }
396
+ return createBoundJoin(join.kind, source, probe, build, required(sourceTables[source], `JOIN table is missing: ${join.table}`), memory);
397
+ });
398
+ // A wildcard select projects the materialized columns of each source, so ORDER BY resolves
399
+ // against those same names.
400
+ const orderSources = sources.map((source, index) => ({
401
+ alias: source.alias,
402
+ columns: [...(sourceTables[index]?.columns.keys() ?? [])],
403
+ }));
404
+ const orderBy = plan.orderBy.map(({ expression, direction, nulls }) => ({
405
+ outputName: orderOutputName(expression, plan.select, orderSources),
406
+ direction,
407
+ ...(nulls === undefined ? {} : { nulls }),
408
+ }));
409
+ const grouped = groupBy.length > 0 || aggregateSpecs.length > 0;
410
+ // A single bare string-column GROUP BY can group on dictionary codes: identical values share a
411
+ // code within one vector, so a code-indexed slot table replaces per-row hashing. Streamed
412
+ // vectors swap dictionaries per window; the accumulator remaps its slot table by value on
413
+ // each swap, so windowed vectors qualify too.
414
+ const groupColumn = groupBy.length === 1 ? groupBy[0] : undefined;
415
+ const codeGrouping = grouped && groupColumn?.kind === "column" && groupColumn.vector.kind === "string"
416
+ ? { source: groupColumn.source, vector: groupColumn.vector }
417
+ : undefined;
418
+ return {
419
+ sourceTables,
420
+ sourceAliases,
421
+ scanSource: 0,
422
+ joins: standardJoins,
423
+ predicates,
424
+ having,
425
+ groupBy,
426
+ groupIndexBySignature,
427
+ aggregates: aggregateSpecs,
428
+ select,
429
+ orderBy,
430
+ grouped,
431
+ ...(codeGrouping === undefined ? {} : { codeGrouping }),
432
+ wildcard: plan.select[0]?.expression.kind === "wildcard",
433
+ ...(plan.limit === undefined ? {} : { limit: plan.limit }),
434
+ ...(plan.offset === undefined ? {} : { offset: plan.offset }),
435
+ };
436
+ }
437
+ function bindExpression(expression, sources, aggregateSpecs, aggregateIndexes, memory, ftsBySignature, ftsStats) {
438
+ const signature = JSON.stringify(expression);
439
+ if (expression.kind === "subquery") {
440
+ throw new TypeError("Subqueries are only supported in WHERE, HAVING, SELECT, and IN");
441
+ }
442
+ if (expression.kind === "parameter") {
443
+ throw new TypeError(`Placeholder $${String(expression.index + 1)} is unbound; pass parameters when executing`);
444
+ }
445
+ if (expression.kind === "fts") {
446
+ // The canonical search shape evaluates the same node in WHERE, SELECT, and ORDER BY;
447
+ // sharing one bound node by signature means one set of dictionary tables, one corpus
448
+ // pass, and one budget reservation instead of three.
449
+ const shared = ftsBySignature?.get(signature);
450
+ if (shared !== undefined)
451
+ return shared;
452
+ if (expression.op === "bm25" && sources.length > 1) {
453
+ throw new TypeError("BM25 requires a single-table query");
454
+ }
455
+ // Every engine entry expands "*" against its catalog before plans reach the executors.
456
+ if (expression.columns === "*") {
457
+ throw new TypeError("Full-text search columns must be expanded before binding");
458
+ }
459
+ const columns = expression.columns.map((columnExpression) => {
460
+ if (columnExpression.kind !== "column") {
461
+ throw new TypeError("Full-text search takes column references");
462
+ }
463
+ const bound = bindExpression(columnExpression, sources, aggregateSpecs, aggregateIndexes, memory, ftsBySignature, ftsStats);
464
+ if (bound.kind !== "column") {
465
+ throw new TypeError("Full-text search takes column references");
466
+ }
467
+ if (bound.vector.kind === "boolean") {
468
+ throw new TypeError(`Full-text search cannot search a boolean column: ${columnExpression.reference}`);
469
+ }
470
+ return bound;
471
+ });
472
+ const servedStats = ftsStats?.get(signature);
473
+ // `.search()` evaluates a MATCH and a BM25 node over the same columns and query; the
474
+ // per-dictionary term tables depend only on columns + query, so the twin ops share one
475
+ // cache array (scoring fields upgrade lazily on the first scored use).
476
+ const sibling = ftsBySignature?.get(JSON.stringify({ ...expression, op: expression.op === "match" ? "bm25" : "match" }));
477
+ const bound = {
478
+ kind: "fts",
479
+ op: expression.op,
480
+ columns,
481
+ terms: cachedQueryTerms(expression.query),
482
+ caches: sibling?.caches ?? columns.map(() => null),
483
+ ...(servedStats === undefined ? {} : { stats: servedStats }),
484
+ ...(memory === undefined ? {} : { memory }),
485
+ signature,
486
+ };
487
+ ftsBySignature?.set(signature, bound);
488
+ return bound;
489
+ }
490
+ if (expression.kind === "window") {
491
+ throw new TypeError("Window functions are only allowed in the select list");
492
+ }
493
+ if (expression.kind === "list") {
494
+ return {
495
+ kind: "list",
496
+ items: expression.items.map((item) => bindExpression(item, sources, aggregateSpecs, aggregateIndexes, memory, ftsBySignature, ftsStats)),
497
+ signature,
498
+ };
499
+ }
500
+ if (expression.kind === "literal" || expression.kind === "wildcard") {
501
+ return { ...expression, signature };
502
+ }
503
+ if (expression.kind === "column") {
504
+ const parts = expression.reference.split(".");
505
+ let matches;
506
+ if (parts.length === 2) {
507
+ const source = sources.findIndex(({ alias }) => alias === parts[0]);
508
+ if (source < 0)
509
+ throw new TypeError(`Unknown table alias: ${parts[0] ?? ""}`);
510
+ matches = [{ source, column: parts[1] ?? "" }];
511
+ }
512
+ else {
513
+ const column = parts[0] ?? "";
514
+ matches = sources.flatMap(({ table }, source) => table?.columns.has(column) === true ? [{ source, column }] : []);
515
+ }
516
+ const match = matches.length === 1 ? matches[0] : undefined;
517
+ if (match === undefined || sources[match.source]?.table?.columns.has(match.column) !== true) {
518
+ throw new TypeError(`Ambiguous or missing column: ${expression.reference}`);
519
+ }
520
+ return {
521
+ kind: "column",
522
+ source: match.source,
523
+ column: match.column,
524
+ vector: required(sources[match.source]?.table?.columns.get(match.column), `Bound vector is missing: ${expression.reference}`),
525
+ signature,
526
+ };
527
+ }
528
+ if (expression.kind === "exists") {
529
+ throw new TypeError("EXISTS subqueries must be resolved before execution");
530
+ }
531
+ if (expression.kind === "binary" || expression.kind === "condition") {
532
+ return {
533
+ kind: expression.kind,
534
+ operator: expression.operator,
535
+ left: bindExpression(expression.left, sources, aggregateSpecs, aggregateIndexes, memory, ftsBySignature, ftsStats),
536
+ right: bindExpression(expression.right, sources, aggregateSpecs, aggregateIndexes, memory, ftsBySignature, ftsStats),
537
+ ...(expression.kind === "condition" && expression.escape !== undefined
538
+ ? { escape: expression.escape }
539
+ : {}),
540
+ signature,
541
+ };
542
+ }
543
+ if (expression.kind === "logical") {
544
+ return {
545
+ kind: "logical",
546
+ operator: expression.operator,
547
+ left: bindExpression(expression.left, sources, aggregateSpecs, aggregateIndexes, memory, ftsBySignature, ftsStats),
548
+ right: bindExpression(expression.right, sources, aggregateSpecs, aggregateIndexes, memory, ftsBySignature, ftsStats),
549
+ signature,
550
+ };
551
+ }
552
+ if (expression.kind === "not") {
553
+ return {
554
+ kind: "not",
555
+ operand: bindExpression(expression.operand, sources, aggregateSpecs, aggregateIndexes, memory, ftsBySignature, ftsStats),
556
+ signature,
557
+ };
558
+ }
559
+ if (expression.kind === "case") {
560
+ const otherwise = expression.otherwise === undefined
561
+ ? undefined
562
+ : bindExpression(expression.otherwise, sources, aggregateSpecs, aggregateIndexes, memory, ftsBySignature, ftsStats);
563
+ return {
564
+ kind: "case",
565
+ branches: expression.branches.map((branch) => ({
566
+ when: bindExpression(branch.when, sources, aggregateSpecs, aggregateIndexes, memory, ftsBySignature, ftsStats),
567
+ then: bindExpression(branch.then, sources, aggregateSpecs, aggregateIndexes, memory, ftsBySignature, ftsStats),
568
+ })),
569
+ ...(otherwise === undefined ? {} : { otherwise }),
570
+ signature,
571
+ };
572
+ }
573
+ const arguments_ = expression.arguments.map((argument) => bindExpression(argument, sources, aggregateSpecs, aggregateIndexes, memory, ftsBySignature, ftsStats));
574
+ if (isScalarFunctionName(expression.name)) {
575
+ return { kind: "call", name: expression.name, arguments: arguments_, signature };
576
+ }
577
+ let aggregateIndex = aggregateIndexes.get(signature);
578
+ if (aggregateIndex === undefined) {
579
+ aggregateIndex = aggregateSpecs.length;
580
+ aggregateIndexes.set(signature, aggregateIndex);
581
+ const argument = required(arguments_[0], `${expression.name} argument is missing`);
582
+ const rawDatetime = (expression.name === "MIN" || expression.name === "MAX" || expression.name === "COUNT") &&
583
+ argument.kind === "column" &&
584
+ argument.vector.kind === "datetime"
585
+ ? { source: argument.source, vector: argument.vector }
586
+ : undefined;
587
+ const rawNumber = argument.kind === "column" && argument.vector.kind === "number"
588
+ ? { source: argument.source, vector: argument.vector }
589
+ : undefined;
590
+ aggregateSpecs.push({
591
+ name: expression.name,
592
+ argument,
593
+ // The signature this slot was keyed by is the JSON of the compiled call, which carries the
594
+ // flag — so COUNT(x) and COUNT(DISTINCT x) in one select land in separate slots.
595
+ ...(expression.distinct === true ? { distinct: true } : {}),
596
+ ...(rawDatetime === undefined ? {} : { rawDatetime }),
597
+ ...(rawNumber === undefined ? {} : { rawNumber }),
598
+ });
599
+ }
600
+ return {
601
+ kind: "call",
602
+ name: expression.name,
603
+ arguments: arguments_,
604
+ aggregateIndex,
605
+ signature,
606
+ };
607
+ }
608
+ function boundChildren(expression) {
609
+ if (expression.kind === "binary" || expression.kind === "condition") {
610
+ return [expression.left, expression.right];
611
+ }
612
+ if (expression.kind === "logical")
613
+ return [expression.left, expression.right];
614
+ if (expression.kind === "not")
615
+ return [expression.operand];
616
+ if (expression.kind === "call")
617
+ return [...expression.arguments];
618
+ if (expression.kind === "list")
619
+ return [...expression.items];
620
+ if (expression.kind === "case") {
621
+ return [
622
+ ...expression.branches.flatMap((branch) => [branch.when, branch.then]),
623
+ ...(expression.otherwise === undefined ? [] : [expression.otherwise]),
624
+ ];
625
+ }
626
+ if (expression.kind === "fts")
627
+ return [...expression.columns];
628
+ return [];
629
+ }
630
+ /**
631
+ * Rebuilds one string column's per-dictionary term table when the resident dictionary changes
632
+ * (streamed vectors swap dictionaries per window). Each dictionary entry tokenizes exactly once;
633
+ * rows then combine per-code masks. The table is real retained memory — one Uint32 per
634
+ * dictionary entry — so it reserves against the query budget, releasing the previous window's
635
+ * reservation on swap.
636
+ */
637
+ function ensureFtsDictionaryCache(expression, columnIndex, vector, scoring) {
638
+ let cache = expression.caches[columnIndex];
639
+ if (cache === null || cache === undefined) {
640
+ cache = { dictionary: undefined, termMask: new Uint32Array(0) };
641
+ expression.caches[columnIndex] = cache;
642
+ }
643
+ // A cache built for matching upgrades in place when scoring first needs the same
644
+ // dictionary's token counts and frequencies (twin MATCH/BM25 nodes share cache arrays).
645
+ // Scoring demand is sticky: once a scored read upgrades, later window rebuilds tokenize
646
+ // once and build both tables instead of a mask-only pass plus an immediate upgrade.
647
+ if (cache.dictionary !== vector.dictionary || (scoring && cache.termTf === undefined)) {
648
+ const withScores = scoring || cache.scoring === true;
649
+ const termCount = expression.terms.length;
650
+ // Match tables cost one Uint32 per entry; scoring adds a token count and per-term
651
+ // frequencies, all part of the modeled query memory.
652
+ const bytesPerEntry = 4 * (withScores ? termCount + 2 : 1);
653
+ cache.reservation?.release();
654
+ delete cache.reservation;
655
+ const reservation = expression.memory?.reserve(vector.dictionary.length * bytesPerEntry, "Full-text dictionary match table");
656
+ if (reservation !== undefined)
657
+ cache.reservation = reservation;
658
+ const masks = new Uint32Array(vector.dictionary.length);
659
+ const tokenCount = withScores ? new Uint32Array(vector.dictionary.length) : undefined;
660
+ const termTf = withScores ? new Uint32Array(vector.dictionary.length * termCount) : undefined;
661
+ for (let code = 0; code < vector.dictionary.length; code += 1) {
662
+ const tokens = tokenize(vector.dictionary[code] ?? "");
663
+ masks[code] = termsMask(tokens, expression.terms);
664
+ if (tokenCount !== undefined)
665
+ tokenCount[code] = tokens.length;
666
+ if (termTf !== undefined) {
667
+ const frequencies = termFrequencies(tokens, expression.terms);
668
+ for (let index = 0; index < termCount; index += 1) {
669
+ termTf[code * termCount + index] = frequencies[index] ?? 0;
670
+ }
671
+ }
672
+ }
673
+ cache.dictionary = vector.dictionary;
674
+ cache.termMask = masks;
675
+ // A mask-only rebuild must not leave a previous dictionary's scoring tables behind: the
676
+ // next scored read checks `termTf === undefined` to decide whether to upgrade.
677
+ if (tokenCount !== undefined)
678
+ cache.tokenCount = tokenCount;
679
+ else
680
+ delete cache.tokenCount;
681
+ if (termTf !== undefined)
682
+ cache.termTf = termTf;
683
+ else
684
+ delete cache.termTf;
685
+ cache.scoring = withScores;
686
+ }
687
+ return cache;
688
+ }
689
+ /**
690
+ * Resolves one column's row index for the three evaluation shapes without a per-row closure:
691
+ * batch evaluation carries per-source row arrays, join-loop evaluation one row index per
692
+ * source, and the statistics pass addresses vectors by absolute row.
693
+ */
694
+ function ftsRowIndex(column, batch, rowsBySource, row) {
695
+ if (batch !== null)
696
+ return batch.rowsBySource[column.source]?.[row] ?? -1;
697
+ if (rowsBySource !== null)
698
+ return rowsBySource[column.source] ?? -1;
699
+ return row;
700
+ }
701
+ /**
702
+ * One row's document accumulated over the bound columns — the single owner of the per-column
703
+ * rule (dictionary tables for strings, render-and-tokenize for numbers and datetimes) shared
704
+ * by matching, scoring, and the statistics pass, so the three can never drift. Writes into the
705
+ * node's reusable scratch (zero allocations per row for string columns) and returns it.
706
+ * `wantScores` additionally gathers token length and per-term frequencies, and only bm25
707
+ * nodes carry the dictionary tables for those; match-only accumulation may stop early once
708
+ * every term is covered.
709
+ */
710
+ function accumulateFtsRow(expression, batch, rowsBySource, row, wantScores) {
711
+ const terms = expression.terms;
712
+ const into = (expression.scratchRow ??= {
713
+ present: false,
714
+ mask: 0,
715
+ length: 0,
716
+ frequencies: new Array(terms.length).fill(0),
717
+ });
718
+ const fullMask = fullTermsMask(terms.length);
719
+ into.present = false;
720
+ into.mask = 0;
721
+ into.length = 0;
722
+ if (wantScores)
723
+ into.frequencies.fill(0);
724
+ for (let index = 0; index < expression.columns.length; index += 1) {
725
+ const column = expression.columns[index];
726
+ if (column === undefined)
727
+ continue;
728
+ const rowIndex = ftsRowIndex(column, batch, rowsBySource, row);
729
+ if (column.vector.kind === "string") {
730
+ const code = stringCodeAt(column.vector, rowIndex);
731
+ if (code === undefined)
732
+ continue;
733
+ into.present = true;
734
+ if (terms.length === 0 && !wantScores)
735
+ continue;
736
+ const cache = ensureFtsDictionaryCache(expression, index, column.vector, wantScores);
737
+ into.mask |= cache.termMask[code] ?? 0;
738
+ if (wantScores) {
739
+ into.length += cache.tokenCount?.[code] ?? 0;
740
+ const termTf = cache.termTf;
741
+ if (termTf !== undefined) {
742
+ for (let term = 0; term < terms.length; term += 1) {
743
+ into.frequencies[term] =
744
+ (into.frequencies[term] ?? 0) + (termTf[code * terms.length + term] ?? 0);
745
+ }
746
+ }
747
+ }
748
+ }
749
+ else {
750
+ const rendered = renderDocumentValue(vectorValue(column.vector, rowIndex));
751
+ if (rendered === undefined)
752
+ continue;
753
+ into.present = true;
754
+ if (terms.length === 0 && !wantScores)
755
+ continue;
756
+ const tokens = tokenize(rendered);
757
+ if (wantScores) {
758
+ into.length += tokens.length;
759
+ const partial = termFrequencies(tokens, terms);
760
+ for (let term = 0; term < terms.length; term += 1) {
761
+ const tf = partial[term] ?? 0;
762
+ into.frequencies[term] = (into.frequencies[term] ?? 0) + tf;
763
+ if (tf > 0)
764
+ into.mask |= 1 << term;
765
+ }
766
+ }
767
+ else {
768
+ into.mask |= termsMask(tokens, terms);
769
+ }
770
+ }
771
+ if (!wantScores && terms.length > 0 && (into.mask & fullMask) === fullMask)
772
+ return into;
773
+ }
774
+ return into;
775
+ }
776
+ /**
777
+ * One whole-scan pass computing BM25 corpus statistics for a bound scoring node, feeding the
778
+ * shared accumulator so its definition (every row is a document; all-null rows have length 0)
779
+ * stays identical across producers. Requires a fully materialized scan; streamed scoring plans
780
+ * carry index-served statistics instead.
781
+ */
782
+ function computeBoundFtsStats(expression) {
783
+ for (const column of expression.columns) {
784
+ if (column.vector.window !== undefined) {
785
+ throw new TypeError("BM25 requires a materialized scan");
786
+ }
787
+ }
788
+ const rowCount = expression.columns[0]?.vector.length ?? 0;
789
+ const accumulator = new FtsStatsAccumulator(expression.terms);
790
+ for (let row = 0; row < rowCount; row += 1) {
791
+ const document = accumulateFtsRow(expression, null, null, row, true);
792
+ accumulator.addDocumentCounts(document.mask, document.length);
793
+ }
794
+ const stats = accumulator.stats;
795
+ expression.stats = stats;
796
+ return stats;
797
+ }
798
+ /** Document-level BM25 score for one row; null when every column is null. */
799
+ function ftsBm25BatchValue(expression, batch, rowsBySource, row) {
800
+ const stats = expression.stats ?? computeBoundFtsStats(expression);
801
+ const document = accumulateFtsRow(expression, batch, rowsBySource, row, true);
802
+ if (!document.present)
803
+ return null;
804
+ return bm25DocumentScore(document.frequencies, document.length, stats);
805
+ }
806
+ /** Document-level MATCH over the bound columns of one row; null when every column is null. */
807
+ function ftsBatchTruth(expression, batch, rowsBySource, row) {
808
+ const document = accumulateFtsRow(expression, batch, rowsBySource, row, false);
809
+ if (!document.present)
810
+ return null;
811
+ const fullMask = fullTermsMask(expression.terms.length);
812
+ return expression.terms.length > 0 && (document.mask & fullMask) === fullMask;
813
+ }
814
+ function expressionSources(expression) {
815
+ if (expression.kind === "column")
816
+ return new Set([expression.source]);
817
+ return new Set(boundChildren(expression).flatMap((child) => [...expressionSources(child)]));
818
+ }
819
+ function createBoundJoin(kind, buildSource, probe, build, table, memory) {
820
+ return {
821
+ kind,
822
+ buildSource,
823
+ probe,
824
+ build,
825
+ lookup: createJoinLookup(table, build, buildSource, memory),
826
+ };
827
+ }
828
+ function createJoinLookup(table, expression, source, memory) {
829
+ if (expression.kind === "column" &&
830
+ expression.source === source &&
831
+ expression.column === table.uniqueKey) {
832
+ const vector = table.columns.get(expression.column);
833
+ const direct = vector === undefined ? undefined : createDirectLookup(vector, memory);
834
+ if (direct !== undefined)
835
+ return direct;
836
+ }
837
+ const index = new ByteJoinIndex(memory, table.rowCount);
838
+ const buildScratch = memory.reserve(safeMemoryProduct(source + 1, Int32Array.BYTES_PER_ELEMENT, "Hash join bind scratch"), `Hash join ${table.name} bind scratch`);
839
+ try {
840
+ const rowBySource = new Int32Array(source + 1);
841
+ rowBySource.fill(-1);
842
+ for (let rowIndex = 0; rowIndex < table.rowCount; rowIndex += 1) {
843
+ rowBySource[source] = rowIndex;
844
+ index.add(evaluateExpression(expression, rowBySource), rowIndex);
845
+ }
846
+ }
847
+ finally {
848
+ buildScratch.release();
849
+ }
850
+ return {
851
+ get unique() {
852
+ return index.unique;
853
+ },
854
+ firstRow: (key) => index.firstRow(key),
855
+ nextRow: (row) => index.nextRow(row),
856
+ };
857
+ }
858
+ function createDirectLookup(vector, memory) {
859
+ if (vector.kind !== "number")
860
+ return undefined;
861
+ let minimum = Number.POSITIVE_INFINITY;
862
+ let maximum = Number.NEGATIVE_INFINITY;
863
+ for (let index = 0; index < vector.length; index += 1) {
864
+ if (!isValid(vector.validity, index))
865
+ continue;
866
+ const value = vector.values[index] ?? 0;
867
+ if (!Number.isSafeInteger(value))
868
+ return undefined;
869
+ minimum = Math.min(minimum, value);
870
+ maximum = Math.max(maximum, value);
871
+ }
872
+ if (!Number.isFinite(minimum)) {
873
+ return { unique: true, firstRow: () => -1, nextRow: () => -1 };
874
+ }
875
+ const range = maximum - minimum + 1;
876
+ if (range > Math.max(1_024, vector.length * 4) || range > 10_000_000)
877
+ return undefined;
878
+ const reservation = memory.reserve(safeMemoryProduct(range, Int32Array.BYTES_PER_ELEMENT, "Direct join lookup"), "Direct join lookup");
879
+ const rowByKey = new Int32Array(range);
880
+ rowByKey.fill(-1);
881
+ for (let index = 0; index < vector.length; index += 1) {
882
+ if (!isValid(vector.validity, index))
883
+ continue;
884
+ const slot = (vector.values[index] ?? 0) - minimum;
885
+ if ((rowByKey[slot] ?? -1) !== -1) {
886
+ reservation.release();
887
+ return undefined;
888
+ }
889
+ rowByKey[slot] = index;
890
+ }
891
+ return {
892
+ unique: true,
893
+ firstRow(key) {
894
+ if (typeof key !== "number" || !Number.isSafeInteger(key))
895
+ return -1;
896
+ const slot = key - minimum;
897
+ return slot < 0 || slot >= rowByKey.length ? -1 : (rowByKey[slot] ?? -1);
898
+ },
899
+ nextRow: () => -1,
900
+ };
901
+ }
902
+ function runScanBatch(plan, start, length, groups, output, memory) {
903
+ const batchMemory = memory.createChild();
904
+ try {
905
+ batchMemory.reserve(safeMemoryProduct(safeMemoryProduct(plan.sourceTables.length, length, "Scan batch row-index count"), Int32Array.BYTES_PER_ELEMENT, "Scan batch row indexes"), "Scan batch row indexes");
906
+ const sourceRows = plan.sourceTables.map(() => new Int32Array(length).fill(-1));
907
+ const scan = sourceRows[plan.scanSource];
908
+ if (scan === undefined)
909
+ return false;
910
+ for (let index = 0; index < length; index += 1)
911
+ scan[index] = start + index;
912
+ const batch = { length, rowsBySource: sourceRows, memory: batchMemory };
913
+ return consumeJoinedBatches(plan, batch, 0, groups, output, memory);
914
+ }
915
+ finally {
916
+ batchMemory.close();
917
+ }
918
+ }
919
+ function executeBoundPlan(plan, memory) {
920
+ const metadataCount = executeMetadataCount(plan, memory);
921
+ if (metadataCount !== undefined)
922
+ return metadataCount;
923
+ const groups = new GroupAccumulator(plan, memory);
924
+ const output = new ResultSink(plan, memory, true);
925
+ const scanRows = plan.sourceTables[plan.scanSource]?.rowCount ?? 0;
926
+ const narrowed = ascendingScanRange(plan, 0, scanRows);
927
+ const ranges = narrowed?.ranges ?? [
928
+ { begin: narrowed?.begin ?? 0, end: narrowed?.end ?? scanRows },
929
+ ];
930
+ scan: for (const range of ranges) {
931
+ for (let start = range.begin; start < range.end; start += DEFAULT_BATCH_ROWS) {
932
+ const length = Math.min(DEFAULT_BATCH_ROWS, range.end - start);
933
+ if (runScanBatch(plan, start, length, groups, output, memory))
934
+ break scan;
935
+ }
936
+ }
937
+ const rows = plan.grouped ? finishGroups(plan, groups.values(), memory) : output.finish();
938
+ return finishResult(plan, rows, memory);
939
+ }
940
+ async function executeBoundPlanAsync(plan, memory, options) {
941
+ const metadataCount = executeMetadataCount(plan, memory);
942
+ if (metadataCount !== undefined)
943
+ return metadataCount;
944
+ const groups = new GroupAccumulator(plan, memory);
945
+ const output = new ResultSink(plan, memory, options.loadScanWindow === undefined);
946
+ const scanRows = plan.sourceTables[plan.scanSource]?.rowCount ?? 0;
947
+ for (let start = 0; start < scanRows;) {
948
+ let length = Math.min(DEFAULT_BATCH_ROWS, scanRows - start);
949
+ // The loader answers synchronously when the batch is already resident — the common case,
950
+ // every batch but the first per block — so the scan loop only pays await on real slides.
951
+ const loaded = options.loadScanWindow?.(start, length);
952
+ const residentEnd = typeof loaded === "number" || loaded === undefined ? loaded : await loaded;
953
+ if (typeof residentEnd === "number" && residentEnd > start) {
954
+ length = Math.min(length, residentEnd - start);
955
+ }
956
+ // Narrowing runs against the resident window, so a streamed scan skips the batches this
957
+ // window cannot answer instead of stepping through them. `windowEnd` is where the loader
958
+ // stopped, which is the block boundary — exactly the span the ordering check covers.
959
+ const windowEnd = typeof residentEnd === "number" && residentEnd > start ? residentEnd : start + length;
960
+ const narrowed = ascendingScanRange(plan, start, windowEnd);
961
+ if (narrowed === undefined) {
962
+ if (runScanBatch(plan, start, length, groups, output, memory))
963
+ break;
964
+ start += length;
965
+ continue;
966
+ }
967
+ // The window is consumed to its end before the loader is asked for anything else: loaders
968
+ // are forward-only, so re-entering with a start part-way into a resident window would ask
969
+ // one to serve ground it has already passed.
970
+ let stopped = false;
971
+ const ranges = narrowed.ranges ?? [{ begin: narrowed.begin, end: narrowed.end }];
972
+ for (const range of ranges) {
973
+ for (let row = range.begin; row < range.end; row += DEFAULT_BATCH_ROWS) {
974
+ const rows = Math.min(DEFAULT_BATCH_ROWS, range.end - row);
975
+ if (runScanBatch(plan, row, rows, groups, output, memory)) {
976
+ stopped = true;
977
+ break;
978
+ }
979
+ }
980
+ if (stopped)
981
+ break;
982
+ }
983
+ if (stopped)
984
+ break;
985
+ start = windowEnd;
986
+ }
987
+ const rows = plan.grouped ? finishGroups(plan, groups.values(), memory) : output.finish();
988
+ return finishResult(plan, rows, memory);
989
+ }
990
+ function createSpillOwnerId() {
991
+ return `query-${globalThis.crypto.randomUUID()}`;
992
+ }
993
+ async function executeBoundPlanWithSortSpill(plan, memory, options) {
994
+ const store = required(options.spillStore, "Query spill store is missing");
995
+ const pageRows = options.spillPageRows ?? DEFAULT_BATCH_ROWS;
996
+ if (!Number.isSafeInteger(pageRows) || pageRows <= 0) {
997
+ throw new RangeError("Query spill page rows must be a positive whole number");
998
+ }
999
+ const columns = plan.wildcard ? wildcardColumnNames(plan) : plan.select.map((item) => item.alias);
1000
+ const ownerId = createSpillOwnerId();
1001
+ const runs = [];
1002
+ let runSequence = 0;
1003
+ try {
1004
+ const scanRows = plan.sourceTables[plan.scanSource]?.rowCount ?? 0;
1005
+ const scanBatchRows = Math.min(DEFAULT_BATCH_ROWS, pageRows);
1006
+ for (let start = 0; start < scanRows;) {
1007
+ let length = Math.min(scanBatchRows, scanRows - start);
1008
+ const loadedSort = options.loadScanWindow?.(start, length);
1009
+ const residentEnd = typeof loadedSort === "number" || loadedSort === undefined ? loadedSort : await loadedSort;
1010
+ if (typeof residentEnd === "number" && residentEnd > start) {
1011
+ length = Math.min(length, residentEnd - start);
1012
+ }
1013
+ const batchMemory = memory.createChild();
1014
+ try {
1015
+ batchMemory.reserve(safeMemoryProduct(safeMemoryProduct(plan.sourceTables.length, length, "Scan batch row-index count"), Int32Array.BYTES_PER_ELEMENT, "Scan batch row indexes"), "Scan batch row indexes");
1016
+ const sourceRows = plan.sourceTables.map(() => new Int32Array(length).fill(-1));
1017
+ const scan = sourceRows[plan.scanSource];
1018
+ if (scan === undefined) {
1019
+ start += length;
1020
+ continue;
1021
+ }
1022
+ for (let index = 0; index < length; index += 1)
1023
+ scan[index] = start + index;
1024
+ await spillJoinedBatches(plan, { length, rowsBySource: sourceRows, memory: batchMemory }, 0, memory, async (batch) => {
1025
+ const outputMemory = memory.createChild();
1026
+ try {
1027
+ const rows = [];
1028
+ projectFilteredBatch(plan, batch, rows, outputMemory);
1029
+ if (rows.length === 0)
1030
+ return;
1031
+ const ordering = outputMemory.reserve(safeMemoryProduct(rows.length, Uint32Array.BYTES_PER_ELEMENT * 2 + Uint8Array.BYTES_PER_ELEMENT, "Spill ordering typed scratch"), "Spill ordering typed scratch");
1032
+ try {
1033
+ stableSortRows(rows, plan.orderBy);
1034
+ }
1035
+ finally {
1036
+ ordering.release();
1037
+ }
1038
+ const runId = `run-${String(runSequence++)}`;
1039
+ await store.putPage(ownerId, runId, 0, encodeSpillRows(columns, rows));
1040
+ runs.push({ id: runId, pageCount: 1 });
1041
+ }
1042
+ finally {
1043
+ outputMemory.close();
1044
+ }
1045
+ });
1046
+ }
1047
+ finally {
1048
+ batchMemory.close();
1049
+ }
1050
+ start += length;
1051
+ }
1052
+ if (runs.length === 0)
1053
+ return { columns, rows: [] };
1054
+ let active = runs;
1055
+ while (active.length > 1) {
1056
+ const merged = [];
1057
+ for (let index = 0; index < active.length; index += 2) {
1058
+ const left = required(active[index], "Left spill run is missing");
1059
+ const right = active[index + 1];
1060
+ if (right === undefined) {
1061
+ merged.push(left);
1062
+ continue;
1063
+ }
1064
+ const outputId = `merge-${String(runSequence++)}`;
1065
+ merged.push(await mergeSpillRuns(store, ownerId, left, right, outputId, columns, plan.orderBy, pageRows, memory));
1066
+ await store.removeRun(ownerId, left.id);
1067
+ await store.removeRun(ownerId, right.id);
1068
+ }
1069
+ active = merged;
1070
+ }
1071
+ const finalRun = required(active[0], "Final spill run is missing");
1072
+ const rows = [];
1073
+ const offset = plan.offset ?? 0;
1074
+ const limit = plan.limit === undefined ? Number.MAX_SAFE_INTEGER : plan.limit + offset;
1075
+ for (let pageIndex = 0; pageIndex < finalRun.pageCount && rows.length < limit; pageIndex += 1) {
1076
+ const bytes = await store.getPage(ownerId, finalRun.id, pageIndex);
1077
+ if (bytes === undefined)
1078
+ throw new Error("Query spill page is missing");
1079
+ for (const row of decodeSpillRows(columns, bytes)) {
1080
+ if (rows.length === limit)
1081
+ break;
1082
+ rows.push(row);
1083
+ }
1084
+ }
1085
+ if (offset > 0)
1086
+ rows.splice(0, Math.min(offset, rows.length));
1087
+ return { columns, rows };
1088
+ }
1089
+ finally {
1090
+ await store.removeOwner(ownerId);
1091
+ }
1092
+ }
1093
+ async function executeBoundPlanWithHashSpill(plan, memory, options) {
1094
+ const store = required(options.spillStore, "Query spill store is missing");
1095
+ const pageRows = options.spillPageRows ?? DEFAULT_BATCH_ROWS;
1096
+ if (!Number.isSafeInteger(pageRows) || pageRows <= 0) {
1097
+ throw new RangeError("Query spill page rows must be a positive whole number");
1098
+ }
1099
+ const partitionCount = 64;
1100
+ const columns = plan.select.map((item) => item.alias);
1101
+ const groupColumnNames = plan.groupBy.map((_, index) => `g${String(index)}`);
1102
+ const aggregateColumnNames = plan.aggregates.map((_, index) => `a${String(index)}`);
1103
+ const spillColumns = [...groupColumnNames, ...aggregateColumnNames];
1104
+ const ownerId = createSpillOwnerId();
1105
+ const partitionPages = new Uint32Array(partitionCount);
1106
+ let runSequence = 0;
1107
+ try {
1108
+ const scanRows = plan.sourceTables[plan.scanSource]?.rowCount ?? 0;
1109
+ // A fixed chunk bounds buffered evaluated values per flush independently of the configured
1110
+ // page size while staying coarse enough to amortize partition-page write transactions.
1111
+ const scanChunkRows = Math.min(DEFAULT_BATCH_ROWS, HASH_SPILL_SCAN_CHUNK_ROWS);
1112
+ for (let start = 0; start < scanRows;) {
1113
+ let length = Math.min(scanChunkRows, scanRows - start);
1114
+ const loadedHash = options.loadScanWindow?.(start, length);
1115
+ const residentEnd = typeof loadedHash === "number" || loadedHash === undefined ? loadedHash : await loadedHash;
1116
+ if (typeof residentEnd === "number" && residentEnd > start) {
1117
+ length = Math.min(length, residentEnd - start);
1118
+ }
1119
+ const batchMemory = memory.createChild();
1120
+ try {
1121
+ batchMemory.reserve(safeMemoryProduct(safeMemoryProduct(plan.sourceTables.length, length, "Scan batch row-index count"), Int32Array.BYTES_PER_ELEMENT, "Scan batch row indexes"), "Scan batch row indexes");
1122
+ const scanRowsBySource = plan.sourceTables.map(() => new Int32Array(length).fill(-1));
1123
+ const scan = scanRowsBySource[plan.scanSource];
1124
+ if (scan === undefined) {
1125
+ start += length;
1126
+ continue;
1127
+ }
1128
+ for (let index = 0; index < length; index += 1)
1129
+ scan[index] = start + index;
1130
+ const partitionBuffers = new Map();
1131
+ await spillJoinedBatches(plan, { length, rowsBySource: scanRowsBySource, memory: batchMemory }, 0, memory,
1132
+ // Each surviving row spills its evaluated group keys and aggregate arguments, so the
1133
+ // partition phase never re-reads source vectors and the scan source may be windowed.
1134
+ async (batch) => {
1135
+ for (let row = 0; row < batch.length; row += 1) {
1136
+ if (!plan.predicates.every((predicate) => evaluateBatchPredicate(plan, predicate, batch, row))) {
1137
+ continue;
1138
+ }
1139
+ const groupValues = plan.groupBy.map((expression) => asQueryValue(evaluateBatchExpression(plan, expression, batch, row)));
1140
+ const spillRow = {};
1141
+ for (let index = 0; index < groupValues.length; index += 1) {
1142
+ spillRow[`g${String(index)}`] = groupValues[index] ?? null;
1143
+ }
1144
+ for (let index = 0; index < plan.aggregates.length; index += 1) {
1145
+ const spec = required(plan.aggregates[index], "Aggregate specification is missing");
1146
+ const raw = spec.argument.kind === "wildcard"
1147
+ ? 1
1148
+ : evaluateBatchExpression(plan, spec.argument, batch, row);
1149
+ spillRow[`a${String(index)}`] =
1150
+ raw === null || raw === undefined ? null : asQueryValue(raw);
1151
+ }
1152
+ batchMemory.tally(queryRowPayloadBytes(spillRow), "Hash spill value row");
1153
+ const partition = hashQueryValues(groupValues) & (partitionCount - 1);
1154
+ const rows = partitionBuffers.get(partition) ?? [];
1155
+ rows.push(spillRow);
1156
+ partitionBuffers.set(partition, rows);
1157
+ }
1158
+ });
1159
+ for (const [partition, rows] of partitionBuffers) {
1160
+ const pageIndex = partitionPages[partition] ?? 0;
1161
+ await store.putPage(ownerId, `partition-${String(partition)}`, pageIndex, encodeSpillRows(spillColumns, rows));
1162
+ partitionPages[partition] = pageIndex + 1;
1163
+ }
1164
+ }
1165
+ finally {
1166
+ batchMemory.close();
1167
+ }
1168
+ start += length;
1169
+ }
1170
+ const runs = [];
1171
+ for (let partition = 0; partition < partitionCount; partition += 1) {
1172
+ const sourcePageCount = partitionPages[partition] ?? 0;
1173
+ if (sourcePageCount === 0)
1174
+ continue;
1175
+ const partitionMemory = memory.createChild();
1176
+ try {
1177
+ const groups = new ByteGroupIndex(partitionMemory);
1178
+ for (let pageIndex = 0; pageIndex < sourcePageCount; pageIndex += 1) {
1179
+ const bytes = await store.getPage(ownerId, `partition-${String(partition)}`, pageIndex);
1180
+ if (bytes === undefined)
1181
+ throw new Error("Query hash spill page is missing");
1182
+ const pageMemory = partitionMemory.createChild();
1183
+ try {
1184
+ pageMemory.reserve(spillRowsModeledBytes(bytes), "Hash spill value rows");
1185
+ for (const spillRow of decodeSpillRows(spillColumns, bytes)) {
1186
+ const groupValues = groupColumnNames.map((name) => spillRow[name] ?? null);
1187
+ const state = groupValues.length === 1
1188
+ ? groups.getOrInsertOne(groupKey(groupValues[0] ?? null), () => createGroupState([groupValues[0] ?? null], plan, partitionMemory))
1189
+ : groups.getOrInsert(groupValues.map(groupKey), () => createGroupState(groupValues, plan, partitionMemory));
1190
+ updateAggregatesFromValues(plan, state, aggregateColumnNames.map((name) => spillRow[name] ?? null), partitionMemory);
1191
+ }
1192
+ }
1193
+ finally {
1194
+ pageMemory.close();
1195
+ }
1196
+ }
1197
+ const rows = finishGroups(plan, groups.values(), partitionMemory);
1198
+ if (plan.orderBy.length > 0) {
1199
+ const ordering = partitionMemory.reserve(safeMemoryProduct(rows.length, Uint32Array.BYTES_PER_ELEMENT * 2 + Uint8Array.BYTES_PER_ELEMENT, "Hash spill ordering typed scratch"), "Hash spill ordering typed scratch");
1200
+ try {
1201
+ stableSortRows(rows, plan.orderBy);
1202
+ }
1203
+ finally {
1204
+ ordering.release();
1205
+ }
1206
+ }
1207
+ const runId = `group-${String(runSequence++)}`;
1208
+ let outputPage = 0;
1209
+ for (let start = 0; start < rows.length; start += pageRows) {
1210
+ await store.putPage(ownerId, runId, outputPage, encodeSpillRows(columns, rows.slice(start, start + pageRows)));
1211
+ outputPage += 1;
1212
+ }
1213
+ runs.push({ id: runId, pageCount: outputPage });
1214
+ }
1215
+ finally {
1216
+ partitionMemory.close();
1217
+ await store.removeRun(ownerId, `partition-${String(partition)}`);
1218
+ }
1219
+ }
1220
+ if (runs.length === 0)
1221
+ return { columns, rows: [] };
1222
+ const finalRun = await mergeAllSpillRuns(store, ownerId, runs, columns, plan.orderBy, pageRows, () => `merge-${String(runSequence++)}`, memory);
1223
+ const spillOffset = plan.offset ?? 0;
1224
+ const result = await readFinalSpillRun(store, ownerId, finalRun, columns, plan.limit === undefined ? undefined : plan.limit + spillOffset);
1225
+ if (spillOffset > 0)
1226
+ result.rows.splice(0, Math.min(spillOffset, result.rows.length));
1227
+ return result;
1228
+ }
1229
+ finally {
1230
+ await store.removeOwner(ownerId);
1231
+ }
1232
+ }
1233
+ async function mergeAllSpillRuns(store, ownerId, runs, columns, orderBy, pageRows, nextRunId, memory) {
1234
+ let active = [...runs];
1235
+ while (active.length > 1) {
1236
+ const merged = [];
1237
+ for (let index = 0; index < active.length; index += 2) {
1238
+ const left = required(active[index], "Left spill run is missing");
1239
+ const right = active[index + 1];
1240
+ if (right === undefined) {
1241
+ merged.push(left);
1242
+ continue;
1243
+ }
1244
+ const outputId = nextRunId();
1245
+ merged.push(await mergeSpillRuns(store, ownerId, left, right, outputId, columns, orderBy, pageRows, memory));
1246
+ await store.removeRun(ownerId, left.id);
1247
+ await store.removeRun(ownerId, right.id);
1248
+ }
1249
+ active = merged;
1250
+ }
1251
+ return required(active[0], "Final spill run is missing");
1252
+ }
1253
+ async function readFinalSpillRun(store, ownerId, run, columns, requestedLimit) {
1254
+ const rows = [];
1255
+ const limit = requestedLimit ?? Number.MAX_SAFE_INTEGER;
1256
+ for (let pageIndex = 0; pageIndex < run.pageCount && rows.length < limit; pageIndex += 1) {
1257
+ const bytes = await store.getPage(ownerId, run.id, pageIndex);
1258
+ if (bytes === undefined) {
1259
+ throw new Error(`Query spill page is missing: ${run.id}/${String(pageIndex)}`);
1260
+ }
1261
+ for (const row of decodeSpillRows(columns, bytes)) {
1262
+ if (rows.length === limit)
1263
+ break;
1264
+ rows.push(row);
1265
+ }
1266
+ }
1267
+ return { columns: [...columns], rows };
1268
+ }
1269
+ const hashScratch = new DataView(new ArrayBuffer(8));
1270
+ /**
1271
+ * Allocation-free FNV-1a over tagged canonical group-key bytes. Only per-execution partition
1272
+ * routing depends on this hash, so it never needs cross-version stability; -0 keeps its sign bit
1273
+ * and hashes apart from 0, matching the group index's key distinction.
1274
+ */
1275
+ function hashQueryValues(values) {
1276
+ let hash = 0x811c9dc5;
1277
+ for (const rawValue of values) {
1278
+ const value = groupKey(rawValue);
1279
+ if (value === null) {
1280
+ hash = Math.imul(hash ^ 0x01, 0x01000193) >>> 0;
1281
+ }
1282
+ else if (typeof value === "boolean") {
1283
+ hash = Math.imul(hash ^ (value ? 0x03 : 0x02), 0x01000193) >>> 0;
1284
+ }
1285
+ else if (typeof value === "number") {
1286
+ hash = Math.imul(hash ^ 0x04, 0x01000193) >>> 0;
1287
+ hashScratch.setFloat64(0, value, true);
1288
+ for (let byte = 0; byte < 8; byte += 1) {
1289
+ hash = Math.imul(hash ^ hashScratch.getUint8(byte), 0x01000193) >>> 0;
1290
+ }
1291
+ }
1292
+ else {
1293
+ hash = Math.imul(hash ^ 0x05, 0x01000193) >>> 0;
1294
+ for (let index = 0; index < value.length; index += 1) {
1295
+ const code = value.charCodeAt(index);
1296
+ hash = Math.imul(hash ^ (code & 0xff), 0x01000193) >>> 0;
1297
+ hash = Math.imul(hash ^ (code >>> 8), 0x01000193) >>> 0;
1298
+ }
1299
+ }
1300
+ hash = Math.imul(hash ^ 0xff, 0x01000193) >>> 0;
1301
+ }
1302
+ return hash;
1303
+ }
1304
+ async function spillJoinedBatches(plan, batch, joinIndex, memory, consume) {
1305
+ const join = plan.joins[joinIndex];
1306
+ if (join === undefined) {
1307
+ await consume(batch);
1308
+ return;
1309
+ }
1310
+ for (const joined of joinBatches(plan, batch, join, memory)) {
1311
+ try {
1312
+ await spillJoinedBatches(plan, joined, joinIndex + 1, memory, consume);
1313
+ }
1314
+ finally {
1315
+ joined.memory?.close();
1316
+ }
1317
+ }
1318
+ }
1319
+ function projectFilteredBatch(plan, batch, output, memory) {
1320
+ for (let row = 0; row < batch.length; row += 1) {
1321
+ if (!passesPredicates(plan, batch, row))
1322
+ continue;
1323
+ const resultRow = projectBatchRow(plan, batch, row);
1324
+ memory.tally(queryRowPayloadBytes(resultRow), "Spill result row");
1325
+ output.push(resultRow);
1326
+ }
1327
+ }
1328
+ /**
1329
+ * Applies a plan's WHERE predicates to one row. This runs once per scanned row, so it iterates
1330
+ * directly rather than through `every`, which allocates a capturing closure per call.
1331
+ */
1332
+ function passesPredicates(plan, batch, row) {
1333
+ for (const predicate of plan.predicates) {
1334
+ if (!evaluateBatchPredicate(plan, predicate, batch, row))
1335
+ return false;
1336
+ }
1337
+ return true;
1338
+ }
1339
+ async function mergeSpillRuns(store, ownerId, left, right, outputId, columns, orderBy, pageRows, memory) {
1340
+ const mergeMemory = memory.createChild();
1341
+ const leftReader = createSpillRunReader(store, ownerId, left, columns, mergeMemory);
1342
+ const rightReader = createSpillRunReader(store, ownerId, right, columns, mergeMemory);
1343
+ let outputPage = [];
1344
+ let outputMemory = mergeMemory.createChild();
1345
+ let pageIndex = 0;
1346
+ const flush = async () => {
1347
+ if (outputPage.length === 0)
1348
+ return;
1349
+ await store.putPage(ownerId, outputId, pageIndex, encodeSpillRows(columns, outputPage));
1350
+ outputPage = [];
1351
+ outputMemory.close();
1352
+ outputMemory = mergeMemory.createChild();
1353
+ pageIndex += 1;
1354
+ };
1355
+ try {
1356
+ let leftRow = await leftReader.next();
1357
+ let rightRow = await rightReader.next();
1358
+ while (leftRow !== undefined || rightRow !== undefined) {
1359
+ if (rightRow === undefined ||
1360
+ (leftRow !== undefined && compareOrderedRows(leftRow, rightRow, orderBy) <= 0)) {
1361
+ const row = required(leftRow, "Left spill row is missing");
1362
+ outputMemory.tally(queryRowPayloadBytes(row), "Spill merge output row");
1363
+ outputPage.push(row);
1364
+ leftRow = await leftReader.next();
1365
+ }
1366
+ else {
1367
+ outputMemory.tally(queryRowPayloadBytes(rightRow), "Spill merge output row");
1368
+ outputPage.push(rightRow);
1369
+ rightRow = await rightReader.next();
1370
+ }
1371
+ if (outputPage.length === pageRows)
1372
+ await flush();
1373
+ }
1374
+ await flush();
1375
+ return { id: outputId, pageCount: pageIndex };
1376
+ }
1377
+ finally {
1378
+ outputMemory.close();
1379
+ leftReader.close();
1380
+ rightReader.close();
1381
+ mergeMemory.close();
1382
+ }
1383
+ }
1384
+ function createSpillRunReader(store, ownerId, run, columns, memory) {
1385
+ let pageIndex = 0;
1386
+ let rows = [];
1387
+ let rowIndex = 0;
1388
+ let pageReservation;
1389
+ return {
1390
+ async next() {
1391
+ while (rowIndex >= rows.length) {
1392
+ if (pageIndex >= run.pageCount)
1393
+ return undefined;
1394
+ const bytes = await store.getPage(ownerId, run.id, pageIndex);
1395
+ if (bytes === undefined)
1396
+ throw new Error("Query spill page is missing");
1397
+ pageReservation?.release();
1398
+ pageReservation = memory.reserve(spillRowsModeledBytes(bytes), "Spill input page");
1399
+ rows = decodeSpillRows(columns, bytes);
1400
+ rowIndex = 0;
1401
+ pageIndex += 1;
1402
+ }
1403
+ const row = rows[rowIndex];
1404
+ rowIndex += 1;
1405
+ return row;
1406
+ },
1407
+ close() {
1408
+ pageReservation?.release();
1409
+ pageReservation = undefined;
1410
+ rows = [];
1411
+ },
1412
+ };
1413
+ }
1414
+ function compareOrderedRows(left, right, orderBy) {
1415
+ for (const order of orderBy) {
1416
+ const placed = explicitNullOrder(left[order.outputName], right[order.outputName], order.nulls);
1417
+ if (placed !== undefined && placed !== 0)
1418
+ return placed;
1419
+ const comparison = compareValues(left[order.outputName], right[order.outputName]);
1420
+ if (comparison !== 0)
1421
+ return order.direction === "desc" ? -comparison : comparison;
1422
+ }
1423
+ return 0;
1424
+ }
1425
+ function encodeSpillRows(columns, rows) {
1426
+ const encoded = rows.map((row) => columns.map((column) => encodeSpillValue(row[column] ?? null)));
1427
+ const payload = vectorTextEncoder.encode(JSON.stringify(encoded));
1428
+ const modeledBytes = rows.reduce((total, row) => safeMemorySum(total, queryRowPayloadBytes(row), "Spill page rows"), 0);
1429
+ if (modeledBytes > 0xffffffff)
1430
+ throw new RangeError("Spill page modeled bytes exceed uint32");
1431
+ const bytes = new Uint8Array(SPILL_PAGE_HEADER_BYTES + payload.byteLength);
1432
+ const header = new DataView(bytes.buffer);
1433
+ header.setUint32(0, SPILL_PAGE_MAGIC, true);
1434
+ header.setUint32(4, modeledBytes, true);
1435
+ bytes.set(payload, SPILL_PAGE_HEADER_BYTES);
1436
+ return bytes;
1437
+ }
1438
+ function decodeSpillRows(columns, bytes) {
1439
+ spillRowsModeledBytes(bytes);
1440
+ const value = JSON.parse(new TextDecoder().decode(bytes.subarray(SPILL_PAGE_HEADER_BYTES)));
1441
+ if (!Array.isArray(value))
1442
+ throw new Error("Query spill page is invalid");
1443
+ return value.map((encodedRow) => {
1444
+ if (!Array.isArray(encodedRow) || encodedRow.length !== columns.length) {
1445
+ throw new Error("Query spill row is invalid");
1446
+ }
1447
+ return Object.fromEntries(columns.map((column, index) => [column, decodeSpillValue(encodedRow[index])]));
1448
+ });
1449
+ }
1450
+ function spillRowsModeledBytes(bytes) {
1451
+ if (bytes.byteLength < SPILL_PAGE_HEADER_BYTES)
1452
+ throw new Error("Query spill page is invalid");
1453
+ const header = new DataView(bytes.buffer, bytes.byteOffset, SPILL_PAGE_HEADER_BYTES);
1454
+ if (header.getUint32(0, true) !== SPILL_PAGE_MAGIC) {
1455
+ throw new Error("Query spill page header is invalid");
1456
+ }
1457
+ return header.getUint32(4, true);
1458
+ }
1459
+ function encodeSpillValue(value) {
1460
+ if (value === null)
1461
+ return [0];
1462
+ if (typeof value === "boolean")
1463
+ return [1, value];
1464
+ if (typeof value === "number")
1465
+ return [2, Object.is(value, -0) ? "-0" : String(value)];
1466
+ if (typeof value === "string")
1467
+ return [3, value];
1468
+ return [4, value.getTime()];
1469
+ }
1470
+ function decodeSpillValue(value) {
1471
+ if (!Array.isArray(value))
1472
+ throw new Error("Query spill value is invalid");
1473
+ const tag = value[0];
1474
+ if (tag === 0)
1475
+ return null;
1476
+ if (tag === 1 && typeof value[1] === "boolean")
1477
+ return value[1];
1478
+ if (tag === 2 && typeof value[1] === "string")
1479
+ return Number(value[1]);
1480
+ if (tag === 3 && typeof value[1] === "string")
1481
+ return value[1];
1482
+ if (tag === 4 && typeof value[1] === "number")
1483
+ return new Date(value[1]);
1484
+ throw new Error("Query spill value is invalid");
1485
+ }
1486
+ function executeMetadataCount(plan, memory) {
1487
+ if (!plan.grouped ||
1488
+ plan.groupBy.length > 0 ||
1489
+ plan.joins.length > 0 ||
1490
+ plan.predicates.length > 0 ||
1491
+ plan.having.length > 0 ||
1492
+ plan.aggregates.length === 0 ||
1493
+ plan.aggregates.some((aggregate) => aggregate.name !== "COUNT" || aggregate.argument.kind !== "wildcard")) {
1494
+ return undefined;
1495
+ }
1496
+ const state = createGroupState([], plan, memory);
1497
+ const rowCount = plan.sourceTables[plan.scanSource]?.rowCount ?? 0;
1498
+ state.counts.fill(rowCount);
1499
+ return finishResult(plan, finishGroups(plan, [state], memory), memory);
1500
+ }
1501
+ function finishResult(plan, inputRows, memory) {
1502
+ const rows = inputRows;
1503
+ if (plan.orderBy.length > 0) {
1504
+ const ordering = memory.reserve(safeMemoryProduct(rows.length, Uint32Array.BYTES_PER_ELEMENT * 2 +
1505
+ Uint8Array.BYTES_PER_ELEMENT +
1506
+ // Per extracted sort key: one slot, plus the null mask a numeric column adds beside
1507
+ // its Float64Array. Modeled for every term, since which columns specialize is only
1508
+ // known once the values are read.
1509
+ plan.orderBy.length * (QUERY_REFERENCE_BYTES + Uint8Array.BYTES_PER_ELEMENT), "Ordering typed scratch"), "Ordering typed scratch");
1510
+ try {
1511
+ stableSortRows(rows, plan.orderBy);
1512
+ }
1513
+ finally {
1514
+ ordering.release();
1515
+ }
1516
+ }
1517
+ const start = plan.offset ?? 0;
1518
+ if (start > 0)
1519
+ rows.splice(0, Math.min(start, rows.length));
1520
+ if (plan.limit !== undefined) {
1521
+ rows.length = Math.min(plan.limit, rows.length);
1522
+ }
1523
+ const columns = plan.wildcard ? wildcardColumnNames(plan) : plan.select.map((item) => item.alias);
1524
+ return { columns, rows };
1525
+ }
1526
+ /** Source indexes a bound expression reads, for pre-join predicate placement. */
1527
+ function prefilterSources(expression, into) {
1528
+ if (expression.kind === "column") {
1529
+ into.add(expression.source);
1530
+ return true;
1531
+ }
1532
+ // FTS and aggregate-bearing expressions stay at the final stage.
1533
+ if (expression.kind === "fts")
1534
+ return false;
1535
+ if (expression.kind === "call" && expression.aggregateIndex !== undefined)
1536
+ return false;
1537
+ for (const child of boundChildren(expression)) {
1538
+ if (!prefilterSources(child, into))
1539
+ return false;
1540
+ }
1541
+ return true;
1542
+ }
1543
+ /**
1544
+ * Remembers which value buffers hold an ascending, null-free run. The key is the buffer
1545
+ * itself, not the vector: a streamed window that covers one block aliases that block's
1546
+ * decoded array, and decoded blocks live in the buffer pool, so a repeated keyed query pays
1547
+ * the ordering check once per block rather than once per scan. A window stitched from several
1548
+ * blocks gets a freshly allocated buffer and so a fresh entry, which is correct because that
1549
+ * buffer describes exactly one window.
1550
+ */
1551
+ const ascendingValueBuffers = new WeakMap();
1552
+ /**
1553
+ * True when every slot of the vector's resident window carries a value and those values never
1554
+ * decrease. Non-ascending columns bail at the first violation, so the check costs a couple of
1555
+ * iterations for the columns it cannot help.
1556
+ */
1557
+ function windowIsAscending(vector) {
1558
+ const values = vector.values;
1559
+ const cached = ascendingValueBuffers.get(values);
1560
+ if (cached !== undefined)
1561
+ return cached;
1562
+ const length = vector.window?.length ?? Math.min(vector.length, values.length);
1563
+ const validity = vector.validity;
1564
+ let ascending = true;
1565
+ let previous = Number.NEGATIVE_INFINITY;
1566
+ for (let slot = 0; slot < length; slot += 1) {
1567
+ // A null has no position in the ordering, and NaN compares false against everything, so
1568
+ // either one puts the window outside what a binary search can answer.
1569
+ if (((validity[slot >>> 3] ?? 0) & (1 << (slot & 7))) === 0) {
1570
+ ascending = false;
1571
+ break;
1572
+ }
1573
+ const value = values[slot] ?? 0;
1574
+ if (!(value >= previous)) {
1575
+ ascending = false;
1576
+ break;
1577
+ }
1578
+ previous = value;
1579
+ }
1580
+ ascendingValueBuffers.set(values, ascending);
1581
+ return ascending;
1582
+ }
1583
+ /** First slot in [begin, end) whose value is at least `target`, over an ascending run. */
1584
+ function lowerBoundSlot(values, begin, end, target) {
1585
+ let low = begin;
1586
+ let high = end;
1587
+ while (low < high) {
1588
+ const middle = (low + high) >>> 1;
1589
+ if ((values[middle] ?? 0) < target)
1590
+ low = middle + 1;
1591
+ else
1592
+ high = middle;
1593
+ }
1594
+ return low;
1595
+ }
1596
+ /** First slot in [begin, end) whose value is greater than `target`, over an ascending run. */
1597
+ function upperBoundSlot(values, begin, end, target) {
1598
+ let low = begin;
1599
+ let high = end;
1600
+ while (low < high) {
1601
+ const middle = (low + high) >>> 1;
1602
+ if ((values[middle] ?? 0) <= target)
1603
+ low = middle + 1;
1604
+ else
1605
+ high = middle;
1606
+ }
1607
+ return low;
1608
+ }
1609
+ /**
1610
+ * Narrows a scan over [begin, end) to the rows a comparison against an ascending column can
1611
+ * still satisfy — the shape a generated key has, and the one a keyed lookup needs to stop
1612
+ * costing a block scan. Returns undefined when no predicate qualifies.
1613
+ *
1614
+ * This only removes rows that provably fail a predicate the scan was going to apply anyway,
1615
+ * so every predicate loop downstream runs unchanged: narrowing subtracts work, never a check.
1616
+ */
1617
+ function ascendingScanRange(plan, begin, end) {
1618
+ if (end - begin < DEFAULT_BATCH_ROWS)
1619
+ return undefined;
1620
+ let low = begin;
1621
+ let high = end;
1622
+ let narrowed = false;
1623
+ for (const predicate of plan.predicates) {
1624
+ const primitive = predicate.primitive ?? predicate.primitiveIn;
1625
+ if (primitive?.source !== plan.scanSource)
1626
+ continue;
1627
+ const vector = primitive.vector;
1628
+ if (vector.kind !== "number" && vector.kind !== "datetime")
1629
+ continue;
1630
+ const windowStart = vector.window?.start ?? 0;
1631
+ const slotBegin = low - windowStart;
1632
+ const slotEnd = high - windowStart;
1633
+ // The range has to sit inside the resident window for the search to read real values.
1634
+ if (slotBegin < 0 || slotEnd > vector.values.length)
1635
+ continue;
1636
+ if (!windowIsAscending(vector))
1637
+ continue;
1638
+ const values = vector.values;
1639
+ if (!("operator" in primitive)) {
1640
+ // NOT IN is satisfied by exactly the rows outside the member span, so the span narrows
1641
+ // nothing -- restricting to it would drop every row the predicate keeps.
1642
+ if (primitive.negated)
1643
+ continue;
1644
+ // A literal list restricts the scan to the span between its smallest and largest member;
1645
+ // the membership test still runs, and decides the rows inside that span.
1646
+ low = lowerBoundSlot(values, slotBegin, slotEnd, primitive.minimum) + windowStart;
1647
+ high = upperBoundSlot(values, low - windowStart, slotEnd, primitive.maximum) + windowStart;
1648
+ narrowed = true;
1649
+ if (low >= high)
1650
+ return { begin: low, end: low };
1651
+ continue;
1652
+ }
1653
+ const target = primitive.value;
1654
+ switch (primitive.operator) {
1655
+ case "=": {
1656
+ const first = lowerBoundSlot(values, slotBegin, slotEnd, target);
1657
+ high = upperBoundSlot(values, first, slotEnd, target) + windowStart;
1658
+ low = first + windowStart;
1659
+ break;
1660
+ }
1661
+ case ">":
1662
+ low = upperBoundSlot(values, slotBegin, slotEnd, target) + windowStart;
1663
+ break;
1664
+ case ">=":
1665
+ low = lowerBoundSlot(values, slotBegin, slotEnd, target) + windowStart;
1666
+ break;
1667
+ case "<":
1668
+ high = lowerBoundSlot(values, slotBegin, slotEnd, target) + windowStart;
1669
+ break;
1670
+ case "<=":
1671
+ high = upperBoundSlot(values, slotBegin, slotEnd, target) + windowStart;
1672
+ break;
1673
+ // `!=` keeps rows on both sides of the target, which is not a range.
1674
+ default:
1675
+ continue;
1676
+ }
1677
+ narrowed = true;
1678
+ if (low >= high)
1679
+ return { begin: low, end: low };
1680
+ }
1681
+ if (low >= high)
1682
+ return narrowed ? { begin: low, end: low } : undefined;
1683
+ const split = splitByListMembers(plan, low, high);
1684
+ if (split !== undefined)
1685
+ return { begin: low, end: high, ranges: split };
1686
+ return narrowed ? { begin: low, end: high } : undefined;
1687
+ }
1688
+ /**
1689
+ * The span between a list's smallest and largest member is only useful when the members sit
1690
+ * close together; for keys spread across the table it is the whole table. On an ascending
1691
+ * column each member can instead be located on its own, turning `key IN (5 scattered values)`
1692
+ * into five binary searches over five tiny ranges rather than one scan of everything between
1693
+ * them. Returns undefined when no list qualifies, or when the split would not pay for itself.
1694
+ */
1695
+ function splitByListMembers(plan, low, high) {
1696
+ for (const predicate of plan.predicates) {
1697
+ const list = predicate.primitiveIn;
1698
+ if (list === undefined || list.negated || list.source !== plan.scanSource)
1699
+ continue;
1700
+ // Each member costs a binary search and yields a batch of its own, so the split only pays
1701
+ // while the member count stays far below the rows it is skipping.
1702
+ if (list.members.size > MAX_SPLIT_LIST_MEMBERS)
1703
+ continue;
1704
+ const vector = list.vector;
1705
+ const windowStart = vector.window?.start ?? 0;
1706
+ const slotBegin = low - windowStart;
1707
+ const slotEnd = high - windowStart;
1708
+ if (slotBegin < 0 || slotEnd > vector.values.length)
1709
+ continue;
1710
+ if (!windowIsAscending(vector))
1711
+ continue;
1712
+ const values = vector.values;
1713
+ const ranges = [];
1714
+ // Ascending members keep the ranges ascending, which the forward-only streamed scan needs.
1715
+ for (const member of [...list.members].sort((left, right) => left - right)) {
1716
+ const first = lowerBoundSlot(values, slotBegin, slotEnd, member);
1717
+ const last = upperBoundSlot(values, first, slotEnd, member);
1718
+ if (first >= last)
1719
+ continue;
1720
+ const previous = ranges[ranges.length - 1];
1721
+ // Adjacent members land in adjacent runs; merging them keeps the batch count down.
1722
+ if (previous !== undefined && previous.end >= first + windowStart) {
1723
+ previous.end = last + windowStart;
1724
+ continue;
1725
+ }
1726
+ ranges.push({ begin: first + windowStart, end: last + windowStart });
1727
+ }
1728
+ let covered = 0;
1729
+ for (const range of ranges)
1730
+ covered += range.end - range.begin;
1731
+ // A split that still visits most of the span saves nothing and costs extra batches.
1732
+ if (covered * 2 > high - low)
1733
+ continue;
1734
+ return ranges;
1735
+ }
1736
+ return undefined;
1737
+ }
1738
+ /** Compacts the selection in place to rows where the primitive comparison holds. */
1739
+ function filterPrimitive(primitive, batch, selection, survivors) {
1740
+ const vector = primitive.vector;
1741
+ const values = vector.values;
1742
+ const validity = vector.validity;
1743
+ const windowStart = vector.window?.start ?? 0;
1744
+ const slots = values.length;
1745
+ const vectorLength = vector.length;
1746
+ const rows = batch.rowsBySource[primitive.source];
1747
+ const target = primitive.value;
1748
+ // Any comparison operator is three independent outcomes: below, equal, above the target.
1749
+ const operator = primitive.operator;
1750
+ const passBelow = operator === "<" || operator === "<=" || operator === "!=" || operator === "<>";
1751
+ const passEqual = operator === "=" || operator === "<=" || operator === ">=";
1752
+ const passAbove = operator === ">" || operator === ">=" || operator === "!=" || operator === "<>";
1753
+ let kept = 0;
1754
+ for (let index = 0; index < survivors; index += 1) {
1755
+ const row = selection[index] ?? 0;
1756
+ const sourceRow = rows?.[row] ?? -1;
1757
+ if (sourceRow < 0 || sourceRow >= vectorLength)
1758
+ continue;
1759
+ const slot = sourceRow - windowStart;
1760
+ if (slot < 0 || slot >= slots) {
1761
+ throw new RangeError("Streamed vector row is outside the resident window");
1762
+ }
1763
+ if (((validity[slot >>> 3] ?? 0) & (1 << (slot & 7))) === 0)
1764
+ continue;
1765
+ const value = values[slot] ?? 0;
1766
+ if (!(value < target ? passBelow : value > target ? passAbove : passEqual))
1767
+ continue;
1768
+ selection[kept] = row;
1769
+ kept += 1;
1770
+ }
1771
+ return kept;
1772
+ }
1773
+ /** Compacts the selection in place to rows whose value is a member of the literal list. */
1774
+ function filterPrimitiveInList(primitive, batch, selection, survivors) {
1775
+ const vector = primitive.vector;
1776
+ const values = vector.values;
1777
+ const validity = vector.validity;
1778
+ const windowStart = vector.window?.start ?? 0;
1779
+ const slots = values.length;
1780
+ const vectorLength = vector.length;
1781
+ const rows = batch.rowsBySource[primitive.source];
1782
+ const members = primitive.members;
1783
+ const negated = primitive.negated;
1784
+ let kept = 0;
1785
+ for (let index = 0; index < survivors; index += 1) {
1786
+ const row = selection[index] ?? 0;
1787
+ const sourceRow = rows?.[row] ?? -1;
1788
+ if (sourceRow < 0 || sourceRow >= vectorLength)
1789
+ continue;
1790
+ const slot = sourceRow - windowStart;
1791
+ if (slot < 0 || slot >= slots) {
1792
+ throw new RangeError("Streamed vector row is outside the resident window");
1793
+ }
1794
+ if (((validity[slot >>> 3] ?? 0) & (1 << (slot & 7))) === 0)
1795
+ continue;
1796
+ if (members.has(values[slot] ?? 0) === negated)
1797
+ continue;
1798
+ selection[kept] = row;
1799
+ kept += 1;
1800
+ }
1801
+ return kept;
1802
+ }
1803
+ /** Compacts the selection to rows where the dictionary-equality comparison holds. */
1804
+ function filterDictionaryEquality(fast, batch, selection, survivors) {
1805
+ const vector = fast.vector;
1806
+ if (fast.cache.dictionary !== vector.dictionary) {
1807
+ fast.cache.dictionary = vector.dictionary;
1808
+ fast.cache.code = vector.dictionary.indexOf(fast.value);
1809
+ }
1810
+ const target = fast.cache.code;
1811
+ const negated = fast.negated;
1812
+ const rows = batch.rowsBySource[fast.source];
1813
+ const codes = vector.codes;
1814
+ const validity = vector.validity;
1815
+ const windowStart = vector.window?.start ?? 0;
1816
+ const slots = codes.length;
1817
+ const vectorLength = vector.length;
1818
+ let kept = 0;
1819
+ for (let index = 0; index < survivors; index += 1) {
1820
+ const row = selection[index] ?? 0;
1821
+ const sourceRow = rows?.[row] ?? -1;
1822
+ if (sourceRow < 0 || sourceRow >= vectorLength)
1823
+ continue;
1824
+ const slot = sourceRow - windowStart;
1825
+ if (slot < 0 || slot >= slots) {
1826
+ throw new RangeError("Streamed vector row is outside the resident window");
1827
+ }
1828
+ if (((validity[slot >>> 3] ?? 0) & (1 << (slot & 7))) === 0)
1829
+ continue;
1830
+ const code = codes[slot] ?? NULL_STRING_CODE;
1831
+ if (code === NULL_STRING_CODE)
1832
+ continue;
1833
+ const matches = target >= 0 && code === target;
1834
+ if (negated ? matches : !matches)
1835
+ continue;
1836
+ selection[kept] = row;
1837
+ kept += 1;
1838
+ }
1839
+ return kept;
1840
+ }
1841
+ /** Compacts the selection to rows where the dictionary LIKE comparison holds. */
1842
+ function filterDictionaryLike(fast, batch, selection, survivors) {
1843
+ const vector = fast.vector;
1844
+ if (fast.cache.dictionary !== vector.dictionary) {
1845
+ fast.cache.dictionary = vector.dictionary;
1846
+ fast.cache.matches = dictionaryLikeMatches(vector.dictionary, fast.pattern, fast.caseInsensitive, fast.escape);
1847
+ }
1848
+ const matches = fast.cache.matches;
1849
+ const negated = fast.negated;
1850
+ const rows = batch.rowsBySource[fast.source];
1851
+ const codes = vector.codes;
1852
+ const validity = vector.validity;
1853
+ const windowStart = vector.window?.start ?? 0;
1854
+ const slots = codes.length;
1855
+ const vectorLength = vector.length;
1856
+ let kept = 0;
1857
+ for (let index = 0; index < survivors; index += 1) {
1858
+ const row = selection[index] ?? 0;
1859
+ const sourceRow = rows?.[row] ?? -1;
1860
+ if (sourceRow < 0 || sourceRow >= vectorLength)
1861
+ continue;
1862
+ const slot = sourceRow - windowStart;
1863
+ if (slot < 0 || slot >= slots) {
1864
+ throw new RangeError("Streamed vector row is outside the resident window");
1865
+ }
1866
+ if (((validity[slot >>> 3] ?? 0) & (1 << (slot & 7))) === 0)
1867
+ continue;
1868
+ const code = codes[slot] ?? NULL_STRING_CODE;
1869
+ if (code === NULL_STRING_CODE)
1870
+ continue;
1871
+ const matched = matches[code] === 1;
1872
+ if (negated ? matched : !matched)
1873
+ continue;
1874
+ selection[kept] = row;
1875
+ kept += 1;
1876
+ }
1877
+ return kept;
1878
+ }
1879
+ // The per-batch selection scratch: batches are bounded by DEFAULT_BATCH_ROWS, spills may use
1880
+ // larger pages, so the scratch grows to the largest batch seen and is trivially small.
1881
+ let selectionScratch = new Uint32Array(DEFAULT_BATCH_ROWS);
1882
+ /**
1883
+ * Compacts the selection with whatever unboxed kernel a predicate compiled to, or returns
1884
+ * undefined when it has none and the caller should fall back to the per-row evaluator.
1885
+ */
1886
+ function applyPredicateKernel(plan, predicate, batch, selection, survivors) {
1887
+ if (predicate.primitive !== undefined) {
1888
+ return filterPrimitive(predicate.primitive, batch, selection, survivors);
1889
+ }
1890
+ if (predicate.primitiveIn !== undefined) {
1891
+ return filterPrimitiveInList(predicate.primitiveIn, batch, selection, survivors);
1892
+ }
1893
+ if (predicate.dictionaryEquality !== undefined) {
1894
+ return filterDictionaryEquality(predicate.dictionaryEquality, batch, selection, survivors);
1895
+ }
1896
+ if (predicate.dictionaryLike !== undefined) {
1897
+ return filterDictionaryLike(predicate.dictionaryLike, batch, selection, survivors);
1898
+ }
1899
+ if (predicate.disjunction !== undefined) {
1900
+ return filterDisjunction(plan, predicate.disjunction, batch, selection, survivors);
1901
+ }
1902
+ return undefined;
1903
+ }
1904
+ // Scratch for the disjunction kernel. Branch predicates are always plain conditions, so a
1905
+ // branch never carries a disjunction of its own and these buffers are never reentered.
1906
+ let disjunctionCandidates = new Uint32Array(DEFAULT_BATCH_ROWS);
1907
+ let disjunctionWork = new Uint32Array(DEFAULT_BATCH_ROWS);
1908
+ let disjunctionMask = new Uint8Array(DEFAULT_BATCH_ROWS);
1909
+ /**
1910
+ * The union kernel: each branch narrows its own copy of the incoming rows, and a row survives
1911
+ * the disjunction if any branch kept it. Marking hits in a byte mask keeps the result in the
1912
+ * original ascending order and costs one pass per branch plus one to compact, instead of
1913
+ * walking the whole boolean tree once per row.
1914
+ */
1915
+ function filterDisjunction(plan, disjunction, batch, selection, survivors) {
1916
+ if (disjunctionCandidates.length < survivors) {
1917
+ disjunctionCandidates = new Uint32Array(survivors);
1918
+ disjunctionWork = new Uint32Array(survivors);
1919
+ }
1920
+ if (disjunctionMask.length < batch.length)
1921
+ disjunctionMask = new Uint8Array(batch.length);
1922
+ const candidates = disjunctionCandidates;
1923
+ const work = disjunctionWork;
1924
+ const mask = disjunctionMask;
1925
+ for (let index = 0; index < survivors; index += 1)
1926
+ candidates[index] = selection[index] ?? 0;
1927
+ for (const branch of disjunction.branches) {
1928
+ for (let index = 0; index < survivors; index += 1)
1929
+ work[index] = candidates[index] ?? 0;
1930
+ let kept = survivors;
1931
+ for (const predicate of branch) {
1932
+ if (kept === 0)
1933
+ break;
1934
+ const compacted = applyPredicateKernel(plan, predicate, batch, work, kept);
1935
+ if (compacted !== undefined) {
1936
+ kept = compacted;
1937
+ continue;
1938
+ }
1939
+ let generic = 0;
1940
+ for (let index = 0; index < kept; index += 1) {
1941
+ const row = work[index] ?? 0;
1942
+ if (!evaluateBatchPredicate(plan, predicate, batch, row))
1943
+ continue;
1944
+ work[generic] = row;
1945
+ generic += 1;
1946
+ }
1947
+ kept = generic;
1948
+ }
1949
+ for (let index = 0; index < kept; index += 1)
1950
+ mask[work[index] ?? 0] = 1;
1951
+ }
1952
+ let kept = 0;
1953
+ for (let index = 0; index < survivors; index += 1) {
1954
+ const row = candidates[index] ?? 0;
1955
+ if (mask[row] !== 1)
1956
+ continue;
1957
+ mask[row] = 0;
1958
+ selection[kept] = row;
1959
+ kept += 1;
1960
+ }
1961
+ return kept;
1962
+ }
1963
+ /**
1964
+ * The no-join predicate kernel: primitive comparisons and dictionary equality/LIKE compact a
1965
+ * shared selection in tight unboxed loops, and only rows surviving those reach the generic
1966
+ * per-row evaluator for whatever predicates remain. Batches with no predicates skip it.
1967
+ */
1968
+ function filterScanBatch(plan, batch) {
1969
+ if (selectionScratch.length < batch.length)
1970
+ selectionScratch = new Uint32Array(batch.length);
1971
+ const selection = selectionScratch;
1972
+ for (let row = 0; row < batch.length; row += 1)
1973
+ selection[row] = row;
1974
+ let survivors = batch.length;
1975
+ let generic;
1976
+ for (const predicate of plan.predicates) {
1977
+ if (survivors === 0)
1978
+ return { selection, survivors };
1979
+ const compacted = applyPredicateKernel(plan, predicate, batch, selection, survivors);
1980
+ if (compacted === undefined)
1981
+ (generic ??= []).push(predicate);
1982
+ else
1983
+ survivors = compacted;
1984
+ }
1985
+ if (generic !== undefined) {
1986
+ for (const predicate of generic) {
1987
+ if (survivors === 0)
1988
+ break;
1989
+ let kept = 0;
1990
+ for (let index = 0; index < survivors; index += 1) {
1991
+ const row = selection[index] ?? 0;
1992
+ if (!evaluateBatchPredicate(plan, predicate, batch, row))
1993
+ continue;
1994
+ selection[kept] = row;
1995
+ kept += 1;
1996
+ }
1997
+ survivors = kept;
1998
+ }
1999
+ }
2000
+ return { selection, survivors };
2001
+ }
2002
+ /**
2003
+ * Predicates whose sources are all materialized before the given join filter the batch first,
2004
+ * so the join and everything downstream never see rows the WHERE clause was going to discard.
2005
+ * Filters are idempotent, so applying one early is pure savings; when every predicate applies
2006
+ * here the result is marked complete and the final pass skips re-checking.
2007
+ */
2008
+ function prefilterBatch(plan, batch, join) {
2009
+ let applicable;
2010
+ for (const predicate of plan.predicates) {
2011
+ const sources = new Set();
2012
+ if (!prefilterSources(predicate.left, sources) || !prefilterSources(predicate.right, sources)) {
2013
+ continue;
2014
+ }
2015
+ let available = true;
2016
+ for (let later = plan.joins.indexOf(join); later < plan.joins.length; later += 1) {
2017
+ if (sources.has(plan.joins[later]?.buildSource ?? -1)) {
2018
+ available = false;
2019
+ break;
2020
+ }
2021
+ }
2022
+ if (available)
2023
+ (applicable ??= []).push(predicate);
2024
+ }
2025
+ if (applicable === undefined)
2026
+ return undefined;
2027
+ const complete = applicable.length === plan.predicates.length;
2028
+ const selection = new Uint32Array(batch.length);
2029
+ for (let row = 0; row < batch.length; row += 1)
2030
+ selection[row] = row;
2031
+ let survivors = batch.length;
2032
+ for (const predicate of applicable) {
2033
+ if (survivors === 0)
2034
+ break;
2035
+ const primitive = predicate.primitive;
2036
+ if (primitive !== undefined) {
2037
+ survivors = filterPrimitive(primitive, batch, selection, survivors);
2038
+ continue;
2039
+ }
2040
+ let kept = 0;
2041
+ for (let index = 0; index < survivors; index += 1) {
2042
+ const row = selection[index] ?? 0;
2043
+ if (!evaluateBatchPredicate(plan, predicate, batch, row))
2044
+ continue;
2045
+ selection[kept] = row;
2046
+ kept += 1;
2047
+ }
2048
+ survivors = kept;
2049
+ }
2050
+ return { selection, survivors, complete };
2051
+ }
2052
+ /** Copies the surviving rows into a compact batch, for joins that cannot take a selection. */
2053
+ function materializeSelection(batch, selection, survivors, memory) {
2054
+ const batchMemory = memory.createChild();
2055
+ try {
2056
+ batchMemory.reserve(safeMemoryProduct(safeMemoryProduct(batch.rowsBySource.length, survivors, "Prefiltered row-index count"), Int32Array.BYTES_PER_ELEMENT, "Prefiltered row indexes"), "Prefiltered row indexes");
2057
+ const rowsBySource = batch.rowsBySource.map((inputRows) => {
2058
+ const outputRows = new Int32Array(survivors);
2059
+ for (let output = 0; output < survivors; output += 1) {
2060
+ outputRows[output] = inputRows[selection[output] ?? 0] ?? -1;
2061
+ }
2062
+ return outputRows;
2063
+ });
2064
+ return { length: survivors, rowsBySource, memory: batchMemory };
2065
+ }
2066
+ catch (error) {
2067
+ batchMemory.close();
2068
+ throw error;
2069
+ }
2070
+ }
2071
+ function consumeJoinedBatches(plan, batch, joinIndex, groups, output, memory, prefiltered = false) {
2072
+ const join = plan.joins[joinIndex];
2073
+ if (join === undefined) {
2074
+ consumeBatch(plan, batch, groups, output, memory, prefiltered);
2075
+ return reachedEarlyLimit(plan, output.size);
2076
+ }
2077
+ let working = batch;
2078
+ let complete = prefiltered;
2079
+ let owned;
2080
+ if (joinIndex === 0 && !prefiltered) {
2081
+ const filtered = prefilterBatch(plan, batch, join);
2082
+ if (filtered !== undefined) {
2083
+ complete = filtered.complete;
2084
+ if (filtered.survivors === 0)
2085
+ return false;
2086
+ if (filtered.survivors < batch.length) {
2087
+ // Compacting up front measured faster than threading the selection into the join:
2088
+ // the probe loop stays dense and the all-match shortcut skips the second copy.
2089
+ working = materializeSelection(batch, filtered.selection, filtered.survivors, memory);
2090
+ owned = working.memory;
2091
+ }
2092
+ }
2093
+ }
2094
+ try {
2095
+ for (const joined of joinBatches(plan, working, join, memory)) {
2096
+ try {
2097
+ if (consumeJoinedBatches(plan, joined, joinIndex + 1, groups, output, memory, complete)) {
2098
+ return true;
2099
+ }
2100
+ }
2101
+ finally {
2102
+ joined.memory?.close();
2103
+ }
2104
+ }
2105
+ return false;
2106
+ }
2107
+ finally {
2108
+ owned?.close();
2109
+ }
2110
+ }
2111
+ function reachedEarlyLimit(plan, outputRows) {
2112
+ // Early termination must still produce the rows the trailing OFFSET will discard.
2113
+ return (!plan.grouped &&
2114
+ plan.orderBy.length === 0 &&
2115
+ plan.limit !== undefined &&
2116
+ outputRows >= plan.limit + (plan.offset ?? 0));
2117
+ }
2118
+ function* joinBatches(plan, input, join, memory) {
2119
+ if (join.loop !== undefined) {
2120
+ yield* loopJoinBatches(plan, input, join, join.loop, memory);
2121
+ return;
2122
+ }
2123
+ if (join.lookup.unique) {
2124
+ yield joinUniqueBatch(plan, input, join, memory);
2125
+ return;
2126
+ }
2127
+ let outputMemory;
2128
+ try {
2129
+ let outputRows;
2130
+ let outputLength = 0;
2131
+ const ensureOutput = () => {
2132
+ if (outputRows !== undefined)
2133
+ return outputRows;
2134
+ outputMemory = memory.createChild();
2135
+ outputMemory.reserve(safeMemoryProduct(safeMemoryProduct(plan.sourceTables.length, DEFAULT_BATCH_ROWS, "Join fan-out row-index count"), Int32Array.BYTES_PER_ELEMENT, "Join fan-out row indexes"), "Join fan-out row indexes");
2136
+ outputRows = plan.sourceTables.map(() => new Int32Array(DEFAULT_BATCH_ROWS));
2137
+ return outputRows;
2138
+ };
2139
+ const emit = function* () {
2140
+ if (outputRows === undefined || outputMemory === undefined || outputLength === 0)
2141
+ return;
2142
+ const batch = {
2143
+ length: outputLength,
2144
+ rowsBySource: outputRows,
2145
+ memory: outputMemory,
2146
+ };
2147
+ outputRows = undefined;
2148
+ outputMemory = undefined;
2149
+ outputLength = 0;
2150
+ yield batch;
2151
+ };
2152
+ for (let row = 0; row < input.length; row += 1) {
2153
+ const probeKey = evaluateBatchExpression(plan, join.probe, input, row);
2154
+ let buildRow = probeKey === null ? -1 : join.lookup.firstRow(probeKey);
2155
+ if (buildRow < 0) {
2156
+ if (join.kind === "left") {
2157
+ appendJoinedRow(ensureOutput(), outputLength, input, row, join.buildSource, -1);
2158
+ outputLength += 1;
2159
+ }
2160
+ }
2161
+ else {
2162
+ while (buildRow >= 0) {
2163
+ appendJoinedRow(ensureOutput(), outputLength, input, row, join.buildSource, buildRow);
2164
+ outputLength += 1;
2165
+ if (outputLength === DEFAULT_BATCH_ROWS)
2166
+ yield* emit();
2167
+ buildRow = join.lookup.nextRow(buildRow);
2168
+ }
2169
+ }
2170
+ if (outputLength === DEFAULT_BATCH_ROWS)
2171
+ yield* emit();
2172
+ }
2173
+ yield* emit();
2174
+ }
2175
+ finally {
2176
+ outputMemory?.close();
2177
+ }
2178
+ }
2179
+ /**
2180
+ * Nested-loop join for general ON conditions: every probe row scans the whole build table and
2181
+ * keeps the pairs whose condition evaluates true under three-valued logic. Output batches reuse
2182
+ * the hash-join fan-out format, so downstream consumption is identical; cost is probe x build.
2183
+ */
2184
+ function* loopJoinBatches(plan, input, join, loop, memory) {
2185
+ let outputMemory;
2186
+ const scratchReservation = memory.reserve(safeMemoryProduct(plan.sourceTables.length, Int32Array.BYTES_PER_ELEMENT, "Loop join scratch row indexes"), "Loop join scratch row indexes");
2187
+ try {
2188
+ const scratch = new Int32Array(plan.sourceTables.length);
2189
+ scratch.fill(-1);
2190
+ let outputRows;
2191
+ let outputLength = 0;
2192
+ const ensureOutput = () => {
2193
+ if (outputRows !== undefined)
2194
+ return outputRows;
2195
+ outputMemory = memory.createChild();
2196
+ outputMemory.reserve(safeMemoryProduct(safeMemoryProduct(plan.sourceTables.length, DEFAULT_BATCH_ROWS, "Join fan-out row-index count"), Int32Array.BYTES_PER_ELEMENT, "Join fan-out row indexes"), "Join fan-out row indexes");
2197
+ outputRows = plan.sourceTables.map(() => new Int32Array(DEFAULT_BATCH_ROWS));
2198
+ return outputRows;
2199
+ };
2200
+ const emit = function* () {
2201
+ if (outputRows === undefined || outputMemory === undefined || outputLength === 0)
2202
+ return;
2203
+ const batch = {
2204
+ length: outputLength,
2205
+ rowsBySource: outputRows,
2206
+ memory: outputMemory,
2207
+ };
2208
+ outputRows = undefined;
2209
+ outputMemory = undefined;
2210
+ outputLength = 0;
2211
+ yield batch;
2212
+ };
2213
+ for (let row = 0; row < input.length; row += 1) {
2214
+ for (let source = 0; source < join.buildSource; source += 1) {
2215
+ scratch[source] = input.rowsBySource[source]?.[row] ?? -1;
2216
+ }
2217
+ let matched = false;
2218
+ for (let buildRow = 0; buildRow < loop.rowCount; buildRow += 1) {
2219
+ scratch[join.buildSource] = buildRow;
2220
+ const holds = booleanTruth(loop.condition, (nested) => evaluateExpression(nested, scratch)) === true;
2221
+ if (!holds)
2222
+ continue;
2223
+ matched = true;
2224
+ appendJoinedRow(ensureOutput(), outputLength, input, row, join.buildSource, buildRow);
2225
+ outputLength += 1;
2226
+ if (outputLength === DEFAULT_BATCH_ROWS)
2227
+ yield* emit();
2228
+ }
2229
+ if (!matched && join.kind === "left") {
2230
+ appendJoinedRow(ensureOutput(), outputLength, input, row, join.buildSource, -1);
2231
+ outputLength += 1;
2232
+ if (outputLength === DEFAULT_BATCH_ROWS)
2233
+ yield* emit();
2234
+ }
2235
+ }
2236
+ yield* emit();
2237
+ }
2238
+ finally {
2239
+ scratchReservation.release();
2240
+ outputMemory?.close();
2241
+ }
2242
+ }
2243
+ function joinUniqueBatch(plan, input, join, memory) {
2244
+ const batchMemory = memory.createChild();
2245
+ try {
2246
+ const selectedReservation = batchMemory.reserve(safeMemoryProduct(input.length, Uint32Array.BYTES_PER_ELEMENT, "Join selection vector"), "Join selection vector");
2247
+ const buildReservation = batchMemory.reserve(safeMemoryProduct(input.length, Int32Array.BYTES_PER_ELEMENT, "Join build rows"), "Join build rows");
2248
+ const selectedRows = new Uint32Array(input.length);
2249
+ const buildRows = new Int32Array(input.length);
2250
+ let outputLength = 0;
2251
+ const probe = join.probe;
2252
+ // Windowed probes qualify: stringCodeAt is window-aware and the code cache re-resolves
2253
+ // whenever the resident window's dictionary object changes.
2254
+ const dictProbe = probe.kind === "column" && probe.vector.kind === "string"
2255
+ ? { source: probe.source, vector: probe.vector }
2256
+ : undefined;
2257
+ if (dictProbe !== undefined) {
2258
+ const vector = dictProbe.vector;
2259
+ if (join.codeLookup?.dictionary !== vector.dictionary) {
2260
+ // -2 marks "not resolved yet"; -1 is a genuine miss.
2261
+ join.codeLookup = {
2262
+ dictionary: vector.dictionary,
2263
+ rows: new Int32Array(vector.dictionary.length).fill(-2),
2264
+ };
2265
+ }
2266
+ const cache = join.codeLookup.rows;
2267
+ const probeRows = input.rowsBySource[dictProbe.source];
2268
+ for (let row = 0; row < input.length; row += 1) {
2269
+ const sourceRow = probeRows?.[row] ?? -1;
2270
+ const code = stringCodeAt(vector, sourceRow);
2271
+ let buildRow = -1;
2272
+ if (code !== undefined) {
2273
+ buildRow = cache[code] ?? -2;
2274
+ if (buildRow === -2) {
2275
+ buildRow = join.lookup.firstRow(vector.dictionary[code] ?? "");
2276
+ cache[code] = buildRow;
2277
+ }
2278
+ }
2279
+ if (buildRow < 0 && join.kind === "inner")
2280
+ continue;
2281
+ selectedRows[outputLength] = row;
2282
+ buildRows[outputLength] = buildRow;
2283
+ outputLength += 1;
2284
+ }
2285
+ }
2286
+ else {
2287
+ for (let row = 0; row < input.length; row += 1) {
2288
+ const probeKey = evaluateBatchExpression(plan, join.probe, input, row);
2289
+ const buildRow = probeKey === null ? -1 : join.lookup.firstRow(probeKey);
2290
+ if (buildRow < 0 && join.kind === "inner")
2291
+ continue;
2292
+ selectedRows[outputLength] = row;
2293
+ buildRows[outputLength] = buildRow;
2294
+ outputLength += 1;
2295
+ }
2296
+ }
2297
+ if (outputLength === input.length) {
2298
+ selectedReservation.release();
2299
+ const rowsBySource = [...input.rowsBySource];
2300
+ rowsBySource[join.buildSource] = buildRows;
2301
+ return { length: outputLength, rowsBySource, memory: batchMemory };
2302
+ }
2303
+ batchMemory.reserve(safeMemoryProduct(safeMemoryProduct(plan.sourceTables.length, outputLength, "Filtered join row-index count"), Int32Array.BYTES_PER_ELEMENT, "Filtered join row indexes"), "Filtered join row indexes");
2304
+ const rowsBySource = input.rowsBySource.map((inputRows, source) => {
2305
+ const outputRows = new Int32Array(outputLength);
2306
+ for (let output = 0; output < outputLength; output += 1) {
2307
+ outputRows[output] =
2308
+ source === join.buildSource
2309
+ ? (buildRows[output] ?? -1)
2310
+ : (inputRows[selectedRows[output] ?? 0] ?? -1);
2311
+ }
2312
+ return outputRows;
2313
+ });
2314
+ selectedReservation.release();
2315
+ buildReservation.release();
2316
+ return { length: outputLength, rowsBySource, memory: batchMemory };
2317
+ }
2318
+ catch (error) {
2319
+ batchMemory.close();
2320
+ throw error;
2321
+ }
2322
+ }
2323
+ function appendJoinedRow(output, outputRow, input, row, buildSource, buildRow) {
2324
+ for (let source = 0; source < output.length; source += 1) {
2325
+ const rows = output[source];
2326
+ if (rows !== undefined) {
2327
+ rows[outputRow] =
2328
+ source === buildSource ? buildRow : (input.rowsBySource[source]?.[row] ?? -1);
2329
+ }
2330
+ }
2331
+ }
2332
+ const DEFERRED_SELECTION_BYTES = 2 * QUERY_REFERENCE_BYTES;
2333
+ /**
2334
+ * Accumulates ungrouped result rows. With ORDER BY and LIMIT the sink keeps only the best
2335
+ * `limit + offset` rows instead of materializing every scanned row for one full sort, so memory
2336
+ * stays bounded by the limit rather than the table. Retention matches a stable sort exactly:
2337
+ * ties resolve by arrival order, so the retained set is precisely the slice a full stable sort
2338
+ * would have produced.
2339
+ *
2340
+ * Two bounded strategies:
2341
+ * - Deferred selection (single-source plans whose order keys resolve to select expressions and
2342
+ * whose scan vectors outlive the scan): rows are tracked as (keys, arrival, source row) and
2343
+ * compacted by sort once the selection buffer reaches twice the capacity; only the final
2344
+ * survivors are projected. A losing row never allocates a result object, so even the
2345
+ * adversarial ascending-input/descending-order case stays allocation-free per row.
2346
+ * - Eager heap (joins, wildcard selects, or windowed scans): each retained row is projected into
2347
+ * a worst-at-root heap; a candidate that cannot beat the current worst is rejected before
2348
+ * projection whenever the order keys are resolvable.
2349
+ */
2350
+ class ResultSink {
2351
+ #plan;
2352
+ #memory;
2353
+ #capacity;
2354
+ #keyExpressions;
2355
+ #deferred;
2356
+ #rows = [];
2357
+ #heap = [];
2358
+ #selection = [];
2359
+ #keyScratch = [];
2360
+ #threshold;
2361
+ /** The threshold's first order key as an unboxed float, when the fast reject applies. */
2362
+ #thresholdFirst;
2363
+ #fastFirstKey;
2364
+ #selectionBytes = 0;
2365
+ #selectionReservedBytes = 0;
2366
+ #seq = 0;
2367
+ constructor(plan, memory, stableScan) {
2368
+ this.#plan = plan;
2369
+ this.#memory = memory;
2370
+ const bounded = !plan.grouped && plan.orderBy.length > 0 && plan.limit !== undefined;
2371
+ this.#capacity = bounded ? (plan.limit ?? 0) + (plan.offset ?? 0) : undefined;
2372
+ let keyExpressions;
2373
+ if (bounded && !plan.wildcard) {
2374
+ keyExpressions = [];
2375
+ for (const order of plan.orderBy) {
2376
+ const item = plan.select.find((selected) => selected.alias === order.outputName);
2377
+ if (item === undefined) {
2378
+ keyExpressions = undefined;
2379
+ break;
2380
+ }
2381
+ keyExpressions.push(item.expression);
2382
+ }
2383
+ }
2384
+ this.#keyExpressions = keyExpressions;
2385
+ this.#deferred =
2386
+ bounded && keyExpressions !== undefined && plan.joins.length === 0 && stableScan;
2387
+ // A bare numeric/datetime first order key rejects most rows with one unboxed comparison
2388
+ // against the current cut line, before any generic key evaluation or allocation. NULLs and
2389
+ // explicit NULLS placement fall through to the full comparison, so semantics are untouched.
2390
+ // Windowed scan vectors qualify too: the batch loop re-reads the resident window per batch.
2391
+ const firstKey = keyExpressions?.[0];
2392
+ const firstOrder = plan.orderBy[0];
2393
+ this.#fastFirstKey =
2394
+ firstKey !== undefined &&
2395
+ firstOrder !== undefined &&
2396
+ firstOrder.nulls === undefined &&
2397
+ firstKey.kind === "column" &&
2398
+ (firstKey.vector.kind === "number" || firstKey.vector.kind === "datetime")
2399
+ ? {
2400
+ vector: firstKey.vector,
2401
+ source: firstKey.source,
2402
+ desc: firstOrder.direction === "desc",
2403
+ }
2404
+ : undefined;
2405
+ }
2406
+ /** Rows accepted so far; only meaningful for the unbounded early-limit check. */
2407
+ get size() {
2408
+ return this.#capacity === undefined ? this.#rows.length : 0;
2409
+ }
2410
+ /**
2411
+ * The bounded-order batch loop: with an unboxed first key and an established cut line, a
2412
+ * whole batch scans in one pass and strictly-worse rows die on a single float comparison.
2413
+ * Returns false when the sink shape needs the per-row path.
2414
+ */
2415
+ tryAddBatch(batch) {
2416
+ const fast = this.#fastFirstKey;
2417
+ if (this.#capacity === undefined || fast === undefined)
2418
+ return false;
2419
+ const rows = batch.rowsBySource[fast.source];
2420
+ const vector = fast.vector;
2421
+ // Window and arrays re-read per batch: a streamed scan replaces them on every window slide.
2422
+ const values = vector.values;
2423
+ const validity = vector.validity;
2424
+ const window = vector.window;
2425
+ const windowStart = window?.start ?? 0;
2426
+ const windowLength = window?.length ?? vector.length;
2427
+ const desc = fast.desc;
2428
+ for (let row = 0; row < batch.length; row += 1) {
2429
+ const threshold = this.#thresholdFirst;
2430
+ if (threshold === undefined || threshold === null) {
2431
+ this.#addSlow(batch, row);
2432
+ continue;
2433
+ }
2434
+ const sourceRow = rows?.[row] ?? -1;
2435
+ const slot = sourceRow - windowStart;
2436
+ if (sourceRow >= 0 && sourceRow < vector.length && slot >= 0 && slot < windowLength) {
2437
+ if (isValid(validity, slot)) {
2438
+ const value = values[slot] ?? 0;
2439
+ if ((desc ? threshold - value : value - threshold) > 0) {
2440
+ this.#seq += 1;
2441
+ continue;
2442
+ }
2443
+ }
2444
+ }
2445
+ this.#addSlow(batch, row);
2446
+ }
2447
+ return true;
2448
+ }
2449
+ #addSlow(batch, row) {
2450
+ if (this.#deferred)
2451
+ this.#addDeferred(batch, row);
2452
+ else
2453
+ this.#addEager(batch, row);
2454
+ }
2455
+ add(batch, row) {
2456
+ if (this.#capacity === undefined) {
2457
+ const resultRow = projectBatchRow(this.#plan, batch, row);
2458
+ this.#memory.tally(queryRowPayloadBytes(resultRow), "Accumulated result row");
2459
+ this.#rows.push(resultRow);
2460
+ return;
2461
+ }
2462
+ if (this.#capacity === 0)
2463
+ return;
2464
+ if (this.#deferred) {
2465
+ this.#addDeferred(batch, row);
2466
+ return;
2467
+ }
2468
+ this.#addEager(batch, row);
2469
+ }
2470
+ /** Returns accepted rows in arrival order, ready for the shared stable sort and trim. */
2471
+ finish() {
2472
+ if (this.#capacity === undefined)
2473
+ return this.#rows;
2474
+ if (!this.#deferred) {
2475
+ return this.#heap.sort((left, right) => left.seq - right.seq).map((entry) => entry.row);
2476
+ }
2477
+ this.#compactSelection();
2478
+ this.#selection.sort((left, right) => left.seq - right.seq);
2479
+ const scanIndex = new Int32Array(1);
2480
+ const rowsBySource = [scanIndex];
2481
+ const batch = { length: 1, rowsBySource };
2482
+ const rows = [];
2483
+ for (const entry of this.#selection) {
2484
+ scanIndex[0] = entry.sourceRow;
2485
+ const resultRow = projectBatchRow(this.#plan, batch, 0);
2486
+ this.#memory.tally(queryRowPayloadBytes(resultRow), "Accumulated result row");
2487
+ rows.push(resultRow);
2488
+ }
2489
+ return rows;
2490
+ }
2491
+ #addDeferred(batch, row) {
2492
+ const fast = this.#fastFirstKey;
2493
+ if (fast !== undefined && this.#thresholdFirst !== undefined && this.#thresholdFirst !== null) {
2494
+ const value = rawFloat64Value(fast.vector, batch.rowsBySource[fast.source]?.[row] ?? -1);
2495
+ if (value !== null) {
2496
+ const comparison = fast.desc ? this.#thresholdFirst - value : value - this.#thresholdFirst;
2497
+ // Strictly worse on the first key is strictly worse overall; the row cannot survive.
2498
+ if (comparison > 0) {
2499
+ this.#seq += 1;
2500
+ return;
2501
+ }
2502
+ }
2503
+ }
2504
+ this.#evaluateKeys(batch, row);
2505
+ const seq = this.#seq;
2506
+ this.#seq += 1;
2507
+ // The candidate arrived after every retained row, so an order-key tie keeps the retained
2508
+ // row — only a strictly better key survives the current cut line.
2509
+ if (this.#threshold !== undefined &&
2510
+ this.#compareKeys(this.#keyScratch, this.#threshold.keys) >= 0) {
2511
+ return;
2512
+ }
2513
+ let bytes = DEFERRED_SELECTION_BYTES;
2514
+ for (let index = 0; index < this.#plan.orderBy.length; index += 1) {
2515
+ bytes = safeMemorySum(bytes, queryValuePayloadBytes(this.#keyScratch[index] ?? null), "Top-N selection entry");
2516
+ }
2517
+ this.#selection.push({
2518
+ keys: this.#keyScratch.slice(0, this.#plan.orderBy.length),
2519
+ seq,
2520
+ sourceRow: batch.rowsBySource[this.#plan.scanSource]?.[row] ?? -1,
2521
+ bytes,
2522
+ });
2523
+ this.#reserveSelectionBytes(bytes);
2524
+ if (this.#selection.length >= (this.#capacity ?? 0) * 2)
2525
+ this.#compactSelection();
2526
+ }
2527
+ /**
2528
+ * Reserves selection-buffer growth at its high-water mark: the buffer is bounded by twice the
2529
+ * limit, so reservations grow monotonically instead of churning a release per evicted entry.
2530
+ */
2531
+ #reserveSelectionBytes(bytes) {
2532
+ this.#selectionBytes += bytes;
2533
+ if (this.#selectionBytes <= this.#selectionReservedBytes)
2534
+ return;
2535
+ this.#memory.reserve(this.#selectionBytes - this.#selectionReservedBytes, "Top-N selection");
2536
+ this.#selectionReservedBytes = this.#selectionBytes;
2537
+ }
2538
+ /**
2539
+ * Trims the selection to the best `capacity` entries and advances the cut line.
2540
+ *
2541
+ * Partition, not sort: which rows survive is all that matters here, because `finish` hands
2542
+ * them back in arrival order for the query's own ORDER BY to sort. Quickselect puts the
2543
+ * capacity-th best entry at its final index with everything better ahead of it, in linear
2544
+ * comparisons rather than n log n. The retained set is identical to what sorting and
2545
+ * truncating produced — the comparator is the same, ties included — and so is the memory,
2546
+ * which is the point: a deep page keeps its bound and stops paying a full sort for it.
2547
+ */
2548
+ #compactSelection() {
2549
+ const capacity = this.#capacity ?? 0;
2550
+ if (this.#selection.length <= capacity)
2551
+ return;
2552
+ this.#selectBest(capacity);
2553
+ this.#selection.length = capacity;
2554
+ let retainedBytes = 0;
2555
+ for (const entry of this.#selection)
2556
+ retainedBytes += entry.bytes;
2557
+ this.#selectionBytes = retainedBytes;
2558
+ // Quickselect leaves the capacity-th best entry at the last retained index, which is the
2559
+ // worst of what survived and therefore the new cut line.
2560
+ this.#threshold = this.#selection[capacity - 1];
2561
+ const first = this.#threshold?.keys[0];
2562
+ this.#thresholdFirst =
2563
+ first === undefined
2564
+ ? undefined
2565
+ : first === null
2566
+ ? null
2567
+ : typeof first === "number"
2568
+ ? first
2569
+ : first instanceof Date
2570
+ ? first.getTime()
2571
+ : undefined;
2572
+ }
2573
+ /**
2574
+ * Quickselect over the selection buffer: after it returns, the first `count` entries are the
2575
+ * `count` best in some order and index `count - 1` holds the worst of them, which is the cut
2576
+ * line. Median-of-three pivots keep the already-sorted input — the common case, since scans
2577
+ * arrive in key order often enough — off the quadratic path.
2578
+ */
2579
+ #selectBest(count) {
2580
+ const entries = this.#selection;
2581
+ const worse = (left, right) => {
2582
+ const comparison = this.#compareKeys(left.keys, right.keys);
2583
+ return comparison !== 0 ? comparison > 0 : left.seq > right.seq;
2584
+ };
2585
+ const swap = (left, right) => {
2586
+ const held = required(entries[left], "Selection entry is missing");
2587
+ entries[left] = required(entries[right], "Selection entry is missing");
2588
+ entries[right] = held;
2589
+ };
2590
+ let low = 0;
2591
+ let high = entries.length - 1;
2592
+ const target = count - 1;
2593
+ while (low < high) {
2594
+ // Median of three, moved to the front as the pivot.
2595
+ const middle = (low + high) >> 1;
2596
+ if (worse(required(entries[low], "Selection entry is missing"), required(entries[middle], "Selection entry is missing")))
2597
+ swap(low, middle);
2598
+ if (worse(required(entries[low], "Selection entry is missing"), required(entries[high], "Selection entry is missing")))
2599
+ swap(low, high);
2600
+ if (worse(required(entries[middle], "Selection entry is missing"), required(entries[high], "Selection entry is missing")))
2601
+ swap(middle, high);
2602
+ swap(low, middle);
2603
+ const pivot = required(entries[low], "Selection entry is missing");
2604
+ let left = low;
2605
+ let right = high + 1;
2606
+ for (;;) {
2607
+ do
2608
+ left += 1;
2609
+ while (left <= high && worse(pivot, required(entries[left], "Selection entry is missing")));
2610
+ do
2611
+ right -= 1;
2612
+ while (worse(required(entries[right], "Selection entry is missing"), pivot));
2613
+ if (left >= right)
2614
+ break;
2615
+ swap(left, right);
2616
+ }
2617
+ swap(low, right);
2618
+ if (right === target)
2619
+ return;
2620
+ if (right > target)
2621
+ high = right - 1;
2622
+ else
2623
+ low = right + 1;
2624
+ }
2625
+ }
2626
+ #addEager(batch, row) {
2627
+ let projected;
2628
+ if (this.#keyExpressions === undefined) {
2629
+ projected = projectBatchRow(this.#plan, batch, row);
2630
+ for (let index = 0; index < this.#plan.orderBy.length; index += 1) {
2631
+ const order = required(this.#plan.orderBy[index], "Order term is missing");
2632
+ this.#keyScratch[index] = projected[order.outputName] ?? null;
2633
+ }
2634
+ }
2635
+ else {
2636
+ this.#evaluateKeys(batch, row);
2637
+ }
2638
+ const seq = this.#seq;
2639
+ this.#seq += 1;
2640
+ const capacity = this.#capacity ?? 0;
2641
+ if (this.#heap.length >= capacity) {
2642
+ const worst = required(this.#heap[0], "Top-N heap root is missing");
2643
+ // An order-key tie keeps the earlier-arriving retained row.
2644
+ if (this.#compareKeys(this.#keyScratch, worst.keys) >= 0)
2645
+ return;
2646
+ const resultRow = projected ?? projectBatchRow(this.#plan, batch, row);
2647
+ const reservation = this.#memory.reserve(this.#entryPayloadBytes(resultRow), "Top-N result row");
2648
+ worst.reservation.release();
2649
+ this.#heap[0] = {
2650
+ row: resultRow,
2651
+ keys: this.#keyScratch.slice(0, this.#plan.orderBy.length),
2652
+ seq,
2653
+ reservation,
2654
+ };
2655
+ this.#siftDown(0);
2656
+ this.#updateEagerThreshold();
2657
+ return;
2658
+ }
2659
+ const resultRow = projected ?? projectBatchRow(this.#plan, batch, row);
2660
+ const reservation = this.#memory.reserve(this.#entryPayloadBytes(resultRow), "Top-N result row");
2661
+ this.#heap.push({
2662
+ row: resultRow,
2663
+ keys: this.#keyScratch.slice(0, this.#plan.orderBy.length),
2664
+ seq,
2665
+ reservation,
2666
+ });
2667
+ this.#siftUp(this.#heap.length - 1);
2668
+ this.#updateEagerThreshold();
2669
+ }
2670
+ /**
2671
+ * The eager cut line for the unboxed batch loop: once the heap is full, its root's first
2672
+ * order key as a float. Non-numeric keys leave the threshold null, which sends every row
2673
+ * through the full comparison.
2674
+ */
2675
+ #updateEagerThreshold() {
2676
+ if (this.#fastFirstKey === undefined)
2677
+ return;
2678
+ if (this.#heap.length < (this.#capacity ?? 0))
2679
+ return;
2680
+ const first = this.#heap[0]?.keys[0];
2681
+ this.#thresholdFirst =
2682
+ typeof first === "number" ? first : first instanceof Date ? first.getTime() : null;
2683
+ }
2684
+ #evaluateKeys(batch, row) {
2685
+ const expressions = required(this.#keyExpressions, "Order keys are missing");
2686
+ for (let index = 0; index < expressions.length; index += 1) {
2687
+ const expression = required(expressions[index], "Order key is missing");
2688
+ this.#keyScratch[index] = asQueryValue(evaluateBatchExpression(this.#plan, expression, batch, row));
2689
+ }
2690
+ }
2691
+ #entryPayloadBytes(row) {
2692
+ let bytes = queryRowPayloadBytes(row);
2693
+ for (let index = 0; index < this.#plan.orderBy.length; index += 1) {
2694
+ bytes = safeMemorySum(bytes, queryValuePayloadBytes(this.#keyScratch[index] ?? null), "Top-N order keys");
2695
+ }
2696
+ return bytes;
2697
+ }
2698
+ #compareKeys(left, right) {
2699
+ const orderBy = this.#plan.orderBy;
2700
+ for (let index = 0; index < orderBy.length; index += 1) {
2701
+ const order = required(orderBy[index], "Order term is missing");
2702
+ const placed = explicitNullOrder(left[index], right[index], order.nulls);
2703
+ if (placed !== undefined && placed !== 0)
2704
+ return placed;
2705
+ const comparison = compareValues(left[index], right[index]);
2706
+ if (comparison !== 0)
2707
+ return order.direction === "desc" ? -comparison : comparison;
2708
+ }
2709
+ return 0;
2710
+ }
2711
+ /** Entry `left` loses to `right` when it sorts later under (keys, arrival). */
2712
+ #isWorse(left, right) {
2713
+ const comparison = this.#compareKeys(left.keys, right.keys);
2714
+ if (comparison !== 0)
2715
+ return comparison > 0;
2716
+ return left.seq > right.seq;
2717
+ }
2718
+ #siftUp(index) {
2719
+ let child = index;
2720
+ while (child > 0) {
2721
+ const parent = (child - 1) >> 1;
2722
+ const childEntry = required(this.#heap[child], "Heap entry is missing");
2723
+ const parentEntry = required(this.#heap[parent], "Heap entry is missing");
2724
+ if (!this.#isWorse(childEntry, parentEntry))
2725
+ break;
2726
+ this.#heap[child] = parentEntry;
2727
+ this.#heap[parent] = childEntry;
2728
+ child = parent;
2729
+ }
2730
+ }
2731
+ #siftDown(index) {
2732
+ let parent = index;
2733
+ for (;;) {
2734
+ let worst = parent;
2735
+ let worstEntry = required(this.#heap[worst], "Heap entry is missing");
2736
+ const left = parent * 2 + 1;
2737
+ const right = left + 1;
2738
+ const leftEntry = this.#heap[left];
2739
+ if (leftEntry !== undefined && this.#isWorse(leftEntry, worstEntry)) {
2740
+ worst = left;
2741
+ worstEntry = leftEntry;
2742
+ }
2743
+ const rightEntry = this.#heap[right];
2744
+ if (rightEntry !== undefined && this.#isWorse(rightEntry, worstEntry)) {
2745
+ worst = right;
2746
+ worstEntry = rightEntry;
2747
+ }
2748
+ if (worst === parent)
2749
+ return;
2750
+ const parentEntry = required(this.#heap[parent], "Heap entry is missing");
2751
+ this.#heap[parent] = worstEntry;
2752
+ this.#heap[worst] = parentEntry;
2753
+ parent = worst;
2754
+ }
2755
+ }
2756
+ }
2757
+ /**
2758
+ * Insertion-ordered group-state store. A single bare string-column GROUP BY resolves each row
2759
+ * through a slot table indexed by the column's dictionary code — no key bytes, no hashing —
2760
+ * while every other shape goes through the byte-keyed group index. Both modes surface states in
2761
+ * first-seen order, matching the row oracle's grouping order.
2762
+ */
2763
+ class GroupAccumulator {
2764
+ #plan;
2765
+ #memory;
2766
+ #index;
2767
+ #codeStates;
2768
+ /** The dictionary #codeStates is laid out for; a streamed window slide swaps it. */
2769
+ #codeDictionary;
2770
+ /** Value-keyed group states so a new window's dictionary remaps to existing groups. */
2771
+ #codeStateByValue = new Map();
2772
+ #nullCodeState;
2773
+ #codeSlotsReserved = 0;
2774
+ #ordered;
2775
+ #codeColumns;
2776
+ #multiCodeColumns;
2777
+ #multiCodeStates;
2778
+ #multiCodeScratch = [];
2779
+ #keyScratch = [];
2780
+ // The miss factories live on the accumulator and read the pending row through these fields, so
2781
+ // the per-row lookup never allocates a capturing closure; execution is synchronous, so the
2782
+ // pending row cannot change while a factory runs.
2783
+ #pendingBatch;
2784
+ #pendingRow = 0;
2785
+ #pendingSingleValue = null;
2786
+ #createPendingSingle = () => createGroupState([this.#pendingSingleValue], this.#plan, this.#memory);
2787
+ #createPendingCompound = () => createGroupState(this.#plan.groupBy.map((expression) => asQueryValue(evaluateBatchExpression(this.#plan, expression, required(this.#pendingBatch, "Pending group batch is missing"), this.#pendingRow))), this.#plan, this.#memory);
2788
+ #fastAggregatesCache;
2789
+ constructor(plan, memory) {
2790
+ this.#plan = plan;
2791
+ this.#memory = memory;
2792
+ this.#index = new ByteGroupIndex(memory);
2793
+ this.#fastAggregatesCache = plan.grouped ? this.#fastAggregates() : undefined;
2794
+ if (plan.grouped && plan.groupBy.length === 0) {
2795
+ this.#index.setEmpty(createGroupState([], plan, memory));
2796
+ }
2797
+ if (plan.codeGrouping !== undefined) {
2798
+ const slots = plan.codeGrouping.vector.dictionary.length + 1;
2799
+ memory.reserve(safeMemoryProduct(slots, QUERY_REFERENCE_BYTES, "Group code slots"), "Group code slots");
2800
+ this.#codeStates = new Array(slots).fill(undefined);
2801
+ this.#codeDictionary = plan.codeGrouping.vector.dictionary;
2802
+ this.#codeSlotsReserved = slots;
2803
+ this.#ordered = [];
2804
+ }
2805
+ // Compound keys substitute the dictionary code for each bare unwindowed string column: codes
2806
+ // are stable and value-unique within one execution, so the key encodes a fixed-width number
2807
+ // instead of re-encoding the string's UTF-8 on every row. Types are stable per position, so
2808
+ // a code can never collide with a genuine number from the same expression.
2809
+ this.#codeColumns =
2810
+ plan.groupBy.length > 1
2811
+ ? plan.groupBy.map((expression) => expression.kind === "column" &&
2812
+ expression.vector.kind === "string" &&
2813
+ expression.vector.window === undefined
2814
+ ? { source: expression.source, vector: expression.vector }
2815
+ : undefined)
2816
+ : [];
2817
+ // When every compound key column is dictionary-coded and the combined code space is small,
2818
+ // group lookup packs codes into one exact integer. Small domains use a direct array; large,
2819
+ // sparse domains use a numeric Map instead of byte-encoding and hashing each compound key.
2820
+ // Each column contributes (dictionary size + 1) slots, the extra one for NULL.
2821
+ if (this.#codeColumns.length > 1 && this.#codeColumns.every((column) => column !== undefined)) {
2822
+ const columns = this.#codeColumns;
2823
+ let slots = 1;
2824
+ for (const column of columns)
2825
+ slots *= column.vector.dictionary.length + 1;
2826
+ if (Number.isSafeInteger(slots) && slots <= MULTI_CODE_GROUP_SLOT_CAP) {
2827
+ try {
2828
+ memory.reserve(safeMemoryProduct(slots, QUERY_REFERENCE_BYTES, "Group code slots"), "Group code slots");
2829
+ }
2830
+ catch (error) {
2831
+ if (!(error instanceof QueryMemoryBudgetError))
2832
+ throw error;
2833
+ return;
2834
+ }
2835
+ this.#multiCodeColumns = columns;
2836
+ this.#codeStates = new Array(slots).fill(undefined);
2837
+ this.#ordered = [];
2838
+ }
2839
+ else if (Number.isSafeInteger(slots)) {
2840
+ this.#multiCodeColumns = columns;
2841
+ this.#multiCodeStates = new Map();
2842
+ this.#ordered = [];
2843
+ }
2844
+ }
2845
+ }
2846
+ /**
2847
+ * Fast aggregate specs for the batch kernel: every aggregate is COUNT(*) or a bare numeric
2848
+ * column under COUNT/SUM/AVG. MIN/MAX keep the generic path for its comparison and memory
2849
+ * accounting semantics. Undefined when any aggregate needs the generic path.
2850
+ */
2851
+ #fastAggregates() {
2852
+ const specs = [];
2853
+ for (const spec of this.#plan.aggregates) {
2854
+ // The kernel counts every row it is given; deduplication needs the per-row path.
2855
+ if (spec.distinct === true)
2856
+ return undefined;
2857
+ if (spec.argument.kind === "wildcard" && spec.name === "COUNT") {
2858
+ specs.push({ kind: "star", sums: false });
2859
+ continue;
2860
+ }
2861
+ if (spec.rawNumber !== undefined &&
2862
+ (spec.name === "COUNT" || spec.name === "SUM" || spec.name === "AVG")) {
2863
+ specs.push({ kind: "column", sums: spec.name !== "COUNT" });
2864
+ continue;
2865
+ }
2866
+ return undefined;
2867
+ }
2868
+ return specs;
2869
+ }
2870
+ /**
2871
+ * The batch kernel for dictionary-coded single-column grouping (or the global group) over
2872
+ * COUNT/SUM/AVG aggregates: one pass with unboxed reads and no per-row dispatch. Returns false
2873
+ * when the plan shape needs the generic per-row path.
2874
+ */
2875
+ consumeFast(batch, passes, hasPredicates) {
2876
+ const plan = this.#plan;
2877
+ const codeGrouping = plan.codeGrouping;
2878
+ const globalGroup = plan.groupBy.length === 0;
2879
+ if (!globalGroup && (codeGrouping === undefined || this.#codeStates === undefined)) {
2880
+ return false;
2881
+ }
2882
+ const specs = this.#fastAggregatesCache;
2883
+ if (specs === undefined)
2884
+ return false;
2885
+ // The purest shape — global COUNT(*) with no predicates — needs no row loop at all.
2886
+ if (globalGroup && !hasPredicates && specs.every((spec) => spec.kind === "star")) {
2887
+ const state = required(this.#index.getEmpty(), "Grouped query state is missing");
2888
+ for (let index = 0; index < specs.length; index += 1) {
2889
+ state.counts[index] = (state.counts[index] ?? 0) + batch.length;
2890
+ }
2891
+ return true;
2892
+ }
2893
+ const groupRows = codeGrouping === undefined ? undefined : batch.rowsBySource[codeGrouping.source];
2894
+ const groupVector = codeGrouping?.vector;
2895
+ if (groupVector !== undefined)
2896
+ this.#ensureCodeStates(groupVector.dictionary);
2897
+ const states = this.#codeStates;
2898
+ const nullCode = groupVector?.dictionary.length ?? 0;
2899
+ const globalState = globalGroup
2900
+ ? required(this.#index.getEmpty(), "Grouped query state is missing")
2901
+ : undefined;
2902
+ // Hoisted per-column reads: the row loop touches only local typed arrays and numbers.
2903
+ const columns = [];
2904
+ let stars = 0;
2905
+ for (let index = 0; index < specs.length; index += 1) {
2906
+ const spec = specs[index];
2907
+ if (spec === undefined || spec.kind === "star") {
2908
+ stars += 1;
2909
+ continue;
2910
+ }
2911
+ const raw = plan.aggregates[index]?.rawNumber;
2912
+ if (raw === undefined)
2913
+ return false;
2914
+ columns.push({
2915
+ index,
2916
+ sums: spec.sums,
2917
+ rows: batch.rowsBySource[raw.source],
2918
+ values: raw.vector.values,
2919
+ validity: raw.vector.validity,
2920
+ windowStart: raw.vector.window?.start ?? 0,
2921
+ length: raw.vector.length,
2922
+ slots: raw.vector.values.length,
2923
+ });
2924
+ }
2925
+ const grouping = groupVector === undefined
2926
+ ? undefined
2927
+ : {
2928
+ codes: groupVector.codes,
2929
+ validity: groupVector.validity,
2930
+ length: groupVector.length,
2931
+ dictionary: groupVector.dictionary,
2932
+ windowStart: groupVector.window?.start ?? 0,
2933
+ slots: groupVector.codes.length,
2934
+ };
2935
+ for (let row = 0; row < batch.length; row += 1) {
2936
+ if (hasPredicates && !passes(row))
2937
+ continue;
2938
+ let state = globalState;
2939
+ if (state === undefined && grouping !== undefined && states !== undefined) {
2940
+ const sourceRow = groupRows?.[row] ?? -1;
2941
+ let code = nullCode;
2942
+ if (sourceRow >= 0 && sourceRow < grouping.length) {
2943
+ const slot = sourceRow - grouping.windowStart;
2944
+ if (slot < 0 || slot >= grouping.slots) {
2945
+ throw new RangeError("Streamed vector row is outside the resident window");
2946
+ }
2947
+ if (isValid(grouping.validity, slot)) {
2948
+ const rawCode = grouping.codes[slot] ?? NULL_STRING_CODE;
2949
+ if (rawCode !== NULL_STRING_CODE)
2950
+ code = rawCode;
2951
+ }
2952
+ }
2953
+ state = states[code];
2954
+ if (state === undefined) {
2955
+ const value = code === nullCode ? null : (grouping.dictionary[code] ?? null);
2956
+ state = createGroupState([value], plan, this.#memory);
2957
+ states[code] = state;
2958
+ this.#registerCodeState(value, state);
2959
+ this.#ordered?.push(state);
2960
+ }
2961
+ }
2962
+ if (state === undefined)
2963
+ return false;
2964
+ const counts = state.counts;
2965
+ if (stars > 0) {
2966
+ for (let index = 0; index < specs.length; index += 1) {
2967
+ if (specs[index]?.kind === "star")
2968
+ counts[index] = (counts[index] ?? 0) + 1;
2969
+ }
2970
+ }
2971
+ for (const column of columns) {
2972
+ const sourceRow = column.rows?.[row] ?? -1;
2973
+ if (sourceRow < 0 || sourceRow >= column.length)
2974
+ continue;
2975
+ const slot = sourceRow - column.windowStart;
2976
+ if (slot < 0 || slot >= column.slots) {
2977
+ throw new RangeError("Streamed vector row is outside the resident window");
2978
+ }
2979
+ if (!isValid(column.validity, slot))
2980
+ continue;
2981
+ counts[column.index] = (counts[column.index] ?? 0) + 1;
2982
+ if (column.sums) {
2983
+ state.sums[column.index] = (state.sums[column.index] ?? 0) + (column.values[slot] ?? 0);
2984
+ }
2985
+ }
2986
+ }
2987
+ return true;
2988
+ }
2989
+ get fastAggregatesCache() {
2990
+ return this.#fastAggregatesCache;
2991
+ }
2992
+ /**
2993
+ * Re-lays the code-slot table for a new window dictionary. Existing groups carry over by
2994
+ * value, so a group's state is shared across every window that mentions its value; the cost
2995
+ * is one map lookup per distinct value per window, never per row. Only used for the
2996
+ * single-column code grouping — compound code keys stay unwindowed.
2997
+ */
2998
+ #ensureCodeStates(dictionary) {
2999
+ if (this.#codeDictionary === dictionary || this.#codeStates === undefined)
3000
+ return;
3001
+ const slots = dictionary.length + 1;
3002
+ if (slots > this.#codeSlotsReserved) {
3003
+ this.#memory.reserve(safeMemoryProduct(slots - this.#codeSlotsReserved, QUERY_REFERENCE_BYTES, "Group code slots"), "Group code slots");
3004
+ this.#codeSlotsReserved = slots;
3005
+ }
3006
+ const next = new Array(slots).fill(undefined);
3007
+ for (let code = 0; code < dictionary.length; code += 1) {
3008
+ const state = this.#codeStateByValue.get(dictionary[code] ?? "");
3009
+ if (state !== undefined)
3010
+ next[code] = state;
3011
+ }
3012
+ next[dictionary.length] = this.#nullCodeState;
3013
+ this.#codeStates = next;
3014
+ this.#codeDictionary = dictionary;
3015
+ }
3016
+ /** Registers a freshly created code-grouped state so later windows can find it by value. */
3017
+ #registerCodeState(value, state) {
3018
+ if (value === null)
3019
+ this.#nullCodeState = state;
3020
+ else
3021
+ this.#codeStateByValue.set(value, state);
3022
+ }
3023
+ /** Resolves the group state for one row, creating it on first touch. */
3024
+ stateFor(batch, row) {
3025
+ const plan = this.#plan;
3026
+ const codeGrouping = plan.codeGrouping;
3027
+ if (codeGrouping !== undefined && this.#codeStates !== undefined) {
3028
+ const sourceRow = batch.rowsBySource[codeGrouping.source]?.[row] ?? -1;
3029
+ const vector = codeGrouping.vector;
3030
+ this.#ensureCodeStates(vector.dictionary);
3031
+ const states = required(this.#codeStates, "Group code slots are missing");
3032
+ let code = vector.dictionary.length;
3033
+ if (sourceRow >= 0 && sourceRow < vector.length) {
3034
+ const windowStart = vector.window?.start ?? 0;
3035
+ const slot = sourceRow - windowStart;
3036
+ if (slot < 0 || slot >= vector.codes.length) {
3037
+ throw new RangeError("Streamed vector row is outside the resident window");
3038
+ }
3039
+ if (isValid(vector.validity, slot)) {
3040
+ const rawCode = vector.codes[slot] ?? NULL_STRING_CODE;
3041
+ if (rawCode !== NULL_STRING_CODE)
3042
+ code = rawCode;
3043
+ }
3044
+ }
3045
+ let state = states[code];
3046
+ if (state === undefined) {
3047
+ const value = code === vector.dictionary.length ? null : (vector.dictionary[code] ?? null);
3048
+ state = createGroupState([value], plan, this.#memory);
3049
+ states[code] = state;
3050
+ this.#registerCodeState(value, state);
3051
+ this.#ordered?.push(state);
3052
+ }
3053
+ return state;
3054
+ }
3055
+ if (plan.groupBy.length === 0) {
3056
+ return required(this.#index.getEmpty(), "Grouped query state is missing");
3057
+ }
3058
+ const multiCode = this.#multiCodeColumns;
3059
+ if (multiCode !== undefined &&
3060
+ (this.#codeStates !== undefined || this.#multiCodeStates !== undefined)) {
3061
+ let slot = 0;
3062
+ for (let index = 0; index < multiCode.length; index += 1) {
3063
+ const column = required(multiCode[index], "Group code column is missing");
3064
+ const vector = column.vector;
3065
+ const sourceRow = batch.rowsBySource[column.source]?.[row] ?? -1;
3066
+ let code = vector.dictionary.length;
3067
+ if (sourceRow >= 0 && sourceRow < vector.length && isValid(vector.validity, sourceRow)) {
3068
+ const rawCode = vector.codes[sourceRow] ?? NULL_STRING_CODE;
3069
+ if (rawCode !== NULL_STRING_CODE)
3070
+ code = rawCode;
3071
+ }
3072
+ this.#multiCodeScratch[index] = code;
3073
+ slot = slot * (vector.dictionary.length + 1) + code;
3074
+ }
3075
+ let state = this.#codeStates?.[slot] ?? this.#multiCodeStates?.get(slot);
3076
+ if (state === undefined) {
3077
+ const groupValues = [];
3078
+ for (let index = 0; index < multiCode.length; index += 1) {
3079
+ const vector = required(multiCode[index], "Group code column is missing").vector;
3080
+ const code = this.#multiCodeScratch[index] ?? vector.dictionary.length;
3081
+ groupValues.push(code === vector.dictionary.length ? null : (vector.dictionary[code] ?? null));
3082
+ }
3083
+ state = createGroupState(groupValues, plan, this.#memory);
3084
+ if (this.#codeStates !== undefined)
3085
+ this.#codeStates[slot] = state;
3086
+ else {
3087
+ this.#memory.tally(PACKED_GROUP_ENTRY_BYTES, "Packed group index entry");
3088
+ this.#multiCodeStates?.set(slot, state);
3089
+ }
3090
+ this.#ordered?.push(state);
3091
+ }
3092
+ return state;
3093
+ }
3094
+ if (plan.groupBy.length === 1) {
3095
+ const groupValue = asQueryValue(evaluateBatchExpression(plan, required(plan.groupBy[0], "Group expression is missing"), batch, row));
3096
+ this.#pendingSingleValue = groupValue;
3097
+ return this.#index.getOrInsertOne(groupKey(groupValue), this.#createPendingSingle);
3098
+ }
3099
+ // The scratch array carries this row's keys without a per-row allocation; the create callback
3100
+ // re-evaluates the group expressions, which runs once per distinct group.
3101
+ for (let index = 0; index < plan.groupBy.length; index += 1) {
3102
+ const codeColumn = this.#codeColumns[index];
3103
+ if (codeColumn !== undefined) {
3104
+ const sourceRow = batch.rowsBySource[codeColumn.source]?.[row] ?? -1;
3105
+ let key = null;
3106
+ if (sourceRow >= 0 &&
3107
+ sourceRow < codeColumn.vector.length &&
3108
+ isValid(codeColumn.vector.validity, sourceRow)) {
3109
+ const rawCode = codeColumn.vector.codes[sourceRow] ?? NULL_STRING_CODE;
3110
+ if (rawCode !== NULL_STRING_CODE)
3111
+ key = rawCode;
3112
+ }
3113
+ this.#keyScratch[index] = key;
3114
+ continue;
3115
+ }
3116
+ const expression = required(plan.groupBy[index], "Group expression is missing");
3117
+ this.#keyScratch[index] = groupKey(asQueryValue(evaluateBatchExpression(plan, expression, batch, row)));
3118
+ }
3119
+ this.#keyScratch.length = plan.groupBy.length;
3120
+ this.#pendingBatch = batch;
3121
+ this.#pendingRow = row;
3122
+ return this.#index.getOrInsert(this.#keyScratch, this.#createPendingCompound);
3123
+ }
3124
+ values() {
3125
+ return this.#ordered ?? this.#index.values();
3126
+ }
3127
+ }
3128
+ function consumeBatch(plan, batch, groups, output, memory, prefiltered = false) {
3129
+ const checkPredicates = plan.predicates.length > 0 && !prefiltered;
3130
+ if (checkPredicates) {
3131
+ // Selection-first: unboxed predicate loops compact the batch before any per-row work.
3132
+ const { selection, survivors } = filterScanBatch(plan, batch);
3133
+ if (survivors === 0)
3134
+ return;
3135
+ if (plan.grouped) {
3136
+ // Group over the compacted survivors so the unboxed aggregate kernel still applies.
3137
+ const working = survivors < batch.length
3138
+ ? materializeSelection(batch, selection, survivors, memory)
3139
+ : batch;
3140
+ try {
3141
+ consumeBatch(plan, working, groups, output, memory, true);
3142
+ }
3143
+ finally {
3144
+ if (working !== batch)
3145
+ working.memory?.close();
3146
+ }
3147
+ return;
3148
+ }
3149
+ for (let index = 0; index < survivors; index += 1) {
3150
+ output.add(batch, selection[index] ?? 0);
3151
+ if (reachedEarlyLimit(plan, output.size))
3152
+ return;
3153
+ }
3154
+ return;
3155
+ }
3156
+ if (plan.grouped && groups.consumeFast(batch, () => true, false))
3157
+ return;
3158
+ if (!plan.grouped && output.tryAddBatch(batch))
3159
+ return;
3160
+ for (let row = 0; row < batch.length; row += 1) {
3161
+ if (plan.grouped) {
3162
+ updateAggregates(plan, groups.stateFor(batch, row), batch, row, memory);
3163
+ }
3164
+ else {
3165
+ output.add(batch, row);
3166
+ if (reachedEarlyLimit(plan, output.size))
3167
+ return;
3168
+ }
3169
+ }
3170
+ }
3171
+ /**
3172
+ * Shared accumulator arrays for groups in a plan with no aggregates — a `GROUP BY` that only
3173
+ * produces its keys, which is exactly the inner block `COUNT(DISTINCT x)` desugars into. That
3174
+ * block makes one group per distinct (key, x) pair, so allocating five per-group accumulators
3175
+ * that nothing can ever write costs five allocations per distinct value. Every writer is bounded
3176
+ * by the aggregate count, so with none there is nothing to write; the plain arrays are frozen so
3177
+ * a future writer that ignores that bound fails loudly instead of corrupting every group.
3178
+ */
3179
+ const EMPTY_ACCUMULATOR = new Float64Array(0);
3180
+ const EMPTY_VALUES = Object.freeze([]);
3181
+ const EMPTY_RESERVATIONS = Object.freeze([]);
3182
+ function createGroupState(groupValues, plan, memory) {
3183
+ let payloadBytes = QUERY_REFERENCE_BYTES;
3184
+ for (const value of groupValues) {
3185
+ payloadBytes = safeMemorySum(payloadBytes, queryValuePayloadBytes(value), "Group state");
3186
+ }
3187
+ payloadBytes = safeMemorySum(payloadBytes, safeMemoryProduct(plan.aggregates.length, AGGREGATE_ACCUMULATOR_BYTES, "Aggregate accumulator state"), "Group state");
3188
+ // tally, not reserve: a group state lives until the context closes and is never released
3189
+ // on its own, so a per-group QueryMemoryReservation object — retained in the context's Set
3190
+ // for the whole query — is pure overhead at one per distinct group.
3191
+ memory.tally(payloadBytes, "Group state");
3192
+ if (plan.aggregates.length === 0) {
3193
+ return {
3194
+ groupValues,
3195
+ counts: EMPTY_ACCUMULATOR,
3196
+ sums: EMPTY_ACCUMULATOR,
3197
+ values: EMPTY_VALUES,
3198
+ valueReservations: EMPTY_RESERVATIONS,
3199
+ valueReservationBytes: EMPTY_ACCUMULATOR,
3200
+ distincts: undefined,
3201
+ };
3202
+ }
3203
+ // Only DISTINCT slots get a set, and only when the plan has one at all — an ordinary grouped
3204
+ // query allocates nothing extra for a feature it does not use.
3205
+ let distincts;
3206
+ for (let index = 0; index < plan.aggregates.length; index += 1) {
3207
+ if (plan.aggregates[index]?.distinct !== true)
3208
+ continue;
3209
+ distincts ??= new Array(plan.aggregates.length);
3210
+ distincts[index] = new Set();
3211
+ }
3212
+ return {
3213
+ groupValues,
3214
+ counts: new Float64Array(plan.aggregates.length),
3215
+ sums: new Float64Array(plan.aggregates.length),
3216
+ values: new Array(plan.aggregates.length),
3217
+ valueReservations: new Array(plan.aggregates.length),
3218
+ valueReservationBytes: new Float64Array(plan.aggregates.length),
3219
+ distincts,
3220
+ };
3221
+ }
3222
+ /**
3223
+ * A Set member standing for one aggregate input. Primitives are their own key — a Set separates
3224
+ * `1` from `"1"` on its own — while a Date has to become its instant, since two Dates for the
3225
+ * same moment are different objects. The NUL prefix keeps that instant from colliding with a
3226
+ * string a row genuinely holds.
3227
+ */
3228
+ function distinctKey(value) {
3229
+ return value instanceof Date ? `d${String(value.getTime())}` : value;
3230
+ }
3231
+ /**
3232
+ * Whether this value is the first of its kind for the slot, folding it into the set when it is.
3233
+ * Growth is tallied rather than reserved for the same reason group state is: the set lives until
3234
+ * the query's memory context closes, so a per-value reservation object would cost more than the
3235
+ * value it tracks.
3236
+ */
3237
+ function firstOfItsKind(state, index, value, memory) {
3238
+ const seen = state.distincts?.[index];
3239
+ if (seen === undefined)
3240
+ return true;
3241
+ const key = distinctKey(value);
3242
+ if (seen.has(key))
3243
+ return false;
3244
+ seen.add(key);
3245
+ memory.tally(safeMemorySum(QUERY_REFERENCE_BYTES, queryValuePayloadBytes(asQueryValue(key)), "Distinct aggregate value"), "Distinct aggregate value");
3246
+ return true;
3247
+ }
3248
+ function updateAggregates(plan, state, batch, row, memory) {
3249
+ for (let index = 0; index < plan.aggregates.length; index += 1) {
3250
+ const spec = required(plan.aggregates[index], "Aggregate specification is missing");
3251
+ // MIN/MAX/COUNT over a bare datetime column track raw epoch milliseconds: boxing a Date per
3252
+ // row only to unbox it in the comparison would dominate the scan. The final read re-boxes
3253
+ // the single surviving value.
3254
+ if (spec.rawDatetime !== undefined) {
3255
+ const sourceRow = batch.rowsBySource[spec.rawDatetime.source]?.[row] ?? -1;
3256
+ applyAggregateValue(spec, state, index, rawFloat64Value(spec.rawDatetime.vector, sourceRow), memory);
3257
+ continue;
3258
+ }
3259
+ // A bare number column reads its Float64Array slot directly and accumulates SUM/AVG into
3260
+ // the typed sums array, so the per-row value never crosses an interpreter dispatch and the
3261
+ // common accumulation path stays unboxed. MIN/MAX keep applyAggregateValue's comparison
3262
+ // semantics (including NaN ordering) through compareValues.
3263
+ // A DISTINCT slot always takes the generic path below: the unboxed branch writes straight
3264
+ // into the accumulators, with no place to ask whether this value has been seen before.
3265
+ if (spec.rawNumber !== undefined && spec.distinct !== true) {
3266
+ const sourceRow = batch.rowsBySource[spec.rawNumber.source]?.[row] ?? -1;
3267
+ const value = rawFloat64Value(spec.rawNumber.vector, sourceRow);
3268
+ if (value !== null) {
3269
+ state.counts[index] = (state.counts[index] ?? 0) + 1;
3270
+ if (spec.name === "SUM" || spec.name === "AVG") {
3271
+ state.sums[index] = (state.sums[index] ?? 0) + value;
3272
+ }
3273
+ else if (spec.name === "MIN") {
3274
+ const current = state.values[index];
3275
+ if (current === undefined || compareValues(value, current) < 0) {
3276
+ replaceAggregateValue(state, index, value, "MIN aggregate value", memory);
3277
+ }
3278
+ }
3279
+ else if (spec.name === "MAX") {
3280
+ const current = state.values[index];
3281
+ if (current === undefined || compareValues(value, current) > 0) {
3282
+ replaceAggregateValue(state, index, value, "MAX aggregate value", memory);
3283
+ }
3284
+ }
3285
+ }
3286
+ continue;
3287
+ }
3288
+ const value = spec.argument.kind === "wildcard"
3289
+ ? 1
3290
+ : evaluateBatchExpression(plan, spec.argument, batch, row);
3291
+ applyAggregateValue(spec, state, index, value, memory);
3292
+ }
3293
+ }
3294
+ /** Reads a float64 slot (number value or raw epoch milliseconds) unboxed, or null when invalid. */
3295
+ function rawFloat64Value(vector, rowIndex) {
3296
+ if (rowIndex < 0 || rowIndex >= vector.length)
3297
+ return null;
3298
+ const window = vector.window;
3299
+ let slot = rowIndex;
3300
+ if (window !== undefined) {
3301
+ slot = rowIndex - window.start;
3302
+ if (slot < 0 || slot >= window.length) {
3303
+ throw new RangeError("Streamed vector row is outside the resident window");
3304
+ }
3305
+ }
3306
+ if (!isValid(vector.validity, slot))
3307
+ return null;
3308
+ return vector.values[slot] ?? 0;
3309
+ }
3310
+ function updateAggregatesFromValues(plan, state, values, memory) {
3311
+ for (let index = 0; index < plan.aggregates.length; index += 1) {
3312
+ const spec = required(plan.aggregates[index], "Aggregate specification is missing");
3313
+ applyAggregateValue(spec, state, index, values[index], memory);
3314
+ }
3315
+ }
3316
+ function applyAggregateValue(spec, state, index, value, memory) {
3317
+ if (value === null || value === undefined)
3318
+ return;
3319
+ // One gate for every path that accumulates: the generic per-row path, the raw datetime path,
3320
+ // and the re-accumulation of spilled rows.
3321
+ if (spec.distinct === true && !firstOfItsKind(state, index, value, memory))
3322
+ return;
3323
+ state.counts[index] = (state.counts[index] ?? 0) + 1;
3324
+ if (spec.name === "SUM" || spec.name === "AVG") {
3325
+ state.sums[index] = (state.sums[index] ?? 0) + numeric(value);
3326
+ }
3327
+ else if (spec.name === "MIN" &&
3328
+ (state.values[index] === undefined || compareValues(value, state.values[index]) < 0)) {
3329
+ replaceAggregateValue(state, index, asQueryValue(value), "MIN aggregate value", memory);
3330
+ }
3331
+ else if (spec.name === "MAX" &&
3332
+ (state.values[index] === undefined || compareValues(value, state.values[index]) > 0)) {
3333
+ replaceAggregateValue(state, index, asQueryValue(value), "MAX aggregate value", memory);
3334
+ }
3335
+ }
3336
+ /**
3337
+ * Installs a MIN/MAX replacement value. The retained reservation only changes when the payload
3338
+ * size does: monotone inputs replace the extreme on nearly every row, and fixed-width values
3339
+ * (numbers, datetimes) would otherwise churn a reserve/release pair per row for the same bytes.
3340
+ */
3341
+ function replaceAggregateValue(state, index, value, label, memory) {
3342
+ const bytes = queryValuePayloadBytes(value);
3343
+ if (bytes !== state.valueReservationBytes[index]) {
3344
+ const replacement = memory.reserve(bytes, label);
3345
+ state.valueReservations[index]?.release();
3346
+ state.valueReservations[index] = replacement;
3347
+ state.valueReservationBytes[index] = bytes;
3348
+ }
3349
+ state.values[index] = value;
3350
+ }
3351
+ function finishGroups(plan, groups, memory) {
3352
+ const rows = [];
3353
+ for (const group of groups) {
3354
+ if (!plan.having.every((predicate) => predicateTruth(predicate, (nested) => evaluateFinalExpression(plan, nested, group)))) {
3355
+ continue;
3356
+ }
3357
+ const row = {};
3358
+ for (const item of plan.select) {
3359
+ row[item.alias] = asQueryValue(evaluateFinalExpression(plan, item.expression, group));
3360
+ }
3361
+ memory.tally(queryRowPayloadBytes(row), "Accumulated grouped result row");
3362
+ rows.push(row);
3363
+ }
3364
+ return rows;
3365
+ }
3366
+ function evaluateFinalExpression(plan, expression, group) {
3367
+ const groupIndex = plan.groupIndexBySignature.get(expression.signature);
3368
+ if (groupIndex !== undefined)
3369
+ return group.groupValues[groupIndex] ?? null;
3370
+ if (expression.kind === "literal")
3371
+ return expression.value;
3372
+ if (expression.kind === "wildcard")
3373
+ return 1;
3374
+ if (expression.kind === "list")
3375
+ throw new TypeError("Value lists are only supported with IN");
3376
+ if (expression.kind === "condition" ||
3377
+ expression.kind === "logical" ||
3378
+ expression.kind === "not") {
3379
+ return booleanTruth(expression, (nested) => evaluateFinalExpression(plan, nested, group));
3380
+ }
3381
+ if (expression.kind === "case") {
3382
+ for (const branch of expression.branches) {
3383
+ const matched = booleanTruth(branch.when, (nested) => evaluateFinalExpression(plan, nested, group));
3384
+ if (matched === true)
3385
+ return evaluateFinalExpression(plan, branch.then, group);
3386
+ }
3387
+ return expression.otherwise === undefined
3388
+ ? null
3389
+ : evaluateFinalExpression(plan, expression.otherwise, group);
3390
+ }
3391
+ if (expression.kind === "column") {
3392
+ throw new TypeError("Selected column must appear in GROUP BY");
3393
+ }
3394
+ if (expression.kind === "fts") {
3395
+ throw new TypeError("Selected full-text expression must appear in GROUP BY");
3396
+ }
3397
+ if (expression.kind === "binary") {
3398
+ return binaryValue(expression.operator, evaluateFinalExpression(plan, expression.left, group), evaluateFinalExpression(plan, expression.right, group));
3399
+ }
3400
+ if (expression.name === "COALESCE") {
3401
+ for (const argument of expression.arguments) {
3402
+ const candidate = evaluateFinalExpression(plan, argument, group);
3403
+ if (candidate !== null && candidate !== undefined)
3404
+ return candidate;
3405
+ }
3406
+ return null;
3407
+ }
3408
+ if (isScalarFunctionName(expression.name)) {
3409
+ return scalarFunctionValue(expression.name, expression.arguments.map((argument) => evaluateFinalExpression(plan, argument, group)));
3410
+ }
3411
+ const aggregateIndex = expression.aggregateIndex ?? -1;
3412
+ const count = group.counts[aggregateIndex];
3413
+ if (count === undefined)
3414
+ throw new Error("Aggregate state is missing");
3415
+ if (expression.name === "COUNT")
3416
+ return count;
3417
+ if (count === 0)
3418
+ return null;
3419
+ const sum = group.sums[aggregateIndex] ?? 0;
3420
+ if (expression.name === "SUM")
3421
+ return sum;
3422
+ if (expression.name === "AVG")
3423
+ return sum / count;
3424
+ const value = group.values[aggregateIndex] ?? null;
3425
+ // Raw-millisecond datetime extremes re-box into a Date only here, once per surviving group.
3426
+ if (typeof value === "number" && plan.aggregates[aggregateIndex]?.rawDatetime !== undefined) {
3427
+ return new Date(value);
3428
+ }
3429
+ return value;
3430
+ }
3431
+ /**
3432
+ * Builds one result row by direct property assignment in stable select order: every projected
3433
+ * row shares one hidden class and the per-row tuple/array churn of an entries-based build never
3434
+ * exists. This runs once per surviving row, so it is the hottest allocation site of a query.
3435
+ */
3436
+ function projectBatchRow(plan, batch, row) {
3437
+ const result = {};
3438
+ if (!plan.wildcard) {
3439
+ for (const item of plan.select) {
3440
+ const value = asQueryValue(evaluateBatchExpression(plan, item.expression, batch, row));
3441
+ if (item.alias === "__proto__")
3442
+ defineSqlResultProperty(result, item.alias, value);
3443
+ else
3444
+ result[item.alias] = value;
3445
+ }
3446
+ return result;
3447
+ }
3448
+ const multiple = plan.sourceTables.length > 1;
3449
+ for (let source = 0; source < plan.sourceTables.length; source += 1) {
3450
+ const table = required(plan.sourceTables[source], "Wildcard source table is missing");
3451
+ const rowIndex = batch.rowsBySource[source]?.[row] ?? -1;
3452
+ const prefix = multiple ? `${plan.sourceAliases[source] ?? ""}.` : "";
3453
+ for (const [name, vector] of table.columns) {
3454
+ const outputName = multiple ? prefix + name : name;
3455
+ const value = vectorValue(vector, rowIndex);
3456
+ if (outputName === "__proto__")
3457
+ defineSqlResultProperty(result, outputName, value);
3458
+ else
3459
+ result[outputName] = value;
3460
+ }
3461
+ }
3462
+ return result;
3463
+ }
3464
+ function wildcardColumnNames(plan) {
3465
+ const multiple = plan.sourceTables.length > 1;
3466
+ return plan.sourceTables.flatMap((table, source) => [...table.columns.keys()].map((name) => multiple ? `${plan.sourceAliases[source] ?? ""}.${name}` : name));
3467
+ }
3468
+ /** Detects `stringColumn = 'literal'` (or !=) so batches compare dictionary codes per row. */
3469
+ function detectDictionaryEquality(predicate) {
3470
+ if (!["=", "!=", "<>"].includes(predicate.operator))
3471
+ return undefined;
3472
+ const sides = [
3473
+ { column: predicate.left, literal: predicate.right },
3474
+ { column: predicate.right, literal: predicate.left },
3475
+ ];
3476
+ for (const { column, literal } of sides) {
3477
+ if (column.kind === "column" &&
3478
+ column.vector.kind === "string" &&
3479
+ literal.kind === "literal" &&
3480
+ typeof literal.value === "string") {
3481
+ return {
3482
+ source: column.source,
3483
+ vector: column.vector,
3484
+ value: literal.value,
3485
+ negated: predicate.operator !== "=",
3486
+ cache: { dictionary: undefined, code: -1 },
3487
+ };
3488
+ }
3489
+ }
3490
+ return undefined;
3491
+ }
3492
+ const primitiveOperators = new Set(["=", "!=", "<>", ">", ">=", "<", "<="]);
3493
+ /** Detects `column <op> literal` over numeric/datetime/boolean columns for unboxed evaluation. */
3494
+ function detectPrimitiveComparison(predicate) {
3495
+ if (!primitiveOperators.has(predicate.operator))
3496
+ return undefined;
3497
+ const comparison = predicate.operator;
3498
+ const sides = [
3499
+ { column: predicate.left, literal: predicate.right, operator: comparison },
3500
+ {
3501
+ column: predicate.right,
3502
+ literal: predicate.left,
3503
+ operator: reverseComparisonOperator(comparison),
3504
+ },
3505
+ ];
3506
+ for (const { column, literal, operator } of sides) {
3507
+ if (column.kind !== "column" || literal.kind !== "literal")
3508
+ continue;
3509
+ const vector = column.vector;
3510
+ const value = literal.value;
3511
+ if (vector.kind === "number" && typeof value === "number") {
3512
+ return { source: column.source, vector, operator, value };
3513
+ }
3514
+ if (vector.kind === "datetime" && value instanceof Date) {
3515
+ return { source: column.source, vector, operator, value: value.getTime() };
3516
+ }
3517
+ if (vector.kind === "boolean" &&
3518
+ typeof value === "boolean" &&
3519
+ (operator === "=" || operator === "!=" || operator === "<>")) {
3520
+ return { source: column.source, vector, operator, value: value ? 1 : 0 };
3521
+ }
3522
+ }
3523
+ return undefined;
3524
+ }
3525
+ /**
3526
+ * Detects `column IN (literal, ...)` over a numeric/datetime column. NULL members are dropped:
3527
+ * a value can never equal NULL, so they add nothing to the membership test — but a list that
3528
+ * is entirely NULL is left to the general path, where `IN (NULL)` keeps its three-valued
3529
+ * answer instead of becoming a plain false.
3530
+ */
3531
+ function detectPrimitiveInList(predicate) {
3532
+ const negated = predicate.operator === "NOT IN";
3533
+ if (predicate.operator !== "IN" && !negated)
3534
+ return undefined;
3535
+ if (predicate.left.kind !== "column" || predicate.right.kind !== "list")
3536
+ return undefined;
3537
+ const vector = predicate.left.vector;
3538
+ if (vector.kind !== "number" && vector.kind !== "datetime")
3539
+ return undefined;
3540
+ const members = new Set();
3541
+ let minimum = Number.POSITIVE_INFINITY;
3542
+ let maximum = Number.NEGATIVE_INFINITY;
3543
+ for (const item of predicate.right.items) {
3544
+ if (item.kind !== "literal")
3545
+ return undefined;
3546
+ const value = item.value;
3547
+ if (value === null) {
3548
+ // A NULL member only ever turns a non-match into unknown, which the filter discards
3549
+ // exactly as it discards false -- so IN may ignore it. NOT IN cannot: with a NULL in
3550
+ // the list it is never true, and a kernel over the remaining members would keep rows.
3551
+ if (negated)
3552
+ return undefined;
3553
+ continue;
3554
+ }
3555
+ if (vector.kind === "number") {
3556
+ if (typeof value !== "number" || !Number.isFinite(value))
3557
+ return undefined;
3558
+ members.add(value);
3559
+ }
3560
+ else {
3561
+ if (!(value instanceof Date))
3562
+ return undefined;
3563
+ members.add(value.getTime());
3564
+ }
3565
+ }
3566
+ if (members.size === 0)
3567
+ return undefined;
3568
+ for (const member of members) {
3569
+ if (member < minimum)
3570
+ minimum = member;
3571
+ if (member > maximum)
3572
+ maximum = member;
3573
+ }
3574
+ return { source: predicate.left.source, vector, members, minimum, maximum, negated };
3575
+ }
3576
+ function reverseComparisonOperator(operator) {
3577
+ if (operator === ">")
3578
+ return "<";
3579
+ if (operator === ">=")
3580
+ return "<=";
3581
+ if (operator === "<")
3582
+ return ">";
3583
+ if (operator === "<=")
3584
+ return ">=";
3585
+ return operator;
3586
+ }
3587
+ /** Reads a numeric/datetime/boolean slot as an unboxed float, or null when invalid. */
3588
+ function rawPrimitiveValue(vector, rowIndex) {
3589
+ if (rowIndex < 0 || rowIndex >= vector.length)
3590
+ return null;
3591
+ const window = vector.window;
3592
+ let slot = rowIndex;
3593
+ if (window !== undefined) {
3594
+ slot = rowIndex - window.start;
3595
+ if (slot < 0 || slot >= window.length) {
3596
+ throw new RangeError("Streamed vector row is outside the resident window");
3597
+ }
3598
+ }
3599
+ if (!isValid(vector.validity, slot))
3600
+ return null;
3601
+ return vector.values[slot] ?? 0;
3602
+ }
3603
+ function primitiveComparisonHolds(primitive, value) {
3604
+ switch (primitive.operator) {
3605
+ case "=":
3606
+ return value === primitive.value;
3607
+ case "!=":
3608
+ case "<>":
3609
+ return value !== primitive.value;
3610
+ case ">":
3611
+ return value > primitive.value;
3612
+ case ">=":
3613
+ return value >= primitive.value;
3614
+ case "<":
3615
+ return value < primitive.value;
3616
+ default:
3617
+ return value <= primitive.value;
3618
+ }
3619
+ }
3620
+ /** Detects `stringColumn LIKE 'literal'` (and NOT/ILIKE) for dictionary-level matching. */
3621
+ function detectDictionaryLike(predicate) {
3622
+ const operator = predicate.operator;
3623
+ if (operator !== "LIKE" &&
3624
+ operator !== "NOT LIKE" &&
3625
+ operator !== "ILIKE" &&
3626
+ operator !== "NOT ILIKE") {
3627
+ return undefined;
3628
+ }
3629
+ const column = predicate.left;
3630
+ const literal = predicate.right;
3631
+ if (column.kind !== "column" ||
3632
+ column.vector.kind !== "string" ||
3633
+ literal.kind !== "literal" ||
3634
+ typeof literal.value !== "string") {
3635
+ return undefined;
3636
+ }
3637
+ return {
3638
+ source: column.source,
3639
+ vector: column.vector,
3640
+ pattern: literal.value,
3641
+ caseInsensitive: operator === "ILIKE" || operator === "NOT ILIKE",
3642
+ ...(predicate.escape === undefined ? {} : { escape: predicate.escape }),
3643
+ negated: operator === "NOT LIKE" || operator === "NOT ILIKE",
3644
+ cache: { dictionary: undefined, matches: new Uint8Array(0) },
3645
+ };
3646
+ }
3647
+ /**
3648
+ * Match table for one (dictionary, pattern) pair, cached across queries on the dictionary's
3649
+ * identity: repeated LIKE scans over a cached columnar table pay the per-entry match once ever.
3650
+ */
3651
+ const dictionaryLikeCache = new WeakMap();
3652
+ function dictionaryLikeMatches(dictionary, pattern, caseInsensitive, escape) {
3653
+ let patterns = dictionaryLikeCache.get(dictionary);
3654
+ if (patterns === undefined) {
3655
+ patterns = new Map();
3656
+ dictionaryLikeCache.set(dictionary, patterns);
3657
+ }
3658
+ const key = `${caseInsensitive ? "i" : "s"}${escape ?? ""} ${pattern}`;
3659
+ let matches = patterns.get(key);
3660
+ if (matches === undefined) {
3661
+ matches = new Uint8Array(dictionary.length);
3662
+ for (let index = 0; index < dictionary.length; index += 1) {
3663
+ matches[index] = likeMatches(pattern, dictionary[index] ?? "", caseInsensitive, escape)
3664
+ ? 1
3665
+ : 0;
3666
+ }
3667
+ if (patterns.size >= 32)
3668
+ patterns.clear();
3669
+ patterns.set(key, matches);
3670
+ }
3671
+ return matches;
3672
+ }
3673
+ function stringCodeAt(vector, rowIndex) {
3674
+ if (rowIndex < 0 || rowIndex >= vector.length)
3675
+ return undefined;
3676
+ const window = vector.window;
3677
+ let slot = rowIndex;
3678
+ if (window !== undefined) {
3679
+ slot = rowIndex - window.start;
3680
+ if (slot < 0 || slot >= window.length) {
3681
+ throw new RangeError(`Streamed vector row ${String(rowIndex)} is outside the resident window ${String(window.start)}..${String(window.start + window.length)}`);
3682
+ }
3683
+ }
3684
+ if (!isValid(vector.validity, slot))
3685
+ return undefined;
3686
+ const code = vector.codes[slot] ?? NULL_STRING_CODE;
3687
+ return code === NULL_STRING_CODE ? undefined : code;
3688
+ }
3689
+ /**
3690
+ * SQL three-valued logic over bound boolean trees; mirrors evaluateBooleanExpression in query.ts
3691
+ * with bound leaves. Only the caller collapses unknown (null) to false.
3692
+ */
3693
+ function booleanTruth(expression, evaluateValue) {
3694
+ if (expression.kind === "logical") {
3695
+ const left = booleanTruth(expression.left, evaluateValue);
3696
+ if (expression.operator === "and") {
3697
+ if (left === false)
3698
+ return false;
3699
+ const right = booleanTruth(expression.right, evaluateValue);
3700
+ if (right === false)
3701
+ return false;
3702
+ return left === null || right === null ? null : true;
3703
+ }
3704
+ if (left === true)
3705
+ return true;
3706
+ const right = booleanTruth(expression.right, evaluateValue);
3707
+ if (right === true)
3708
+ return true;
3709
+ return left === null || right === null ? null : false;
3710
+ }
3711
+ if (expression.kind === "not") {
3712
+ const value = booleanTruth(expression.operand, evaluateValue);
3713
+ return value === null ? null : !value;
3714
+ }
3715
+ if (expression.kind === "condition") {
3716
+ const operator = expression.operator;
3717
+ if (operator === "IS TRUE")
3718
+ return booleanTruth(expression.left, evaluateValue);
3719
+ if (operator === "IS NULL" || operator === "IS NOT NULL") {
3720
+ const value = evaluateValue(expression.left);
3721
+ const isNull = value === null || value === undefined;
3722
+ return operator === "IS NULL" ? isNull : !isNull;
3723
+ }
3724
+ if (operator === "IN" || operator === "NOT IN") {
3725
+ if (expression.right.kind !== "list")
3726
+ throw new TypeError("IN requires a value list");
3727
+ const probe = evaluateValue(expression.left);
3728
+ if (probe === null || probe === undefined)
3729
+ return null;
3730
+ const membership = cachedListMembership(expression.right, expression.right.items);
3731
+ if (membership !== null) {
3732
+ if (membership.set.has(comparable(probe)))
3733
+ return operator === "IN";
3734
+ if (membership.hasNull)
3735
+ return null;
3736
+ return operator === "NOT IN";
3737
+ }
3738
+ let sawNull = false;
3739
+ for (const item of expression.right.items) {
3740
+ const value = evaluateValue(item);
3741
+ if (value === null || value === undefined) {
3742
+ sawNull = true;
3743
+ continue;
3744
+ }
3745
+ if (comparable(value) === comparable(probe))
3746
+ return operator === "IN";
3747
+ }
3748
+ if (sawNull)
3749
+ return null;
3750
+ return operator === "NOT IN";
3751
+ }
3752
+ {
3753
+ const quantified = parseQuantified(operator);
3754
+ if (quantified !== undefined) {
3755
+ if (expression.right.kind !== "list") {
3756
+ throw new TypeError("ANY/ALL subqueries must be resolved before evaluation");
3757
+ }
3758
+ return quantifiedComparison(quantified.comparison, quantified.quantifier, evaluateValue(expression.left), expression.right.items.map((item) => evaluateValue(item)));
3759
+ }
3760
+ }
3761
+ if (operator === "LIKE" ||
3762
+ operator === "NOT LIKE" ||
3763
+ operator === "ILIKE" ||
3764
+ operator === "NOT ILIKE") {
3765
+ const value = evaluateValue(expression.left);
3766
+ const pattern = evaluateValue(expression.right);
3767
+ if (value === null || value === undefined || pattern === null || pattern === undefined) {
3768
+ return null;
3769
+ }
3770
+ if (typeof value !== "string" || typeof pattern !== "string") {
3771
+ throw new TypeError("LIKE requires string operands");
3772
+ }
3773
+ const matched = likeMatches(pattern, value, operator === "ILIKE" || operator === "NOT ILIKE", expression.escape);
3774
+ return operator === "LIKE" || operator === "ILIKE" ? matched : !matched;
3775
+ }
3776
+ if (operator === "IS DISTINCT FROM" || operator === "IS NOT DISTINCT FROM") {
3777
+ const distinct = distinctFromComparison(evaluateValue(expression.left), evaluateValue(expression.right));
3778
+ return operator === "IS DISTINCT FROM" ? distinct : !distinct;
3779
+ }
3780
+ const left = evaluateValue(expression.left);
3781
+ const right = evaluateValue(expression.right);
3782
+ if (left === null || left === undefined || right === null || right === undefined)
3783
+ return null;
3784
+ const a = comparable(left);
3785
+ const b = comparable(right);
3786
+ if (operator === "=")
3787
+ return a === b;
3788
+ if (operator === "!=" || operator === "<>")
3789
+ return a !== b;
3790
+ const comparison = compareValues(a, b);
3791
+ if (operator === ">")
3792
+ return comparison > 0;
3793
+ if (operator === ">=")
3794
+ return comparison >= 0;
3795
+ if (operator === "<")
3796
+ return comparison < 0;
3797
+ return comparison <= 0;
3798
+ }
3799
+ const value = evaluateValue(expression);
3800
+ if (value === null || value === undefined)
3801
+ return null;
3802
+ if (typeof value === "boolean")
3803
+ return value;
3804
+ throw new TypeError("Boolean conditions require boolean operands");
3805
+ }
3806
+ function predicateTruth(predicate, evaluateValue) {
3807
+ if (predicate.operator === "IS TRUE") {
3808
+ return booleanTruth(predicate.left, evaluateValue) === true;
3809
+ }
3810
+ return (booleanTruth({
3811
+ kind: "condition",
3812
+ operator: predicate.operator,
3813
+ left: predicate.left,
3814
+ right: predicate.right,
3815
+ ...(predicate.escape === undefined ? {} : { escape: predicate.escape }),
3816
+ signature: "",
3817
+ }, evaluateValue) === true);
3818
+ }
3819
+ function evaluateBatchPredicate(plan, predicate, batch, row) {
3820
+ const fast = predicate.dictionaryEquality;
3821
+ if (fast !== undefined) {
3822
+ const code = stringCodeAt(fast.vector, batch.rowsBySource[fast.source]?.[row] ?? -1);
3823
+ if (code === undefined)
3824
+ return false;
3825
+ if (fast.cache.dictionary !== fast.vector.dictionary) {
3826
+ fast.cache.dictionary = fast.vector.dictionary;
3827
+ fast.cache.code = fast.vector.dictionary.indexOf(fast.value);
3828
+ }
3829
+ const matches = fast.cache.code >= 0 && code === fast.cache.code;
3830
+ return fast.negated ? !matches : matches;
3831
+ }
3832
+ const primitive = predicate.primitive;
3833
+ if (primitive !== undefined) {
3834
+ const value = rawPrimitiveValue(primitive.vector, batch.rowsBySource[primitive.source]?.[row] ?? -1);
3835
+ return value === null ? false : primitiveComparisonHolds(primitive, value);
3836
+ }
3837
+ const like = predicate.dictionaryLike;
3838
+ if (like !== undefined) {
3839
+ const code = stringCodeAt(like.vector, batch.rowsBySource[like.source]?.[row] ?? -1);
3840
+ if (code === undefined)
3841
+ return false;
3842
+ if (like.cache.dictionary !== like.vector.dictionary) {
3843
+ like.cache.dictionary = like.vector.dictionary;
3844
+ like.cache.matches = dictionaryLikeMatches(like.vector.dictionary, like.pattern, like.caseInsensitive, like.escape);
3845
+ }
3846
+ const matched = like.cache.matches[code] === 1;
3847
+ return like.negated ? !matched : matched;
3848
+ }
3849
+ if (predicate.operator === "IS TRUE" ||
3850
+ predicate.operator === "LIKE" ||
3851
+ predicate.operator === "NOT LIKE" ||
3852
+ predicate.operator === "ILIKE" ||
3853
+ predicate.operator === "NOT ILIKE" ||
3854
+ parseQuantified(predicate.operator) !== undefined) {
3855
+ return predicateTruth(predicate, (nested) => evaluateBatchExpression(plan, nested, batch, row));
3856
+ }
3857
+ if (predicate.operator === "IN" || predicate.operator === "NOT IN") {
3858
+ if (predicate.right.kind !== "list")
3859
+ throw new TypeError("IN requires a value list");
3860
+ const membership = cachedListMembership(predicate.right, predicate.right.items);
3861
+ if (membership !== null) {
3862
+ const value = evaluateBatchExpression(plan, predicate.left, batch, row);
3863
+ if (value === null || value === undefined)
3864
+ return false;
3865
+ if (membership.set.has(comparable(value)))
3866
+ return predicate.operator === "IN";
3867
+ return predicate.operator === "NOT IN" && !membership.hasNull;
3868
+ }
3869
+ return inListHolds(predicate.operator, evaluateBatchExpression(plan, predicate.left, batch, row), predicate.right.items.map((item) => evaluateBatchExpression(plan, item, batch, row)));
3870
+ }
3871
+ return comparisonValue(predicate.operator, evaluateBatchExpression(plan, predicate.left, batch, row), evaluateBatchExpression(plan, predicate.right, batch, row));
3872
+ }
3873
+ /**
3874
+ * SQL membership semantics: a NULL probe never matches, and NOT IN cannot be satisfied when the
3875
+ * list contains NULL because the comparison is unknown rather than false.
3876
+ */
3877
+ function inListHolds(operator, value, items) {
3878
+ if (value === null || value === undefined)
3879
+ return false;
3880
+ let hasNull = false;
3881
+ for (const item of items) {
3882
+ if (item === null || item === undefined) {
3883
+ hasNull = true;
3884
+ continue;
3885
+ }
3886
+ if (comparable(value) === comparable(item))
3887
+ return operator === "IN";
3888
+ }
3889
+ return operator === "NOT IN" && !hasNull;
3890
+ }
3891
+ function evaluateBatchExpression(plan, expression, batch, row) {
3892
+ if (expression.kind === "literal")
3893
+ return expression.value;
3894
+ if (expression.kind === "wildcard")
3895
+ return 1;
3896
+ if (expression.kind === "list")
3897
+ throw new TypeError("Value lists are only supported with IN");
3898
+ if (expression.kind === "condition" ||
3899
+ expression.kind === "logical" ||
3900
+ expression.kind === "not") {
3901
+ return booleanTruth(expression, (nested) => evaluateBatchExpression(plan, nested, batch, row));
3902
+ }
3903
+ if (expression.kind === "case") {
3904
+ for (const branch of expression.branches) {
3905
+ const matched = booleanTruth(branch.when, (nested) => evaluateBatchExpression(plan, nested, batch, row));
3906
+ if (matched === true)
3907
+ return evaluateBatchExpression(plan, branch.then, batch, row);
3908
+ }
3909
+ return expression.otherwise === undefined
3910
+ ? null
3911
+ : evaluateBatchExpression(plan, expression.otherwise, batch, row);
3912
+ }
3913
+ if (expression.kind === "column") {
3914
+ return vectorValue(expression.vector, batch.rowsBySource[expression.source]?.[row] ?? -1);
3915
+ }
3916
+ if (expression.kind === "fts") {
3917
+ return expression.op === "match"
3918
+ ? ftsBatchTruth(expression, batch, null, row)
3919
+ : ftsBm25BatchValue(expression, batch, null, row);
3920
+ }
3921
+ if (expression.kind === "binary") {
3922
+ return binaryValue(expression.operator, evaluateBatchExpression(plan, expression.left, batch, row), evaluateBatchExpression(plan, expression.right, batch, row));
3923
+ }
3924
+ if (expression.name === "COALESCE") {
3925
+ for (const argument of expression.arguments) {
3926
+ const candidate = evaluateBatchExpression(plan, argument, batch, row);
3927
+ if (candidate !== null && candidate !== undefined)
3928
+ return candidate;
3929
+ }
3930
+ return null;
3931
+ }
3932
+ if (!isScalarFunctionName(expression.name))
3933
+ throw new TypeError(`${expression.name} requires grouped execution`);
3934
+ return scalarFunctionValue(expression.name, expression.arguments.map((argument) => evaluateBatchExpression(plan, argument, batch, row)));
3935
+ }
3936
+ function evaluateExpression(expression, rowsBySource) {
3937
+ if (expression.kind === "literal")
3938
+ return expression.value;
3939
+ if (expression.kind === "wildcard")
3940
+ return 1;
3941
+ if (expression.kind === "list")
3942
+ throw new TypeError("Value lists are only supported with IN");
3943
+ if (expression.kind === "condition" ||
3944
+ expression.kind === "logical" ||
3945
+ expression.kind === "not") {
3946
+ return booleanTruth(expression, (nested) => evaluateExpression(nested, rowsBySource));
3947
+ }
3948
+ if (expression.kind === "case") {
3949
+ for (const branch of expression.branches) {
3950
+ const matched = booleanTruth(branch.when, (nested) => evaluateExpression(nested, rowsBySource));
3951
+ if (matched === true)
3952
+ return evaluateExpression(branch.then, rowsBySource);
3953
+ }
3954
+ return expression.otherwise === undefined
3955
+ ? null
3956
+ : evaluateExpression(expression.otherwise, rowsBySource);
3957
+ }
3958
+ if (expression.kind === "column") {
3959
+ return vectorValue(expression.vector, rowsBySource[expression.source] ?? -1);
3960
+ }
3961
+ if (expression.kind === "fts") {
3962
+ return expression.op === "match"
3963
+ ? ftsBatchTruth(expression, null, rowsBySource, 0)
3964
+ : ftsBm25BatchValue(expression, null, rowsBySource, 0);
3965
+ }
3966
+ if (expression.kind === "binary") {
3967
+ return binaryValue(expression.operator, evaluateExpression(expression.left, rowsBySource), evaluateExpression(expression.right, rowsBySource));
3968
+ }
3969
+ if (expression.name === "COALESCE") {
3970
+ for (const argument of expression.arguments) {
3971
+ const candidate = evaluateExpression(argument, rowsBySource);
3972
+ if (candidate !== null && candidate !== undefined)
3973
+ return candidate;
3974
+ }
3975
+ return null;
3976
+ }
3977
+ if (!isScalarFunctionName(expression.name))
3978
+ throw new TypeError(`${expression.name} requires grouped execution`);
3979
+ return scalarFunctionValue(expression.name, expression.arguments.map((argument) => evaluateExpression(argument, rowsBySource)));
3980
+ }
3981
+ function binaryValue(operator, left, right) {
3982
+ if (left === null || left === undefined || right === null || right === undefined)
3983
+ return null;
3984
+ if (operator === "||") {
3985
+ if (typeof left !== "string" || typeof right !== "string") {
3986
+ throw new TypeError("|| requires string operands");
3987
+ }
3988
+ return left + right;
3989
+ }
3990
+ const a = numeric(left);
3991
+ const b = numeric(right);
3992
+ if (operator === "+")
3993
+ return a + b;
3994
+ if (operator === "-")
3995
+ return a - b;
3996
+ if (operator === "*")
3997
+ return a * b;
3998
+ // Division and remainder by zero are NULL, matching SQLite, not Infinity/NaN.
3999
+ if (b === 0)
4000
+ return null;
4001
+ return operator === "%" ? a % b : a / b;
4002
+ }
4003
+ function comparisonValue(operator, leftValue, rightValue) {
4004
+ if (operator === "IS NULL")
4005
+ return leftValue === null || leftValue === undefined;
4006
+ if (operator === "IS NOT NULL")
4007
+ return leftValue !== null && leftValue !== undefined;
4008
+ if (operator === "IN" || operator === "NOT IN") {
4009
+ throw new TypeError("IN is only supported in WHERE predicates");
4010
+ }
4011
+ if (operator === "IS DISTINCT FROM")
4012
+ return distinctFromComparison(leftValue, rightValue);
4013
+ if (operator === "IS NOT DISTINCT FROM")
4014
+ return !distinctFromComparison(leftValue, rightValue);
4015
+ if (operator === "LIKE" ||
4016
+ operator === "NOT LIKE" ||
4017
+ operator === "ILIKE" ||
4018
+ operator === "NOT ILIKE") {
4019
+ if (leftValue === null ||
4020
+ leftValue === undefined ||
4021
+ rightValue === null ||
4022
+ rightValue === undefined) {
4023
+ return false;
4024
+ }
4025
+ if (typeof leftValue !== "string" || typeof rightValue !== "string") {
4026
+ throw new TypeError("LIKE requires string operands");
4027
+ }
4028
+ const matched = likeMatches(rightValue, leftValue, operator === "ILIKE" || operator === "NOT ILIKE");
4029
+ return operator === "LIKE" || operator === "ILIKE" ? matched : !matched;
4030
+ }
4031
+ if (leftValue === null ||
4032
+ leftValue === undefined ||
4033
+ rightValue === null ||
4034
+ rightValue === undefined) {
4035
+ return false;
4036
+ }
4037
+ const left = comparable(leftValue);
4038
+ const right = comparable(rightValue);
4039
+ if (operator === "=")
4040
+ return left === right;
4041
+ if (operator === "!=" || operator === "<>")
4042
+ return left !== right;
4043
+ const comparison = compareValues(left, right);
4044
+ if (operator === ">")
4045
+ return comparison > 0;
4046
+ if (operator === ">=")
4047
+ return comparison >= 0;
4048
+ if (operator === "<")
4049
+ return comparison < 0;
4050
+ return comparison <= 0;
4051
+ }
4052
+ function comparable(value) {
4053
+ return value instanceof Date ? value.getTime() : value;
4054
+ }
4055
+ function groupKey(value) {
4056
+ const comparableValue = comparable(value);
4057
+ if (typeof comparableValue === "number" && !Number.isFinite(comparableValue))
4058
+ return null;
4059
+ if (comparableValue === null ||
4060
+ typeof comparableValue === "boolean" ||
4061
+ typeof comparableValue === "number" ||
4062
+ typeof comparableValue === "string") {
4063
+ return comparableValue;
4064
+ }
4065
+ throw new TypeError("Group keys must be SQL scalar values");
4066
+ }
4067
+ function stableSortRows(rows, orderBy) {
4068
+ if (rows.length > 0xffffffff)
4069
+ throw new RangeError("Too many rows to order");
4070
+ const indexes = new Uint32Array(rows.length);
4071
+ const scratch = new Uint32Array(rows.length);
4072
+ for (let index = 0; index < indexes.length; index += 1)
4073
+ indexes[index] = index;
4074
+ let source = indexes;
4075
+ let target = scratch;
4076
+ // One prepared column per term, extracted once; see sort-keys.ts for why the comparison
4077
+ // rather than the merge is what a sort costs. Direction and explicit NULL placement are
4078
+ // resolved here, outside the comparison, because they never vary row to row.
4079
+ const termCount = orderBy.length;
4080
+ const terms = orderBy.map((order) => ({
4081
+ /** Signed placement for an explicit NULLS FIRST/LAST, or 0 when the default applies. */
4082
+ nullPlacement: order.nulls === undefined ? 0 : order.nulls === "first" ? 1 : -1,
4083
+ descending: order.direction === "desc",
4084
+ }));
4085
+ const columns = orderBy.map((order) => buildSortKeyColumn(rows.length, (index) => required(rows[index], "Ordering row is missing")[order.outputName]));
4086
+ const compareIndexes = (leftIndex, rightIndex) => {
4087
+ for (let term = 0; term < termCount; term += 1) {
4088
+ const column = required(columns[term], "Order keys are missing");
4089
+ const { nullPlacement, descending } = required(terms[term], "Order term is missing");
4090
+ if (nullPlacement !== 0) {
4091
+ // An explicit placement is absolute: direction must not negate it.
4092
+ const leftNull = column.isNull(leftIndex);
4093
+ const rightNull = column.isNull(rightIndex);
4094
+ if (leftNull || rightNull) {
4095
+ if (leftNull && rightNull)
4096
+ continue;
4097
+ return (leftNull ? -1 : 1) * nullPlacement;
4098
+ }
4099
+ }
4100
+ const comparison = column.compare(leftIndex, rightIndex);
4101
+ if (comparison !== 0)
4102
+ return descending ? -comparison : comparison;
4103
+ }
4104
+ return 0;
4105
+ };
4106
+ for (let width = 1; width < rows.length; width *= 2) {
4107
+ for (let start = 0; start < rows.length; start += width * 2) {
4108
+ const middle = Math.min(start + width, rows.length);
4109
+ const end = Math.min(start + width * 2, rows.length);
4110
+ let left = start;
4111
+ let right = middle;
4112
+ for (let output = start; output < end; output += 1) {
4113
+ if (right >= end ||
4114
+ (left < middle && compareIndexes(source[left] ?? 0, source[right] ?? 0) <= 0)) {
4115
+ target[output] = source[left] ?? 0;
4116
+ left += 1;
4117
+ }
4118
+ else {
4119
+ target[output] = source[right] ?? 0;
4120
+ right += 1;
4121
+ }
4122
+ }
4123
+ }
4124
+ [source, target] = [target, source];
4125
+ }
4126
+ if (source !== indexes)
4127
+ indexes.set(source);
4128
+ const visited = new Uint8Array(rows.length);
4129
+ for (let start = 0; start < rows.length; start += 1) {
4130
+ if (visited[start] === 1)
4131
+ continue;
4132
+ const first = required(rows[start], "Ordering row is missing");
4133
+ let output = start;
4134
+ let input = indexes[output] ?? start;
4135
+ while (input !== start) {
4136
+ visited[output] = 1;
4137
+ rows[output] = required(rows[input], "Ordering row is missing");
4138
+ output = input;
4139
+ input = indexes[output] ?? start;
4140
+ }
4141
+ visited[output] = 1;
4142
+ rows[output] = first;
4143
+ }
4144
+ }
4145
+ function compareValues(left, right) {
4146
+ const a = left instanceof Date ? left.getTime() : left;
4147
+ const b = right instanceof Date ? right.getTime() : right;
4148
+ if (a === b)
4149
+ return 0;
4150
+ if (a === null || a === undefined)
4151
+ return -1;
4152
+ if (b === null || b === undefined)
4153
+ return 1;
4154
+ if (typeof a === "number" && typeof b === "number") {
4155
+ if (Number.isNaN(a))
4156
+ return Number.isNaN(b) ? 0 : 1;
4157
+ if (Number.isNaN(b))
4158
+ return -1;
4159
+ return a - b;
4160
+ }
4161
+ if (typeof a === "string" && typeof b === "string")
4162
+ return compareSqlStrings(a, b);
4163
+ if (typeof a === "boolean" && typeof b === "boolean")
4164
+ return Number(a) - Number(b);
4165
+ throw new TypeError("Values must have comparable SQL types");
4166
+ }
4167
+ function numeric(value) {
4168
+ if (typeof value !== "number")
4169
+ throw new TypeError("Arithmetic and numeric aggregates require numbers");
4170
+ return value;
4171
+ }
4172
+ function asQueryValue(value) {
4173
+ if (value === null ||
4174
+ typeof value === "boolean" ||
4175
+ typeof value === "number" ||
4176
+ typeof value === "string" ||
4177
+ value instanceof Date) {
4178
+ return value;
4179
+ }
4180
+ if (value === undefined)
4181
+ return null;
4182
+ throw new TypeError("Query produced an unsupported value");
4183
+ }
4184
+ function required(value, message) {
4185
+ if (value === undefined)
4186
+ throw new Error(message);
4187
+ return value;
4188
+ }
4189
+ function queryRowPayloadBytes(row) {
4190
+ // for-in rather than Object.values: this runs once per result row, and Object.values
4191
+ // allocates a throwaway array of every value each time. Every term is non-negative, so one
4192
+ // range check at the end is equivalent to checking each addition.
4193
+ let total = QUERY_REFERENCE_BYTES;
4194
+ for (const key in row)
4195
+ total += queryValuePayloadBytes(row[key] ?? null);
4196
+ if (!Number.isSafeInteger(total) || total < 0) {
4197
+ throw new RangeError("Result row payload exceeds the safe integer range");
4198
+ }
4199
+ return total;
4200
+ }
4201
+ function queryValuePayloadBytes(value) {
4202
+ if (value === null)
4203
+ return QUERY_VALUE_TAG_BYTES;
4204
+ if (typeof value === "boolean")
4205
+ return QUERY_VALUE_TAG_BYTES + 1;
4206
+ if (typeof value === "number" || value instanceof Date) {
4207
+ return QUERY_VALUE_TAG_BYTES + Float64Array.BYTES_PER_ELEMENT;
4208
+ }
4209
+ // Accounted as one byte per UTF-16 code unit: exact for the dominant Latin-1 case, O(1)
4210
+ // instead of a per-value UTF-8 encode that allocates a throwaway buffer per string.
4211
+ return safeMemorySum(QUERY_VALUE_TAG_BYTES, value.length, "String query value payload");
4212
+ }
4213
+ function safeMemorySum(left, right, label) {
4214
+ const total = left + right;
4215
+ if (!Number.isSafeInteger(total) || total < 0) {
4216
+ throw new RangeError(`${label} exceeds the safe integer range`);
4217
+ }
4218
+ return total;
4219
+ }
4220
+ function safeMemoryProduct(left, right, label) {
4221
+ const product = left * right;
4222
+ if (!Number.isSafeInteger(product) || product < 0) {
4223
+ throw new RangeError(`${label} exceeds the safe integer range`);
4224
+ }
4225
+ return product;
4226
+ }
4227
+ //# sourceMappingURL=vector.js.map