squirreling 0.16.12 → 0.16.13
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 +1 -1
- package/src/expression/evaluate.js +8 -4
- package/src/plan/plan.js +86 -163
- package/src/plan/predicates.js +29 -0
- package/src/plan/subqueries.js +285 -0
- package/src/validation/windows.js +42 -0
package/package.json
CHANGED
|
@@ -158,10 +158,14 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
158
158
|
? { ...context, outerRow: row, outerAliases: new Set(outerScope) }
|
|
159
159
|
: context
|
|
160
160
|
const gen = executeStatement({ query: node.subquery, context: subContext, outerScope }).rows()
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
161
|
+
try {
|
|
162
|
+
const { value } = await gen.next()
|
|
163
|
+
if (!value) return null
|
|
164
|
+
if (!(await gen.next()).done) throw new Error('Scalar subquery returned more than one row')
|
|
165
|
+
return await value.cells[value.columns[0]]()
|
|
166
|
+
} finally {
|
|
167
|
+
await gen.return(undefined)
|
|
168
|
+
}
|
|
165
169
|
}
|
|
166
170
|
|
|
167
171
|
// Unary operators
|
package/src/plan/plan.js
CHANGED
|
@@ -2,13 +2,16 @@ import { dataSourceColumns } from '../backend/dataSource.js'
|
|
|
2
2
|
import { derivedAlias } from '../expression/alias.js'
|
|
3
3
|
import { parseSql } from '../parse/parse.js'
|
|
4
4
|
import { findAggregate } from '../validation/aggregates.js'
|
|
5
|
+
import { findWindow } from '../validation/windows.js'
|
|
5
6
|
import { ParseError } from '../validation/parseErrors.js'
|
|
6
7
|
import { ColumnNotFoundError, TableNotFoundError } from '../validation/tables.js'
|
|
7
8
|
import { validateNoIdentifiers, validateScan, validateTableRefs } from '../validation/tables.js'
|
|
8
9
|
import { collectColumnsFromExpr, collectScopeColumns, extractColumns, fromAlias, inferSelectSourceColumns, inferStatementColumns, statementScope, tableFunctionColumnNames } from './columns.js'
|
|
10
|
+
import { andPredicate, collectConjuncts } from './predicates.js'
|
|
11
|
+
import { decorrelateExists, decorrelateScalars } from './subqueries.js'
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
|
-
* @import { AsyncDataSource, DerivedColumn, ExprNode, FromFunction, FromSubquery, FromTable, IdentifierNode, JoinClause, OrderByItem, PlanSqlOptions, ScanOptions, SelectColumn, SelectStatement, SetOperationStatement, Statement
|
|
14
|
+
* @import { AsyncDataSource, DerivedColumn, ExprNode, FromFunction, FromSubquery, FromTable, IdentifierNode, JoinClause, OrderByItem, PlanSqlOptions, ScanOptions, SelectColumn, SelectStatement, SetOperationStatement, Statement } from '../types.js'
|
|
12
15
|
* @import { HashJoinNode, QueryPlan, SemiJoinNode, TableFunctionNode, WindowSpec } from './types.js'
|
|
13
16
|
*/
|
|
14
17
|
|
|
@@ -177,7 +180,7 @@ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns, outer
|
|
|
177
180
|
const scopeColumns = collectScopeColumns({ select, cteColumns, tables })
|
|
178
181
|
/** @type {Map<string, ExprNode>} */
|
|
179
182
|
const aliases = new Map()
|
|
180
|
-
|
|
183
|
+
let columns = select.columns.map(col => {
|
|
181
184
|
if (col.type === 'derived') {
|
|
182
185
|
validateTableRefs(col.expr, scopeTables, scopeColumns)
|
|
183
186
|
const expr = resolveAliases(col.expr, aliases)
|
|
@@ -193,7 +196,7 @@ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns, outer
|
|
|
193
196
|
}
|
|
194
197
|
return col
|
|
195
198
|
})
|
|
196
|
-
|
|
199
|
+
let orderBy = resolveOrderByAliases(select.orderBy, aliases)
|
|
197
200
|
|
|
198
201
|
// Validate qualified references in other clauses
|
|
199
202
|
validateTableRefs(select.where, scopeTables, scopeColumns)
|
|
@@ -298,9 +301,18 @@ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns, outer
|
|
|
298
301
|
}
|
|
299
302
|
select = { ...select, where: residual }
|
|
300
303
|
}
|
|
304
|
+
const scalar = decorrelateScalars({
|
|
305
|
+
select, columns, orderBy, scopeAliases, scopeColumns, sourceAlias, tables, cteColumns,
|
|
306
|
+
rewriteProjection: !useGrouping && !windows.length,
|
|
307
|
+
planQuery: query => planStatement({ stmt: query, tables, ctePlans, cteColumns }),
|
|
308
|
+
})
|
|
309
|
+
columns = scalar.columns
|
|
310
|
+
select = scalar.select
|
|
311
|
+
orderBy = scalar.orderBy
|
|
312
|
+
const { scalarJoins, scalarWhere } = scalar
|
|
301
313
|
if (!select.joins.length) {
|
|
302
314
|
hints.where = select.where
|
|
303
|
-
if (!needsBuffering && !select.distinct && !semiJoins.length) {
|
|
315
|
+
if (!needsBuffering && !select.distinct && !semiJoins.length && !scalarWhere) {
|
|
304
316
|
hints.limit = select.limit
|
|
305
317
|
hints.offset = select.offset
|
|
306
318
|
}
|
|
@@ -313,7 +325,7 @@ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns, outer
|
|
|
313
325
|
|
|
314
326
|
// Add JOINs
|
|
315
327
|
if (select.joins.length) {
|
|
316
|
-
plan = planJoin({ left: plan, joins: select.joins, leftTable: sourceAlias, ctePlans, cteColumns, perTableColumns, tables, outerScope })
|
|
328
|
+
plan = planJoin({ from: select.from, left: plan, joins: select.joins, leftTable: sourceAlias, ctePlans, cteColumns, perTableColumns, tables, outerScope })
|
|
317
329
|
}
|
|
318
330
|
|
|
319
331
|
// Whether FROM resolved to our own direct table scan
|
|
@@ -330,6 +342,17 @@ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns, outer
|
|
|
330
342
|
|
|
331
343
|
for (const semiJoin of semiJoins) plan = { ...semiJoin, left: plan }
|
|
332
344
|
|
|
345
|
+
for (const joinScalar of scalarJoins) plan = joinScalar(plan)
|
|
346
|
+
if (scalarWhere) plan = { type: 'Filter', condition: scalarWhere, child: plan }
|
|
347
|
+
|
|
348
|
+
// Bare stars must only include the original query's relations.
|
|
349
|
+
columns = scalarJoins.length ? columns.flatMap(col =>
|
|
350
|
+
col.type === 'star' && !col.table
|
|
351
|
+
? [sourceAlias, ...select.joins.map(join => join.alias ?? join.table)]
|
|
352
|
+
.filter(alias => alias !== undefined).map(table => ({ ...col, table }))
|
|
353
|
+
: [col]
|
|
354
|
+
) : columns
|
|
355
|
+
|
|
333
356
|
if (useGrouping) {
|
|
334
357
|
// Aggregation path: GROUP BY or scalar aggregate
|
|
335
358
|
// HAVING is integrated into aggregate nodes for access to group context
|
|
@@ -389,7 +412,7 @@ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns, outer
|
|
|
389
412
|
// So the order is: Sort -> Project -> Distinct -> Limit
|
|
390
413
|
|
|
391
414
|
// Fast path for SELECT * without joins
|
|
392
|
-
const isPassthrough = select.columns.length === 1 && select.columns[0].type === 'star' && !select.joins.length
|
|
415
|
+
const isPassthrough = select.columns.length === 1 && select.columns[0].type === 'star' && !select.joins.length && !scalarJoins.length
|
|
393
416
|
if (!isPassthrough) {
|
|
394
417
|
let projectColumns = columns
|
|
395
418
|
// When parent only needs specific columns, drop unneeded projections
|
|
@@ -401,7 +424,9 @@ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns, outer
|
|
|
401
424
|
// Normalize identifiers to match the child's cell-key layout, so the
|
|
402
425
|
// Project executor can look up cells by exact name instead of relying
|
|
403
426
|
// on the evaluator's suffix-search fallback.
|
|
404
|
-
const sourceColumns = inferSelectSourceColumns({ select, cteColumns, tables })
|
|
427
|
+
const sourceColumns = inferSelectSourceColumns({ select, cteColumns, tables }).map(name =>
|
|
428
|
+
scalarJoins.length && !name.includes('.') ? `${sourceAlias}.${name}` : name
|
|
429
|
+
)
|
|
405
430
|
projectColumns = projectColumns.map(col =>
|
|
406
431
|
col.type === 'derived'
|
|
407
432
|
? { ...col, expr: normalizeIdentifiers(col.expr, sourceColumns) }
|
|
@@ -414,7 +439,7 @@ function planSelect({ select, ctePlans, cteColumns, tables, parentColumns, outer
|
|
|
414
439
|
plan = { type: 'Distinct', child: plan }
|
|
415
440
|
}
|
|
416
441
|
|
|
417
|
-
if (!(isOwnScan && !needsBuffering && !select.distinct && !semiJoins.length) && (select.limit !== undefined || select.offset)) {
|
|
442
|
+
if (!(isOwnScan && !needsBuffering && !select.distinct && !semiJoins.length && !scalarWhere) && (select.limit !== undefined || select.offset)) {
|
|
418
443
|
if (select.limit !== undefined) pushLimitIntoSort(plan, select.limit, select.offset)
|
|
419
444
|
plan = { type: 'Limit', limit: select.limit, offset: select.offset, child: plan }
|
|
420
445
|
}
|
|
@@ -545,6 +570,7 @@ function planTableFunction(from) {
|
|
|
545
570
|
|
|
546
571
|
/**
|
|
547
572
|
* @param {object} options
|
|
573
|
+
* @param {FromTable | FromSubquery | FromFunction | undefined} options.from
|
|
548
574
|
* @param {QueryPlan} options.left - the left side of the join (FROM or previous joins)
|
|
549
575
|
* @param {JoinClause[]} options.joins - array of join clauses
|
|
550
576
|
* @param {string} options.leftTable - name/alias of the left table
|
|
@@ -555,9 +581,10 @@ function planTableFunction(from) {
|
|
|
555
581
|
* @param {string[]} [options.outerScope] - aliases from an outer query (for correlated subqueries)
|
|
556
582
|
* @returns {QueryPlan}
|
|
557
583
|
*/
|
|
558
|
-
function planJoin({ left, joins, leftTable, ctePlans, cteColumns, perTableColumns, tables, outerScope }) {
|
|
584
|
+
function planJoin({ from, left, joins, leftTable, ctePlans, cteColumns, perTableColumns, tables, outerScope }) {
|
|
559
585
|
let plan = left
|
|
560
586
|
let currentLeftTable = leftTable
|
|
587
|
+
const sources = [from]
|
|
561
588
|
|
|
562
589
|
// Running scope for lateral UNNEST arg validation — excludes the current
|
|
563
590
|
// join's own alias (self-reference) and later joins (forward reference).
|
|
@@ -569,6 +596,7 @@ function planJoin({ left, joins, leftTable, ctePlans, cteColumns, perTableColumn
|
|
|
569
596
|
|
|
570
597
|
for (const join of joins) {
|
|
571
598
|
const rightTable = join.alias ?? join.table
|
|
599
|
+
sources.push(join.fromFunction ?? joinSource(join))
|
|
572
600
|
|
|
573
601
|
// LATERAL table function: right side references left-side columns.
|
|
574
602
|
if (join.fromFunction) {
|
|
@@ -641,7 +669,7 @@ function planJoin({ left, joins, leftTable, ctePlans, cteColumns, perTableColumn
|
|
|
641
669
|
// `USING (cols)` desugars to an equi-condition `left.col = right.col` per
|
|
642
670
|
// column, which routes through the hash-join path like any other ON.
|
|
643
671
|
const condition = join.on ?? (join.using && buildUsingCondition(join.using, join))
|
|
644
|
-
const keys = condition && extractEquiKeys({ condition,
|
|
672
|
+
const keys = condition && extractEquiKeys({ condition, owner: joinColumnOwner(sources, tables, cteColumns), using: !!join.using })
|
|
645
673
|
if (keys) {
|
|
646
674
|
/** @type {HashJoinNode} */
|
|
647
675
|
const hashJoin = {
|
|
@@ -853,11 +881,11 @@ function buildUsingCondition(using, pos) {
|
|
|
853
881
|
*
|
|
854
882
|
* @param {object} options
|
|
855
883
|
* @param {ExprNode} options.condition
|
|
856
|
-
* @param {string} options.
|
|
857
|
-
* @param {
|
|
884
|
+
* @param {(expr: ExprNode) => string} options.owner
|
|
885
|
+
* @param {boolean} options.using
|
|
858
886
|
* @returns {{ leftKeys: ExprNode[], rightKeys: ExprNode[], residual?: ExprNode } | undefined}
|
|
859
887
|
*/
|
|
860
|
-
function extractEquiKeys({ condition,
|
|
888
|
+
function extractEquiKeys({ condition, owner, using }) {
|
|
861
889
|
/** @type {ExprNode[]} */
|
|
862
890
|
const conjuncts = []
|
|
863
891
|
collectConjuncts(condition, conjuncts)
|
|
@@ -868,7 +896,7 @@ function extractEquiKeys({ condition, leftTable, rightTable }) {
|
|
|
868
896
|
/** @type {ExprNode[]} */
|
|
869
897
|
const residuals = []
|
|
870
898
|
for (const conjunct of conjuncts) {
|
|
871
|
-
const eq = classifyEquiConjunct(conjunct,
|
|
899
|
+
const eq = classifyEquiConjunct(conjunct, owner, using)
|
|
872
900
|
if (eq) {
|
|
873
901
|
leftKeys.push(eq.leftKey)
|
|
874
902
|
rightKeys.push(eq.rightKey)
|
|
@@ -887,23 +915,6 @@ function extractEquiKeys({ condition, leftTable, rightTable }) {
|
|
|
887
915
|
return residual ? { leftKeys, rightKeys, residual } : { leftKeys, rightKeys }
|
|
888
916
|
}
|
|
889
917
|
|
|
890
|
-
/**
|
|
891
|
-
* Walks an ON expression, flattening top-level AND conjuncts. Non-AND nodes
|
|
892
|
-
* are pushed verbatim. Used to expose individual predicates so equi-keys can
|
|
893
|
-
* be lifted out of an AND chain.
|
|
894
|
-
*
|
|
895
|
-
* @param {ExprNode} node
|
|
896
|
-
* @param {ExprNode[]} out
|
|
897
|
-
*/
|
|
898
|
-
function collectConjuncts(node, out) {
|
|
899
|
-
if (node.type === 'binary' && node.op === 'AND') {
|
|
900
|
-
collectConjuncts(node.left, out)
|
|
901
|
-
collectConjuncts(node.right, out)
|
|
902
|
-
return
|
|
903
|
-
}
|
|
904
|
-
out.push(node)
|
|
905
|
-
}
|
|
906
|
-
|
|
907
918
|
/**
|
|
908
919
|
* Push local ON and WHERE conjuncts according to which side is preserved.
|
|
909
920
|
* Only recurse through this SELECT's join chain, never through shared CTEs
|
|
@@ -926,27 +937,11 @@ function pushJoinPredicates(plan, where, select, tables, cteColumns, inherited =
|
|
|
926
937
|
const rightSource = joinSource(last)
|
|
927
938
|
const sources = [...leftSources, rightSource]
|
|
928
939
|
const aliases = sources.map(source => fromAlias(source))
|
|
929
|
-
const
|
|
930
|
-
|
|
931
|
-
: source?.type === 'table'
|
|
932
|
-
? cteColumns?.get(source.table?.toLowerCase()) ?? dataSourceColumns(tables[source.table])
|
|
933
|
-
: [])
|
|
940
|
+
const owner = joinColumnOwner(sources, tables, cteColumns)
|
|
941
|
+
const columns = sources.map(source => sourceColumns(source, tables, cteColumns))
|
|
934
942
|
if (new Set(aliases).size !== aliases.length ||
|
|
935
943
|
columns.some(names => aliases.some(alias => names.includes(alias)))) return where
|
|
936
944
|
|
|
937
|
-
/**
|
|
938
|
-
* @param {ExprNode} expr
|
|
939
|
-
* @returns {'left' | 'right' | 'literal' | 'unsafe'}
|
|
940
|
-
*/
|
|
941
|
-
function owner(expr) {
|
|
942
|
-
if (expr.type === 'literal') return 'literal'
|
|
943
|
-
if (expr.type !== 'identifier') return 'unsafe'
|
|
944
|
-
const matches = columns.map((names, i) => names.includes(expr.name) ? i : -1).filter(i => i >= 0)
|
|
945
|
-
const index = expr.prefix ? aliases.indexOf(expr.prefix) : matches.length === 1 ? matches[0] : -1
|
|
946
|
-
if (index < 0 || !columns[index].includes(expr.name)) return 'unsafe'
|
|
947
|
-
return index < leftSources.length ? 'left' : 'right'
|
|
948
|
-
}
|
|
949
|
-
|
|
950
945
|
// Do not change the interpretation of ambiguous or misoriented hash keys.
|
|
951
946
|
if (plan.type === 'HashJoin' && (plan.leftKeys.some(key => owner(key) !== 'left') ||
|
|
952
947
|
plan.rightKeys.some(key => owner(key) !== 'right'))) return where
|
|
@@ -1122,39 +1117,59 @@ function rejectsNulls(expr, isMissing) {
|
|
|
1122
1117
|
}
|
|
1123
1118
|
|
|
1124
1119
|
/**
|
|
1125
|
-
* @param {
|
|
1126
|
-
* @param {
|
|
1127
|
-
* @
|
|
1120
|
+
* @param {FromTable | FromSubquery | FromFunction | undefined} source
|
|
1121
|
+
* @param {Record<string, AsyncDataSource>} [tables]
|
|
1122
|
+
* @param {Map<string, string[]>} [cteColumns]
|
|
1123
|
+
* @returns {string[]}
|
|
1124
|
+
*/
|
|
1125
|
+
function sourceColumns(source, tables, cteColumns) {
|
|
1126
|
+
if (source?.type === 'subquery') return inferStatementColumns({ stmt: source.query, tables, cteColumns })
|
|
1127
|
+
if (source?.type === 'function') return tableFunctionColumnNames(source)
|
|
1128
|
+
if (source?.type === 'table') return cteColumns?.get(source.table.toLowerCase()) ?? (tables?.[source.table] ? dataSourceColumns(tables[source.table]) : [])
|
|
1129
|
+
return []
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/**
|
|
1133
|
+
* Resolve references against every visible source, not the synthetic alias
|
|
1134
|
+
* used to merge rows from earlier joins. Unknown or ambiguous references
|
|
1135
|
+
* remain residual predicates rather than becoming incorrectly oriented keys.
|
|
1136
|
+
*
|
|
1137
|
+
* @param {(FromTable | FromSubquery | FromFunction | undefined)[]} sources
|
|
1138
|
+
* @param {Record<string, AsyncDataSource>} [tables]
|
|
1139
|
+
* @param {Map<string, string[]>} [cteColumns]
|
|
1140
|
+
* @returns {(expr: ExprNode) => 'left' | 'right' | 'literal' | 'unsafe'}
|
|
1128
1141
|
*/
|
|
1129
|
-
function
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1142
|
+
function joinColumnOwner(sources, tables, cteColumns) {
|
|
1143
|
+
const aliases = sources.map(source => fromAlias(source))
|
|
1144
|
+
const columns = sources.map(source => sourceColumns(source, tables, cteColumns))
|
|
1145
|
+
const unsafe = new Set(aliases).size !== aliases.length ||
|
|
1146
|
+
columns.some(names => aliases.some(alias => alias !== undefined && names.includes(alias)))
|
|
1147
|
+
return function owner(expr) {
|
|
1148
|
+
if (expr.type === 'literal') return 'literal'
|
|
1149
|
+
if (unsafe || expr.type !== 'identifier') return 'unsafe'
|
|
1150
|
+
const matches = columns.map((names, i) => names.includes(expr.name) ? i : -1).filter(i => i >= 0)
|
|
1151
|
+
const index = expr.prefix ? aliases.indexOf(expr.prefix) : matches.length === 1 ? matches[0] : -1
|
|
1152
|
+
if (index < 0) return 'unsafe'
|
|
1153
|
+
if (columns[index].length && !columns[index].includes(expr.name)) return 'unsafe'
|
|
1154
|
+
return index === sources.length - 1 ? 'right' : 'left'
|
|
1155
|
+
}
|
|
1133
1156
|
}
|
|
1134
1157
|
|
|
1135
1158
|
/**
|
|
1136
|
-
*
|
|
1137
|
-
*
|
|
1138
|
-
* Returns undefined when the conjunct is not a `<identifier> = <identifier>`
|
|
1139
|
-
* predicate. When the prefixes don't unambiguously identify a side, falls
|
|
1140
|
-
* through to the original orientation — matches the prior single-equi
|
|
1141
|
-
* behavior so unprefixed columns still produce a hash join.
|
|
1159
|
+
* Orient only equalities whose identifiers belong to opposite inputs.
|
|
1160
|
+
* USING explicitly binds its unqualified identifiers to their respective sides.
|
|
1142
1161
|
*
|
|
1143
1162
|
* @param {ExprNode} conjunct
|
|
1144
|
-
* @param {string}
|
|
1145
|
-
* @param {
|
|
1163
|
+
* @param {(expr: ExprNode) => string} owner
|
|
1164
|
+
* @param {boolean} using
|
|
1146
1165
|
* @returns {{ leftKey: ExprNode, rightKey: ExprNode } | undefined}
|
|
1147
1166
|
*/
|
|
1148
|
-
function classifyEquiConjunct(conjunct,
|
|
1167
|
+
function classifyEquiConjunct(conjunct, owner, using) {
|
|
1149
1168
|
if (conjunct.type !== 'binary' || conjunct.op !== '=') return undefined
|
|
1150
1169
|
const { left, right } = conjunct
|
|
1151
1170
|
if (left.type !== 'identifier' || right.type !== 'identifier') return undefined
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
if (leftRefsRight && rightRefsLeft) {
|
|
1155
|
-
return { leftKey: right, rightKey: left }
|
|
1156
|
-
}
|
|
1157
|
-
return { leftKey: left, rightKey: right }
|
|
1171
|
+
if (using || owner(left) === 'left' && owner(right) === 'right') return { leftKey: left, rightKey: right }
|
|
1172
|
+
if (owner(left) === 'right' && owner(right) === 'left') return { leftKey: right, rightKey: left }
|
|
1158
1173
|
}
|
|
1159
1174
|
|
|
1160
1175
|
/**
|
|
@@ -1297,47 +1312,6 @@ function expectNoWindowFunction(expr, clause) {
|
|
|
1297
1312
|
}
|
|
1298
1313
|
}
|
|
1299
1314
|
|
|
1300
|
-
/**
|
|
1301
|
-
* @param {ExprNode | undefined} expr
|
|
1302
|
-
* @returns {WindowFunctionNode | undefined}
|
|
1303
|
-
*/
|
|
1304
|
-
function findWindow(expr) {
|
|
1305
|
-
if (!expr) return undefined
|
|
1306
|
-
if (expr.type === 'window') return expr
|
|
1307
|
-
if (expr.type === 'binary') return findWindow(expr.left) || findWindow(expr.right)
|
|
1308
|
-
if (expr.type === 'unary') return findWindow(expr.argument)
|
|
1309
|
-
if (expr.type === 'function') {
|
|
1310
|
-
for (const arg of expr.args) {
|
|
1311
|
-
const found = findWindow(arg)
|
|
1312
|
-
if (found) return found
|
|
1313
|
-
}
|
|
1314
|
-
return undefined
|
|
1315
|
-
}
|
|
1316
|
-
if (expr.type === 'cast') return findWindow(expr.expr)
|
|
1317
|
-
if (expr.type === 'subscript') return findWindow(expr.expr) || findWindow(expr.index)
|
|
1318
|
-
if (expr.type === 'in valuelist') {
|
|
1319
|
-
const found = findWindow(expr.expr)
|
|
1320
|
-
if (found) return found
|
|
1321
|
-
for (const val of expr.values) {
|
|
1322
|
-
const f = findWindow(val)
|
|
1323
|
-
if (f) return f
|
|
1324
|
-
}
|
|
1325
|
-
return undefined
|
|
1326
|
-
}
|
|
1327
|
-
if (expr.type === 'case') {
|
|
1328
|
-
if (expr.caseExpr) {
|
|
1329
|
-
const f = findWindow(expr.caseExpr)
|
|
1330
|
-
if (f) return f
|
|
1331
|
-
}
|
|
1332
|
-
for (const w of expr.whenClauses) {
|
|
1333
|
-
const f = findWindow(w.condition) || findWindow(w.result)
|
|
1334
|
-
if (f) return f
|
|
1335
|
-
}
|
|
1336
|
-
if (expr.elseResult) return findWindow(expr.elseResult)
|
|
1337
|
-
}
|
|
1338
|
-
return undefined
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
1315
|
/**
|
|
1342
1316
|
* Checks if every SELECT column is a plain COUNT(*).
|
|
1343
1317
|
*
|
|
@@ -1356,54 +1330,3 @@ function isAllCountStar(columns) {
|
|
|
1356
1330
|
!col.expr.filter
|
|
1357
1331
|
)
|
|
1358
1332
|
}
|
|
1359
|
-
|
|
1360
|
-
/**
|
|
1361
|
-
* Pulls plain equality correlations out of a row-preserving EXISTS query.
|
|
1362
|
-
* Limits and aggregation must stay on the per-outer-row execution path.
|
|
1363
|
-
*
|
|
1364
|
-
* @param {Statement} query
|
|
1365
|
-
* @param {string[]} outerScope
|
|
1366
|
-
* @param {Record<string, AsyncDataSource>} [tables]
|
|
1367
|
-
* @param {Map<string, string[]>} [cteColumns]
|
|
1368
|
-
* @returns {{ query: SelectStatement, left: IdentifierNode[] } | undefined}
|
|
1369
|
-
*/
|
|
1370
|
-
function decorrelateExists(query, outerScope, tables, cteColumns) {
|
|
1371
|
-
if (query.type !== 'select' || query.limit !== undefined || query.offset ||
|
|
1372
|
-
query.groupBy.length || query.having ||
|
|
1373
|
-
query.columns.some(col => col.type === 'derived' && (findAggregate(col.expr) || col.expr.type === 'window'))) return
|
|
1374
|
-
const scope = statementScope(query) ?? []
|
|
1375
|
-
/** @type {IdentifierNode[]} */
|
|
1376
|
-
const left = []
|
|
1377
|
-
/** @type {DerivedColumn[]} */
|
|
1378
|
-
const columns = []
|
|
1379
|
-
/** @type {ExprNode[]} */
|
|
1380
|
-
const conjuncts = []
|
|
1381
|
-
if (query.where) collectConjuncts(query.where, conjuncts)
|
|
1382
|
-
/** @type {ExprNode | undefined} */
|
|
1383
|
-
let where
|
|
1384
|
-
for (const term of conjuncts) {
|
|
1385
|
-
const previousKeys = columns.length
|
|
1386
|
-
if (term.type === 'binary' && term.op === '=' && term.left.type === 'identifier' && term.right.type === 'identifier') {
|
|
1387
|
-
for (const [inner, outer] of [[term.left, term.right], [term.right, term.left]]) {
|
|
1388
|
-
if (inner.prefix && scope.includes(inner.prefix) && outer.prefix &&
|
|
1389
|
-
outerScope.includes(outer.prefix) && !scope.includes(outer.prefix)) {
|
|
1390
|
-
left.push(outer)
|
|
1391
|
-
columns.push({ type: 'derived', expr: inner, alias: `__exists_key_${columns.length}`, positionStart: inner.positionStart, positionEnd: inner.positionEnd })
|
|
1392
|
-
break
|
|
1393
|
-
}
|
|
1394
|
-
}
|
|
1395
|
-
if (columns.length > previousKeys) continue
|
|
1396
|
-
}
|
|
1397
|
-
where = andPredicate(where, term)
|
|
1398
|
-
}
|
|
1399
|
-
if (!left.length) return
|
|
1400
|
-
// Check every remaining clause, including nested subqueries and projections,
|
|
1401
|
-
// before discarding the projection and ordering that EXISTS does not read.
|
|
1402
|
-
/** @type {IdentifierNode[]} */
|
|
1403
|
-
const remaining = []
|
|
1404
|
-
collectColumnsFromExpr({ type: 'subquery', subquery: { ...query, where }, positionStart: query.positionStart, positionEnd: query.positionEnd }, remaining, undefined, {
|
|
1405
|
-
tables, cteColumns, outerAliases: new Set(outerScope),
|
|
1406
|
-
})
|
|
1407
|
-
if (remaining.length) return
|
|
1408
|
-
return { query: { ...query, columns, where, distinct: false, orderBy: [] }, left }
|
|
1409
|
-
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** @import { ExprNode } from '../types.js' */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Walks an ON expression, flattening top-level AND conjuncts. Non-AND nodes
|
|
5
|
+
* are pushed verbatim. Used to expose individual predicates so equi-keys can
|
|
6
|
+
* be lifted out of an AND chain.
|
|
7
|
+
*
|
|
8
|
+
* @param {ExprNode} node
|
|
9
|
+
* @param {ExprNode[]} out
|
|
10
|
+
*/
|
|
11
|
+
export function collectConjuncts(node, out) {
|
|
12
|
+
if (node.type === 'binary' && node.op === 'AND') {
|
|
13
|
+
collectConjuncts(node.left, out)
|
|
14
|
+
collectConjuncts(node.right, out)
|
|
15
|
+
return
|
|
16
|
+
}
|
|
17
|
+
out.push(node)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @param {ExprNode | undefined} left
|
|
22
|
+
* @param {ExprNode} right
|
|
23
|
+
* @returns {ExprNode}
|
|
24
|
+
*/
|
|
25
|
+
export function andPredicate(left, right) {
|
|
26
|
+
return left
|
|
27
|
+
? { type: 'binary', op: 'AND', left, right, positionStart: left.positionStart, positionEnd: right.positionEnd }
|
|
28
|
+
: right
|
|
29
|
+
}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import { derivedAlias } from '../expression/alias.js'
|
|
2
|
+
import { findAggregate } from '../validation/aggregates.js'
|
|
3
|
+
import { findWindow } from '../validation/windows.js'
|
|
4
|
+
import { collectColumnsFromExpr, statementScope } from './columns.js'
|
|
5
|
+
import { andPredicate, collectConjuncts } from './predicates.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @import { AsyncDataSource, DerivedColumn, ExprNode, IdentifierNode, OrderByItem, SelectColumn, SelectStatement, Statement } from '../types.js'
|
|
9
|
+
* @import { QueryPlan } from './types.js'
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Replaces eligible scalar aggregates with grouped LEFT JOINs and exact
|
|
14
|
+
* empty-group values. Returns correlated WHERE terms for filtering after joins.
|
|
15
|
+
*
|
|
16
|
+
* @param {object} options
|
|
17
|
+
* @param {SelectStatement} options.select
|
|
18
|
+
* @param {SelectColumn[]} options.columns
|
|
19
|
+
* @param {OrderByItem[]} options.orderBy
|
|
20
|
+
* @param {string[]} options.scopeAliases
|
|
21
|
+
* @param {Set<string>} options.scopeColumns
|
|
22
|
+
* @param {string | undefined} options.sourceAlias
|
|
23
|
+
* @param {boolean} options.rewriteProjection
|
|
24
|
+
* @param {(query: SelectStatement) => QueryPlan} options.planQuery
|
|
25
|
+
* @param {Record<string, AsyncDataSource>} [options.tables]
|
|
26
|
+
* @param {Map<string, string[]>} [options.cteColumns]
|
|
27
|
+
* @returns {{ select: SelectStatement, columns: SelectColumn[], orderBy: OrderByItem[], scalarJoins: ((left: QueryPlan) => QueryPlan)[], scalarWhere: ExprNode | undefined }}
|
|
28
|
+
*/
|
|
29
|
+
export function decorrelateScalars({ select, columns, orderBy, scopeAliases, scopeColumns, sourceAlias, rewriteProjection, planQuery, tables, cteColumns }) {
|
|
30
|
+
columns = [...columns]
|
|
31
|
+
// Keep original expressions for source column demand; synthetic scalar cells
|
|
32
|
+
// are computed above the scan and must never appear in pushdown hints.
|
|
33
|
+
/** @type {((left: QueryPlan) => QueryPlan)[]} */
|
|
34
|
+
const scalarJoins = []
|
|
35
|
+
/**
|
|
36
|
+
* @param {ExprNode} expr
|
|
37
|
+
* @returns {ExprNode}
|
|
38
|
+
*/
|
|
39
|
+
function rewriteScalar(expr) {
|
|
40
|
+
if (expr.type !== 'subquery' || select.limit === 0) return expr
|
|
41
|
+
const query = expr.subquery
|
|
42
|
+
if (query.type !== 'select' || query.columns.length !== 1) return expr
|
|
43
|
+
const column = query.columns[0]
|
|
44
|
+
if (column.type !== 'derived' || !findAggregate(column.expr) || findWindow(column.expr)) return expr
|
|
45
|
+
// Reuse EXISTS' residual-reference check without discarding the aggregate.
|
|
46
|
+
const keys = decorrelateExists(query, scopeAliases, tables, cteColumns, true)
|
|
47
|
+
if (!keys) return expr
|
|
48
|
+
const inner = keys.query.columns.map(col => col.type === 'derived' ? col.expr : column.expr)
|
|
49
|
+
// Hash equality is only equivalent to scalar equality for matching known
|
|
50
|
+
// primitive types. Unknown schemas and coercive comparisons stay per-row.
|
|
51
|
+
if (inner.some((key, i) => {
|
|
52
|
+
if (key.type !== 'identifier') return true
|
|
53
|
+
const type = correlationType(query, key, tables, cteColumns)
|
|
54
|
+
return !type || type !== correlationType(select, keys.left[i], tables, cteColumns)
|
|
55
|
+
})) return expr
|
|
56
|
+
let name = `__scalar_${scalarJoins.length}`
|
|
57
|
+
while ([...scopeColumns, ...scopeAliases].some(col => col === name || col.startsWith(name + '_'))) name += '_'
|
|
58
|
+
const position = { positionStart: expr.positionStart, positionEnd: expr.positionEnd }
|
|
59
|
+
const valueName = name + '_value'
|
|
60
|
+
const keyName = name + '_key'
|
|
61
|
+
const emptyAlias = name + '_empty'
|
|
62
|
+
const innerKey = scalarCorrelationKey(inner)
|
|
63
|
+
const outerKey = scalarCorrelationKey(keys.left)
|
|
64
|
+
/** @type {IdentifierNode} */
|
|
65
|
+
const joinedKey = { ...position, type: 'identifier', prefix: name, name: keyName }
|
|
66
|
+
/** @type {SelectStatement} */
|
|
67
|
+
const grouped = {
|
|
68
|
+
...keys.query,
|
|
69
|
+
groupBy: [innerKey],
|
|
70
|
+
// Evaluate the original projection before introducing synthetic aliases.
|
|
71
|
+
columns: [
|
|
72
|
+
{ ...column, alias: valueName },
|
|
73
|
+
{ ...position, type: 'derived', expr: innerKey, alias: keyName },
|
|
74
|
+
],
|
|
75
|
+
}
|
|
76
|
+
const right = planQuery(grouped)
|
|
77
|
+
const scope = statementScope(query) ?? []
|
|
78
|
+
// A scalar aggregate over zero rows computes the exact empty-group value,
|
|
79
|
+
// including COUNT arithmetic and aggregate expressions with lazy branches.
|
|
80
|
+
/** @type {QueryPlan} */
|
|
81
|
+
const empty = {
|
|
82
|
+
type: 'Subquery', scope,
|
|
83
|
+
child: {
|
|
84
|
+
type: 'ScalarAggregate', columns: [{ ...column, alias: valueName }],
|
|
85
|
+
child: { type: 'Limit', limit: 0, child: { type: 'SingleRow' } },
|
|
86
|
+
},
|
|
87
|
+
}
|
|
88
|
+
/** @type {ExprNode | undefined} */
|
|
89
|
+
let residual
|
|
90
|
+
for (const key of keys.left) {
|
|
91
|
+
// Map treats NaN as equal to itself; SQL equality does not.
|
|
92
|
+
residual = andPredicate(residual, { ...position, type: 'binary', op: '=', left: key, right: key })
|
|
93
|
+
}
|
|
94
|
+
scalarJoins.push(left => ({
|
|
95
|
+
type: 'HashJoin', joinType: 'LEFT', leftAlias: sourceAlias ?? '', rightAlias: name,
|
|
96
|
+
leftKeys: [outerKey], rightKeys: [{ ...joinedKey, prefix: undefined }], residual,
|
|
97
|
+
right: { type: 'Subquery', child: right, scope },
|
|
98
|
+
left: {
|
|
99
|
+
type: 'NestedLoopJoin', joinType: 'INNER', leftAlias: sourceAlias ?? '', rightAlias: emptyAlias, right: empty,
|
|
100
|
+
condition: { ...position, type: 'literal', value: true }, left,
|
|
101
|
+
},
|
|
102
|
+
}))
|
|
103
|
+
return {
|
|
104
|
+
...position, type: 'case',
|
|
105
|
+
whenClauses: [{
|
|
106
|
+
...position,
|
|
107
|
+
condition: { ...position, type: 'unary', op: 'IS NULL', argument: joinedKey },
|
|
108
|
+
result: { ...position, type: 'identifier', prefix: emptyAlias, name: valueName },
|
|
109
|
+
}],
|
|
110
|
+
elseResult: { ...position, type: 'identifier', prefix: name, name: valueName },
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Outer aggregates need group-aware placement, so retain their existing path.
|
|
114
|
+
if (rewriteProjection) {
|
|
115
|
+
for (let i = 0; i < columns.length; i++) {
|
|
116
|
+
const col = columns[i]
|
|
117
|
+
if (col.type === 'derived') {
|
|
118
|
+
const before = scalarJoins.length
|
|
119
|
+
const expr = mapScalarExpressions(col.expr, rewriteScalar)
|
|
120
|
+
if (scalarJoins.length > before) columns[i] = { ...col, alias: col.alias ?? derivedAlias(col.expr), expr }
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
orderBy = orderBy.map(term => ({ ...term, expr: mapScalarExpressions(term.expr, rewriteScalar) }))
|
|
124
|
+
}
|
|
125
|
+
/** @type {ExprNode | undefined} */
|
|
126
|
+
let scalarWhere
|
|
127
|
+
if (select.where) {
|
|
128
|
+
/** @type {ExprNode[]} */
|
|
129
|
+
const terms = []
|
|
130
|
+
collectConjuncts(select.where, terms)
|
|
131
|
+
/** @type {ExprNode | undefined} */
|
|
132
|
+
let scanWhere
|
|
133
|
+
for (const term of terms) {
|
|
134
|
+
const before = scalarJoins.length
|
|
135
|
+
const rewritten = mapScalarExpressions(term, rewriteScalar)
|
|
136
|
+
if (scalarJoins.length > before) scalarWhere = andPredicate(scalarWhere, rewritten)
|
|
137
|
+
else scanWhere = andPredicate(scanWhere, term)
|
|
138
|
+
}
|
|
139
|
+
select = { ...select, where: scanWhere }
|
|
140
|
+
}
|
|
141
|
+
return { select, columns, orderBy, scalarJoins, scalarWhere }
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Pulls plain equality correlations out of EXISTS or scalar aggregate queries.
|
|
146
|
+
* Limits, HAVING, and existing grouping stay on the per-outer-row path.
|
|
147
|
+
*
|
|
148
|
+
* @param {Statement} query
|
|
149
|
+
* @param {string[]} outerScope
|
|
150
|
+
* @param {Record<string, AsyncDataSource>} [tables]
|
|
151
|
+
* @param {Map<string, string[]>} [cteColumns]
|
|
152
|
+
* @param {boolean} [aggregate]
|
|
153
|
+
* @returns {{ query: SelectStatement, left: IdentifierNode[] } | undefined}
|
|
154
|
+
*/
|
|
155
|
+
export function decorrelateExists(query, outerScope, tables, cteColumns, aggregate = false) {
|
|
156
|
+
if (query.type !== 'select' || query.limit !== undefined || query.offset ||
|
|
157
|
+
query.groupBy.length || query.having ||
|
|
158
|
+
!aggregate && query.columns.some(col => col.type === 'derived' && (findAggregate(col.expr) || col.expr.type === 'window'))) return
|
|
159
|
+
const scope = statementScope(query) ?? []
|
|
160
|
+
/** @type {IdentifierNode[]} */
|
|
161
|
+
const left = []
|
|
162
|
+
/** @type {DerivedColumn[]} */
|
|
163
|
+
const columns = []
|
|
164
|
+
/** @type {ExprNode[]} */
|
|
165
|
+
const conjuncts = []
|
|
166
|
+
if (query.where) collectConjuncts(query.where, conjuncts)
|
|
167
|
+
/** @type {ExprNode | undefined} */
|
|
168
|
+
let where
|
|
169
|
+
for (const term of conjuncts) {
|
|
170
|
+
const previousKeys = columns.length
|
|
171
|
+
if (term.type === 'binary' && term.op === '=' && term.left.type === 'identifier' && term.right.type === 'identifier') {
|
|
172
|
+
for (const [inner, outer] of [[term.left, term.right], [term.right, term.left]]) {
|
|
173
|
+
if (inner.prefix && scope.includes(inner.prefix) && outer.prefix &&
|
|
174
|
+
outerScope.includes(outer.prefix) && !scope.includes(outer.prefix)) {
|
|
175
|
+
left.push(outer)
|
|
176
|
+
columns.push({ type: 'derived', expr: inner, alias: `__exists_key_${columns.length}`, positionStart: inner.positionStart, positionEnd: inner.positionEnd })
|
|
177
|
+
break
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (columns.length > previousKeys) continue
|
|
181
|
+
}
|
|
182
|
+
where = andPredicate(where, term)
|
|
183
|
+
}
|
|
184
|
+
if (!left.length) return
|
|
185
|
+
// Check every remaining clause, including nested subqueries and projections,
|
|
186
|
+
// before discarding the projection and ordering that EXISTS does not read.
|
|
187
|
+
/** @type {IdentifierNode[]} */
|
|
188
|
+
const remaining = []
|
|
189
|
+
collectColumnsFromExpr({ type: 'subquery', subquery: { ...query, where }, positionStart: query.positionStart, positionEnd: query.positionEnd }, remaining, undefined, {
|
|
190
|
+
tables, cteColumns, outerAliases: new Set(outerScope),
|
|
191
|
+
})
|
|
192
|
+
if (remaining.length) return
|
|
193
|
+
return { query: { ...query, columns, where, distinct: false, orderBy: [] }, left }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Rewrites scalar expressions without entering a nested lexical query scope.
|
|
198
|
+
* @param {ExprNode} node
|
|
199
|
+
* @param {(node: ExprNode) => ExprNode} rewrite
|
|
200
|
+
* @returns {ExprNode}
|
|
201
|
+
*/
|
|
202
|
+
function mapScalarExpressions(node, rewrite) {
|
|
203
|
+
if (!node) return node
|
|
204
|
+
if (node.type === 'unary') {
|
|
205
|
+
return { ...node, argument: mapScalarExpressions(node.argument, rewrite) }
|
|
206
|
+
}
|
|
207
|
+
if (node.type === 'binary') {
|
|
208
|
+
return { ...node, left: mapScalarExpressions(node.left, rewrite), right: mapScalarExpressions(node.right, rewrite) }
|
|
209
|
+
}
|
|
210
|
+
if (node.type === 'function') {
|
|
211
|
+
return { ...node, args: node.args.map(arg => mapScalarExpressions(arg, rewrite)) }
|
|
212
|
+
}
|
|
213
|
+
if (node.type === 'cast') {
|
|
214
|
+
return { ...node, expr: mapScalarExpressions(node.expr, rewrite) }
|
|
215
|
+
}
|
|
216
|
+
if (node.type === 'in valuelist') {
|
|
217
|
+
return {
|
|
218
|
+
...node,
|
|
219
|
+
expr: mapScalarExpressions(node.expr, rewrite),
|
|
220
|
+
values: node.values.map(v => mapScalarExpressions(v, rewrite)),
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
if (node.type === 'subscript') {
|
|
224
|
+
return {
|
|
225
|
+
...node,
|
|
226
|
+
expr: mapScalarExpressions(node.expr, rewrite),
|
|
227
|
+
index: mapScalarExpressions(node.index, rewrite),
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
if (node.type === 'case') {
|
|
231
|
+
return {
|
|
232
|
+
...node,
|
|
233
|
+
caseExpr: mapScalarExpressions(node.caseExpr, rewrite),
|
|
234
|
+
whenClauses: node.whenClauses.map(w => ({
|
|
235
|
+
...w,
|
|
236
|
+
condition: mapScalarExpressions(w.condition, rewrite),
|
|
237
|
+
result: mapScalarExpressions(w.result, rewrite),
|
|
238
|
+
})),
|
|
239
|
+
elseResult: mapScalarExpressions(node.elseResult, rewrite),
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return rewrite(node)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Looks up a correlation column's declared primitive type in its lexical scope.
|
|
247
|
+
* Derived relations and CTEs need type propagation before they can use this rewrite.
|
|
248
|
+
* @param {SelectStatement} query
|
|
249
|
+
* @param {IdentifierNode} key
|
|
250
|
+
* @param {Record<string, AsyncDataSource>} [tables]
|
|
251
|
+
* @param {Map<string, string[]>} [cteColumns]
|
|
252
|
+
* @returns {string | undefined}
|
|
253
|
+
*/
|
|
254
|
+
function correlationType(query, key, tables, cteColumns) {
|
|
255
|
+
const { from } = query
|
|
256
|
+
const table = from?.type === 'table' && (from.alias ?? from.table) === key.prefix ? from.table
|
|
257
|
+
: query.joins.find(join => !join.subquery && !join.fromFunction && (join.alias ?? join.table) === key.prefix)?.table
|
|
258
|
+
if (!table || cteColumns?.has(table)) return
|
|
259
|
+
const type = tables?.[table]?.schema?.fields.find(field => field.name === key.name)?.dataType.type
|
|
260
|
+
return type && ['string', 'number', 'bigint', 'boolean'].includes(type) ? type : undefined
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Encodes composite keys with length-prefixed components so ordinary hash
|
|
265
|
+
* grouping and joins cannot confuse tuple boundaries. CONCAT propagates NULL.
|
|
266
|
+
* @param {ExprNode[]} keys
|
|
267
|
+
* @returns {ExprNode}
|
|
268
|
+
*/
|
|
269
|
+
function scalarCorrelationKey(keys) {
|
|
270
|
+
if (keys.length === 1) return keys[0]
|
|
271
|
+
const { positionStart, positionEnd } = keys[0]
|
|
272
|
+
const position = { positionStart, positionEnd }
|
|
273
|
+
return {
|
|
274
|
+
...position, type: 'function', funcName: 'CONCAT',
|
|
275
|
+
args: keys.flatMap(key => {
|
|
276
|
+
/** @type {ExprNode} */
|
|
277
|
+
const text = { ...position, type: 'cast', expr: key, toType: 'TEXT' }
|
|
278
|
+
return [
|
|
279
|
+
/** @type {ExprNode} */ { ...position, type: 'function', funcName: 'LENGTH', args: [text] },
|
|
280
|
+
/** @type {ExprNode} */ { ...position, type: 'literal', value: ':' },
|
|
281
|
+
text,
|
|
282
|
+
]
|
|
283
|
+
}),
|
|
284
|
+
}
|
|
285
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/** @import { ExprNode, WindowFunctionNode } from '../types.js' */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @param {ExprNode | undefined} expr
|
|
5
|
+
* @returns {WindowFunctionNode | undefined}
|
|
6
|
+
*/
|
|
7
|
+
export function findWindow(expr) {
|
|
8
|
+
if (!expr) return undefined
|
|
9
|
+
if (expr.type === 'window') return expr
|
|
10
|
+
if (expr.type === 'binary') return findWindow(expr.left) || findWindow(expr.right)
|
|
11
|
+
if (expr.type === 'unary') return findWindow(expr.argument)
|
|
12
|
+
if (expr.type === 'function') {
|
|
13
|
+
for (const arg of expr.args) {
|
|
14
|
+
const found = findWindow(arg)
|
|
15
|
+
if (found) return found
|
|
16
|
+
}
|
|
17
|
+
return undefined
|
|
18
|
+
}
|
|
19
|
+
if (expr.type === 'cast') return findWindow(expr.expr)
|
|
20
|
+
if (expr.type === 'subscript') return findWindow(expr.expr) || findWindow(expr.index)
|
|
21
|
+
if (expr.type === 'in valuelist') {
|
|
22
|
+
const found = findWindow(expr.expr)
|
|
23
|
+
if (found) return found
|
|
24
|
+
for (const val of expr.values) {
|
|
25
|
+
const f = findWindow(val)
|
|
26
|
+
if (f) return f
|
|
27
|
+
}
|
|
28
|
+
return undefined
|
|
29
|
+
}
|
|
30
|
+
if (expr.type === 'case') {
|
|
31
|
+
if (expr.caseExpr) {
|
|
32
|
+
const f = findWindow(expr.caseExpr)
|
|
33
|
+
if (f) return f
|
|
34
|
+
}
|
|
35
|
+
for (const w of expr.whenClauses) {
|
|
36
|
+
const f = findWindow(w.condition) || findWindow(w.result)
|
|
37
|
+
if (f) return f
|
|
38
|
+
}
|
|
39
|
+
if (expr.elseResult) return findWindow(expr.elseResult)
|
|
40
|
+
}
|
|
41
|
+
return undefined
|
|
42
|
+
}
|