squirreling 0.16.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.16.1",
3
+ "version": "0.16.2",
4
4
  "description": "Squirreling Async SQL Engine",
5
5
  "author": "Hyperparam",
6
6
  "homepage": "https://hyperparam.app",
@@ -4,6 +4,7 @@ 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
 
@@ -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' && !isStringFunc(funcName)) return undefined
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
  },
@@ -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
- const str = String(a)
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, '\\$&')
@@ -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
- const str = args[0]
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
+ }