squirreling 0.12.1 → 0.12.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/README.md CHANGED
@@ -154,14 +154,14 @@ Squirreling mostly follows the SQL standard. The following features are supporte
154
154
 
155
155
  ### Functions
156
156
 
157
- - Aggregate: `COUNT`, `SUM`, `AVG`, `MIN`, `MAX`, `MEDIAN`, `PERCENTILE_CONT`, `APPROX_QUANTILE`, `STDDEV_POP`, `STDDEV_SAMP`, `JSON_ARRAYAGG`
157
+ - Aggregate: `COUNT`, `SUM`, `AVG`, `MIN`, `MAX`, `MEDIAN`, `PERCENTILE_CONT`, `APPROX_QUANTILE`, `STDDEV_POP`, `STDDEV_SAMP`, `JSON_ARRAYAGG`, `STRING_AGG`
158
158
  - String: `CONCAT`, `SUBSTRING`, `REPLACE`, `LENGTH`, `UPPER`, `LOWER`, `TRIM`, `LEFT`, `RIGHT`, `INSTR`, `POSITION`, `STRPOS`
159
159
  - Math: `ABS`, `SIGN`, `CEIL`, `FLOOR`, `ROUND`, `MOD`, `RAND`, `RANDOM`, `LN`, `LOG10`, `EXP`, `POWER`, `SQRT`
160
160
  - Trig: `SIN`, `COS`, `TAN`, `COT`, `ASIN`, `ACOS`, `ATAN`, `ATAN2`, `DEGREES`, `RADIANS`, `PI`
161
161
  - Date: `CURRENT_DATE`, `CURRENT_TIME`, `CURRENT_TIMESTAMP`, `DATE_PART`, `DATE_TRUNC`, `EXTRACT`, `INTERVAL`
162
- - Json: `JSON_VALUE`, `JSON_QUERY`, `JSON_EXTRACT`, `JSON_OBJECT`
162
+ - Json: `JSON_VALUE`, `JSON_QUERY`, `JSON_EXTRACT`, `JSON_OBJECT`, `JSON_ARRAY_LENGTH`
163
163
  - Array: `ARRAY_LENGTH`, `ARRAY_POSITION`, `ARRAY_SORT`, `CARDINALITY`
164
- - Regex: `REGEXP_SUBSTR`, `REGEXP_EXTRACT`, `REGEXP_REPLACE`
164
+ - Regex: `REGEXP_SUBSTR`, `REGEXP_EXTRACT`, `REGEXP_REPLACE`, `REGEXP_MATCHES`
165
165
  - Spatial: `ST_GeomFromText`, `ST_MakeEnvelope`, `ST_AsText`, `ST_Intersects`, `ST_Contains`, `ST_ContainsProperly`, `ST_Within`, `ST_Overlaps`, `ST_Touches`, `ST_Equals`, `ST_Crosses`, `ST_Covers`, `ST_CoveredBy`, `ST_DWithin`
166
- - Conditional: `COALESCE`, `NULLIF`
166
+ - Conditional: `COALESCE`, `NULLIF`, `GREATEST`, `LEAST`
167
167
  - User-defined functions (UDFs)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.12.1",
3
+ "version": "0.12.2",
4
4
  "description": "Squirreling Async SQL Engine",
5
5
  "author": "Hyperparam",
6
6
  "homepage": "https://hyperparam.app",
@@ -39,11 +39,11 @@
39
39
  "test": "vitest run"
40
40
  },
41
41
  "devDependencies": {
42
- "@types/node": "25.5.2",
43
- "@vitest/coverage-v8": "4.1.3",
42
+ "@types/node": "25.6.0",
43
+ "@vitest/coverage-v8": "4.1.4",
44
44
  "eslint": "9.39.2",
45
45
  "eslint-plugin-jsdoc": "62.9.0",
46
46
  "typescript": "6.0.2",
47
- "vitest": "4.1.3"
47
+ "vitest": "4.1.4"
48
48
  }
49
49
  }
@@ -15,7 +15,7 @@ export function asyncRow(obj, columns) {
15
15
  for (const key of columns) {
16
16
  cells[key] = () => Promise.resolve(obj[key])
17
17
  }
18
- return { columns, cells }
18
+ return { columns, cells, resolved: obj }
19
19
  }
20
20
 
21
21
  /**
@@ -34,13 +34,14 @@ export function memorySource({ data, columns }) {
34
34
  }
35
35
  const firstColumns = Object.keys(data[0])
36
36
  // Check first 1000 rows for consistent columns
37
+ const firstColSet = new Set(firstColumns)
37
38
  for (let i = 1; i < data.length && i < 1000; i++) {
38
39
  const rowColumns = Object.keys(data[i])
39
40
  const missing = firstColumns.find(col => !rowColumns.includes(col))
40
41
  if (missing) {
41
42
  throw new Error(`Inconsistent data, column "${missing}" not found in row ${i}`)
42
43
  }
43
- const extra = rowColumns.find(col => !firstColumns.includes(col))
44
+ const extra = rowColumns.find(col => !firstColSet.has(col))
44
45
  if (extra) {
45
46
  throw new Error(`Inconsistent data, unexpected column "${extra}" found in row ${i}`)
46
47
  }
@@ -54,11 +55,12 @@ export function memorySource({ data, columns }) {
54
55
  // Only apply offset and limit if no where clause
55
56
  const start = !where ? offset ?? 0 : 0
56
57
  const end = !where && limit !== undefined ? start + limit : data.length
58
+ const rowColumns = scanColumns ?? columns
57
59
  return {
58
60
  async *rows() {
59
61
  for (let i = start; i < end && i < data.length; i++) {
60
62
  if (signal?.aborted) break
61
- yield asyncRow(data[i], scanColumns ?? columns)
63
+ yield asyncRow(data[i], rowColumns)
62
64
  }
63
65
  },
64
66
  appliedWhere: false,
@@ -62,7 +62,7 @@ export function executeHashAggregate(plan, context) {
62
62
  return {
63
63
  columns: selectColumnNames(plan.columns, child.columns),
64
64
  maxRows: child.maxRows,
65
- async *rows () {
65
+ async *rows() {
66
66
  // Collect all rows
67
67
  /** @type {AsyncRow[]} */
68
68
  const allRows = []
@@ -136,7 +136,7 @@ export function executeScalarAggregate(plan, context) {
136
136
  columns: selectColumnNames(plan.columns, child.columns),
137
137
  numRows: plan.having ? undefined : 1,
138
138
  maxRows: 1,
139
- async *rows () {
139
+ async *rows() {
140
140
  // Collect all rows into single group
141
141
  /** @type {AsyncRow[]} */
142
142
  const group = []
@@ -10,7 +10,7 @@ import { executeSort } from './sort.js'
10
10
  import { addBounds, minBounds, stableRowKey } from './utils.js'
11
11
 
12
12
  /**
13
- * @import { AsyncCells, AsyncDataSource, AsyncRow, ExecuteContext, ExecuteSqlOptions, ExprNode, QueryResults, SelectColumn, Statement } from '../types.js'
13
+ * @import { AsyncCells, AsyncDataSource, AsyncRow, DerivedColumn, ExecuteContext, ExecuteSqlOptions, ExprNode, IdentifierNode, QueryResults, SelectColumn, SqlPrimitive, Statement } from '../types.js'
14
14
  * @import { CountNode, DistinctNode, FilterNode, LimitNode, ProjectNode, QueryPlan, ScanNode, SetOperationNode } from '../plan/types.js'
15
15
  */
16
16
 
@@ -24,14 +24,27 @@ export function executeSql({ tables, query, functions, signal }) {
24
24
  const parsed = typeof query === 'string' ? parseSql({ query, functions }) : query
25
25
 
26
26
  // Normalize tables: convert arrays to AsyncDataSource
27
+ // Fast path: skip normalization when no arrays are present
28
+ let needsNormalization = false
29
+ const tableKeys = Object.keys(tables)
30
+ for (let i = 0; i < tableKeys.length; i++) {
31
+ if (Array.isArray(tables[tableKeys[i]])) {
32
+ needsNormalization = true
33
+ break
34
+ }
35
+ }
36
+
27
37
  /** @type {Record<string, AsyncDataSource>} */
28
- const normalizedTables = {}
29
- for (const [name, data] of Object.entries(tables)) {
30
- if (Array.isArray(data)) {
31
- normalizedTables[name] = memorySource({ data })
32
- } else {
33
- normalizedTables[name] = data
38
+ let normalizedTables
39
+ if (needsNormalization) {
40
+ normalizedTables = {}
41
+ for (let i = 0; i < tableKeys.length; i++) {
42
+ const name = tableKeys[i]
43
+ const data = tables[name]
44
+ normalizedTables[name] = Array.isArray(data) ? memorySource({ data }) : data
34
45
  }
46
+ } else {
47
+ normalizedTables = /** @type {Record<string, AsyncDataSource>} */ (tables)
35
48
  }
36
49
 
37
50
  const context = { tables: normalizedTables, functions, signal }
@@ -88,7 +101,7 @@ export function executePlan({ plan, context }) {
88
101
  } else if (plan.type === 'SetOperation') {
89
102
  return executeSetOperation(plan, context)
90
103
  }
91
- return { columns: [], async *rows () {} }
104
+ return { columns: [], async *rows() {} }
92
105
  }
93
106
 
94
107
  /**
@@ -142,7 +155,7 @@ function executeScan(plan, context) {
142
155
  columns: [column],
143
156
  numRows: scanRows,
144
157
  maxRows: scanRows,
145
- async *rows () {
158
+ async *rows() {
146
159
  const columns = [column]
147
160
  for await (const chunk of chunks) {
148
161
  if (signal?.aborted) return
@@ -172,7 +185,7 @@ function executeScan(plan, context) {
172
185
  columns: plan.hints.columns ?? table.columns,
173
186
  numRows: !plan.hints.where ? scanRows : undefined,
174
187
  maxRows: scanRows,
175
- async *rows () {
188
+ async *rows() {
176
189
  let result = scanResult.rows()
177
190
 
178
191
  // Apply WHERE if data source did not
@@ -205,7 +218,7 @@ function executeCount(plan, context) {
205
218
  columns: plan.columns.map(col => col.alias ?? derivedAlias(col.expr)),
206
219
  numRows: 1,
207
220
  maxRows: 1,
208
- async *rows () {
221
+ async *rows() {
209
222
  // Use source numRows if available
210
223
  let count = table.numRows
211
224
  if (count === undefined) {
@@ -344,37 +357,86 @@ function executeFilter(plan, context) {
344
357
  */
345
358
  function executeProject(plan, context) {
346
359
  const child = executePlan({ plan: plan.child, context })
360
+
361
+ // Pre-compute column names for derived columns (avoids per-row derivedAlias calls)
362
+ const hasStar = plan.columns.some(col => col.type === 'star')
363
+
364
+ /** @type {string[] | undefined} */
365
+ let staticColumns
366
+ /** @type {{ alias: string, sourceName: string }[] | undefined} */
367
+ let identifierMap
368
+ if (!hasStar) {
369
+ const derived = /** @type {DerivedColumn[]} */ (plan.columns)
370
+ staticColumns = derived.map(col => col.alias ?? derivedAlias(col.expr))
371
+ const allIdentifiers = derived.every(col =>
372
+ col.expr.type === 'identifier' && !col.expr.prefix
373
+ )
374
+ if (allIdentifiers) {
375
+ identifierMap = derived.map((col, i) => ({
376
+ alias: staticColumns[i],
377
+ sourceName: /** @type {IdentifierNode} */ (col.expr).name,
378
+ }))
379
+ }
380
+ }
381
+
347
382
  return {
348
383
  columns: selectColumnNames(plan.columns, child.columns),
349
384
  numRows: child.numRows,
350
385
  maxRows: child.maxRows,
351
- async *rows () {
386
+ async *rows() {
352
387
  let rowIndex = 0
388
+ let identifierMapValidated = false
353
389
 
354
390
  for await (const row of child.rows()) {
355
391
  if (context.signal?.aborted) return
356
392
  rowIndex++
393
+
394
+ // Validate identifier fast path on first row (may fail for JOINs with prefixed columns)
395
+ if (identifierMap && !identifierMapValidated) {
396
+ identifierMapValidated = true
397
+ if (!identifierMap.every(m => m.sourceName in row.cells)) {
398
+ identifierMap = undefined
399
+ }
400
+ }
401
+
402
+ // Fast path: all columns are simple identifier references
403
+ if (identifierMap) {
404
+ /** @type {AsyncCells} */
405
+ const cells = {}
406
+ const source = row.resolved
407
+ /** @type {Record<string, SqlPrimitive> | undefined} */
408
+ const resolved = source ? {} : undefined
409
+ for (const { alias, sourceName } of identifierMap) {
410
+ cells[alias] = row.cells[sourceName]
411
+ if (resolved && source) resolved[alias] = source[sourceName]
412
+ }
413
+ yield resolved
414
+ ? { columns: staticColumns, cells, resolved }
415
+ : { columns: staticColumns, cells }
416
+ continue
417
+ }
418
+
357
419
  const currentRowIndex = rowIndex
358
420
 
359
421
  /** @type {string[]} */
360
- const columns = []
422
+ const columns = staticColumns ?? []
361
423
  /** @type {AsyncCells} */
362
424
  const cells = {}
363
425
 
364
- for (const col of plan.columns) {
426
+ for (let i = 0; i < plan.columns.length; i++) {
427
+ const col = plan.columns[i]
365
428
  if (col.type === 'star') {
366
429
  const prefix = col.table ? `${col.table}.` : undefined
367
430
  for (const key of row.columns) {
368
431
  if (prefix && !key.startsWith(prefix)) continue
369
- // Strip table prefix for output column names
370
432
  const dotIndex = key.indexOf('.')
371
433
  const outputKey = prefix ? key.substring(prefix.length) : dotIndex >= 0 ? key.substring(dotIndex + 1) : key
372
434
  columns.push(outputKey)
373
435
  cells[outputKey] = row.cells[key]
374
436
  }
375
437
  } else {
376
- const alias = col.alias ?? derivedAlias(col.expr)
377
- columns.push(alias)
438
+ const alias = staticColumns ? staticColumns[i] : (col.alias ?? derivedAlias(col.expr))
439
+ if (!staticColumns) columns.push(alias)
378
440
  cells[alias] = () => evaluateExpr({
379
441
  node: col.expr,
380
442
  row,
@@ -402,7 +464,7 @@ function executeDistinct(plan, context) {
402
464
  return {
403
465
  columns: child.columns,
404
466
  maxRows: child.maxRows,
405
- async *rows () {
467
+ async *rows() {
406
468
  const { signal } = context
407
469
  const MAX_CHUNK = 256
408
470
 
@@ -478,7 +540,7 @@ function executeSetOperation(plan, context) {
478
540
  columns: left.columns,
479
541
  numRows: addBounds(left.numRows, right.numRows),
480
542
  maxRows: addBounds(left.maxRows, right.maxRows),
481
- async *rows () {
543
+ async *rows() {
482
544
  // UNION ALL: yield all rows from both sides
483
545
  yield* left.rows()
484
546
  yield* right.rows()
@@ -490,7 +552,7 @@ function executeSetOperation(plan, context) {
490
552
  return {
491
553
  columns: left.columns,
492
554
  maxRows: addBounds(left.maxRows, right.maxRows),
493
- async *rows () {
555
+ async *rows() {
494
556
  // UNION: yield deduplicated rows from both sides
495
557
  const seen = new Set()
496
558
  for await (const row of left.rows()) {
@@ -518,7 +580,7 @@ function executeSetOperation(plan, context) {
518
580
  return {
519
581
  columns: left.columns,
520
582
  maxRows: minBounds(left.maxRows, right.maxRows),
521
- async *rows () {
583
+ async *rows() {
522
584
  // Materialize right side keys
523
585
  /** @type {Map<any, number>} */
524
586
  const rightKeys = new Map()
@@ -560,7 +622,7 @@ function executeSetOperation(plan, context) {
560
622
  return {
561
623
  columns: left.columns,
562
624
  maxRows: left.maxRows,
563
- async *rows () {
625
+ async *rows() {
564
626
  // Materialize right side keys
565
627
  /** @type {Map<any, number>} */
566
628
  const rightKeys = new Map()
@@ -19,7 +19,7 @@ export function executeNestedLoopJoin(plan, context) {
19
19
  const right = executePlan({ plan: plan.right, context })
20
20
  return {
21
21
  columns: mergeColumnNames(left.columns, right.columns, plan.leftAlias, plan.rightAlias),
22
- async *rows () {
22
+ async *rows() {
23
23
  const leftTable = plan.leftAlias
24
24
  const rightTable = plan.rightAlias
25
25
 
@@ -97,7 +97,7 @@ export function executePositionalJoin(plan, context) {
97
97
  columns: mergeColumnNames(left.columns, right.columns, plan.leftAlias, plan.rightAlias),
98
98
  numRows,
99
99
  maxRows: maxBounds(left.maxRows, right.maxRows),
100
- async *rows () {
100
+ async *rows() {
101
101
  const { signal } = context
102
102
  const leftTable = plan.leftAlias
103
103
  const rightTable = plan.rightAlias
@@ -143,7 +143,7 @@ export function executeHashJoin(plan, context) {
143
143
  const right = executePlan({ plan: plan.right, context })
144
144
  return {
145
145
  columns: mergeColumnNames(left.columns, right.columns, plan.leftAlias, plan.rightAlias),
146
- async *rows () {
146
+ async *rows() {
147
147
  const leftTable = plan.leftAlias
148
148
  const rightTable = plan.rightAlias
149
149
 
@@ -20,7 +20,7 @@ export function executeSort(plan, context) {
20
20
  columns: child.columns,
21
21
  numRows: child.numRows,
22
22
  maxRows: child.maxRows,
23
- async *rows () {
23
+ async *rows() {
24
24
  // Buffer all rows
25
25
  /** @type {AsyncRow[]} */
26
26
  const rows = []
@@ -2,6 +2,8 @@
2
2
  * @import { AsyncRow, OrderByItem, QueryResults, SqlPrimitive } from '../types.js'
3
3
  */
4
4
 
5
+ const primitiveTypes = new Set(['number', 'bigint', 'boolean', 'string'])
6
+
5
7
  /**
6
8
  * Compares two values for a single ORDER BY term, handling nulls and direction
7
9
  *
@@ -24,10 +26,9 @@ export function compareForTerm(a, b, term) {
24
26
  // Compare non-null values
25
27
  if (a == b) return 0
26
28
 
27
- const primitives = ['number', 'bigint', 'boolean', 'string']
28
29
  let cmp
29
- if (primitives.includes(typeof a) && primitives.includes(typeof b)) {
30
- cmp = a < b ? -1 : a > b ? 1 : 0
30
+ if (primitiveTypes.has(typeof a) && primitiveTypes.has(typeof b)) {
31
+ cmp = a < b ? -1 : 1
31
32
  } else {
32
33
  const aa = String(a)
33
34
  const bb = String(b)
@@ -51,6 +52,29 @@ export async function collect(results) {
51
52
  for await (const asyncRow of results.rows()) {
52
53
  rows.push(asyncRow)
53
54
  }
55
+
56
+ // Fast path: if all rows have pre-materialized data, skip Promise overhead
57
+ let allMaterialized = rows.length > 0
58
+ for (let i = 0; i < rows.length; i++) {
59
+ if (!rows[i].resolved) {
60
+ allMaterialized = false
61
+ break
62
+ }
63
+ }
64
+ if (allMaterialized) {
65
+ const result = new Array(rows.length)
66
+ for (let i = 0; i < rows.length; i++) {
67
+ const row = rows[i]
68
+ /** @type {Record<string, SqlPrimitive>} */
69
+ const item = {}
70
+ for (const col of row.columns) {
71
+ item[col] = row.resolved[col]
72
+ }
73
+ result[i] = item
74
+ }
75
+ return result
76
+ }
77
+
54
78
  return Promise.all(rows.map(async asyncRow => {
55
79
  const values = await Promise.all(asyncRow.columns.map(k => asyncRow.cells[k]()))
56
80
  /** @type {Record<string, SqlPrimitive>} */
@@ -272,6 +272,32 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
272
272
  ))
273
273
  }
274
274
  }
275
+
276
+ if (funcName === 'STRING_AGG') {
277
+ const separatorNode = node.args[1]
278
+ const separator = String(await evaluateExpr({ node: separatorNode, row: filteredRows[0] ?? { columns: [], cells: {} }, context }))
279
+ /** @type {string[]} */
280
+ const values = []
281
+ if (node.distinct) {
282
+ const seen = new Set()
283
+ for (const row of filteredRows) {
284
+ const v = await evaluateExpr({ node: argNode, row, context })
285
+ if (v == null) continue
286
+ const str = String(v)
287
+ const key = keyify(str)
288
+ if (!seen.has(key)) {
289
+ seen.add(key)
290
+ values.push(str)
291
+ }
292
+ }
293
+ } else {
294
+ for (const row of filteredRows) {
295
+ const v = await evaluateExpr({ node: argNode, row, context })
296
+ if (v != null) values.push(String(v))
297
+ }
298
+ }
299
+ return values.length === 0 ? null : values.join(separator)
300
+ }
275
301
  }
276
302
 
277
303
  /** @type {SqlPrimitive[]} */
@@ -311,6 +337,20 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
311
337
  return val1 == await val2 ? null : val1
312
338
  }
313
339
 
340
+ if (funcName === 'GREATEST' || funcName === 'LEAST') {
341
+ // Skip nulls; return null if all inputs are null
342
+ const isGreatest = funcName === 'GREATEST'
343
+ /** @type {SqlPrimitive} */
344
+ let best = null
345
+ for (const arg of args) {
346
+ if (arg == null) continue
347
+ if (best == null || (isGreatest ? arg > best : arg < best)) {
348
+ best = arg
349
+ }
350
+ }
351
+ return best
352
+ }
353
+
314
354
  if (funcName === 'DATE_TRUNC') {
315
355
  return dateTrunc(args[0], args[1])
316
356
  }
@@ -357,6 +397,25 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
357
397
  return result
358
398
  }
359
399
 
400
+ if (funcName === 'JSON_ARRAY_LENGTH') {
401
+ let arr = args[0]
402
+ if (arr == null) return null
403
+ if (typeof arr === 'string') {
404
+ try {
405
+ arr = JSON.parse(arr)
406
+ } catch {
407
+ throw new ArgValueError({
408
+ ...node,
409
+ message: 'invalid JSON string',
410
+ hint: 'Argument must be valid JSON.',
411
+ rowIndex,
412
+ })
413
+ }
414
+ }
415
+ if (!Array.isArray(arr)) return null
416
+ return arr.length
417
+ }
418
+
360
419
  if (funcName === 'ARRAY_LENGTH' || funcName === 'CARDINALITY') {
361
420
  const arr = args[0]
362
421
  if (!Array.isArray(arr)) return null
@@ -78,6 +78,27 @@ export function evaluateRegexpFunc({ funcName, node, args, rowIndex }) {
78
78
  return null
79
79
  }
80
80
 
81
+ if (funcName === 'REGEXP_MATCHES') {
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)
100
+ }
101
+
81
102
  if (funcName === 'REGEXP_REPLACE') {
82
103
  const str = args[0]
83
104
  const pattern = args[1]
package/src/types.d.ts CHANGED
@@ -46,6 +46,9 @@ export interface ExecuteContext {
46
46
  export interface AsyncRow {
47
47
  columns: string[]
48
48
  cells: AsyncCells
49
+ // Optional pre-materialized row values keyed by output column name.
50
+ // When present, consumers can skip the AsyncCell Promise roundtrip.
51
+ resolved?: Record<string, SqlPrimitive>
49
52
  }
50
53
  export type AsyncCells = Record<string, AsyncCell>
51
54
  export type AsyncCell = () => Promise<SqlPrimitive>
@@ -110,9 +113,9 @@ export interface UserDefinedFunction {
110
113
  arguments: FunctionSignature
111
114
  }
112
115
 
113
- export type AggregateFunc = 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX' | 'JSON_ARRAYAGG' | 'STDDEV_SAMP' | 'STDDEV_POP' | 'MEDIAN' | 'PERCENTILE_CONT' | 'APPROX_QUANTILE'
116
+ export type AggregateFunc = 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX' | 'JSON_ARRAYAGG' | 'STDDEV_SAMP' | 'STDDEV_POP' | 'MEDIAN' | 'PERCENTILE_CONT' | 'APPROX_QUANTILE' | 'STRING_AGG'
114
117
 
115
- export type RegExpFunction = 'REGEXP_SUBSTR' | 'REGEXP_EXTRACT' | 'REGEXP_REPLACE'
118
+ export type RegExpFunction = 'REGEXP_SUBSTR' | 'REGEXP_EXTRACT' | 'REGEXP_REPLACE' | 'REGEXP_MATCHES'
116
119
 
117
120
  export type MathFunc =
118
121
  | 'FLOOR'
@@ -11,7 +11,7 @@ export const niladicFuncs = ['CURRENT_DATE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP'
11
11
  * @returns {name is AggregateFunc}
12
12
  */
13
13
  export function isAggregateFunc(name) {
14
- return ['COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'JSON_ARRAYAGG', 'STDDEV_SAMP', 'STDDEV_POP', 'MEDIAN', 'PERCENTILE_CONT', 'APPROX_QUANTILE'].includes(name)
14
+ return ['COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'JSON_ARRAYAGG', 'STDDEV_SAMP', 'STDDEV_POP', 'MEDIAN', 'PERCENTILE_CONT', 'APPROX_QUANTILE', 'STRING_AGG'].includes(name)
15
15
  }
16
16
 
17
17
  /**
@@ -31,7 +31,7 @@ export function isMathFunc(name) {
31
31
  * @returns {name is RegExpFunction}
32
32
  */
33
33
  export function isRegexpFunc(name) {
34
- return ['REGEXP_SUBSTR', 'REGEXP_EXTRACT', 'REGEXP_REPLACE'].includes(name)
34
+ return ['REGEXP_SUBSTR', 'REGEXP_EXTRACT', 'REGEXP_REPLACE', 'REGEXP_MATCHES'].includes(name)
35
35
  }
36
36
 
37
37
  /**
@@ -112,6 +112,7 @@ export const FUNCTION_SIGNATURES = {
112
112
  REGEXP_SUBSTR: { min: 2, max: 4, signature: 'string, pattern[, position[, occurrence]]' },
113
113
  REGEXP_EXTRACT: { min: 2, max: 4, signature: 'string, pattern[, position[, occurrence]]' },
114
114
  REGEXP_REPLACE: { min: 3, max: 5, signature: 'string, pattern, replacement[, position[, occurrence]]' },
115
+ REGEXP_MATCHES: { min: 2, max: 2, signature: 'string, pattern' },
115
116
 
116
117
  // Date/time functions
117
118
  RANDOM: { min: 0, max: 0, signature: '' },
@@ -154,6 +155,7 @@ export const FUNCTION_SIGNATURES = {
154
155
  JSON_QUERY: { min: 2, max: 2, signature: 'expression, path' },
155
156
  JSON_EXTRACT: { min: 2, max: 2, signature: 'expression, path' },
156
157
  JSON_OBJECT: { min: 0, signature: 'key1, value1[, ...]' },
158
+ JSON_ARRAY_LENGTH: { min: 1, max: 1, signature: 'array' },
157
159
  JSON_ARRAYAGG: { min: 1, max: 1, signature: 'expression' },
158
160
 
159
161
  // Array functions
@@ -165,6 +167,8 @@ export const FUNCTION_SIGNATURES = {
165
167
  // Conditional functions
166
168
  COALESCE: { min: 1, signature: 'value1, value2[, ...]' },
167
169
  NULLIF: { min: 2, max: 2, signature: 'value1, value2' },
170
+ GREATEST: { min: 1, signature: 'value1[, value2, ...]' },
171
+ LEAST: { min: 1, signature: 'value1[, value2, ...]' },
168
172
 
169
173
  // Aggregate functions
170
174
  COUNT: { min: 1, max: 1, signature: 'expression' },
@@ -177,6 +181,7 @@ export const FUNCTION_SIGNATURES = {
177
181
  MEDIAN: { min: 1, max: 1, signature: 'expression' },
178
182
  PERCENTILE_CONT: { min: 2, max: 2, signature: 'fraction, expression' },
179
183
  APPROX_QUANTILE: { min: 2, max: 2, signature: 'expression, fraction' },
184
+ STRING_AGG: { min: 2, max: 2, signature: 'expression, separator' },
180
185
 
181
186
  // Spatial functions
182
187
  ST_INTERSECTS: { min: 2, max: 2, signature: 'geometry, geometry' },