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.
@@ -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
+ }
@@ -29,12 +29,25 @@ export function applyBinaryOp(op, a, b) {
29
29
  return String(a) + String(b)
30
30
  }
31
31
 
32
- // Comparison and logical operators
33
- if (a == null || b == null) {
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
- if (op === 'AND') return Boolean(a) && Boolean(b)
37
- if (op === 'OR') return Boolean(a) || Boolean(b)
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) {