squirreling 0.12.17 → 0.12.19
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 +2 -2
- package/src/ast.d.ts +1 -1
- package/src/execute/execute.js +19 -0
- package/src/index.d.ts +12 -0
- package/src/index.js +1 -0
- package/src/parse/extractTables.js +127 -0
- package/src/parse/joins.js +2 -2
- package/src/parse/parse.js +28 -3
- package/src/parse/tokenize.js +1 -1
- package/src/plan/columns.js +15 -6
- package/src/plan/plan.js +17 -9
- package/src/plan/types.d.ts +6 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.19",
|
|
4
4
|
"description": "Squirreling Async SQL Engine",
|
|
5
5
|
"author": "Hyperparam",
|
|
6
6
|
"homepage": "https://hyperparam.app",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"test": "vitest run"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@types/node": "25.6.
|
|
42
|
+
"@types/node": "25.6.2",
|
|
43
43
|
"@vitest/coverage-v8": "4.1.5",
|
|
44
44
|
"eslint": "9.39.4",
|
|
45
45
|
"eslint-plugin-jsdoc": "62.9.0",
|
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
|
|
15
|
+
from?: FromTable | FromSubquery | FromFunction
|
|
16
16
|
joins: JoinClause[]
|
|
17
17
|
where?: ExprNode
|
|
18
18
|
groupBy: ExprNode[]
|
package/src/execute/execute.js
CHANGED
|
@@ -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
|
package/src/index.d.ts
CHANGED
|
@@ -53,6 +53,18 @@ export function executePlan(options: { plan: QueryPlan, context: ExecuteContext
|
|
|
53
53
|
*/
|
|
54
54
|
export function parseSql(options: ParseSqlOptions): Statement
|
|
55
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Collects every external table referenced from FROM and JOIN clauses in a
|
|
58
|
+
* parsed statement, descending into subqueries (IN, EXISTS, derived tables,
|
|
59
|
+
* scalar subqueries) and the branches of compound queries. CTE names defined
|
|
60
|
+
* by an enclosing WITH are skipped. Returned in first-seen order with
|
|
61
|
+
* duplicates removed.
|
|
62
|
+
*
|
|
63
|
+
* @param statement - parsed SQL statement (output of `parseSql`)
|
|
64
|
+
* @returns table names referenced in the query, excluding CTE aliases
|
|
65
|
+
*/
|
|
66
|
+
export function extractTables(statement: Statement): string[]
|
|
67
|
+
|
|
56
68
|
/**
|
|
57
69
|
* Builds a query plan from a SQL query string or AST
|
|
58
70
|
*
|
package/src/index.js
CHANGED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { ExprNode, Statement } from '../types.js'
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Collect every external table referenced from FROM and JOIN clauses in a
|
|
7
|
+
* parsed statement, including those inside subqueries (IN, EXISTS, derived
|
|
8
|
+
* tables, scalar subqueries) and the branches of compound (UNION /
|
|
9
|
+
* INTERSECT / EXCEPT) queries. CTE names defined by an enclosing WITH are
|
|
10
|
+
* skipped, including across sibling CTEs and nested WITHs. The result is
|
|
11
|
+
* the set of names a caller would need to provide as `tables` to
|
|
12
|
+
* `executeSql`.
|
|
13
|
+
*
|
|
14
|
+
* Returned in first-seen order with duplicates removed. Names are returned
|
|
15
|
+
* in the original case they were written in the query.
|
|
16
|
+
*
|
|
17
|
+
* @param {Statement} statement
|
|
18
|
+
* @returns {string[]}
|
|
19
|
+
*/
|
|
20
|
+
export function extractTables(statement) {
|
|
21
|
+
/** @type {Set<string>} */
|
|
22
|
+
const refs = new Set()
|
|
23
|
+
walkStatement(statement, new Set(), refs)
|
|
24
|
+
return [...refs]
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {Statement} stmt
|
|
29
|
+
* @param {Set<string>} cteScope - lowercased CTE names visible at this point
|
|
30
|
+
* @param {Set<string>} refs
|
|
31
|
+
* @returns {void}
|
|
32
|
+
*/
|
|
33
|
+
function walkStatement(stmt, cteScope, refs) {
|
|
34
|
+
if (stmt.type === 'with') {
|
|
35
|
+
const scope = new Set(cteScope)
|
|
36
|
+
for (const cte of stmt.ctes) {
|
|
37
|
+
walkStatement(cte.query, scope, refs)
|
|
38
|
+
scope.add(cte.name.toLowerCase())
|
|
39
|
+
}
|
|
40
|
+
walkStatement(stmt.query, scope, refs)
|
|
41
|
+
return
|
|
42
|
+
}
|
|
43
|
+
if (stmt.type === 'compound') {
|
|
44
|
+
walkStatement(stmt.left, cteScope, refs)
|
|
45
|
+
walkStatement(stmt.right, cteScope, refs)
|
|
46
|
+
for (const o of stmt.orderBy) walkExpr(o.expr, cteScope, refs)
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
// select
|
|
50
|
+
if (!stmt.from) {
|
|
51
|
+
// FROM-less SELECT (e.g. `SELECT 1`), no source tables
|
|
52
|
+
} else if (stmt.from.type === 'table') {
|
|
53
|
+
if (!cteScope.has(stmt.from.table.toLowerCase())) refs.add(stmt.from.table)
|
|
54
|
+
} else if (stmt.from.type === 'subquery') {
|
|
55
|
+
walkStatement(stmt.from.query, cteScope, refs)
|
|
56
|
+
} else {
|
|
57
|
+
for (const a of stmt.from.args) walkExpr(a, cteScope, refs)
|
|
58
|
+
}
|
|
59
|
+
for (const j of stmt.joins) {
|
|
60
|
+
if (j.fromFunction) {
|
|
61
|
+
for (const a of j.fromFunction.args) walkExpr(a, cteScope, refs)
|
|
62
|
+
} else if (!cteScope.has(j.table.toLowerCase())) {
|
|
63
|
+
refs.add(j.table)
|
|
64
|
+
}
|
|
65
|
+
if (j.on) walkExpr(j.on, cteScope, refs)
|
|
66
|
+
}
|
|
67
|
+
for (const c of stmt.columns) {
|
|
68
|
+
if (c.type === 'derived') walkExpr(c.expr, cteScope, refs)
|
|
69
|
+
}
|
|
70
|
+
if (stmt.where) walkExpr(stmt.where, cteScope, refs)
|
|
71
|
+
for (const g of stmt.groupBy) walkExpr(g, cteScope, refs)
|
|
72
|
+
if (stmt.having) walkExpr(stmt.having, cteScope, refs)
|
|
73
|
+
for (const o of stmt.orderBy) walkExpr(o.expr, cteScope, refs)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* @param {ExprNode} expr
|
|
78
|
+
* @param {Set<string>} cteScope
|
|
79
|
+
* @param {Set<string>} refs
|
|
80
|
+
* @returns {void}
|
|
81
|
+
*/
|
|
82
|
+
function walkExpr(expr, cteScope, refs) {
|
|
83
|
+
switch (expr.type) {
|
|
84
|
+
case 'unary':
|
|
85
|
+
walkExpr(expr.argument, cteScope, refs)
|
|
86
|
+
return
|
|
87
|
+
case 'binary':
|
|
88
|
+
walkExpr(expr.left, cteScope, refs)
|
|
89
|
+
walkExpr(expr.right, cteScope, refs)
|
|
90
|
+
return
|
|
91
|
+
case 'function':
|
|
92
|
+
for (const a of expr.args) walkExpr(a, cteScope, refs)
|
|
93
|
+
if (expr.filter) walkExpr(expr.filter, cteScope, refs)
|
|
94
|
+
return
|
|
95
|
+
case 'window':
|
|
96
|
+
for (const a of expr.args) walkExpr(a, cteScope, refs)
|
|
97
|
+
for (const p of expr.partitionBy) walkExpr(p, cteScope, refs)
|
|
98
|
+
for (const o of expr.orderBy) walkExpr(o.expr, cteScope, refs)
|
|
99
|
+
return
|
|
100
|
+
case 'cast':
|
|
101
|
+
walkExpr(expr.expr, cteScope, refs)
|
|
102
|
+
return
|
|
103
|
+
case 'in':
|
|
104
|
+
walkExpr(expr.expr, cteScope, refs)
|
|
105
|
+
walkStatement(expr.subquery, cteScope, refs)
|
|
106
|
+
return
|
|
107
|
+
case 'in valuelist':
|
|
108
|
+
walkExpr(expr.expr, cteScope, refs)
|
|
109
|
+
for (const v of expr.values) walkExpr(v, cteScope, refs)
|
|
110
|
+
return
|
|
111
|
+
case 'exists':
|
|
112
|
+
case 'not exists':
|
|
113
|
+
walkStatement(expr.subquery, cteScope, refs)
|
|
114
|
+
return
|
|
115
|
+
case 'case':
|
|
116
|
+
if (expr.caseExpr) walkExpr(expr.caseExpr, cteScope, refs)
|
|
117
|
+
for (const w of expr.whenClauses) {
|
|
118
|
+
walkExpr(w.condition, cteScope, refs)
|
|
119
|
+
walkExpr(w.result, cteScope, refs)
|
|
120
|
+
}
|
|
121
|
+
if (expr.elseResult) walkExpr(expr.elseResult, cteScope, refs)
|
|
122
|
+
return
|
|
123
|
+
case 'subquery':
|
|
124
|
+
walkStatement(expr.subquery, cteScope, refs)
|
|
125
|
+
}
|
|
126
|
+
// 'literal' / 'identifier' / 'interval' / 'star' are leaves with no children.
|
|
127
|
+
}
|
package/src/parse/joins.js
CHANGED
|
@@ -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 {
|
|
83
|
+
/** @type {FromFunction} */
|
|
84
84
|
const fromFunction = {
|
|
85
85
|
type: 'function',
|
|
86
86
|
funcName,
|
package/src/parse/parse.js
CHANGED
|
@@ -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
|
-
|
|
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 (
|
|
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)
|
package/src/parse/tokenize.js
CHANGED
|
@@ -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.
|
|
5
|
+
* @import { Token } from '../types.js'
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
const NUMBER_REGEX = /^-?(?:\d+n|(?:\d+\.?\d*|\d*\.\d+)(?:[eE][+-]?\d+)?)/
|
package/src/plan/columns.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
11
|
-
* @import { QueryPlan, WindowSpec } from './types.
|
|
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
|
|
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
|
|
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 {
|
|
382
|
-
* @returns {
|
|
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 {
|
|
468
|
+
/** @type {HashJoinNode} */
|
|
461
469
|
const hashJoin = {
|
|
462
470
|
type: 'HashJoin',
|
|
463
471
|
joinType: join.joinType,
|
package/src/plan/types.d.ts
CHANGED
|
@@ -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'
|