squirreling 0.15.2 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/backend/batch.js +236 -0
- package/src/backend/batchAdapters.js +178 -0
- package/src/backend/dataSource.js +17 -1
- package/src/execute/aggregates.js +2 -1
- package/src/execute/batchResults.js +28 -0
- package/src/execute/batches.js +274 -0
- package/src/execute/execute.js +440 -24
- package/src/execute/streamingAggregate.js +151 -1
- package/src/execute/utils.js +37 -0
- package/src/expression/batch.js +659 -0
- package/src/expression/binary.js +17 -4
- package/src/expression/evaluate.js +39 -109
- package/src/expression/scalar.js +105 -0
- package/src/index.d.ts +44 -1
- package/src/index.js +9 -0
- package/src/internalTypes.d.ts +28 -0
- package/src/plan/columns.js +105 -59
- package/src/plan/plan.js +12 -3
- package/src/plan/types.d.ts +3 -0
- package/src/types.d.ts +156 -4
- package/src/validation/tables.js +4 -2
|
@@ -0,0 +1,659 @@
|
|
|
1
|
+
import { composeSelections, readBatchColumn, selectVector, selectedRowCount, valueAt } from '../backend/batch.js'
|
|
2
|
+
import { isPlainObject, sqlEquals } from '../execute/utils.js'
|
|
3
|
+
import { yieldToEventLoop } from '../execute/yield.js'
|
|
4
|
+
import { isStringFunc } from '../validation/functions.js'
|
|
5
|
+
import { ColumnNotFoundError } from '../validation/tables.js'
|
|
6
|
+
import { applyBinaryOp } from './binary.js'
|
|
7
|
+
import { applyCast, evaluateJsonExtract } from './scalar.js'
|
|
8
|
+
import { evaluateStringFunc } from './strings.js'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @import { CompiledBatchExpression, CompileState, ValueKernel } from '../internalTypes.js'
|
|
12
|
+
* @import { AsyncBatch, ColumnReadRequest, ColumnResult, ColumnVector, ExprNode, FunctionNode, RowSelection, SqlPrimitive } from '../types.js'
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const YIELD_INTERVAL = 4000
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Compiles a supported scalar expression into one column-level evaluation.
|
|
19
|
+
* Dependencies resolve at most once per batch selection; the synchronous
|
|
20
|
+
* kernel then evaluates every selected row without per-cell promises.
|
|
21
|
+
*
|
|
22
|
+
* Unsupported expressions return `undefined` so an operator can retain its
|
|
23
|
+
* existing row evaluator without changing semantics.
|
|
24
|
+
*
|
|
25
|
+
* @param {ExprNode} expression
|
|
26
|
+
* @param {readonly string[]} columns
|
|
27
|
+
* @returns {CompiledBatchExpression | undefined}
|
|
28
|
+
*/
|
|
29
|
+
export function compileBatchExpression(expression, columns) {
|
|
30
|
+
return compileEvaluator(expression, columns)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Compiles an expression as one vector kernel when every dependency may be
|
|
35
|
+
* read eagerly. Lazy control flow falls back to selection-aware evaluators
|
|
36
|
+
* that only read columns for rows whose branch is reached.
|
|
37
|
+
*
|
|
38
|
+
* @param {ExprNode} node
|
|
39
|
+
* @param {readonly string[]} columns
|
|
40
|
+
* @returns {CompiledBatchExpression | undefined}
|
|
41
|
+
*/
|
|
42
|
+
function compileEvaluator(node, columns) {
|
|
43
|
+
const kernel = compileKernelEvaluator(node, columns)
|
|
44
|
+
if (kernel) return kernel
|
|
45
|
+
|
|
46
|
+
if (node.type === 'unary') {
|
|
47
|
+
const argument = compileEvaluator(node.argument, columns)
|
|
48
|
+
if (!argument) return undefined
|
|
49
|
+
return {
|
|
50
|
+
async evaluate(context) {
|
|
51
|
+
const vector = await argument.evaluate(context)
|
|
52
|
+
return evaluateValues(context, function unaryValue(rowIndex) {
|
|
53
|
+
const value = valueAt(vector, rowIndex)
|
|
54
|
+
if (node.op === '-') return value == null ? null : -value
|
|
55
|
+
if (node.op === 'NOT') return value == null ? null : !value
|
|
56
|
+
if (node.op === 'IS NULL') return value == null
|
|
57
|
+
return value != null
|
|
58
|
+
})
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (node.type === 'binary') {
|
|
64
|
+
if (node.left.type === 'interval' || node.right.type === 'interval') return undefined
|
|
65
|
+
const left = compileEvaluator(node.left, columns)
|
|
66
|
+
const right = compileEvaluator(node.right, columns)
|
|
67
|
+
if (!left || !right) return undefined
|
|
68
|
+
if (node.op === 'AND' || node.op === 'OR') {
|
|
69
|
+
const operator = node.op
|
|
70
|
+
return {
|
|
71
|
+
evaluate(context) {
|
|
72
|
+
return evaluateLogical(operator, left, right, context)
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
async evaluate(context) {
|
|
78
|
+
const [leftVector, rightVector] = await Promise.all([
|
|
79
|
+
left.evaluate(context),
|
|
80
|
+
right.evaluate(context),
|
|
81
|
+
])
|
|
82
|
+
return evaluateValues(context, function binaryValue(rowIndex) {
|
|
83
|
+
return applyBinaryOp(node.op, valueAt(leftVector, rowIndex), valueAt(rightVector, rowIndex))
|
|
84
|
+
})
|
|
85
|
+
},
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (node.type === 'cast') {
|
|
90
|
+
const argument = compileEvaluator(node.expr, columns)
|
|
91
|
+
if (!argument) return undefined
|
|
92
|
+
return {
|
|
93
|
+
async evaluate(context) {
|
|
94
|
+
const vector = await argument.evaluate(context)
|
|
95
|
+
return evaluateValues(context, function castValue(rowIndex, streamRowIndex) {
|
|
96
|
+
return applyCast(node, valueAt(vector, rowIndex), streamRowIndex + 1)
|
|
97
|
+
})
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (node.type === 'function') return compileFunctionEvaluator(node, columns)
|
|
103
|
+
if (node.type === 'case') return compileCaseEvaluator(node, columns)
|
|
104
|
+
return undefined
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* @param {ExprNode} node
|
|
109
|
+
* @param {readonly string[]} columns
|
|
110
|
+
* @returns {CompiledBatchExpression | undefined}
|
|
111
|
+
*/
|
|
112
|
+
function compileKernelEvaluator(node, columns) {
|
|
113
|
+
/** @type {CompileState} */
|
|
114
|
+
const state = {
|
|
115
|
+
columns,
|
|
116
|
+
dependencies: [],
|
|
117
|
+
dependencyPositions: new Map(),
|
|
118
|
+
}
|
|
119
|
+
const kernel = compileValueKernel(node, state)
|
|
120
|
+
if (!kernel) return undefined
|
|
121
|
+
const { dependencies } = state
|
|
122
|
+
return {
|
|
123
|
+
evaluate(context) {
|
|
124
|
+
const { batch, selection, signal, rowOffset = 0, rowOrdinals } = context
|
|
125
|
+
signal?.throwIfAborted()
|
|
126
|
+
const results = dependencies.map(function readDependency(columnIndex) {
|
|
127
|
+
return readBatchColumn({ batch, columnIndex, selection, signal })
|
|
128
|
+
})
|
|
129
|
+
const vectors = resolveVectors(results)
|
|
130
|
+
if (vectors instanceof Promise) {
|
|
131
|
+
return vectors.then(function evaluateResolved(resolved) {
|
|
132
|
+
signal?.throwIfAborted()
|
|
133
|
+
return evaluateKernel(kernel, resolved, selection, signal, rowOffset, rowOrdinals)
|
|
134
|
+
})
|
|
135
|
+
}
|
|
136
|
+
return evaluateKernel(kernel, vectors, selection, signal, rowOffset, rowOrdinals)
|
|
137
|
+
},
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @param {ExprNode} node
|
|
143
|
+
* @param {CompileState} state
|
|
144
|
+
* @returns {ValueKernel | undefined}
|
|
145
|
+
*/
|
|
146
|
+
function compileValueKernel(node, state) {
|
|
147
|
+
if (node.type === 'literal') {
|
|
148
|
+
return function literalValue() { return node.value }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (node.type === 'identifier') {
|
|
152
|
+
const accesses = resolveIdentifier(node, state.columns)
|
|
153
|
+
if (!accesses) return undefined
|
|
154
|
+
const dependencyPositions = accesses.map(function registerAccess(access) {
|
|
155
|
+
let dependencyPosition = state.dependencyPositions.get(access.columnIndex)
|
|
156
|
+
if (dependencyPosition === undefined) {
|
|
157
|
+
dependencyPosition = state.dependencies.length
|
|
158
|
+
state.dependencies.push(access.columnIndex)
|
|
159
|
+
state.dependencyPositions.set(access.columnIndex, dependencyPosition)
|
|
160
|
+
}
|
|
161
|
+
return dependencyPosition
|
|
162
|
+
})
|
|
163
|
+
return function identifierValue(vectors, rowIndex, streamRowIndex) {
|
|
164
|
+
for (let index = 0; index < accesses.length; index++) {
|
|
165
|
+
const access = accesses[index]
|
|
166
|
+
const value = valueAt(vectors[dependencyPositions[index]], rowIndex)
|
|
167
|
+
if (!access.field) return value
|
|
168
|
+
if (isPlainObject(value) && Object.prototype.hasOwnProperty.call(value, access.field)) {
|
|
169
|
+
return value[access.field]
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
throw new ColumnNotFoundError({
|
|
173
|
+
missingColumn: `${node.prefix}.${node.name}`,
|
|
174
|
+
availableColumns: [...state.columns],
|
|
175
|
+
rowIndex: streamRowIndex + 1,
|
|
176
|
+
...node,
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (node.type === 'unary') {
|
|
182
|
+
const argument = compileValueKernel(node.argument, state)
|
|
183
|
+
if (!argument) return undefined
|
|
184
|
+
return function unaryValue(vectors, rowIndex, streamRowIndex) {
|
|
185
|
+
const value = argument(vectors, rowIndex, streamRowIndex)
|
|
186
|
+
if (node.op === '-') return value == null ? null : -value
|
|
187
|
+
if (node.op === 'NOT') return value == null ? null : !value
|
|
188
|
+
if (node.op === 'IS NULL') return value == null
|
|
189
|
+
return value != null
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (node.type === 'binary') {
|
|
194
|
+
if (node.left.type === 'interval' || node.right.type === 'interval') return undefined
|
|
195
|
+
if ((node.op === 'AND' || node.op === 'OR') && readsIdentifier(node.right)) return undefined
|
|
196
|
+
const left = compileValueKernel(node.left, state)
|
|
197
|
+
const right = compileValueKernel(node.right, state)
|
|
198
|
+
if (!left || !right) return undefined
|
|
199
|
+
return function binaryValue(vectors, rowIndex, streamRowIndex) {
|
|
200
|
+
const leftValue = left(vectors, rowIndex, streamRowIndex)
|
|
201
|
+
if (node.op === 'AND' && leftValue != null && !leftValue) return false
|
|
202
|
+
if (node.op === 'OR' && leftValue != null && Boolean(leftValue)) return true
|
|
203
|
+
const rightValue = right(vectors, rowIndex, streamRowIndex)
|
|
204
|
+
return applyBinaryOp(node.op, leftValue, rightValue)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (node.type === 'cast') {
|
|
209
|
+
const argument = compileValueKernel(node.expr, state)
|
|
210
|
+
if (!argument) return undefined
|
|
211
|
+
return function castValue(vectors, rowIndex, streamRowIndex) {
|
|
212
|
+
return applyCast(node, argument(vectors, rowIndex, streamRowIndex), streamRowIndex + 1)
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (node.type === 'function') {
|
|
217
|
+
return compileFunctionKernel(node, state)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return undefined
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* @param {FunctionNode} node
|
|
225
|
+
* @param {CompileState} state
|
|
226
|
+
* @returns {ValueKernel | undefined}
|
|
227
|
+
*/
|
|
228
|
+
function compileFunctionKernel(node, state) {
|
|
229
|
+
const funcName = node.funcName.toUpperCase()
|
|
230
|
+
if (node.distinct || node.filter) return undefined
|
|
231
|
+
if (funcName === 'COALESCE' && node.args.slice(1).some(readsIdentifier)) return undefined
|
|
232
|
+
/** @type {ValueKernel[]} */
|
|
233
|
+
const arguments_ = []
|
|
234
|
+
for (const argumentNode of node.args) {
|
|
235
|
+
const argument = compileValueKernel(argumentNode, state)
|
|
236
|
+
if (!argument) return undefined
|
|
237
|
+
arguments_.push(argument)
|
|
238
|
+
}
|
|
239
|
+
if (funcName === 'COALESCE') {
|
|
240
|
+
return function coalesceValue(vectors, rowIndex, streamRowIndex) {
|
|
241
|
+
for (const argument of arguments_) {
|
|
242
|
+
const value = argument(vectors, rowIndex, streamRowIndex)
|
|
243
|
+
if (value != null) return value
|
|
244
|
+
}
|
|
245
|
+
return null
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
if (funcName === 'NULLIF') {
|
|
249
|
+
return function nullIfValue(vectors, rowIndex, streamRowIndex) {
|
|
250
|
+
const first = arguments_[0](vectors, rowIndex, streamRowIndex)
|
|
251
|
+
const second = arguments_[1](vectors, rowIndex, streamRowIndex)
|
|
252
|
+
return sqlEquals(first, second) ? null : first
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
if (funcName === 'JSON_VALUE' || funcName === 'JSON_QUERY' || funcName === 'JSON_EXTRACT' || funcName === 'JSON_EXTRACT_STRING') {
|
|
256
|
+
return function jsonValue(vectors, rowIndex, streamRowIndex) {
|
|
257
|
+
const args = arguments_.map(function argumentValue(argument) {
|
|
258
|
+
return argument(vectors, rowIndex, streamRowIndex)
|
|
259
|
+
})
|
|
260
|
+
return evaluateJsonExtract({ funcName, node, args, rowIndex: streamRowIndex + 1 })
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (!isStringFunc(funcName)) return undefined
|
|
264
|
+
return function stringFunctionValue(vectors, rowIndex, streamRowIndex) {
|
|
265
|
+
const args = arguments_.map(function argumentValue(argument) {
|
|
266
|
+
return argument(vectors, rowIndex, streamRowIndex)
|
|
267
|
+
})
|
|
268
|
+
return evaluateStringFunc({ funcName, node, args, rowIndex: streamRowIndex + 1 })
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* @param {FunctionNode} node
|
|
274
|
+
* @param {readonly string[]} columns
|
|
275
|
+
* @returns {CompiledBatchExpression | undefined}
|
|
276
|
+
*/
|
|
277
|
+
function compileFunctionEvaluator(node, columns) {
|
|
278
|
+
const funcName = node.funcName.toUpperCase()
|
|
279
|
+
if (node.distinct || node.filter) return undefined
|
|
280
|
+
/** @type {CompiledBatchExpression[]} */
|
|
281
|
+
const arguments_ = []
|
|
282
|
+
for (const argumentNode of node.args) {
|
|
283
|
+
const argument = compileEvaluator(argumentNode, columns)
|
|
284
|
+
if (!argument) return undefined
|
|
285
|
+
arguments_.push(argument)
|
|
286
|
+
}
|
|
287
|
+
if (funcName === 'COALESCE') {
|
|
288
|
+
return {
|
|
289
|
+
evaluate(context) {
|
|
290
|
+
return evaluateCoalesce(arguments_, context)
|
|
291
|
+
},
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (funcName !== 'NULLIF' && funcName !== 'JSON_VALUE' && funcName !== 'JSON_QUERY' &&
|
|
295
|
+
funcName !== 'JSON_EXTRACT' && funcName !== 'JSON_EXTRACT_STRING' && !isStringFunc(funcName)) return undefined
|
|
296
|
+
return {
|
|
297
|
+
async evaluate(context) {
|
|
298
|
+
const vectors = await Promise.all(arguments_.map(function evaluateArgument(argument) {
|
|
299
|
+
return argument.evaluate(context)
|
|
300
|
+
}))
|
|
301
|
+
return evaluateValues(context, function functionValue(rowIndex, streamRowIndex) {
|
|
302
|
+
const args = vectors.map(function argumentValue(vector) { return valueAt(vector, rowIndex) })
|
|
303
|
+
if (funcName === 'NULLIF') return sqlEquals(args[0], args[1]) ? null : args[0]
|
|
304
|
+
if (funcName === 'JSON_VALUE' || funcName === 'JSON_QUERY' || funcName === 'JSON_EXTRACT' || funcName === 'JSON_EXTRACT_STRING') {
|
|
305
|
+
return evaluateJsonExtract({ funcName, node, args, rowIndex: streamRowIndex + 1 })
|
|
306
|
+
}
|
|
307
|
+
return evaluateStringFunc({ funcName, node, args, rowIndex: streamRowIndex + 1 })
|
|
308
|
+
})
|
|
309
|
+
},
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* @param {import('../types.js').CaseNode} node
|
|
315
|
+
* @param {readonly string[]} columns
|
|
316
|
+
* @returns {CompiledBatchExpression | undefined}
|
|
317
|
+
*/
|
|
318
|
+
function compileCaseEvaluator(node, columns) {
|
|
319
|
+
const caseExpression = node.caseExpr ? compileEvaluator(node.caseExpr, columns) : undefined
|
|
320
|
+
if (node.caseExpr && !caseExpression) return undefined
|
|
321
|
+
/** @type {{ condition: CompiledBatchExpression, result: CompiledBatchExpression }[]} */
|
|
322
|
+
const clauses = []
|
|
323
|
+
for (const clause of node.whenClauses) {
|
|
324
|
+
const condition = compileEvaluator(clause.condition, columns)
|
|
325
|
+
const result = compileEvaluator(clause.result, columns)
|
|
326
|
+
if (!condition || !result) return undefined
|
|
327
|
+
clauses.push({ condition, result })
|
|
328
|
+
}
|
|
329
|
+
const elseResult = node.elseResult ? compileEvaluator(node.elseResult, columns) : undefined
|
|
330
|
+
if (node.elseResult && !elseResult) return undefined
|
|
331
|
+
return {
|
|
332
|
+
evaluate(context) {
|
|
333
|
+
return evaluateCase(caseExpression, clauses, elseResult, context)
|
|
334
|
+
},
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Evaluates the right side only for rows whose left side does not determine
|
|
340
|
+
* the logical result. The resulting subset is also passed to deferred source
|
|
341
|
+
* and computed columns, preserving row evaluator short-circuit behavior.
|
|
342
|
+
*
|
|
343
|
+
* @param {'AND' | 'OR'} operator
|
|
344
|
+
* @param {CompiledBatchExpression} left
|
|
345
|
+
* @param {CompiledBatchExpression} right
|
|
346
|
+
* @param {ColumnReadRequest} context
|
|
347
|
+
* @returns {Promise<ColumnVector>}
|
|
348
|
+
*/
|
|
349
|
+
async function evaluateLogical(operator, left, right, context) {
|
|
350
|
+
const leftVector = await left.evaluate(context)
|
|
351
|
+
const length = selectedRowCount(context.selection)
|
|
352
|
+
/** @type {SqlPrimitive[]} */
|
|
353
|
+
const values = new Array(length)
|
|
354
|
+
const needed = new Uint32Array(length)
|
|
355
|
+
let neededCount = 0
|
|
356
|
+
await visitRows(length, context.signal, function classifyLeft(rowIndex) {
|
|
357
|
+
const value = valueAt(leftVector, rowIndex)
|
|
358
|
+
const decided = operator === 'AND'
|
|
359
|
+
? value != null && !value
|
|
360
|
+
: value != null && Boolean(value)
|
|
361
|
+
if (decided) {
|
|
362
|
+
values[rowIndex] = operator === 'AND' ? false : true
|
|
363
|
+
} else {
|
|
364
|
+
needed[neededCount++] = rowIndex
|
|
365
|
+
}
|
|
366
|
+
})
|
|
367
|
+
if (neededCount === 0) return { type: 'values', values, length }
|
|
368
|
+
|
|
369
|
+
const neededIndices = needed.subarray(0, neededCount)
|
|
370
|
+
const rightVector = await right.evaluate(subsetContext(context, neededIndices))
|
|
371
|
+
await visitRows(neededCount, context.signal, function combineRight(subsetIndex) {
|
|
372
|
+
const rowIndex = neededIndices[subsetIndex]
|
|
373
|
+
values[rowIndex] = applyBinaryOp(
|
|
374
|
+
operator,
|
|
375
|
+
valueAt(leftVector, rowIndex),
|
|
376
|
+
valueAt(rightVector, subsetIndex)
|
|
377
|
+
)
|
|
378
|
+
})
|
|
379
|
+
return { type: 'values', values, length }
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* @param {CompiledBatchExpression | undefined} caseExpression
|
|
384
|
+
* @param {{ condition: CompiledBatchExpression, result: CompiledBatchExpression }[]} clauses
|
|
385
|
+
* @param {CompiledBatchExpression | undefined} elseResult
|
|
386
|
+
* @param {ColumnReadRequest} context
|
|
387
|
+
* @returns {Promise<ColumnVector>}
|
|
388
|
+
*/
|
|
389
|
+
async function evaluateCase(caseExpression, clauses, elseResult, context) {
|
|
390
|
+
const length = selectedRowCount(context.selection)
|
|
391
|
+
/** @type {SqlPrimitive[]} */
|
|
392
|
+
const values = new Array(length)
|
|
393
|
+
const caseVector = caseExpression ? await caseExpression.evaluate(context) : undefined
|
|
394
|
+
let remaining = allIndices(length)
|
|
395
|
+
|
|
396
|
+
for (const clause of clauses) {
|
|
397
|
+
if (remaining.length === 0) break
|
|
398
|
+
const remainingContext = subsetContext(context, remaining)
|
|
399
|
+
const conditionVector = await clause.condition.evaluate(remainingContext)
|
|
400
|
+
const matched = new Uint32Array(remaining.length)
|
|
401
|
+
const unmatched = new Uint32Array(remaining.length)
|
|
402
|
+
let matchedCount = 0
|
|
403
|
+
let unmatchedCount = 0
|
|
404
|
+
await visitRows(remaining.length, context.signal, function classifyCondition(subsetIndex) {
|
|
405
|
+
const rowIndex = remaining[subsetIndex]
|
|
406
|
+
const condition = valueAt(conditionVector, subsetIndex)
|
|
407
|
+
const matches = caseVector
|
|
408
|
+
? sqlEquals(valueAt(caseVector, rowIndex), condition)
|
|
409
|
+
: Boolean(condition)
|
|
410
|
+
if (matches) matched[matchedCount++] = rowIndex
|
|
411
|
+
else unmatched[unmatchedCount++] = rowIndex
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
if (matchedCount > 0) {
|
|
415
|
+
const matchedIndices = matched.subarray(0, matchedCount)
|
|
416
|
+
const resultVector = await clause.result.evaluate(subsetContext(context, matchedIndices))
|
|
417
|
+
await visitRows(matchedCount, context.signal, function scatterResult(subsetIndex) {
|
|
418
|
+
values[matchedIndices[subsetIndex]] = valueAt(resultVector, subsetIndex)
|
|
419
|
+
})
|
|
420
|
+
}
|
|
421
|
+
remaining = unmatched.subarray(0, unmatchedCount)
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
if (remaining.length > 0 && elseResult) {
|
|
425
|
+
const resultVector = await elseResult.evaluate(subsetContext(context, remaining))
|
|
426
|
+
await visitRows(remaining.length, context.signal, function scatterElse(subsetIndex) {
|
|
427
|
+
values[remaining[subsetIndex]] = valueAt(resultVector, subsetIndex)
|
|
428
|
+
})
|
|
429
|
+
} else {
|
|
430
|
+
for (const rowIndex of remaining) values[rowIndex] = null
|
|
431
|
+
}
|
|
432
|
+
return { type: 'values', values, length }
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Evaluates each argument only for rows that remained null after the previous
|
|
437
|
+
* argument, matching COALESCE's lazy row semantics.
|
|
438
|
+
*
|
|
439
|
+
* @param {CompiledBatchExpression[]} arguments_
|
|
440
|
+
* @param {ColumnReadRequest} context
|
|
441
|
+
* @returns {Promise<ColumnVector>}
|
|
442
|
+
*/
|
|
443
|
+
async function evaluateCoalesce(arguments_, context) {
|
|
444
|
+
const length = selectedRowCount(context.selection)
|
|
445
|
+
/** @type {SqlPrimitive[]} */
|
|
446
|
+
const values = new Array(length)
|
|
447
|
+
let remaining = allIndices(length)
|
|
448
|
+
for (const argument of arguments_) {
|
|
449
|
+
if (remaining.length === 0) break
|
|
450
|
+
const vector = await argument.evaluate(subsetContext(context, remaining))
|
|
451
|
+
const nullRows = new Uint32Array(remaining.length)
|
|
452
|
+
let nullCount = 0
|
|
453
|
+
await visitRows(remaining.length, context.signal, function chooseValue(subsetIndex) {
|
|
454
|
+
const rowIndex = remaining[subsetIndex]
|
|
455
|
+
const value = valueAt(vector, subsetIndex)
|
|
456
|
+
if (value == null) nullRows[nullCount++] = rowIndex
|
|
457
|
+
else values[rowIndex] = value
|
|
458
|
+
})
|
|
459
|
+
remaining = nullRows.subarray(0, nullCount)
|
|
460
|
+
}
|
|
461
|
+
for (const rowIndex of remaining) values[rowIndex] = null
|
|
462
|
+
return { type: 'values', values, length }
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* @param {ColumnReadRequest} context
|
|
467
|
+
* @param {Uint32Array} indices - positions in the context's selected rows
|
|
468
|
+
* @returns {ColumnReadRequest}
|
|
469
|
+
*/
|
|
470
|
+
function subsetContext(context, indices) {
|
|
471
|
+
const length = selectedRowCount(context.selection)
|
|
472
|
+
const selection = composeSelections(context.selection, {
|
|
473
|
+
type: 'indices',
|
|
474
|
+
indices,
|
|
475
|
+
length,
|
|
476
|
+
})
|
|
477
|
+
/** @type {ColumnVector} */
|
|
478
|
+
const rowOrdinals = context.rowOrdinals
|
|
479
|
+
? selectVector(context.rowOrdinals, { type: 'indices', indices, length })
|
|
480
|
+
: { type: 'typed', values: indices, length: indices.length }
|
|
481
|
+
return { ...context, selection, rowOrdinals }
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* @param {number} length
|
|
486
|
+
* @returns {Uint32Array}
|
|
487
|
+
*/
|
|
488
|
+
function allIndices(length) {
|
|
489
|
+
const indices = new Uint32Array(length)
|
|
490
|
+
for (let index = 0; index < length; index++) indices[index] = index
|
|
491
|
+
return indices
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* @param {ColumnReadRequest} context
|
|
496
|
+
* @param {(rowIndex: number, streamRowIndex: number) => SqlPrimitive} evaluate
|
|
497
|
+
* @returns {Promise<ColumnVector>}
|
|
498
|
+
*/
|
|
499
|
+
async function evaluateValues(context, evaluate) {
|
|
500
|
+
const length = selectedRowCount(context.selection)
|
|
501
|
+
/** @type {SqlPrimitive[]} */
|
|
502
|
+
const values = new Array(length)
|
|
503
|
+
await visitRows(length, context.signal, function evaluateRow(rowIndex) {
|
|
504
|
+
values[rowIndex] = evaluate(rowIndex, streamRowIndex(context, rowIndex))
|
|
505
|
+
})
|
|
506
|
+
return { type: 'values', values, length }
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* @param {number} length
|
|
511
|
+
* @param {AbortSignal | undefined} signal
|
|
512
|
+
* @param {(rowIndex: number) => void} visit
|
|
513
|
+
* @returns {Promise<void>}
|
|
514
|
+
*/
|
|
515
|
+
async function visitRows(length, signal, visit) {
|
|
516
|
+
for (let start = 0; start < length; start += YIELD_INTERVAL) {
|
|
517
|
+
if (signal && start > 0) await yieldToEventLoop()
|
|
518
|
+
signal?.throwIfAborted()
|
|
519
|
+
const end = Math.min(start + YIELD_INTERVAL, length)
|
|
520
|
+
for (let rowIndex = start; rowIndex < end; rowIndex++) visit(rowIndex)
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* @param {ColumnReadRequest} context
|
|
526
|
+
* @param {number} rowIndex
|
|
527
|
+
* @returns {number}
|
|
528
|
+
*/
|
|
529
|
+
function streamRowIndex(context, rowIndex) {
|
|
530
|
+
const ordinal = context.rowOrdinals ? Number(valueAt(context.rowOrdinals, rowIndex)) : rowIndex
|
|
531
|
+
return (context.rowOffset ?? 0) + ordinal
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* @param {import('../types.js').IdentifierNode} identifier
|
|
536
|
+
* @param {readonly string[]} columns
|
|
537
|
+
* @returns {{ columnIndex: number, field?: string }[] | undefined}
|
|
538
|
+
*/
|
|
539
|
+
function resolveIdentifier(identifier, columns) {
|
|
540
|
+
const sourceName = identifier.prefix
|
|
541
|
+
? `${identifier.prefix}.${identifier.name}`
|
|
542
|
+
: identifier.name
|
|
543
|
+
const exact = columns.lastIndexOf(sourceName)
|
|
544
|
+
if (exact >= 0) return [{ columnIndex: exact }]
|
|
545
|
+
|
|
546
|
+
if (identifier.prefix) {
|
|
547
|
+
const prefix = `${identifier.prefix}.`
|
|
548
|
+
const prefixedMatches = []
|
|
549
|
+
const baseMatches = []
|
|
550
|
+
const baseSuffix = `.${identifier.prefix}`
|
|
551
|
+
for (let index = 0; index < columns.length; index++) {
|
|
552
|
+
const fieldName = columns[index]
|
|
553
|
+
if (fieldName.startsWith(prefix)) prefixedMatches.push(index)
|
|
554
|
+
if (fieldName === identifier.prefix || fieldName.endsWith(baseSuffix)) baseMatches.push(index)
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/** @type {{ columnIndex: number, field?: string }[]} */
|
|
558
|
+
const accesses = []
|
|
559
|
+
if (prefixedMatches.length === 1) {
|
|
560
|
+
accesses.push({ columnIndex: prefixedMatches[0], field: identifier.name })
|
|
561
|
+
}
|
|
562
|
+
if (baseMatches.length === 1) {
|
|
563
|
+
accesses.push({ columnIndex: baseMatches[0], field: identifier.name })
|
|
564
|
+
}
|
|
565
|
+
const bare = columns.lastIndexOf(identifier.name)
|
|
566
|
+
if (bare >= 0) accesses.push({ columnIndex: bare })
|
|
567
|
+
return accesses.length > 0 ? accesses : undefined
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const suffix = `.${identifier.name}`
|
|
571
|
+
const matches = []
|
|
572
|
+
for (let i = 0; i < columns.length; i++) {
|
|
573
|
+
if (columns[i].endsWith(suffix)) matches.push(i)
|
|
574
|
+
}
|
|
575
|
+
return matches.length === 1 ? [{ columnIndex: matches[0] }] : undefined
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* @param {ValueKernel} kernel
|
|
580
|
+
* @param {ColumnVector[]} vectors
|
|
581
|
+
* @param {RowSelection} selection
|
|
582
|
+
* @param {AbortSignal} [signal]
|
|
583
|
+
* @param {number} [rowOffset]
|
|
584
|
+
* @param {ColumnVector} [rowOrdinals]
|
|
585
|
+
* @returns {ColumnResult}
|
|
586
|
+
*/
|
|
587
|
+
function evaluateKernel(kernel, vectors, selection, signal, rowOffset = 0, rowOrdinals) {
|
|
588
|
+
const length = selectedRowCount(selection)
|
|
589
|
+
if (signal && length > YIELD_INTERVAL) {
|
|
590
|
+
return evaluateKernelAsync(kernel, vectors, length, signal, rowOffset, rowOrdinals)
|
|
591
|
+
}
|
|
592
|
+
/** @type {SqlPrimitive[]} */
|
|
593
|
+
const values = new Array(length)
|
|
594
|
+
for (let rowIndex = 0; rowIndex < length; rowIndex++) {
|
|
595
|
+
if (rowIndex % YIELD_INTERVAL === 0) signal?.throwIfAborted()
|
|
596
|
+
const ordinal = rowOrdinals ? Number(valueAt(rowOrdinals, rowIndex)) : rowIndex
|
|
597
|
+
values[rowIndex] = kernel(vectors, rowIndex, rowOffset + ordinal)
|
|
598
|
+
}
|
|
599
|
+
return { type: 'values', values, length }
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Evaluates a large kernel in macrotask-sized chunks so timer-based aborts
|
|
604
|
+
* can fire while a native batch is being processed.
|
|
605
|
+
*
|
|
606
|
+
* @param {ValueKernel} kernel
|
|
607
|
+
* @param {ColumnVector[]} vectors
|
|
608
|
+
* @param {number} length
|
|
609
|
+
* @param {AbortSignal} signal
|
|
610
|
+
* @param {number} rowOffset
|
|
611
|
+
* @param {ColumnVector} [rowOrdinals]
|
|
612
|
+
* @returns {Promise<ColumnVector>}
|
|
613
|
+
*/
|
|
614
|
+
async function evaluateKernelAsync(kernel, vectors, length, signal, rowOffset, rowOrdinals) {
|
|
615
|
+
/** @type {SqlPrimitive[]} */
|
|
616
|
+
const values = new Array(length)
|
|
617
|
+
for (let start = 0; start < length; start += YIELD_INTERVAL) {
|
|
618
|
+
if (start > 0) await yieldToEventLoop()
|
|
619
|
+
signal.throwIfAborted()
|
|
620
|
+
const end = Math.min(start + YIELD_INTERVAL, length)
|
|
621
|
+
for (let rowIndex = start; rowIndex < end; rowIndex++) {
|
|
622
|
+
const ordinal = rowOrdinals ? Number(valueAt(rowOrdinals, rowIndex)) : rowIndex
|
|
623
|
+
values[rowIndex] = kernel(vectors, rowIndex, rowOffset + ordinal)
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return { type: 'values', values, length }
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Returns whether a supported expression subtree can trigger a column read.
|
|
631
|
+
*
|
|
632
|
+
* @param {ExprNode} node
|
|
633
|
+
* @returns {boolean}
|
|
634
|
+
*/
|
|
635
|
+
function readsIdentifier(node) {
|
|
636
|
+
if (node.type === 'identifier') return true
|
|
637
|
+
if (node.type === 'unary') return readsIdentifier(node.argument)
|
|
638
|
+
if (node.type === 'binary') return readsIdentifier(node.left) || readsIdentifier(node.right)
|
|
639
|
+
if (node.type === 'cast') return readsIdentifier(node.expr)
|
|
640
|
+
if (node.type === 'function') return node.args.some(readsIdentifier)
|
|
641
|
+
return false
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* @param {ColumnResult[]} results
|
|
646
|
+
* @returns {ColumnVector[] | Promise<ColumnVector[]>}
|
|
647
|
+
*/
|
|
648
|
+
function resolveVectors(results) {
|
|
649
|
+
if (results.some(function isPromise(result) { return result instanceof Promise })) {
|
|
650
|
+
return Promise.all(results)
|
|
651
|
+
}
|
|
652
|
+
/** @type {ColumnVector[]} */
|
|
653
|
+
const vectors = []
|
|
654
|
+
for (const result of results) {
|
|
655
|
+
if (result instanceof Promise) throw new Error('Unexpected asynchronous column result')
|
|
656
|
+
vectors.push(result)
|
|
657
|
+
}
|
|
658
|
+
return vectors
|
|
659
|
+
}
|
package/src/expression/binary.js
CHANGED
|
@@ -29,12 +29,25 @@ export function applyBinaryOp(op, a, b) {
|
|
|
29
29
|
return String(a) + String(b)
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
//
|
|
33
|
-
|
|
32
|
+
// Logical operators use Kleene three-valued logic: null is UNKNOWN, which
|
|
33
|
+
// only resolves when the other operand decides the answer on its own
|
|
34
|
+
if (op === 'AND') {
|
|
35
|
+
if (a != null && !a || b != null && !b) return false
|
|
36
|
+
if (a == null || b == null) return null
|
|
37
|
+
return true
|
|
38
|
+
}
|
|
39
|
+
if (op === 'OR') {
|
|
40
|
+
if (a != null && Boolean(a) || b != null && Boolean(b)) return true
|
|
41
|
+
if (a == null || b == null) return null
|
|
34
42
|
return false
|
|
35
43
|
}
|
|
36
|
-
|
|
37
|
-
|
|
44
|
+
|
|
45
|
+
// A comparison with a null operand is UNKNOWN, not false. The difference is
|
|
46
|
+
// invisible to a WHERE (both exclude the row) but not to a NOT above it,
|
|
47
|
+
// which must keep UNKNOWN as UNKNOWN rather than flip false to true
|
|
48
|
+
if (a == null || b == null) {
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
38
51
|
// Compare Date values by their time so distinct instances for the same
|
|
39
52
|
// instant are equal, matching SQL TIMESTAMP semantics rather than JS identity.
|
|
40
53
|
if (a instanceof Date && b instanceof Date) {
|