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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.16.0",
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.10",
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.10"
47
+ "vitest": "4.1.11"
48
48
  }
49
49
  }
@@ -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 instanceof Promise) {
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 instanceof Promise) {
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, ColumnResult, ColumnVector, RowsToBatchesOptions, SqlPrimitive } from '../types.js'
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
- let values = makeValueBuffers(columnNames.length)
25
- let rowCount = 0
24
+ /** @type {AsyncRow[]} */
25
+ let bufferedRows = []
26
26
 
27
27
  for await (const row of rows) {
28
28
  options?.signal?.throwIfAborted()
29
- for (let columnIndex = 0; columnIndex < columnNames.length; columnIndex++) {
30
- const name = columnNames[columnIndex]
31
- values[columnIndex].push(await readRowCell(row, name))
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 (rowCount > 0) yield loadedBatch(values, rowCount)
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 resolveColumns(results)
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
- }
@@ -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, ColumnResult, ColumnVector, ReadColumn, RowSelection, SqlPrimitive } from '../types.js'
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 instanceof Promise ? await result : 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 = resolveVectors(results)
142
- const vectors = resolved instanceof Promise ? await resolved : 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
- }
@@ -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
  *
@@ -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 = resolveVectors(results)
130
- if (vectors instanceof Promise) {
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' && !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
  },
@@ -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
- }
@@ -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
+ }