squirreling 0.12.18 → 0.12.20

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
@@ -161,7 +161,7 @@ Squirreling mostly follows the SQL standard. The following features are supporte
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
163
  - Json: `JSON_VALUE`, `JSON_QUERY`, `JSON_EXTRACT`, `JSON_OBJECT`, `JSON_ARRAY_LENGTH`, `JSON_VALID`, `JSON_TYPE`
164
- - Array: `ARRAY_LENGTH`, `ARRAY_POSITION`, `ARRAY_CONTAINS`, `ARRAY_SORT`, `CARDINALITY`, `SIZE`
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`
167
167
  - 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`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.12.18",
3
+ "version": "0.12.20",
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.6.0",
43
- "@vitest/coverage-v8": "4.1.5",
42
+ "@types/node": "25.8.0",
43
+ "@vitest/coverage-v8": "4.1.6",
44
44
  "eslint": "9.39.4",
45
45
  "eslint-plugin-jsdoc": "62.9.0",
46
46
  "typescript": "6.0.3",
47
- "vitest": "4.1.5"
47
+ "vitest": "4.1.6"
48
48
  }
49
49
  }
package/src/ast.d.ts CHANGED
@@ -12,7 +12,7 @@ export interface SelectStatement extends AstBase {
12
12
  type: 'select'
13
13
  distinct: boolean
14
14
  columns: SelectColumn[]
15
- from: FromTable | FromSubquery | FromFunction
15
+ from?: FromTable | FromSubquery | FromFunction
16
16
  joins: JoinClause[]
17
17
  where?: ExprNode
18
18
  groupBy: ExprNode[]
@@ -83,6 +83,8 @@ export function executeStatement({ query, context, outerScope }) {
83
83
  export function executePlan({ plan, context }) {
84
84
  if (plan.type === 'Scan') {
85
85
  return executeScan(plan, context)
86
+ } else if (plan.type === 'SingleRow') {
87
+ return executeSingleRow()
86
88
  } else if (plan.type === 'Count') {
87
89
  return executeCount(plan, context)
88
90
  } else if (plan.type === 'Filter') {
@@ -117,6 +119,23 @@ export function executePlan({ plan, context }) {
117
119
  return { columns: [], async *rows() {} }
118
120
  }
119
121
 
122
+ /**
123
+ * Yields exactly one empty row. Used for FROM-less SELECT like `SELECT 1`,
124
+ * where the projection produces the output from constant expressions.
125
+ *
126
+ * @returns {QueryResults}
127
+ */
128
+ function executeSingleRow() {
129
+ return {
130
+ columns: [],
131
+ numRows: 1,
132
+ maxRows: 1,
133
+ async *rows() {
134
+ yield { columns: [], cells: {} }
135
+ },
136
+ }
137
+ }
138
+
120
139
  /**
121
140
  * Executes a table-valued function (e.g. UNNEST, JSON_EACH).
122
141
  * Evaluates the argument once against the outer row (for lateral joins) or an
@@ -27,7 +27,11 @@ export function compareForTerm(a, b, term) {
27
27
  if (a == b) return 0
28
28
 
29
29
  let cmp
30
- if (primitiveTypes.has(typeof a) && primitiveTypes.has(typeof b)) {
30
+ if (a instanceof Date && b instanceof Date) {
31
+ const at = a.getTime()
32
+ const bt = b.getTime()
33
+ cmp = at < bt ? -1 : at > bt ? 1 : 0
34
+ } else if (primitiveTypes.has(typeof a) && primitiveTypes.has(typeof b)) {
31
35
  cmp = a < b ? -1 : 1
32
36
  } else {
33
37
  const aa = String(a)
@@ -121,6 +125,19 @@ export function maxBounds(a, b) {
121
125
  return a ?? b
122
126
  }
123
127
 
128
+ /**
129
+ * SQL equality for primitives. Two Date instances for the same instant compare
130
+ * equal (JS `==` would compare by identity).
131
+ *
132
+ * @param {SqlPrimitive} a
133
+ * @param {SqlPrimitive} b
134
+ * @returns {boolean}
135
+ */
136
+ export function sqlEquals(a, b) {
137
+ if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime()
138
+ return a == b
139
+ }
140
+
124
141
  /**
125
142
  * Returns true for plain object SqlPrimitive values, excluding null, arrays, and Dates.
126
143
  *
@@ -29,6 +29,18 @@ export function applyBinaryOp(op, a, b) {
29
29
  }
30
30
  if (op === 'AND') return Boolean(a) && Boolean(b)
31
31
  if (op === 'OR') return Boolean(a) || Boolean(b)
32
+ // Compare Date values by their time so distinct instances for the same
33
+ // instant are equal, matching SQL TIMESTAMP semantics rather than JS identity.
34
+ if (a instanceof Date && b instanceof Date) {
35
+ const at = a.getTime()
36
+ const bt = b.getTime()
37
+ if (op === '!=' || op === '<>') return at !== bt
38
+ if (op === '=' || op === '==') return at === bt
39
+ if (op === '<') return at < bt
40
+ if (op === '<=') return at <= bt
41
+ if (op === '>') return at > bt
42
+ if (op === '>=') return at >= bt
43
+ }
32
44
  if (op === '!=' || op === '<>') return a != b
33
45
  if (op === '=' || op === '==') return a == b
34
46
  if (op === '<') return a < b
@@ -1,5 +1,5 @@
1
1
  import { executeStatement } from '../execute/execute.js'
2
- import { isPlainObject, keyify, stringify } from '../execute/utils.js'
2
+ import { isPlainObject, keyify, sqlEquals, stringify } from '../execute/utils.js'
3
3
  import { ArgValueError, ExecutionError } from '../validation/executionErrors.js'
4
4
  import { isAggregateFunc, isMathFunc, isRegexpFunc, isSpatialFunc, isStringFunc } from '../validation/functions.js'
5
5
  import { UnknownFunctionError } from '../validation/parseErrors.js'
@@ -516,7 +516,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
516
516
  return arr.length
517
517
  }
518
518
 
519
- if (funcName === 'ARRAY_LENGTH' || funcName === 'CARDINALITY' || funcName === 'SIZE') {
519
+ if (funcName === 'ARRAY_LENGTH' || funcName === 'LIST_LENGTH' || funcName === 'LEN' || funcName === 'CARDINALITY' || funcName === 'SIZE') {
520
520
  const arr = args[0]
521
521
  if (!Array.isArray(arr)) return null
522
522
  if (funcName === 'ARRAY_LENGTH' && args.length === 2) {
@@ -539,19 +539,31 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
539
539
  return arr.length
540
540
  }
541
541
 
542
- if (funcName === 'ARRAY_POSITION') {
542
+ if (funcName === 'ARRAY_POSITION' || funcName === 'LIST_POSITION') {
543
543
  const [arr, target] = args
544
544
  if (!Array.isArray(arr)) return null
545
545
  const index = arr.indexOf(target)
546
546
  return index === -1 ? null : index + 1
547
547
  }
548
548
 
549
- if (funcName === 'ARRAY_CONTAINS') {
549
+ if (funcName === 'ARRAY_CONTAINS' || funcName === 'LIST_CONTAINS') {
550
550
  const [arr, target] = args
551
551
  if (!Array.isArray(arr)) return null
552
552
  return arr.includes(target)
553
553
  }
554
554
 
555
+ if (funcName === 'ARRAY_APPEND' || funcName === 'LIST_APPEND') {
556
+ const [arr, element] = args
557
+ if (!Array.isArray(arr)) return null
558
+ return [...arr, element]
559
+ }
560
+
561
+ if (funcName === 'ARRAY_CONCAT' || funcName === 'LIST_CONCAT') {
562
+ const [a, b] = args
563
+ if (!Array.isArray(a) || !Array.isArray(b)) return null
564
+ return [...a, ...b]
565
+ }
566
+
555
567
  if (funcName === 'ARRAY_SORT') {
556
568
  const arr = args[0]
557
569
  if (!Array.isArray(arr)) return null
@@ -667,7 +679,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
667
679
  const exprVal = await evaluateExpr({ node: node.expr, row, rowIndex, rows, context })
668
680
  for (const valueNode of node.values) {
669
681
  const val = await evaluateExpr({ node: valueNode, row, rowIndex, rows, context })
670
- if (exprVal == val) return true
682
+ if (sqlEquals(exprVal, val)) return true
671
683
  }
672
684
  return false
673
685
  }
@@ -677,7 +689,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
677
689
  const subResult = executeStatement({ query: node.subquery, context })
678
690
  for await (const resRow of subResult.rows()) {
679
691
  const value = await resRow.cells[resRow.columns[0]]()
680
- if (exprVal == value) return true
692
+ if (sqlEquals(exprVal, value)) return true
681
693
  }
682
694
  return false
683
695
  }
@@ -703,7 +715,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
703
715
  for (const whenClause of node.whenClauses) {
704
716
  const whenValue = await evaluateExpr({ node: whenClause.condition, row, rowIndex, rows, context })
705
717
  // compare caseValue with condition or evaluate as boolean
706
- if (caseValue !== undefined ? caseValue == whenValue : whenValue) {
718
+ if (caseValue !== undefined ? sqlEquals(caseValue, whenValue) : whenValue) {
707
719
  return evaluateExpr({ node: whenClause.result, row, rowIndex, rows, context })
708
720
  }
709
721
  }
@@ -7,7 +7,7 @@
7
7
  * parsed statement, including those inside subqueries (IN, EXISTS, derived
8
8
  * tables, scalar subqueries) and the branches of compound (UNION /
9
9
  * INTERSECT / EXCEPT) queries. CTE names defined by an enclosing WITH are
10
- * skipped, including across sibling CTEs and nested WITHs the result is
10
+ * skipped, including across sibling CTEs and nested WITHs. The result is
11
11
  * the set of names a caller would need to provide as `tables` to
12
12
  * `executeSql`.
13
13
  *
@@ -47,7 +47,9 @@ function walkStatement(stmt, cteScope, refs) {
47
47
  return
48
48
  }
49
49
  // select
50
- if (stmt.from.type === 'table') {
50
+ if (!stmt.from) {
51
+ // FROM-less SELECT (e.g. `SELECT 1`), no source tables
52
+ } else if (stmt.from.type === 'table') {
51
53
  if (!cteScope.has(stmt.from.table.toLowerCase())) refs.add(stmt.from.table)
52
54
  } else if (stmt.from.type === 'subquery') {
53
55
  walkStatement(stmt.from.query, cteScope, refs)
@@ -6,7 +6,7 @@ import { isTableFunctionStart, parseFromFunction, parseTableAlias, tableFunction
6
6
  import { consume, current, expect, match } from './state.js'
7
7
 
8
8
  /**
9
- * @import { ExprNode, JoinClause, JoinType, ParserState } from '../types.js'
9
+ * @import { ExprNode, FromFunction, JoinClause, JoinType, ParserState } from '../types.js'
10
10
  */
11
11
 
12
12
  /**
@@ -80,7 +80,7 @@ export function parseJoins(state) {
80
80
  })
81
81
  }
82
82
 
83
- /** @type {import('../ast.js').FromFunction} */
83
+ /** @type {FromFunction} */
84
84
  const fromFunction = {
85
85
  type: 'function',
86
86
  funcName,
@@ -12,6 +12,13 @@ import { tokenizeSql } from './tokenize.js'
12
12
  * @import { CTEDefinition, ExprNode, FromFunction, FromSubquery, FromTable, OrderByItem, ParseSqlOptions, ParserState, SelectColumn, SelectStatement, SetOperationStatement, SetOperator, Statement } from '../types.js'
13
13
  */
14
14
 
15
+ // Keywords that may legitimately follow the SELECT column list in place of FROM.
16
+ // Anything else is a hint that the user forgot the FROM keyword.
17
+ const CONTINUATION_KEYWORDS = new Set([
18
+ 'WHERE', 'GROUP', 'HAVING', 'ORDER', 'LIMIT', 'OFFSET',
19
+ 'UNION', 'INTERSECT', 'EXCEPT',
20
+ ])
21
+
15
22
  /**
16
23
  * @param {ParseSqlOptions} options
17
24
  * @returns {Statement}
@@ -176,20 +183,38 @@ function parseSelect(state) {
176
183
  let distinct = false
177
184
 
178
185
  // Support duckdb-style shorthand "FROM table"
186
+ let hasFrom = true
179
187
  if (match(state, 'keyword', 'FROM')) {
180
188
  columns = [{ type: 'star', positionStart, positionEnd: positionStart }]
181
189
  } else {
182
190
  expect(state, 'keyword', 'SELECT')
183
191
  distinct = match(state, 'keyword', 'DISTINCT')
184
192
  columns = parseSelectList(state)
185
- expect(state, 'keyword', 'FROM')
193
+ hasFrom = !!match(state, 'keyword', 'FROM')
194
+ // After the column list, if FROM is missing and the next token isn't a
195
+ // valid continuation (clause keyword, set operator, terminator), the user
196
+ // likely forgot a FROM keyword. Report that rather than letting parsing
197
+ // throw a less-specific end-of-query error.
198
+ if (!hasFrom) {
199
+ const tok = current(state)
200
+ const isContinuation = tok.type === 'eof' ||
201
+ tok.type === 'semicolon' ||
202
+ tok.type === 'paren' && tok.value === ')' ||
203
+ tok.type === 'keyword' && CONTINUATION_KEYWORDS.has(tok.value)
204
+ if (!isContinuation) {
205
+ throw parseError(state, 'FROM')
206
+ }
207
+ }
186
208
  }
187
209
 
188
210
  // Check if it's a subquery, table function, or table name
189
- /** @type {FromTable | FromSubquery | FromFunction} */
211
+ /** @type {FromTable | FromSubquery | FromFunction | undefined} */
190
212
  let from
191
213
  const fromTok = current(state)
192
- if (fromTok.type === 'paren' && fromTok.value === '(') {
214
+ if (!hasFrom) {
215
+ // No FROM clause: constant SELECT like "SELECT 1"
216
+ from = undefined
217
+ } else if (fromTok.type === 'paren' && fromTok.value === '(') {
193
218
  // Subquery: SELECT * FROM (SELECT ...) AS alias
194
219
  expect(state, 'paren', '(')
195
220
  const query = parseStatement(state)
@@ -2,7 +2,7 @@ import { KEYWORDS } from '../validation/keywords.js'
2
2
  import { InvalidLiteralError, ParseError, UnexpectedCharError } from '../validation/parseErrors.js'
3
3
 
4
4
  /**
5
- * @import { Token } from '../types.d.ts'
5
+ * @import { Token } from '../types.js'
6
6
  */
7
7
 
8
8
  const NUMBER_REGEX = /^-?(?:\d+n|(?:\d+\.?\d*|\d*\.\d+)(?:[eE][+-]?\d+)?)/
@@ -6,10 +6,11 @@ import { derivedAlias } from '../expression/alias.js'
6
6
  */
7
7
 
8
8
  /**
9
- * @param {FromTable | FromSubquery | FromFunction} from
10
- * @returns {string}
9
+ * @param {FromTable | FromSubquery | FromFunction | undefined} from
10
+ * @returns {string | undefined}
11
11
  */
12
12
  export function fromAlias(from) {
13
+ if (!from) return undefined
13
14
  if (from.alias) return from.alias
14
15
  if (from.type === 'table') return from.table
15
16
  if (from.type === 'function') return from.funcName.toLowerCase()
@@ -31,7 +32,9 @@ export function fromAlias(from) {
31
32
  export function statementScope(stmt) {
32
33
  if (stmt.type === 'with') return statementScope(stmt.query)
33
34
  if (stmt.type === 'compound') return undefined
34
- return [fromAlias(stmt.from), ...stmt.joins.map(j => j.alias ?? j.table)]
35
+ const alias = fromAlias(stmt.from)
36
+ const joins = stmt.joins.map(j => j.alias ?? j.table)
37
+ return alias === undefined ? joins : [alias, ...joins]
35
38
  }
36
39
 
37
40
  /**
@@ -64,7 +67,10 @@ export function extractColumns({ select, parentColumns }) {
64
67
  const result = new Map()
65
68
 
66
69
  // Build alias list from FROM + JOINs
67
- const aliases = [fromAlias(select.from)]
70
+ /** @type {string[]} */
71
+ const aliases = []
72
+ const sourceAlias = fromAlias(select.from)
73
+ if (sourceAlias !== undefined) aliases.push(sourceAlias)
68
74
  for (const join of select.joins) {
69
75
  aliases.push(join.alias ?? join.table)
70
76
  }
@@ -142,7 +148,9 @@ export function extractColumns({ select, parentColumns }) {
142
148
  collectColumnsFromExpr(expr, identifiers, selectAliases)
143
149
  }
144
150
  collectColumnsFromExpr(select.having, identifiers, selectAliases)
145
- const visibleLateralAliases = [fromAlias(select.from)]
151
+ /** @type {string[]} */
152
+ const visibleLateralAliases = []
153
+ if (sourceAlias !== undefined) visibleLateralAliases.push(sourceAlias)
146
154
  for (const join of select.joins) {
147
155
  collectColumnsFromExpr(join.on, identifiers)
148
156
  const joinAlias = join.alias ?? join.table
@@ -298,7 +306,7 @@ function collectColumnsFromStatement(stmt, columns) {
298
306
  if (col.type === 'derived') collectColumnsFromExpr(col.expr, columns)
299
307
  }
300
308
  collectColumnsFromExpr(stmt.where, columns)
301
- if (stmt.from?.type === 'subquery') {
309
+ if (stmt.from && stmt.from.type === 'subquery') {
302
310
  collectColumnsFromStatement(stmt.from.query, columns)
303
311
  }
304
312
  for (const join of stmt.joins) {
@@ -384,6 +392,7 @@ export function inferStatementColumns({ stmt, cteColumns, tables }) {
384
392
  * @returns {string[]}
385
393
  */
386
394
  export function inferSelectSourceColumns({ select, cteColumns, tables }) {
395
+ if (!select.from) return []
387
396
  if (select.from.type === 'subquery') {
388
397
  return inferStatementColumns({ stmt: select.from.query, cteColumns, tables })
389
398
  }
package/src/plan/plan.js CHANGED
@@ -7,8 +7,8 @@ import { validateNoIdentifiers, validateScan, validateTableRefs } from '../valid
7
7
  import { collectScopeColumns, extractColumns, fromAlias, inferSelectSourceColumns, inferStatementColumns, statementScope, tableFunctionColumnNames } from './columns.js'
8
8
 
9
9
  /**
10
- * @import { AsyncDataSource, ExprNode, DerivedColumn, IdentifierNode, JoinClause, OrderByItem, PlanSqlOptions, ScanOptions, SelectColumn, SelectStatement, SetOperationStatement, Statement, WindowFunctionNode } from '../types.js'
11
- * @import { QueryPlan, WindowSpec } from './types.d.ts'
10
+ * @import { AsyncDataSource, DerivedColumn, ExprNode, FromFunction, IdentifierNode, JoinClause, OrderByItem, PlanSqlOptions, ScanOptions, SelectColumn, SelectStatement, SetOperationStatement, Statement, WindowFunctionNode } from '../types.js'
11
+ * @import { HashJoinNode, QueryPlan, TableFunctionNode, WindowSpec } from './types.js'
12
12
  */
13
13
 
14
14
  /**
@@ -153,12 +153,17 @@ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns, outer
153
153
  const bufferingWindows = windows.some(w => w.partitionBy.length > 0 || w.orderBy.length > 0)
154
154
  const needsBuffering = useGrouping || select.orderBy.length > 0 || bufferingWindows
155
155
 
156
- // Source alias for FROM clause
156
+ // Source alias for FROM clause (undefined for FROM-less SELECT)
157
157
  const sourceAlias = fromAlias(select.from)
158
158
 
159
159
  // Resolve aliases (and validate qualified references)
160
160
  // Include outerScope aliases so correlated references pass validation
161
- const scopeTables = Object.fromEntries([sourceAlias, ...select.joins.map(j => j.alias ?? j.table), ...outerScope ?? []].map(a => [a, true]))
161
+ const scopeAliases = [
162
+ ...sourceAlias !== undefined ? [sourceAlias] : [],
163
+ ...select.joins.map(j => j.alias ?? j.table),
164
+ ...outerScope ?? [],
165
+ ]
166
+ const scopeTables = Object.fromEntries(scopeAliases.map(a => [a, true]))
162
167
  // Bare column names in scope, so the validator can recognize struct-field
163
168
  // dot access on a column (e.g. `item.name` where `item` is an unnested
164
169
  // struct column) rather than rejecting `item` as an unknown table.
@@ -201,10 +206,10 @@ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns, outer
201
206
  /** @type {ScanOptions} */
202
207
  const hints = {}
203
208
  const perTableColumns = extractColumns({ select: originalSelect, parentColumns })
204
- hints.columns = perTableColumns.get(sourceAlias)
209
+ if (sourceAlias !== undefined) hints.columns = perTableColumns.get(sourceAlias)
205
210
  // Empty columns array means no columns were referenced, but a FROM subquery
206
211
  // still needs its own columns (e.g. for DISTINCT). Treat empty as unrestricted.
207
- if (hints.columns?.length === 0 && select.from.type === 'subquery') {
212
+ if (hints.columns?.length === 0 && select.from?.type === 'subquery') {
208
213
  hints.columns = undefined
209
214
  }
210
215
  if (!select.joins.length) {
@@ -334,6 +339,9 @@ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns, outer
334
339
  * @returns {QueryPlan}
335
340
  */
336
341
  function planFrom({ select, ctePlans, cteColumns, hints, tables, outerScope }) {
342
+ if (!select.from) {
343
+ return { type: 'SingleRow' }
344
+ }
337
345
  if (select.from.type === 'table') {
338
346
  const ctePlan = ctePlans?.get(select.from.table.toLowerCase())
339
347
  if (ctePlan) {
@@ -378,8 +386,8 @@ function planFrom({ select, ctePlans, cteColumns, hints, tables, outerScope }) {
378
386
  /**
379
387
  * Builds a TableFunction plan node for a FromFunction AST.
380
388
  *
381
- * @param {import('../types.js').FromFunction} from
382
- * @returns {import('./types.d.ts').TableFunctionNode}
389
+ * @param {FromFunction} from
390
+ * @returns {TableFunctionNode}
383
391
  */
384
392
  function planTableFunction(from) {
385
393
  return {
@@ -457,7 +465,7 @@ function planJoin({ left, joins, leftTable, ctePlans, cteColumns, perTableColumn
457
465
  } else {
458
466
  const keys = join.on && extractEquiKeys({ condition: join.on, leftTable: currentLeftTable, rightTable })
459
467
  if (keys) {
460
- /** @type {import('./types.d.ts').HashJoinNode} */
468
+ /** @type {HashJoinNode} */
461
469
  const hashJoin = {
462
470
  type: 'HashJoin',
463
471
  joinType: join.joinType,
@@ -2,6 +2,7 @@ import { DerivedColumn, ExprNode, JoinType, OrderByItem, ScanOptions, SelectColu
2
2
 
3
3
  export type QueryPlan =
4
4
  | ScanNode
5
+ | SingleRowNode
5
6
  | CountNode
6
7
  | FilterNode
7
8
  | ProjectNode
@@ -25,6 +26,11 @@ export interface ScanNode {
25
26
  hints: ScanOptions
26
27
  }
27
28
 
29
+ // Source for FROM-less SELECT like `SELECT 1`. Yields exactly one empty row.
30
+ export interface SingleRowNode {
31
+ type: 'SingleRow'
32
+ }
33
+
28
34
  // Count node for COUNT(*) optimization
29
35
  export interface CountNode {
30
36
  type: 'Count'
@@ -184,9 +184,17 @@ export const FUNCTION_SIGNATURES = {
184
184
 
185
185
  // Array functions
186
186
  ARRAY_LENGTH: { min: 1, max: 2, signature: 'array[, dimension]' },
187
+ LIST_LENGTH: { min: 1, max: 1, signature: 'array' },
188
+ LEN: { min: 1, max: 1, signature: 'array' },
187
189
  ARRAY_POSITION: { min: 2, max: 2, signature: 'array, element' },
190
+ LIST_POSITION: { min: 2, max: 2, signature: 'array, element' },
188
191
  ARRAY_CONTAINS: { min: 2, max: 2, signature: 'array, element' },
192
+ LIST_CONTAINS: { min: 2, max: 2, signature: 'array, element' },
189
193
  ARRAY_SORT: { min: 1, max: 1, signature: 'array' },
194
+ ARRAY_APPEND: { min: 2, max: 2, signature: 'array, element' },
195
+ LIST_APPEND: { min: 2, max: 2, signature: 'array, element' },
196
+ ARRAY_CONCAT: { min: 2, max: 2, signature: 'array1, array2' },
197
+ LIST_CONCAT: { min: 2, max: 2, signature: 'array1, array2' },
190
198
  CARDINALITY: { min: 1, max: 1, signature: 'array' },
191
199
  SIZE: { min: 1, max: 1, signature: 'array' },
192
200