@minnowdb/core 0.0.1 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +38 -1
- package/dist/block-format/block.d.ts +46 -0
- package/dist/block-format/block.d.ts.map +1 -0
- package/dist/block-format/block.js +244 -0
- package/dist/block-format/block.js.map +1 -0
- package/dist/block-format/checksum.d.ts +7 -0
- package/dist/block-format/checksum.d.ts.map +1 -0
- package/dist/block-format/checksum.js +56 -0
- package/dist/block-format/checksum.js.map +1 -0
- package/dist/block-format/codecs.d.ts +24 -0
- package/dist/block-format/codecs.d.ts.map +1 -0
- package/dist/block-format/codecs.js +102 -0
- package/dist/block-format/codecs.js.map +1 -0
- package/dist/block-format/column.d.ts +8 -0
- package/dist/block-format/column.d.ts.map +1 -0
- package/dist/block-format/column.js +166 -0
- package/dist/block-format/column.js.map +1 -0
- package/dist/block-format/index.d.ts +7 -0
- package/dist/block-format/index.d.ts.map +1 -0
- package/dist/block-format/index.js +7 -0
- package/dist/block-format/index.js.map +1 -0
- package/dist/block-format/physical.d.ts +20 -0
- package/dist/block-format/physical.d.ts.map +1 -0
- package/dist/block-format/physical.js +439 -0
- package/dist/block-format/physical.js.map +1 -0
- package/dist/block-format/types.d.ts +93 -0
- package/dist/block-format/types.d.ts.map +1 -0
- package/dist/block-format/types.js +2 -0
- package/dist/block-format/types.js.map +1 -0
- package/dist/engine/artifact-cache.d.ts +22 -0
- package/dist/engine/artifact-cache.d.ts.map +1 -0
- package/dist/engine/artifact-cache.js +68 -0
- package/dist/engine/artifact-cache.js.map +1 -0
- package/dist/engine/batch.d.ts +28 -0
- package/dist/engine/batch.d.ts.map +1 -0
- package/dist/engine/batch.js +38 -0
- package/dist/engine/batch.js.map +1 -0
- package/dist/engine/buffered-writer.d.ts +43 -0
- package/dist/engine/buffered-writer.d.ts.map +1 -0
- package/dist/engine/buffered-writer.js +158 -0
- package/dist/engine/buffered-writer.js.map +1 -0
- package/dist/engine/catalog.d.ts +82 -0
- package/dist/engine/catalog.d.ts.map +1 -0
- package/dist/engine/catalog.js +47 -0
- package/dist/engine/catalog.js.map +1 -0
- package/dist/engine/client.d.ts +211 -0
- package/dist/engine/client.d.ts.map +1 -0
- package/dist/engine/client.js +538 -0
- package/dist/engine/client.js.map +1 -0
- package/dist/engine/coordinator.d.ts +17 -0
- package/dist/engine/coordinator.d.ts.map +1 -0
- package/dist/engine/coordinator.js +60 -0
- package/dist/engine/coordinator.js.map +1 -0
- package/dist/engine/database.d.ts +661 -0
- package/dist/engine/database.d.ts.map +1 -0
- package/dist/engine/database.js +10313 -0
- package/dist/engine/database.js.map +1 -0
- package/dist/engine/defaults.d.ts +33 -0
- package/dist/engine/defaults.d.ts.map +1 -0
- package/dist/engine/defaults.js +87 -0
- package/dist/engine/defaults.js.map +1 -0
- package/dist/engine/errors.d.ts +55 -0
- package/dist/engine/errors.d.ts.map +1 -0
- package/dist/engine/errors.js +81 -0
- package/dist/engine/errors.js.map +1 -0
- package/dist/engine/fts.d.ts +93 -0
- package/dist/engine/fts.d.ts.map +1 -0
- package/dist/engine/fts.js +273 -0
- package/dist/engine/fts.js.map +1 -0
- package/dist/engine/group-index.d.ts +32 -0
- package/dist/engine/group-index.d.ts.map +1 -0
- package/dist/engine/group-index.js +366 -0
- package/dist/engine/group-index.js.map +1 -0
- package/dist/engine/index.d.ts +13 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +12 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/join-index.d.ts +12 -0
- package/dist/engine/join-index.d.ts.map +1 -0
- package/dist/engine/join-index.js +217 -0
- package/dist/engine/join-index.js.map +1 -0
- package/dist/engine/live.d.ts +86 -0
- package/dist/engine/live.d.ts.map +1 -0
- package/dist/engine/live.js +289 -0
- package/dist/engine/live.js.map +1 -0
- package/dist/engine/memory.d.ts +37 -0
- package/dist/engine/memory.d.ts.map +1 -0
- package/dist/engine/memory.js +119 -0
- package/dist/engine/memory.js.map +1 -0
- package/dist/engine/optimizer.d.ts +20 -0
- package/dist/engine/optimizer.d.ts.map +1 -0
- package/dist/engine/optimizer.js +1170 -0
- package/dist/engine/optimizer.js.map +1 -0
- package/dist/engine/query-cache.d.ts +10 -0
- package/dist/engine/query-cache.d.ts.map +1 -0
- package/dist/engine/query-cache.js +58 -0
- package/dist/engine/query-cache.js.map +1 -0
- package/dist/engine/query.d.ts +844 -0
- package/dist/engine/query.d.ts.map +1 -0
- package/dist/engine/query.js +7450 -0
- package/dist/engine/query.js.map +1 -0
- package/dist/engine/schema-wire.d.ts +92 -0
- package/dist/engine/schema-wire.d.ts.map +1 -0
- package/dist/engine/schema-wire.js +115 -0
- package/dist/engine/schema-wire.js.map +1 -0
- package/dist/engine/schema.d.ts +394 -0
- package/dist/engine/schema.d.ts.map +1 -0
- package/dist/engine/schema.js +709 -0
- package/dist/engine/schema.js.map +1 -0
- package/dist/engine/sort-keys.d.ts +36 -0
- package/dist/engine/sort-keys.d.ts.map +1 -0
- package/dist/engine/sort-keys.js +60 -0
- package/dist/engine/sort-keys.js.map +1 -0
- package/dist/engine/sql-json.d.ts +30 -0
- package/dist/engine/sql-json.d.ts.map +1 -0
- package/dist/engine/sql-json.js +120 -0
- package/dist/engine/sql-json.js.map +1 -0
- package/dist/engine/sql-semantics.d.ts +31 -0
- package/dist/engine/sql-semantics.d.ts.map +1 -0
- package/dist/engine/sql-semantics.js +121 -0
- package/dist/engine/sql-semantics.js.map +1 -0
- package/dist/engine/vector.d.ts +87 -0
- package/dist/engine/vector.d.ts.map +1 -0
- package/dist/engine/vector.js +4227 -0
- package/dist/engine/vector.js.map +1 -0
- package/dist/engine/worker-host.d.ts +48 -0
- package/dist/engine/worker-host.d.ts.map +1 -0
- package/dist/engine/worker-host.js +501 -0
- package/dist/engine/worker-host.js.map +1 -0
- package/dist/engine/worker.d.ts +2 -0
- package/dist/engine/worker.d.ts.map +1 -0
- package/dist/engine/worker.js +14 -0
- package/dist/engine/worker.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/plan/index.d.ts +17 -0
- package/dist/plan/index.d.ts.map +1 -0
- package/dist/plan/index.js +17 -0
- package/dist/plan/index.js.map +1 -0
- package/dist/storage/fixture-shape.d.ts +42 -0
- package/dist/storage/fixture-shape.d.ts.map +1 -0
- package/dist/storage/fixture-shape.js +146 -0
- package/dist/storage/fixture-shape.js.map +1 -0
- package/dist/storage/index.d.ts +5 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +5 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/indexeddb.d.ts +139 -0
- package/dist/storage/indexeddb.d.ts.map +1 -0
- package/dist/storage/indexeddb.js +3161 -0
- package/dist/storage/indexeddb.js.map +1 -0
- package/dist/storage/memory.d.ts +121 -0
- package/dist/storage/memory.d.ts.map +1 -0
- package/dist/storage/memory.js +1455 -0
- package/dist/storage/memory.js.map +1 -0
- package/dist/storage/snapshot.d.ts +108 -0
- package/dist/storage/snapshot.d.ts.map +1 -0
- package/dist/storage/snapshot.js +276 -0
- package/dist/storage/snapshot.js.map +1 -0
- package/dist/storage/types.d.ts +895 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +1549 -0
- package/dist/storage/types.js.map +1 -0
- package/dist/testing/index.d.ts +76 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +233 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/seeds.d.ts +11 -0
- package/dist/testing/seeds.d.ts.map +1 -0
- package/dist/testing/seeds.js +50 -0
- package/dist/testing/seeds.js.map +1 -0
- package/dist/transactions/index.d.ts +148 -0
- package/dist/transactions/index.d.ts.map +1 -0
- package/dist/transactions/index.js +662 -0
- package/dist/transactions/index.js.map +1 -0
- package/dist/worker-protocol/index.d.ts +84 -0
- package/dist/worker-protocol/index.d.ts.map +1 -0
- package/dist/worker-protocol/index.js +117 -0
- package/dist/worker-protocol/index.js.map +1 -0
- package/package.json +66 -16
- package/sql-feature-matrix.json +1275 -0
- package/index.d.ts +0 -1
- 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
|