squirreling 0.12.22 → 0.12.24
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 +2 -2
- package/package.json +5 -5
- package/src/ast.d.ts +2 -0
- package/src/expression/binary.js +1 -1
- package/src/expression/evaluate.js +20 -1
- package/src/parse/extractTables.js +2 -0
- package/src/parse/joins.js +59 -11
- package/src/plan/columns.js +15 -0
- package/src/plan/plan.js +75 -12
- package/src/types.d.ts +1 -1
- package/src/validation/functions.js +3 -1
- package/src/validation/keywords.js +3 -3
package/README.md
CHANGED
|
@@ -141,7 +141,7 @@ Squirreling mostly follows the SQL standard. The following features are supporte
|
|
|
141
141
|
- `SELECT` statements with `DISTINCT`, `WHERE`, `ORDER BY`, `LIMIT`, `OFFSET`
|
|
142
142
|
- `WITH` clause for Common Table Expressions (CTEs)
|
|
143
143
|
- Subqueries in `SELECT`, `FROM`, `WHERE`, and correlated subqueries
|
|
144
|
-
- `JOIN` operations: `INNER JOIN`, `LEFT JOIN`, `RIGHT JOIN`, `FULL JOIN`, `CROSS JOIN`, `POSITIONAL JOIN`, `LATERAL VIEW [OUTER] EXPLODE(...)`
|
|
144
|
+
- `JOIN` operations: `INNER JOIN`, `LEFT JOIN`, `RIGHT JOIN`, `FULL JOIN`, `CROSS JOIN`, `POSITIONAL JOIN`, `LATERAL VIEW [OUTER] EXPLODE(...)`, with `ON` or `USING (col, ...)` conditions
|
|
145
145
|
- `GROUP BY` and `HAVING` clauses
|
|
146
146
|
- Set operations: `UNION`, `UNION ALL`, `INTERSECT`, `INTERSECT ALL`, `EXCEPT`, `EXCEPT ALL`
|
|
147
147
|
- Expressions: `CASE`, `CAST`, `BETWEEN`, `IN`, `LIKE`, `IS NULL`, `IS NOT NULL`
|
|
@@ -160,7 +160,7 @@ Squirreling mostly follows the SQL standard. The following features are supporte
|
|
|
160
160
|
- Math: `ABS`, `SIGN`, `CEIL`, `FLOOR`, `ROUND`, `MOD`, `RAND`, `RANDOM`, `LN`, `LOG10`, `EXP`, `POWER`, `SQRT`
|
|
161
161
|
- Trig: `SIN`, `COS`, `TAN`, `COT`, `ASIN`, `ACOS`, `ATAN`, `ATAN2`, `DEGREES`, `RADIANS`, `PI`
|
|
162
162
|
- Date: `CURRENT_DATE`, `CURRENT_TIME`, `CURRENT_TIMESTAMP`, `DATE_DIFF`, `DATEDIFF`, `DATE_PART`, `DATE_TRUNC`, `EPOCH`, `EXTRACT`, `INTERVAL`
|
|
163
|
-
- Json: `JSON_VALUE`, `JSON_QUERY`, `JSON_EXTRACT`, `JSON_OBJECT`, `JSON_ARRAY_LENGTH`, `JSON_VALID`, `JSON_TYPE`
|
|
163
|
+
- Json: `JSON_VALUE`, `JSON_QUERY`, `JSON_EXTRACT`, `JSON_OBJECT`, `JSON_ARRAY_LENGTH`, `JSON_VALID`, `JSON_TYPE`, `JSON_KEYS`
|
|
164
164
|
- Array: `ARRAY_LENGTH`, `ARRAY_POSITION`, `ARRAY_CONTAINS`, `ARRAY_SORT`, `ARRAY_APPEND`, `ARRAY_CONCAT`, `LEN`, `CARDINALITY`, `SIZE`
|
|
165
165
|
- Table functions: `UNNEST`, `EXPLODE`, `JSON_EACH`
|
|
166
166
|
- Regex: `REGEXP_SUBSTR`, `REGEXP_EXTRACT`, `REGEXP_REPLACE`, `REGEXP_MATCHES`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.24",
|
|
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.9.
|
|
43
|
-
"@vitest/coverage-v8": "4.1.
|
|
42
|
+
"@types/node": "25.9.3",
|
|
43
|
+
"@vitest/coverage-v8": "4.1.8",
|
|
44
44
|
"eslint": "9.39.4",
|
|
45
|
-
"eslint-plugin-jsdoc": "63.0.
|
|
45
|
+
"eslint-plugin-jsdoc": "63.0.2",
|
|
46
46
|
"typescript": "6.0.3",
|
|
47
|
-
"vitest": "4.1.
|
|
47
|
+
"vitest": "4.1.8"
|
|
48
48
|
}
|
|
49
49
|
}
|
package/src/ast.d.ts
CHANGED
|
@@ -210,7 +210,9 @@ export interface JoinClause extends AstBase {
|
|
|
210
210
|
table: string
|
|
211
211
|
alias?: string
|
|
212
212
|
on?: ExprNode
|
|
213
|
+
using?: string[]
|
|
213
214
|
fromFunction?: FromFunction
|
|
215
|
+
subquery?: FromSubquery
|
|
214
216
|
}
|
|
215
217
|
|
|
216
218
|
// All AST node derive from this base, which includes position info for error reporting and other purposes
|
package/src/expression/binary.js
CHANGED
|
@@ -55,7 +55,7 @@ export function applyBinaryOp(op, a, b) {
|
|
|
55
55
|
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
56
56
|
.replace(/%/g, '.*')
|
|
57
57
|
.replace(/_/g, '.')
|
|
58
|
-
const regex = new RegExp(`^${regexPattern}$`, '
|
|
58
|
+
const regex = new RegExp(`^${regexPattern}$`, 'is')
|
|
59
59
|
return regex.test(str)
|
|
60
60
|
}
|
|
61
61
|
|
|
@@ -332,7 +332,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
332
332
|
return values[lower] + (values[upper] - values[lower]) * (pos - lower)
|
|
333
333
|
}
|
|
334
334
|
|
|
335
|
-
if (funcName === 'JSON_ARRAYAGG' || funcName === 'ARRAY_AGG') {
|
|
335
|
+
if (funcName === 'JSON_ARRAYAGG' || funcName === 'ARRAY_AGG' || funcName === 'LIST') {
|
|
336
336
|
const allValues = await evaluateAll(argNode, filteredRows, context)
|
|
337
337
|
if (node.distinct) {
|
|
338
338
|
/** @type {SqlPrimitive[]} */
|
|
@@ -516,6 +516,25 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
516
516
|
return typeof value
|
|
517
517
|
}
|
|
518
518
|
|
|
519
|
+
if (funcName === 'JSON_KEYS') {
|
|
520
|
+
let value = args[0]
|
|
521
|
+
if (value == null) return null
|
|
522
|
+
if (typeof value === 'string') {
|
|
523
|
+
try {
|
|
524
|
+
value = JSON.parse(value)
|
|
525
|
+
} catch {
|
|
526
|
+
throw new ArgValueError({
|
|
527
|
+
...node,
|
|
528
|
+
message: 'invalid JSON string',
|
|
529
|
+
hint: 'Argument must be valid JSON.',
|
|
530
|
+
rowIndex,
|
|
531
|
+
})
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value) || value instanceof Date) return null
|
|
535
|
+
return Object.keys(value)
|
|
536
|
+
}
|
|
537
|
+
|
|
519
538
|
if (funcName === 'JSON_ARRAY_LENGTH') {
|
|
520
539
|
let arr = args[0]
|
|
521
540
|
if (arr == null) return null
|
|
@@ -59,6 +59,8 @@ function walkStatement(stmt, cteScope, refs) {
|
|
|
59
59
|
for (const j of stmt.joins) {
|
|
60
60
|
if (j.fromFunction) {
|
|
61
61
|
for (const a of j.fromFunction.args) walkExpr(a, cteScope, refs)
|
|
62
|
+
} else if (j.subquery) {
|
|
63
|
+
walkStatement(j.subquery.query, cteScope, refs)
|
|
62
64
|
} else if (!cteScope.has(j.table.toLowerCase())) {
|
|
63
65
|
refs.add(j.table)
|
|
64
66
|
}
|
package/src/parse/joins.js
CHANGED
|
@@ -2,11 +2,11 @@ import { expectNoAggregate } from '../validation/aggregates.js'
|
|
|
2
2
|
import { isTableFunction, validateFunctionArgs } from '../validation/functions.js'
|
|
3
3
|
import { ParseError } from '../validation/parseErrors.js'
|
|
4
4
|
import { parseExpression } from './expression.js'
|
|
5
|
-
import { isTableFunctionStart, parseFromFunction, parseTableAlias, tableFunctionColumnCount, tableFunctionDefaultColumns } from './parse.js'
|
|
5
|
+
import { isTableFunctionStart, parseFromFunction, parseStatement, parseTableAlias, tableFunctionColumnCount, tableFunctionDefaultColumns } from './parse.js'
|
|
6
6
|
import { consume, current, expect, match } from './state.js'
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
* @import { ExprNode, FromFunction, JoinClause, JoinType, ParserState } from '../types.js'
|
|
9
|
+
* @import { ExprNode, FromFunction, FromSubquery, JoinClause, JoinType, ParserState } from '../types.js'
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -218,26 +218,74 @@ export function parseJoins(state) {
|
|
|
218
218
|
})
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
-
//
|
|
222
|
-
const
|
|
223
|
-
|
|
221
|
+
// Subquery on the right side: JOIN (SELECT ...) AS alias ON ...
|
|
222
|
+
const rightTok = current(state)
|
|
223
|
+
/** @type {FromSubquery | undefined} */
|
|
224
|
+
let subquery
|
|
225
|
+
let tableName
|
|
226
|
+
/** @type {string | undefined} */
|
|
227
|
+
let tableAlias
|
|
228
|
+
let endPos
|
|
229
|
+
if (rightTok.type === 'paren' && rightTok.value === '(') {
|
|
230
|
+
consume(state)
|
|
231
|
+
const query = parseStatement(state)
|
|
232
|
+
expect(state, 'paren', ')')
|
|
233
|
+
tableAlias = parseTableAlias(state)
|
|
234
|
+
if (!tableAlias) {
|
|
235
|
+
throw new ParseError({
|
|
236
|
+
message: 'Subquery in JOIN must have an alias',
|
|
237
|
+
positionStart: rightTok.positionStart,
|
|
238
|
+
positionEnd: state.lastPos,
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
endPos = state.lastPos
|
|
242
|
+
subquery = {
|
|
243
|
+
type: 'subquery',
|
|
244
|
+
query,
|
|
245
|
+
alias: tableAlias,
|
|
246
|
+
positionStart: rightTok.positionStart,
|
|
247
|
+
positionEnd: endPos,
|
|
248
|
+
}
|
|
249
|
+
tableName = tableAlias
|
|
250
|
+
} else {
|
|
251
|
+
// Parse table name and optional alias
|
|
252
|
+
const tableTok = expect(state, 'identifier')
|
|
253
|
+
tableName = tableTok.value
|
|
254
|
+
tableAlias = parseTableAlias(state)
|
|
255
|
+
endPos = tableTok.positionEnd
|
|
256
|
+
}
|
|
224
257
|
|
|
225
|
-
// Parse ON condition (not for POSITIONAL joins)
|
|
258
|
+
// Parse ON condition or USING column list (not for POSITIONAL joins)
|
|
226
259
|
/** @type {ExprNode | undefined} */
|
|
227
260
|
let condition
|
|
261
|
+
/** @type {string[] | undefined} */
|
|
262
|
+
let using
|
|
228
263
|
if (joinType !== 'POSITIONAL') {
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
264
|
+
if (match(state, 'keyword', 'USING')) {
|
|
265
|
+
expect(state, 'paren', '(')
|
|
266
|
+
using = []
|
|
267
|
+
while (true) {
|
|
268
|
+
const colTok = expect(state, 'identifier')
|
|
269
|
+
using.push(colTok.value)
|
|
270
|
+
if (!match(state, 'comma')) break
|
|
271
|
+
}
|
|
272
|
+
expect(state, 'paren', ')')
|
|
273
|
+
} else {
|
|
274
|
+
expect(state, 'keyword', 'ON')
|
|
275
|
+
condition = parseExpression(state)
|
|
276
|
+
expectNoAggregate(condition, 'JOIN ON')
|
|
277
|
+
}
|
|
232
278
|
}
|
|
233
279
|
|
|
234
280
|
joins.push({
|
|
235
281
|
joinType,
|
|
236
|
-
table:
|
|
282
|
+
table: tableName,
|
|
237
283
|
alias: tableAlias,
|
|
238
284
|
on: condition,
|
|
285
|
+
using,
|
|
286
|
+
subquery,
|
|
239
287
|
positionStart: tok.positionStart,
|
|
240
|
-
positionEnd:
|
|
288
|
+
positionEnd: endPos,
|
|
241
289
|
})
|
|
242
290
|
}
|
|
243
291
|
|
package/src/plan/columns.js
CHANGED
|
@@ -153,6 +153,13 @@ export function extractColumns({ select, parentColumns }) {
|
|
|
153
153
|
if (sourceAlias !== undefined) visibleLateralAliases.push(sourceAlias)
|
|
154
154
|
for (const join of select.joins) {
|
|
155
155
|
collectColumnsFromExpr(join.on, identifiers)
|
|
156
|
+
// USING columns are equi-join keys on both sides; keep them in every
|
|
157
|
+
// table's needed set so projection pushdown can't prune the join key.
|
|
158
|
+
if (join.using) {
|
|
159
|
+
for (const col of join.using) {
|
|
160
|
+
for (const [, set] of perTable) set?.add(col)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
156
163
|
const joinAlias = join.alias ?? join.table
|
|
157
164
|
if (join.fromFunction) {
|
|
158
165
|
/** @type {IdentifierNode[]} */
|
|
@@ -413,6 +420,10 @@ export function inferSelectSourceColumns({ select, cteColumns, tables }) {
|
|
|
413
420
|
for (const col of tableFunctionColumnNames(join.fromFunction)) {
|
|
414
421
|
result.push(`${joinAlias}.${col}`)
|
|
415
422
|
}
|
|
423
|
+
} else if (join.subquery) {
|
|
424
|
+
for (const col of inferStatementColumns({ stmt: join.subquery.query, cteColumns, tables })) {
|
|
425
|
+
result.push(`${joinAlias}.${col}`)
|
|
426
|
+
}
|
|
416
427
|
} else {
|
|
417
428
|
for (const col of lookupTableColumns(join.table, cteColumns, tables)) {
|
|
418
429
|
result.push(`${joinAlias}.${col}`)
|
|
@@ -439,6 +450,10 @@ export function inferSelectSourceColumns({ select, cteColumns, tables }) {
|
|
|
439
450
|
for (const col of tableFunctionColumnNames(join.fromFunction)) {
|
|
440
451
|
result.push(`${joinAlias}.${col}`)
|
|
441
452
|
}
|
|
453
|
+
} else if (join.subquery) {
|
|
454
|
+
for (const col of inferStatementColumns({ stmt: join.subquery.query, cteColumns, tables })) {
|
|
455
|
+
result.push(`${joinAlias}.${col}`)
|
|
456
|
+
}
|
|
442
457
|
} else {
|
|
443
458
|
for (const col of lookupTableColumns(join.table, cteColumns, tables)) {
|
|
444
459
|
result.push(`${joinAlias}.${col}`)
|
package/src/plan/plan.js
CHANGED
|
@@ -447,23 +447,55 @@ function planJoin({ left, joins, leftTable, ctePlans, cteColumns, perTableColumn
|
|
|
447
447
|
continue
|
|
448
448
|
}
|
|
449
449
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
450
|
+
/** @type {QueryPlan} */
|
|
451
|
+
let rightScan
|
|
452
|
+
if (join.subquery) {
|
|
453
|
+
// Subquery on the right side of the join (derived table). Mirror the
|
|
454
|
+
// FROM-clause subquery handling: plan the inner statement, push down the
|
|
455
|
+
// columns the outer query needs, and wrap in the inner scope so
|
|
456
|
+
// correlated subqueries inside resolve against the right aliases.
|
|
457
|
+
let subColumns = perTableColumns.get(rightTable)
|
|
458
|
+
// Empty array means no columns referenced, but the derived table still
|
|
459
|
+
// needs its own columns. Treat empty as unrestricted.
|
|
460
|
+
if (subColumns?.length === 0) subColumns = undefined
|
|
461
|
+
const subPlan = planStatement({
|
|
462
|
+
stmt: join.subquery.query,
|
|
463
|
+
ctePlans,
|
|
464
|
+
cteColumns,
|
|
465
|
+
tables,
|
|
466
|
+
outerScope,
|
|
467
|
+
parentColumns: subColumns?.map(name => ({ type: 'identifier', name, positionStart: 0, positionEnd: 0 })),
|
|
468
|
+
})
|
|
469
|
+
const availableColumns = inferStatementColumns({ stmt: join.subquery.query, cteColumns, tables })
|
|
470
|
+
if (subColumns && availableColumns.length) {
|
|
471
|
+
const missingColumn = subColumns.find(col => !availableColumns.includes(col))
|
|
472
|
+
if (missingColumn) {
|
|
473
|
+
throw new ColumnNotFoundError({ missingColumn, availableColumns, ...join.subquery })
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
const innerScope = statementScope(join.subquery.query)
|
|
477
|
+
rightScan = innerScope ? { type: 'Subquery', scope: innerScope, child: subPlan } : subPlan
|
|
456
478
|
} else {
|
|
457
|
-
|
|
458
|
-
|
|
479
|
+
const ctePlan = ctePlans?.get(join.table.toLowerCase())
|
|
480
|
+
/** @type {ScanOptions} */
|
|
481
|
+
const rightHints = {}
|
|
482
|
+
if (!ctePlan) {
|
|
483
|
+
rightHints.columns = perTableColumns.get(rightTable)
|
|
484
|
+
validateScan({ ...join, hints: rightHints, tables })
|
|
485
|
+
} else {
|
|
486
|
+
// For CTE joins, use CTE column metadata for hints
|
|
487
|
+
rightHints.columns = perTableColumns.get(rightTable) ?? cteColumns?.get(join.table.toLowerCase())
|
|
488
|
+
}
|
|
489
|
+
rightScan = ctePlan ?? { type: 'Scan', table: join.table, hints: rightHints }
|
|
459
490
|
}
|
|
460
|
-
/** @type {QueryPlan} */
|
|
461
|
-
const rightScan = ctePlan ?? { type: 'Scan', table: join.table, hints: rightHints }
|
|
462
491
|
|
|
463
492
|
if (join.joinType === 'POSITIONAL') {
|
|
464
493
|
plan = { type: 'PositionalJoin', leftAlias: currentLeftTable, rightAlias: rightTable, left: plan, right: rightScan }
|
|
465
494
|
} else {
|
|
466
|
-
|
|
495
|
+
// `USING (cols)` desugars to an equi-condition `left.col = right.col` per
|
|
496
|
+
// column, which routes through the hash-join path like any other ON.
|
|
497
|
+
const condition = join.on ?? (join.using && buildUsingCondition(join.using, join))
|
|
498
|
+
const keys = condition && extractEquiKeys({ condition, leftTable: currentLeftTable, rightTable })
|
|
467
499
|
if (keys) {
|
|
468
500
|
/** @type {HashJoinNode} */
|
|
469
501
|
const hashJoin = {
|
|
@@ -484,7 +516,7 @@ function planJoin({ left, joins, leftTable, ctePlans, cteColumns, perTableColumn
|
|
|
484
516
|
joinType: join.joinType,
|
|
485
517
|
leftAlias: currentLeftTable,
|
|
486
518
|
rightAlias: rightTable,
|
|
487
|
-
condition
|
|
519
|
+
condition,
|
|
488
520
|
left: plan,
|
|
489
521
|
right: rightScan,
|
|
490
522
|
}
|
|
@@ -623,6 +655,37 @@ function normalizeIdentifiers(node, sourceColumns) {
|
|
|
623
655
|
return node
|
|
624
656
|
}
|
|
625
657
|
|
|
658
|
+
/**
|
|
659
|
+
* Builds the join condition for a `JOIN ... USING (cols)` clause: an AND of
|
|
660
|
+
* `col = col` equalities using unprefixed identifiers. The hash-join path
|
|
661
|
+
* evaluates the left key against the left row and the right key against the
|
|
662
|
+
* right row, so each unqualified name resolves unambiguously on its own side.
|
|
663
|
+
*
|
|
664
|
+
* @param {string[]} using - shared column names from the USING clause
|
|
665
|
+
* @param {{ positionStart: number, positionEnd: number }} pos - position info for the synthesized exprs
|
|
666
|
+
* @returns {ExprNode | undefined}
|
|
667
|
+
*/
|
|
668
|
+
function buildUsingCondition(using, pos) {
|
|
669
|
+
const { positionStart, positionEnd } = pos
|
|
670
|
+
/** @type {ExprNode | undefined} */
|
|
671
|
+
let condition
|
|
672
|
+
for (const col of using) {
|
|
673
|
+
/** @type {ExprNode} */
|
|
674
|
+
const eq = {
|
|
675
|
+
type: 'binary',
|
|
676
|
+
op: '=',
|
|
677
|
+
left: { type: 'identifier', name: col, positionStart, positionEnd },
|
|
678
|
+
right: { type: 'identifier', name: col, positionStart, positionEnd },
|
|
679
|
+
positionStart,
|
|
680
|
+
positionEnd,
|
|
681
|
+
}
|
|
682
|
+
condition = condition === undefined
|
|
683
|
+
? eq
|
|
684
|
+
: { type: 'binary', op: 'AND', left: condition, right: eq, positionStart, positionEnd }
|
|
685
|
+
}
|
|
686
|
+
return condition
|
|
687
|
+
}
|
|
688
|
+
|
|
626
689
|
/**
|
|
627
690
|
* Splits a join ON expression into equi-key pairs and a residual predicate so
|
|
628
691
|
* the planner can route AND-of-equis (with optional range/inequality
|
package/src/types.d.ts
CHANGED
|
@@ -129,7 +129,7 @@ export interface UserDefinedFunction {
|
|
|
129
129
|
arguments: FunctionSignature
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
export type AggregateFunc = 'COUNT' | 'COUNTIF' | 'SUM' | 'AVG' | 'MIN' | 'MAX' | 'ARRAY_AGG' | 'JSON_ARRAYAGG' | 'STDDEV_SAMP' | 'STDDEV_POP' | 'MEDIAN' | 'PERCENTILE_CONT' | 'APPROX_QUANTILE' | 'STRING_AGG'
|
|
132
|
+
export type AggregateFunc = 'COUNT' | 'COUNTIF' | 'SUM' | 'AVG' | 'MIN' | 'MAX' | 'ARRAY_AGG' | 'LIST' | 'JSON_ARRAYAGG' | 'STDDEV_SAMP' | 'STDDEV_POP' | 'MEDIAN' | 'PERCENTILE_CONT' | 'APPROX_QUANTILE' | 'STRING_AGG'
|
|
133
133
|
|
|
134
134
|
export type RegExpFunction = 'REGEXP_SUBSTR' | 'REGEXP_EXTRACT' | 'REGEXP_REPLACE' | 'REGEXP_MATCHES'
|
|
135
135
|
|
|
@@ -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', 'COUNTIF', 'SUM', 'AVG', 'MIN', 'MAX', 'ARRAY_AGG', 'JSON_ARRAYAGG', 'STDDEV_SAMP', 'STDDEV_POP', 'MEDIAN', 'PERCENTILE_CONT', 'APPROX_QUANTILE', 'STRING_AGG'].includes(name)
|
|
14
|
+
return ['COUNT', 'COUNTIF', 'SUM', 'AVG', 'MIN', 'MAX', 'ARRAY_AGG', 'LIST', 'JSON_ARRAYAGG', 'STDDEV_SAMP', 'STDDEV_POP', 'MEDIAN', 'PERCENTILE_CONT', 'APPROX_QUANTILE', 'STRING_AGG'].includes(name)
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
/**
|
|
@@ -179,8 +179,10 @@ export const FUNCTION_SIGNATURES = {
|
|
|
179
179
|
JSON_ARRAY_LENGTH: { min: 1, max: 1, signature: 'array' },
|
|
180
180
|
JSON_VALID: { min: 1, max: 1, signature: 'value' },
|
|
181
181
|
JSON_TYPE: { min: 1, max: 1, signature: 'value' },
|
|
182
|
+
JSON_KEYS: { min: 1, max: 1, signature: 'value' },
|
|
182
183
|
JSON_ARRAYAGG: { min: 1, max: 1, signature: 'expression' },
|
|
183
184
|
ARRAY_AGG: { min: 1, max: 1, signature: 'expression' },
|
|
185
|
+
LIST: { min: 1, max: 1, signature: 'expression' },
|
|
184
186
|
|
|
185
187
|
// Array functions
|
|
186
188
|
ARRAY_LENGTH: { min: 1, max: 2, signature: 'array[, dimension]' },
|
|
@@ -3,7 +3,7 @@ export const KEYWORDS = new Set([
|
|
|
3
3
|
'HAVING', 'ORDER', 'ASC', 'DESC', 'NULLS', 'LIMIT', 'OFFSET', 'AS', 'ALL',
|
|
4
4
|
'DISTINCT', 'TRUE', 'FALSE', 'NULL', 'LIKE', 'IN', 'EXISTS', 'BETWEEN',
|
|
5
5
|
'CASE', 'WHEN', 'THEN', 'ELSE', 'END', 'JOIN', 'INNER', 'LEFT', 'RIGHT',
|
|
6
|
-
'FULL', 'OUTER', 'CROSS', 'POSITIONAL', 'LATERAL', 'VIEW', 'ON', 'INTERVAL', 'DAY', 'MONTH', 'YEAR',
|
|
6
|
+
'FULL', 'OUTER', 'CROSS', 'POSITIONAL', 'LATERAL', 'VIEW', 'ON', 'USING', 'INTERVAL', 'DAY', 'MONTH', 'YEAR',
|
|
7
7
|
'HOUR', 'MINUTE', 'SECOND', 'FILTER', 'WITHIN',
|
|
8
8
|
'UNION', 'INTERSECT', 'EXCEPT',
|
|
9
9
|
])
|
|
@@ -17,7 +17,7 @@ export const RESERVED_KEYWORDS = new Set([
|
|
|
17
17
|
'EXISTS', 'CASE', 'WHEN', 'THEN', 'ELSE', 'END', 'INTERVAL',
|
|
18
18
|
'GROUP', 'BY', 'HAVING', 'ORDER', 'LIMIT', 'OFFSET',
|
|
19
19
|
'AS', 'ALL', 'DISTINCT',
|
|
20
|
-
'JOIN', 'INNER', 'LEFT', 'RIGHT', 'FULL', 'OUTER', 'ON',
|
|
20
|
+
'JOIN', 'INNER', 'LEFT', 'RIGHT', 'FULL', 'OUTER', 'ON', 'USING',
|
|
21
21
|
'UNION', 'INTERSECT', 'EXCEPT',
|
|
22
22
|
])
|
|
23
23
|
|
|
@@ -30,6 +30,6 @@ export const RESERVED_AFTER_COLUMN = new Set([
|
|
|
30
30
|
// Keywords that cannot be used as table aliases
|
|
31
31
|
export const RESERVED_AFTER_TABLE = new Set([
|
|
32
32
|
'WHERE', 'GROUP', 'HAVING', 'ORDER', 'LIMIT', 'OFFSET', 'JOIN', 'INNER',
|
|
33
|
-
'LEFT', 'RIGHT', 'FULL', 'CROSS', 'ON', 'POSITIONAL', 'LATERAL',
|
|
33
|
+
'LEFT', 'RIGHT', 'FULL', 'CROSS', 'ON', 'USING', 'POSITIONAL', 'LATERAL',
|
|
34
34
|
'UNION', 'INTERSECT', 'EXCEPT',
|
|
35
35
|
])
|