squirreling 0.16.0 → 0.16.2
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 +3 -3
- package/src/backend/batch.js +33 -4
- package/src/backend/batchAdapters.js +31 -34
- package/src/execute/batches.js +5 -22
- package/src/execute/execute.js +1 -20
- package/src/execute/rowScope.js +27 -0
- package/src/execute/streamingAggregate.js +4 -22
- package/src/expression/batch.js +19 -21
- package/src/expression/binary.js +7 -1
- package/src/expression/regexp.js +39 -18
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.2",
|
|
4
4
|
"description": "Squirreling Async SQL Engine",
|
|
5
5
|
"author": "Hyperparam",
|
|
6
6
|
"homepage": "https://hyperparam.app",
|
|
@@ -40,10 +40,10 @@
|
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
42
|
"@types/node": "26.2.0",
|
|
43
|
-
"@vitest/coverage-v8": "4.1.
|
|
43
|
+
"@vitest/coverage-v8": "4.1.11",
|
|
44
44
|
"eslint": "9.39.4",
|
|
45
45
|
"eslint-plugin-jsdoc": "64.2.1",
|
|
46
46
|
"typescript": "7.0.2",
|
|
47
|
-
"vitest": "4.1.
|
|
47
|
+
"vitest": "4.1.11"
|
|
48
48
|
}
|
|
49
49
|
}
|
package/src/backend/batch.js
CHANGED
|
@@ -17,6 +17,35 @@ export function selectedRowCount(selection) {
|
|
|
17
17
|
return selection.indices.length
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Recognizes promises and thenables without relying on realm identity.
|
|
22
|
+
*
|
|
23
|
+
* @template T
|
|
24
|
+
* @param {T | PromiseLike<T>} value
|
|
25
|
+
* @returns {value is PromiseLike<T>}
|
|
26
|
+
*/
|
|
27
|
+
export function isPromiseLike(value) {
|
|
28
|
+
if (value === null || typeof value !== 'object' && typeof value !== 'function') return false
|
|
29
|
+
return typeof Reflect.get(value, 'then') === 'function'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Avoids a promise boundary when every column is already loaded synchronously.
|
|
34
|
+
*
|
|
35
|
+
* @param {ColumnResult[]} results
|
|
36
|
+
* @returns {ColumnVector[] | Promise<ColumnVector[]>}
|
|
37
|
+
*/
|
|
38
|
+
export function resolveColumnResults(results) {
|
|
39
|
+
if (results.some(isPromiseLike)) return Promise.all(results)
|
|
40
|
+
/** @type {ColumnVector[]} */
|
|
41
|
+
const vectors = []
|
|
42
|
+
for (const result of results) {
|
|
43
|
+
if (isPromiseLike(result)) throw new Error('Unexpected asynchronous column result')
|
|
44
|
+
vectors.push(result)
|
|
45
|
+
}
|
|
46
|
+
return vectors
|
|
47
|
+
}
|
|
48
|
+
|
|
20
49
|
/**
|
|
21
50
|
* Creates base-aligned ordinal values for the rows in a selection.
|
|
22
51
|
*
|
|
@@ -158,8 +187,8 @@ export function readBatchColumn({ batch, columnIndex, selection = batch.selectio
|
|
|
158
187
|
: undefined,
|
|
159
188
|
})
|
|
160
189
|
const validated = validateColumnResult(result, selectedRowCount(selection))
|
|
161
|
-
if (validated
|
|
162
|
-
const settled = validated.then(function cacheResolved(vector) {
|
|
190
|
+
if (isPromiseLike(validated)) {
|
|
191
|
+
const settled = Promise.resolve(validated).then(function cacheResolved(vector) {
|
|
163
192
|
cache.pending.delete(signal)
|
|
164
193
|
cache.resolved ??= vector
|
|
165
194
|
return vector
|
|
@@ -215,8 +244,8 @@ function selectionIndexAt(selection, index) {
|
|
|
215
244
|
* @returns {ColumnResult}
|
|
216
245
|
*/
|
|
217
246
|
function validateColumnResult(result, expectedLength) {
|
|
218
|
-
if (result
|
|
219
|
-
return result.then(function validateResolvedVector(vector) {
|
|
247
|
+
if (isPromiseLike(result)) {
|
|
248
|
+
return Promise.resolve(result).then(function validateResolvedVector(vector) {
|
|
220
249
|
return validateVectorLength(vector, expectedLength)
|
|
221
250
|
})
|
|
222
251
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { readBatchColumn, selectVector, selectedRowCount, valueAt } from './batch.js'
|
|
1
|
+
import { readBatchColumn, resolveColumnResults, selectVector, selectedRowCount, valueAt } from './batch.js'
|
|
2
2
|
import { yieldToEventLoop } from '../execute/yield.js'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* @import { AsyncBatch, AsyncCells, AsyncRow,
|
|
5
|
+
* @import { AsyncBatch, AsyncCells, AsyncRow, RowsToBatchesOptions, SqlPrimitive } from '../types.js'
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const DEFAULT_BATCH_ROWS = 1024
|
|
@@ -21,24 +21,19 @@ export async function* rowsToBatches(rows, columnNames, options) {
|
|
|
21
21
|
if (!Number.isInteger(batchRows) || batchRows <= 0) {
|
|
22
22
|
throw new RangeError(`batchRows must be a positive integer, got ${batchRows}`)
|
|
23
23
|
}
|
|
24
|
-
|
|
25
|
-
let
|
|
24
|
+
/** @type {AsyncRow[]} */
|
|
25
|
+
let bufferedRows = []
|
|
26
26
|
|
|
27
27
|
for await (const row of rows) {
|
|
28
28
|
options?.signal?.throwIfAborted()
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
rowCount++
|
|
34
|
-
if (rowCount === batchRows) {
|
|
35
|
-
yield loadedBatch(values, rowCount)
|
|
36
|
-
values = makeValueBuffers(columnNames.length)
|
|
37
|
-
rowCount = 0
|
|
29
|
+
bufferedRows.push(row)
|
|
30
|
+
if (bufferedRows.length === batchRows) {
|
|
31
|
+
yield await materializeRows(bufferedRows, columnNames)
|
|
32
|
+
bufferedRows = []
|
|
38
33
|
}
|
|
39
34
|
}
|
|
40
35
|
|
|
41
|
-
if (
|
|
36
|
+
if (bufferedRows.length > 0) yield await materializeRows(bufferedRows, columnNames)
|
|
42
37
|
options?.signal?.throwIfAborted()
|
|
43
38
|
}
|
|
44
39
|
|
|
@@ -100,7 +95,7 @@ export async function collectBatches(batches, names, signal) {
|
|
|
100
95
|
const results = batch.columns.map(function readColumn(_column, columnIndex) {
|
|
101
96
|
return readBatchColumn({ batch, columnIndex, signal })
|
|
102
97
|
})
|
|
103
|
-
const vectors = await
|
|
98
|
+
const vectors = await resolveColumnResults(results)
|
|
104
99
|
const rowCount = selectedRowCount(batch.selection)
|
|
105
100
|
for (let rowIndex = 0; rowIndex < rowCount; rowIndex++) {
|
|
106
101
|
if (signal && rowIndex % 4000 === 0) {
|
|
@@ -133,6 +128,27 @@ async function readRowCell(row, name) {
|
|
|
133
128
|
return await cell()
|
|
134
129
|
}
|
|
135
130
|
|
|
131
|
+
/**
|
|
132
|
+
* @param {AsyncRow[]} rows
|
|
133
|
+
* @param {string[]} columnNames
|
|
134
|
+
* @returns {Promise<AsyncBatch>}
|
|
135
|
+
*/
|
|
136
|
+
async function materializeRows(rows, columnNames) {
|
|
137
|
+
const values = makeValueBuffers(columnNames.length)
|
|
138
|
+
/** @type {Promise<void>[]} */
|
|
139
|
+
const pendingReads = []
|
|
140
|
+
for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
|
|
141
|
+
for (let columnIndex = 0; columnIndex < columnNames.length; columnIndex++) {
|
|
142
|
+
const name = columnNames[columnIndex]
|
|
143
|
+
pendingReads.push(readRowCell(rows[rowIndex], name).then(function storeValue(value) {
|
|
144
|
+
values[columnIndex][rowIndex] = value
|
|
145
|
+
}))
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
await Promise.all(pendingReads)
|
|
149
|
+
return loadedBatch(values, rows.length)
|
|
150
|
+
}
|
|
151
|
+
|
|
136
152
|
/**
|
|
137
153
|
* @param {number} count
|
|
138
154
|
* @returns {SqlPrimitive[][]}
|
|
@@ -157,22 +173,3 @@ function loadedBatch(values, rowCount) {
|
|
|
157
173
|
}),
|
|
158
174
|
}
|
|
159
175
|
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Avoids a promise boundary when every column is already loaded synchronously.
|
|
163
|
-
*
|
|
164
|
-
* @param {ColumnResult[]} results
|
|
165
|
-
* @returns {ColumnVector[] | Promise<ColumnVector[]>}
|
|
166
|
-
*/
|
|
167
|
-
function resolveColumns(results) {
|
|
168
|
-
if (results.some(function isPromise(result) { return result instanceof Promise })) {
|
|
169
|
-
return Promise.all(results)
|
|
170
|
-
}
|
|
171
|
-
/** @type {ColumnVector[]} */
|
|
172
|
-
const vectors = []
|
|
173
|
-
for (const result of results) {
|
|
174
|
-
if (result instanceof Promise) throw new Error('Unexpected asynchronous column result')
|
|
175
|
-
vectors.push(result)
|
|
176
|
-
}
|
|
177
|
-
return vectors
|
|
178
|
-
}
|
package/src/execute/batches.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { readBatchColumn, selectBatch, selectedRowCount, selectionOrdinals, valueAt } from '../backend/batch.js'
|
|
1
|
+
import { isPromiseLike, readBatchColumn, resolveColumnResults, selectBatch, selectedRowCount, selectionOrdinals, valueAt } from '../backend/batch.js'
|
|
2
2
|
import { keyify } from './utils.js'
|
|
3
3
|
import { yieldToEventLoop } from './yield.js'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* @import { BatchProjection, CompiledBatchExpression } from '../internalTypes.js'
|
|
7
|
-
* @import { AsyncBatch, BatchColumn,
|
|
7
|
+
* @import { AsyncBatch, BatchColumn, ColumnVector, ReadColumn, RowSelection, SqlPrimitive } from '../types.js'
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
const INITIAL_FILTER_WINDOW_ROWS = 256
|
|
@@ -95,7 +95,7 @@ export async function* filterBatches(batches, expression, signal, targetRows) {
|
|
|
95
95
|
signal,
|
|
96
96
|
rowOffset: rowOffset + start,
|
|
97
97
|
})
|
|
98
|
-
const predicate = result
|
|
98
|
+
const predicate = isPromiseLike(result) ? await result : result
|
|
99
99
|
const { selection, selectedCount } = predicateSelection(predicate, windowRowCount)
|
|
100
100
|
start = end
|
|
101
101
|
matchedRows += selectedCount
|
|
@@ -138,8 +138,8 @@ export async function* distinctBatches(batches, signal) {
|
|
|
138
138
|
const results = batch.columns.map(function readColumn(_column, columnIndex) {
|
|
139
139
|
return readBatchColumn({ batch, columnIndex, signal })
|
|
140
140
|
})
|
|
141
|
-
const resolved =
|
|
142
|
-
const vectors = resolved
|
|
141
|
+
const resolved = resolveColumnResults(results)
|
|
142
|
+
const vectors = isPromiseLike(resolved) ? await resolved : resolved
|
|
143
143
|
const rowCount = selectedRowCount(batch.selection)
|
|
144
144
|
const indices = new Uint32Array(rowCount)
|
|
145
145
|
/** @type {SqlPrimitive[]} */
|
|
@@ -255,20 +255,3 @@ function predicateSelection(predicate, rowCount) {
|
|
|
255
255
|
selectedCount,
|
|
256
256
|
}
|
|
257
257
|
}
|
|
258
|
-
|
|
259
|
-
/**
|
|
260
|
-
* @param {ColumnResult[]} results
|
|
261
|
-
* @returns {ColumnVector[] | Promise<ColumnVector[]>}
|
|
262
|
-
*/
|
|
263
|
-
function resolveVectors(results) {
|
|
264
|
-
if (results.some(function isPromise(result) { return result instanceof Promise })) {
|
|
265
|
-
return Promise.all(results)
|
|
266
|
-
}
|
|
267
|
-
/** @type {ColumnVector[]} */
|
|
268
|
-
const vectors = []
|
|
269
|
-
for (const result of results) {
|
|
270
|
-
if (result instanceof Promise) throw new Error('Unexpected asynchronous column result')
|
|
271
|
-
vectors.push(result)
|
|
272
|
-
}
|
|
273
|
-
return vectors
|
|
274
|
-
}
|
package/src/execute/execute.js
CHANGED
|
@@ -12,6 +12,7 @@ import { executeHashAggregate, executeScalarAggregate } from './aggregates.js'
|
|
|
12
12
|
import { batchResult } from './batchResults.js'
|
|
13
13
|
import { distinctBatches, filterBatches, limitBatches, projectExpressionBatches } from './batches.js'
|
|
14
14
|
import { executeHashJoin, executeNestedLoopJoin, executePositionalJoin } from './join.js'
|
|
15
|
+
import { referencesRowScope } from './rowScope.js'
|
|
15
16
|
import { normalizeScanColumnResult } from './scanColumn.js'
|
|
16
17
|
import { executeSort } from './sort.js'
|
|
17
18
|
import { addBounds, minBounds, stableRowKey } from './utils.js'
|
|
@@ -720,26 +721,6 @@ function compileUnscopedBatchExpression(expression, columns, context) {
|
|
|
720
721
|
: compileBatchExpression(expression, columns)
|
|
721
722
|
}
|
|
722
723
|
|
|
723
|
-
/**
|
|
724
|
-
* Returns whether an expression reads a qualified identifier from row scope.
|
|
725
|
-
*
|
|
726
|
-
* @param {ExprNode} expression
|
|
727
|
-
* @param {string[]} columns
|
|
728
|
-
* @param {ExecuteContext} context
|
|
729
|
-
* @returns {boolean}
|
|
730
|
-
*/
|
|
731
|
-
function referencesRowScope(expression, columns, context) {
|
|
732
|
-
/** @type {IdentifierNode[]} */
|
|
733
|
-
const identifiers = []
|
|
734
|
-
collectColumnsFromExpr(expression, identifiers)
|
|
735
|
-
return identifiers.some(function scopedIdentifier(identifier) {
|
|
736
|
-
return Boolean(identifier.prefix && (
|
|
737
|
-
context.outerAliases?.has(identifier.prefix) ||
|
|
738
|
-
context.scope?.includes(identifier.prefix) && columns.includes(identifier.prefix)
|
|
739
|
-
))
|
|
740
|
-
})
|
|
741
|
-
}
|
|
742
|
-
|
|
743
724
|
/**
|
|
744
725
|
* Executes a filter operation (WHERE clause)
|
|
745
726
|
*
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { collectColumnsFromExpr } from '../plan/columns.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @import { ExecuteContext, ExprNode, IdentifierNode } from '../types.js'
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Returns whether an expression reads a qualified identifier from row scope.
|
|
9
|
+
* A current-scope prefix is ambiguous only when it is also a child column and
|
|
10
|
+
* could therefore mean struct-field access.
|
|
11
|
+
*
|
|
12
|
+
* @param {ExprNode} expression
|
|
13
|
+
* @param {readonly string[]} columns
|
|
14
|
+
* @param {ExecuteContext} context
|
|
15
|
+
* @returns {boolean}
|
|
16
|
+
*/
|
|
17
|
+
export function referencesRowScope(expression, columns, context) {
|
|
18
|
+
/** @type {IdentifierNode[]} */
|
|
19
|
+
const identifiers = []
|
|
20
|
+
collectColumnsFromExpr(expression, identifiers)
|
|
21
|
+
return identifiers.some(function scopedIdentifier(identifier) {
|
|
22
|
+
return Boolean(identifier.prefix && (
|
|
23
|
+
context.outerAliases?.has(identifier.prefix) ||
|
|
24
|
+
context.scope?.includes(identifier.prefix) && columns.includes(identifier.prefix)
|
|
25
|
+
))
|
|
26
|
+
})
|
|
27
|
+
}
|
|
@@ -5,6 +5,7 @@ import { evaluateAll, evaluateExpr } from '../expression/evaluate.js'
|
|
|
5
5
|
import { collectColumnsFromExpr } from '../plan/columns.js'
|
|
6
6
|
import { isAggregateFunc } from '../validation/functions.js'
|
|
7
7
|
import { finalizeAccumulator, newAccumulator, updateAccumulator } from './accumulator.js'
|
|
8
|
+
import { referencesRowScope } from './rowScope.js'
|
|
8
9
|
import { sortEntriesByTerms } from './sort.js'
|
|
9
10
|
import { keyify } from './utils.js'
|
|
10
11
|
import { yieldToEventLoop } from './yield.js'
|
|
@@ -437,7 +438,7 @@ function compileBatchAggregateInputs(groupBy, specs, columns, context) {
|
|
|
437
438
|
/** @type {CompiledBatchExpression[]} */
|
|
438
439
|
const keys = []
|
|
439
440
|
for (const expression of groupBy) {
|
|
440
|
-
if (referencesRowScope(expression, context)) return undefined
|
|
441
|
+
if (referencesRowScope(expression, columns, context)) return undefined
|
|
441
442
|
const key = compileBatchExpression(expression, columns)
|
|
442
443
|
if (!key) return undefined
|
|
443
444
|
keys.push(key)
|
|
@@ -449,8 +450,8 @@ function compileBatchAggregateInputs(groupBy, specs, columns, context) {
|
|
|
449
450
|
const args = []
|
|
450
451
|
for (const spec of specs) {
|
|
451
452
|
if (spec.node.filter && !spec.star) return undefined
|
|
452
|
-
if (spec.node.filter && referencesRowScope(spec.node.filter, context)) return undefined
|
|
453
|
-
if (!spec.star && referencesRowScope(spec.node.args[0], context)) return undefined
|
|
453
|
+
if (spec.node.filter && referencesRowScope(spec.node.filter, columns, context)) return undefined
|
|
454
|
+
if (!spec.star && referencesRowScope(spec.node.args[0], columns, context)) return undefined
|
|
454
455
|
const filter = spec.node.filter
|
|
455
456
|
? compileBatchExpression(spec.node.filter, columns)
|
|
456
457
|
: undefined
|
|
@@ -468,25 +469,6 @@ function compileBatchAggregateInputs(groupBy, specs, columns, context) {
|
|
|
468
469
|
}
|
|
469
470
|
}
|
|
470
471
|
|
|
471
|
-
/**
|
|
472
|
-
* Returns whether an expression reads a qualified identifier whose table
|
|
473
|
-
* scope the batch compiler cannot distinguish from struct-field access.
|
|
474
|
-
*
|
|
475
|
-
* @param {ExprNode} expression
|
|
476
|
-
* @param {ExecuteContext} context
|
|
477
|
-
* @returns {boolean}
|
|
478
|
-
*/
|
|
479
|
-
function referencesRowScope(expression, context) {
|
|
480
|
-
/** @type {IdentifierNode[]} */
|
|
481
|
-
const identifiers = []
|
|
482
|
-
collectColumnsFromExpr(expression, identifiers)
|
|
483
|
-
return identifiers.some(function isScopedReference(identifier) {
|
|
484
|
-
return Boolean(identifier.prefix && (
|
|
485
|
-
context.scope?.includes(identifier.prefix) || context.outerAliases?.has(identifier.prefix)
|
|
486
|
-
))
|
|
487
|
-
})
|
|
488
|
-
}
|
|
489
|
-
|
|
490
472
|
/**
|
|
491
473
|
* Resolves one set of compiled expressions against a batch.
|
|
492
474
|
*
|
package/src/expression/batch.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { composeSelections, readBatchColumn, selectVector, selectedRowCount, valueAt } from '../backend/batch.js'
|
|
1
|
+
import { composeSelections, isPromiseLike, readBatchColumn, resolveColumnResults, selectVector, selectedRowCount, valueAt } from '../backend/batch.js'
|
|
2
2
|
import { isPlainObject, sqlEquals } from '../execute/utils.js'
|
|
3
3
|
import { yieldToEventLoop } from '../execute/yield.js'
|
|
4
4
|
import { isStringFunc } from '../validation/functions.js'
|
|
5
5
|
import { ColumnNotFoundError } from '../validation/tables.js'
|
|
6
6
|
import { applyBinaryOp } from './binary.js'
|
|
7
|
+
import { evaluateRegexpLike } from './regexp.js'
|
|
7
8
|
import { applyCast, evaluateJsonExtract } from './scalar.js'
|
|
8
9
|
import { evaluateStringFunc } from './strings.js'
|
|
9
10
|
|
|
@@ -126,8 +127,8 @@ function compileKernelEvaluator(node, columns) {
|
|
|
126
127
|
const results = dependencies.map(function readDependency(columnIndex) {
|
|
127
128
|
return readBatchColumn({ batch, columnIndex, selection, signal })
|
|
128
129
|
})
|
|
129
|
-
const vectors =
|
|
130
|
-
if (vectors
|
|
130
|
+
const vectors = resolveColumnResults(results)
|
|
131
|
+
if (isPromiseLike(vectors)) {
|
|
131
132
|
return vectors.then(function evaluateResolved(resolved) {
|
|
132
133
|
signal?.throwIfAborted()
|
|
133
134
|
return evaluateKernel(kernel, resolved, selection, signal, rowOffset, rowOrdinals)
|
|
@@ -260,6 +261,15 @@ function compileFunctionKernel(node, state) {
|
|
|
260
261
|
return evaluateJsonExtract({ funcName, node, args, rowIndex: streamRowIndex + 1 })
|
|
261
262
|
}
|
|
262
263
|
}
|
|
264
|
+
if (funcName === 'REGEXP_LIKE') {
|
|
265
|
+
const cache = node.args[1]?.type === 'literal' ? {} : undefined
|
|
266
|
+
return function regexpLikeValue(vectors, rowIndex, streamRowIndex) {
|
|
267
|
+
const args = arguments_.map(function argumentValue(argument) {
|
|
268
|
+
return argument(vectors, rowIndex, streamRowIndex)
|
|
269
|
+
})
|
|
270
|
+
return evaluateRegexpLike({ node, args, rowIndex: streamRowIndex + 1, cache })
|
|
271
|
+
}
|
|
272
|
+
}
|
|
263
273
|
if (!isStringFunc(funcName)) return undefined
|
|
264
274
|
return function stringFunctionValue(vectors, rowIndex, streamRowIndex) {
|
|
265
275
|
const args = arguments_.map(function argumentValue(argument) {
|
|
@@ -292,7 +302,9 @@ function compileFunctionEvaluator(node, columns) {
|
|
|
292
302
|
}
|
|
293
303
|
}
|
|
294
304
|
if (funcName !== 'NULLIF' && funcName !== 'JSON_VALUE' && funcName !== 'JSON_QUERY' &&
|
|
295
|
-
funcName !== 'JSON_EXTRACT' && funcName !== 'JSON_EXTRACT_STRING' &&
|
|
305
|
+
funcName !== 'JSON_EXTRACT' && funcName !== 'JSON_EXTRACT_STRING' && funcName !== 'REGEXP_LIKE' &&
|
|
306
|
+
!isStringFunc(funcName)) return undefined
|
|
307
|
+
const regexpCache = funcName === 'REGEXP_LIKE' && node.args[1]?.type === 'literal' ? {} : undefined
|
|
296
308
|
return {
|
|
297
309
|
async evaluate(context) {
|
|
298
310
|
const vectors = await Promise.all(arguments_.map(function evaluateArgument(argument) {
|
|
@@ -304,6 +316,9 @@ function compileFunctionEvaluator(node, columns) {
|
|
|
304
316
|
if (funcName === 'JSON_VALUE' || funcName === 'JSON_QUERY' || funcName === 'JSON_EXTRACT' || funcName === 'JSON_EXTRACT_STRING') {
|
|
305
317
|
return evaluateJsonExtract({ funcName, node, args, rowIndex: streamRowIndex + 1 })
|
|
306
318
|
}
|
|
319
|
+
if (funcName === 'REGEXP_LIKE') {
|
|
320
|
+
return evaluateRegexpLike({ node, args, rowIndex: streamRowIndex + 1, cache: regexpCache })
|
|
321
|
+
}
|
|
307
322
|
return evaluateStringFunc({ funcName, node, args, rowIndex: streamRowIndex + 1 })
|
|
308
323
|
})
|
|
309
324
|
},
|
|
@@ -640,20 +655,3 @@ function readsIdentifier(node) {
|
|
|
640
655
|
if (node.type === 'function') return node.args.some(readsIdentifier)
|
|
641
656
|
return false
|
|
642
657
|
}
|
|
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
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { stringify } from '../execute/utils.js'
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* @import { BinaryOp, SqlPrimitive } from '../types.js'
|
|
3
5
|
*/
|
|
@@ -68,7 +70,11 @@ export function applyBinaryOp(op, a, b) {
|
|
|
68
70
|
if (op === '>=') return a >= b
|
|
69
71
|
|
|
70
72
|
if (op === 'LIKE') {
|
|
71
|
-
|
|
73
|
+
// Objects, arrays, and Dates stringify as JSON, the same coercion as
|
|
74
|
+
// CAST(x AS VARCHAR), so `x LIKE p` and `CAST(x AS VARCHAR) LIKE p` agree.
|
|
75
|
+
// String() would collapse every object to '[object Object]', making LIKE
|
|
76
|
+
// a silent never-match on JSON-typed columns.
|
|
77
|
+
const str = typeof a === 'object' ? stringify(a) : String(a)
|
|
72
78
|
const pattern = String(b)
|
|
73
79
|
const regexPattern = pattern
|
|
74
80
|
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
package/src/expression/regexp.js
CHANGED
|
@@ -79,24 +79,7 @@ export function evaluateRegexpFunc({ funcName, node, args, rowIndex }) {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
if (funcName === 'REGEXP_MATCHES' || funcName === 'REGEXP_LIKE') {
|
|
82
|
-
|
|
83
|
-
const pattern = args[1]
|
|
84
|
-
if (str == null || pattern == null) return null
|
|
85
|
-
const strVal = String(str)
|
|
86
|
-
const patternStr = String(pattern)
|
|
87
|
-
|
|
88
|
-
let regex
|
|
89
|
-
try {
|
|
90
|
-
regex = new RegExp(patternStr)
|
|
91
|
-
} catch (/** @type {any} */ error) {
|
|
92
|
-
throw new ArgValueError({
|
|
93
|
-
...node,
|
|
94
|
-
message: `invalid regex pattern: ${error.message}`,
|
|
95
|
-
rowIndex,
|
|
96
|
-
})
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return regex.test(strVal)
|
|
82
|
+
return evaluateRegexpLike({ node, args, rowIndex })
|
|
100
83
|
}
|
|
101
84
|
|
|
102
85
|
if (funcName === 'REGEXP_REPLACE') {
|
|
@@ -168,3 +151,41 @@ export function evaluateRegexpFunc({ funcName, node, args, rowIndex }) {
|
|
|
168
151
|
|
|
169
152
|
throw new Error(`Unsupported regexp function: ${funcName}`)
|
|
170
153
|
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Evaluates REGEXP_LIKE/REGEXP_MATCHES with an optional single-pattern cache.
|
|
157
|
+
* Batch kernels use the cache for literal patterns so the RegExp is compiled
|
|
158
|
+
* once, while scalar and dynamic-pattern evaluation retain row-local behavior.
|
|
159
|
+
* Compilation stays lazy so a null string still returns null without validating
|
|
160
|
+
* an otherwise invalid pattern, matching the scalar evaluator.
|
|
161
|
+
*
|
|
162
|
+
* @param {Object} options
|
|
163
|
+
* @param {FunctionNode} options.node
|
|
164
|
+
* @param {SqlPrimitive[]} options.args
|
|
165
|
+
* @param {number} [options.rowIndex]
|
|
166
|
+
* @param {{ pattern?: string, regex?: RegExp }} [options.cache]
|
|
167
|
+
* @returns {SqlPrimitive}
|
|
168
|
+
*/
|
|
169
|
+
export function evaluateRegexpLike({ node, args, rowIndex, cache }) {
|
|
170
|
+
const string = args[0]
|
|
171
|
+
const pattern = args[1]
|
|
172
|
+
if (string == null || pattern == null) return null
|
|
173
|
+
const patternString = String(pattern)
|
|
174
|
+
let regex = cache?.pattern === patternString ? cache.regex : undefined
|
|
175
|
+
if (!regex) {
|
|
176
|
+
try {
|
|
177
|
+
regex = new RegExp(patternString)
|
|
178
|
+
} catch (/** @type {any} */ error) {
|
|
179
|
+
throw new ArgValueError({
|
|
180
|
+
...node,
|
|
181
|
+
message: `invalid regex pattern: ${error.message}`,
|
|
182
|
+
rowIndex,
|
|
183
|
+
})
|
|
184
|
+
}
|
|
185
|
+
if (cache) {
|
|
186
|
+
cache.pattern = patternString
|
|
187
|
+
cache.regex = regex
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return regex.test(String(string))
|
|
191
|
+
}
|