squirreling 0.7.9 → 0.8.0
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 +46 -0
- package/package.json +6 -6
- package/src/backend/dataSource.js +52 -47
- package/src/execute/aggregates.js +150 -0
- package/src/execute/columns.js +0 -39
- package/src/execute/execute.js +158 -415
- package/src/execute/join.js +179 -333
- package/src/execute/sort.js +99 -0
- package/src/execute/utils.js +18 -49
- package/src/executionErrors.js +10 -10
- package/src/expression/binary.js +51 -0
- package/src/{execute → expression}/date.js +18 -18
- package/src/{execute/expression.js → expression/evaluate.js} +61 -62
- package/src/{execute → expression}/math.js +46 -81
- package/src/{execute → expression}/regexp.js +7 -7
- package/src/{execute → expression}/strings.js +33 -45
- package/src/index.d.ts +15 -1
- package/src/parse/expression.js +42 -50
- package/src/parse/joins.js +10 -11
- package/src/parse/parse.js +14 -3
- package/src/parse/state.js +2 -1
- package/src/parse/types.d.ts +30 -0
- package/src/plan/plan.js +234 -0
- package/src/plan/types.d.ts +101 -0
- package/src/types.d.ts +19 -39
- package/src/validation.js +66 -2
- package/src/validationErrors.js +9 -7
- package/src/execute/having.js +0 -202
- package/src/execute/tableSource.js +0 -63
package/src/execute/join.js
CHANGED
|
@@ -1,197 +1,218 @@
|
|
|
1
|
+
import { evaluateExpr } from '../expression/evaluate.js'
|
|
1
2
|
import { missingClauseError } from '../parseErrors.js'
|
|
2
|
-
import { evaluateExpr } from './expression.js'
|
|
3
|
-
import { resolveTableSource } from './tableSource.js'
|
|
4
3
|
import { stringify } from './utils.js'
|
|
4
|
+
import { executePlan } from './execute.js'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* @import {
|
|
7
|
+
* @import { AsyncCells, AsyncRow } from '../types.js'
|
|
8
|
+
* @import { ExecuteContext, HashJoinNode, NestedLoopJoinNode, PositionalJoinNode } from '../plan/types.js'
|
|
8
9
|
*/
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
|
-
* Executes
|
|
12
|
+
* Executes a nested loop join operation
|
|
12
13
|
*
|
|
13
|
-
* @param {
|
|
14
|
-
* @param {
|
|
15
|
-
* @
|
|
16
|
-
* @param {string} options.leftTable - name of the left table (for column prefixing)
|
|
17
|
-
* @param {Record<string, AsyncDataSource>} options.tables - all available tables
|
|
18
|
-
* @param {WithClause} [options.withClause] - WITH clause containing CTE definitions
|
|
19
|
-
* @param {Record<string, UserDefinedFunction>} [options.functions]
|
|
20
|
-
* @param {Function} [options.executeSelectFn] - function to execute SELECT for CTEs (passed to avoid circular dep)
|
|
21
|
-
* @param {AbortSignal} [options.signal]
|
|
22
|
-
* @returns {Promise<AsyncDataSource>} data source yielding joined rows
|
|
14
|
+
* @param {NestedLoopJoinNode} plan
|
|
15
|
+
* @param {ExecuteContext} context
|
|
16
|
+
* @yields {AsyncRow}
|
|
23
17
|
*/
|
|
24
|
-
export async function
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (joins.length === 1) {
|
|
29
|
-
const join = joins[0]
|
|
30
|
-
const rightSource = resolveTableSource(join.table, tables, withClause, executeSelectFn, functions, signal)
|
|
31
|
-
|
|
32
|
-
// Buffer right rows for hash index (required for hash join)
|
|
33
|
-
/** @type {AsyncRow[]} */
|
|
34
|
-
const rightRows = []
|
|
35
|
-
for await (const row of rightSource.scan({})) {
|
|
36
|
-
rightRows.push(row)
|
|
37
|
-
}
|
|
18
|
+
export async function* executeNestedLoopJoin(plan, context) {
|
|
19
|
+
const { tables, functions, signal } = context
|
|
20
|
+
const leftTable = plan.leftAlias
|
|
21
|
+
const rightTable = plan.rightAlias
|
|
38
22
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
async *scan(options) {
|
|
45
|
-
const { signal } = options
|
|
46
|
-
if (join.joinType === 'POSITIONAL') {
|
|
47
|
-
yield* positionalJoin({
|
|
48
|
-
leftRows: leftSource.scan(options),
|
|
49
|
-
rightRows,
|
|
50
|
-
leftTable: currentLeftTable,
|
|
51
|
-
rightTable,
|
|
52
|
-
signal,
|
|
53
|
-
})
|
|
54
|
-
} else {
|
|
55
|
-
yield* hashJoin({
|
|
56
|
-
leftRows: leftSource.scan(options), // Stream directly, not buffered
|
|
57
|
-
rightRows,
|
|
58
|
-
join,
|
|
59
|
-
leftTable: currentLeftTable,
|
|
60
|
-
rightTable,
|
|
61
|
-
tables,
|
|
62
|
-
functions,
|
|
63
|
-
signal,
|
|
64
|
-
})
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
}
|
|
23
|
+
if (!plan.condition) {
|
|
24
|
+
throw missingClauseError({
|
|
25
|
+
missing: 'ON condition',
|
|
26
|
+
context: 'JOIN',
|
|
27
|
+
})
|
|
68
28
|
}
|
|
69
29
|
|
|
70
|
-
//
|
|
30
|
+
// Buffer right rows
|
|
71
31
|
/** @type {AsyncRow[]} */
|
|
72
|
-
|
|
73
|
-
for await (const row of
|
|
74
|
-
|
|
32
|
+
const rightRows = []
|
|
33
|
+
for await (const row of executePlan(plan.right, context)) {
|
|
34
|
+
if (signal?.aborted) return
|
|
35
|
+
rightRows.push(row)
|
|
75
36
|
}
|
|
76
37
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
38
|
+
const rightCols = rightRows.length ? rightRows[0].columns : []
|
|
39
|
+
const rightPrefixedCols = prefixColumns(rightCols, rightTable)
|
|
40
|
+
|
|
41
|
+
/** @type {string[] | null} */
|
|
42
|
+
let leftPrefixedCols = null
|
|
43
|
+
/** @type {Set<AsyncRow> | null} */
|
|
44
|
+
const matchedRightRows = plan.joinType === 'RIGHT' || plan.joinType === 'FULL' ? new Set() : null
|
|
45
|
+
|
|
46
|
+
for await (const leftRow of executePlan(plan.left, context)) {
|
|
47
|
+
if (signal?.aborted) break
|
|
81
48
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
for await (const row of rightSource.scan({})) {
|
|
85
|
-
rightRows.push(row)
|
|
49
|
+
if (!leftPrefixedCols) {
|
|
50
|
+
leftPrefixedCols = prefixColumns(leftRow.columns, leftTable)
|
|
86
51
|
}
|
|
87
52
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
? positionalJoin({
|
|
96
|
-
leftRows,
|
|
97
|
-
rightRows,
|
|
98
|
-
leftTable: currentLeftTable,
|
|
99
|
-
rightTable,
|
|
100
|
-
})
|
|
101
|
-
: hashJoin({
|
|
102
|
-
leftRows,
|
|
103
|
-
rightRows,
|
|
104
|
-
join,
|
|
105
|
-
leftTable: currentLeftTable,
|
|
106
|
-
rightTable,
|
|
53
|
+
let hasMatch = false
|
|
54
|
+
|
|
55
|
+
for (const rightRow of rightRows) {
|
|
56
|
+
const tempMerged = mergeRows(leftRow, rightRow, leftTable, rightTable)
|
|
57
|
+
const matches = await evaluateExpr({
|
|
58
|
+
node: plan.condition,
|
|
59
|
+
row: tempMerged,
|
|
107
60
|
tables,
|
|
108
61
|
functions,
|
|
62
|
+
signal,
|
|
109
63
|
})
|
|
110
|
-
|
|
111
|
-
|
|
64
|
+
|
|
65
|
+
if (matches) {
|
|
66
|
+
hasMatch = true
|
|
67
|
+
if (matchedRightRows) matchedRightRows.add(rightRow)
|
|
68
|
+
yield tempMerged
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!hasMatch && (plan.joinType === 'LEFT' || plan.joinType === 'FULL')) {
|
|
73
|
+
const nullRight = createNullRow(rightPrefixedCols)
|
|
74
|
+
yield mergeRows(leftRow, nullRight, leftTable, rightTable)
|
|
112
75
|
}
|
|
113
|
-
|
|
76
|
+
}
|
|
114
77
|
|
|
115
|
-
|
|
116
|
-
|
|
78
|
+
// Unmatched right rows for RIGHT/FULL joins
|
|
79
|
+
if (matchedRightRows) {
|
|
80
|
+
for (const rightRow of rightRows) {
|
|
81
|
+
if (!matchedRightRows.has(rightRow)) {
|
|
82
|
+
const nullLeft = createNullRow(leftPrefixedCols || [])
|
|
83
|
+
yield mergeRows(nullLeft, rightRow, leftTable, rightTable)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
117
86
|
}
|
|
87
|
+
}
|
|
118
88
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Executes a positional join operation
|
|
91
|
+
*
|
|
92
|
+
* @param {PositionalJoinNode} plan
|
|
93
|
+
* @param {ExecuteContext} context
|
|
94
|
+
* @yields {AsyncRow}
|
|
95
|
+
*/
|
|
96
|
+
export async function* executePositionalJoin(plan, context) {
|
|
97
|
+
const { signal } = context
|
|
98
|
+
const leftTable = plan.leftAlias
|
|
99
|
+
const rightTable = plan.rightAlias
|
|
100
|
+
|
|
101
|
+
// Buffer both sides (required for positional join)
|
|
102
|
+
/** @type {AsyncRow[]} */
|
|
103
|
+
const leftRows = []
|
|
104
|
+
for await (const row of executePlan(plan.left, context)) {
|
|
105
|
+
if (signal?.aborted) return
|
|
106
|
+
leftRows.push(row)
|
|
107
|
+
}
|
|
122
108
|
|
|
123
109
|
/** @type {AsyncRow[]} */
|
|
124
110
|
const rightRows = []
|
|
125
|
-
for await (const row of
|
|
111
|
+
for await (const row of executePlan(plan.right, context)) {
|
|
112
|
+
if (signal?.aborted) return
|
|
126
113
|
rightRows.push(row)
|
|
127
114
|
}
|
|
128
115
|
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
rightTable,
|
|
141
|
-
signal,
|
|
142
|
-
})
|
|
143
|
-
} else {
|
|
144
|
-
yield* hashJoin({
|
|
145
|
-
leftRows,
|
|
146
|
-
rightRows,
|
|
147
|
-
join,
|
|
148
|
-
leftTable: currentLeftTable,
|
|
149
|
-
rightTable,
|
|
150
|
-
tables,
|
|
151
|
-
functions,
|
|
152
|
-
signal,
|
|
153
|
-
})
|
|
154
|
-
}
|
|
155
|
-
},
|
|
116
|
+
const maxLen = Math.max(leftRows.length, rightRows.length)
|
|
117
|
+
const leftCols = leftRows[0]?.columns ?? []
|
|
118
|
+
const rightCols = rightRows[0]?.columns ?? []
|
|
119
|
+
const leftPrefixedCols = prefixColumns(leftCols, leftTable)
|
|
120
|
+
const rightPrefixedCols = prefixColumns(rightCols, rightTable)
|
|
121
|
+
|
|
122
|
+
for (let i = 0; i < maxLen; i++) {
|
|
123
|
+
if (signal?.aborted) return
|
|
124
|
+
const leftRow = leftRows[i] ?? createNullRow(leftPrefixedCols)
|
|
125
|
+
const rightRow = rightRows[i] ?? createNullRow(rightPrefixedCols)
|
|
126
|
+
yield mergeRows(leftRow, rightRow, leftTable, rightTable)
|
|
156
127
|
}
|
|
157
128
|
}
|
|
158
129
|
|
|
159
130
|
/**
|
|
160
|
-
*
|
|
161
|
-
* Returns true if the expression is an identifier prefixed with the table name.
|
|
131
|
+
* Executes a hash join operation
|
|
162
132
|
*
|
|
163
|
-
* @param {
|
|
164
|
-
* @param {
|
|
165
|
-
* @
|
|
133
|
+
* @param {HashJoinNode} plan
|
|
134
|
+
* @param {ExecuteContext} context
|
|
135
|
+
* @yields {AsyncRow}
|
|
166
136
|
*/
|
|
167
|
-
function
|
|
168
|
-
|
|
169
|
-
|
|
137
|
+
export async function* executeHashJoin(plan, context) {
|
|
138
|
+
const { tables, functions, signal } = context
|
|
139
|
+
const leftTable = plan.leftAlias
|
|
140
|
+
const rightTable = plan.rightAlias
|
|
170
141
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
* @returns {{ leftKey: ExprNode, rightKey: ExprNode } | undefined}
|
|
179
|
-
*/
|
|
180
|
-
function extractJoinKeys(onCondition, leftTable, rightTable) {
|
|
181
|
-
if (onCondition.type === 'binary' && onCondition.op === '=') {
|
|
182
|
-
const { left, right } = onCondition
|
|
142
|
+
// Buffer right rows and build hash map
|
|
143
|
+
/** @type {AsyncRow[]} */
|
|
144
|
+
const rightRows = []
|
|
145
|
+
for await (const row of executePlan(plan.right, context)) {
|
|
146
|
+
if (signal?.aborted) return
|
|
147
|
+
rightRows.push(row)
|
|
148
|
+
}
|
|
183
149
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
150
|
+
/** @type {Map<string, AsyncRow[]>} */
|
|
151
|
+
const hashMap = new Map()
|
|
152
|
+
for (const rightRow of rightRows) {
|
|
153
|
+
const keyValue = await evaluateExpr({
|
|
154
|
+
node: plan.rightKey,
|
|
155
|
+
row: rightRow,
|
|
156
|
+
tables,
|
|
157
|
+
functions,
|
|
158
|
+
signal,
|
|
159
|
+
})
|
|
160
|
+
if (keyValue == null) continue
|
|
161
|
+
const keyStr = stringify(keyValue)
|
|
162
|
+
let bucket = hashMap.get(keyStr)
|
|
163
|
+
if (!bucket) {
|
|
164
|
+
bucket = []
|
|
165
|
+
hashMap.set(keyStr, bucket)
|
|
166
|
+
}
|
|
167
|
+
bucket.push(rightRow)
|
|
168
|
+
}
|
|
187
169
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
170
|
+
// Get column info for NULL row generation
|
|
171
|
+
const rightCols = rightRows.length ? rightRows[0].columns : []
|
|
172
|
+
const rightPrefixedCols = prefixColumns(rightCols, rightTable)
|
|
173
|
+
|
|
174
|
+
/** @type {string[] | null} */
|
|
175
|
+
let leftPrefixedCols = null
|
|
176
|
+
/** @type {Set<AsyncRow> | null} */
|
|
177
|
+
const matchedRightRows = plan.joinType === 'RIGHT' || plan.joinType === 'FULL' ? new Set() : null
|
|
178
|
+
|
|
179
|
+
// Probe phase: stream left rows
|
|
180
|
+
for await (const leftRow of executePlan(plan.left, context)) {
|
|
181
|
+
if (signal?.aborted) break
|
|
182
|
+
|
|
183
|
+
if (!leftPrefixedCols) {
|
|
184
|
+
leftPrefixedCols = prefixColumns(leftRow.columns, leftTable)
|
|
191
185
|
}
|
|
192
186
|
|
|
193
|
-
|
|
194
|
-
|
|
187
|
+
const keyValue = await evaluateExpr({
|
|
188
|
+
node: plan.leftKey,
|
|
189
|
+
row: leftRow,
|
|
190
|
+
tables,
|
|
191
|
+
functions,
|
|
192
|
+
signal,
|
|
193
|
+
})
|
|
194
|
+
const keyStr = stringify(keyValue)
|
|
195
|
+
const matchingRightRows = hashMap.get(keyStr)
|
|
196
|
+
|
|
197
|
+
if (matchingRightRows?.length) {
|
|
198
|
+
for (const rightRow of matchingRightRows) {
|
|
199
|
+
if (matchedRightRows) matchedRightRows.add(rightRow)
|
|
200
|
+
yield mergeRows(leftRow, rightRow, leftTable, rightTable)
|
|
201
|
+
}
|
|
202
|
+
} else if (plan.joinType === 'LEFT' || plan.joinType === 'FULL') {
|
|
203
|
+
const nullRight = createNullRow(rightPrefixedCols)
|
|
204
|
+
yield mergeRows(leftRow, nullRight, leftTable, rightTable)
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Unmatched right rows for RIGHT/FULL joins
|
|
209
|
+
if (matchedRightRows) {
|
|
210
|
+
for (const rightRow of rightRows) {
|
|
211
|
+
if (!matchedRightRows.has(rightRow)) {
|
|
212
|
+
const nullLeft = createNullRow(leftPrefixedCols || [])
|
|
213
|
+
yield mergeRows(nullLeft, rightRow, leftTable, rightTable)
|
|
214
|
+
}
|
|
215
|
+
}
|
|
195
216
|
}
|
|
196
217
|
}
|
|
197
218
|
|
|
@@ -252,187 +273,12 @@ function mergeRows(leftRow, rightRow, leftTable, rightTable) {
|
|
|
252
273
|
}
|
|
253
274
|
|
|
254
275
|
/**
|
|
255
|
-
*
|
|
256
|
-
* Matches rows by their index position (row 0 with row 0, row 1 with row 1, etc.).
|
|
257
|
-
* When tables have different lengths, the shorter table is padded with NULLs.
|
|
276
|
+
* Prefixes column names with table alias, keeping already-prefixed columns as-is
|
|
258
277
|
*
|
|
259
|
-
* @param {
|
|
260
|
-
* @param {
|
|
261
|
-
* @
|
|
262
|
-
* @param {string} params.leftTable - name of left table (for column prefixing)
|
|
263
|
-
* @param {string} params.rightTable - name of right table (for column prefixing, may be alias)
|
|
264
|
-
* @param {AbortSignal} [params.signal] - abort signal for cancellation
|
|
265
|
-
* @yields {AsyncRow} joined rows
|
|
278
|
+
* @param {string[]} cols
|
|
279
|
+
* @param {string} table
|
|
280
|
+
* @returns {string[]}
|
|
266
281
|
*/
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
/** @type {AsyncRow[]} */
|
|
270
|
-
const leftArr = []
|
|
271
|
-
for await (const row of leftRows) {
|
|
272
|
-
if (signal?.aborted) return
|
|
273
|
-
leftArr.push(row)
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const maxLen = Math.max(leftArr.length, rightRows.length)
|
|
277
|
-
|
|
278
|
-
// Get column info for NULL row creation
|
|
279
|
-
const leftCols = leftArr[0]?.columns ?? []
|
|
280
|
-
const rightCols = rightRows[0]?.columns ?? []
|
|
281
|
-
const leftPrefixedCols = leftCols.flatMap(col =>
|
|
282
|
-
col.includes('.') ? [col] : [`${leftTable}.${col}`, col]
|
|
283
|
-
)
|
|
284
|
-
const rightPrefixedCols = rightCols.flatMap(col =>
|
|
285
|
-
col.includes('.') ? [col] : [`${rightTable}.${col}`, col]
|
|
286
|
-
)
|
|
287
|
-
|
|
288
|
-
for (let i = 0; i < maxLen; i++) {
|
|
289
|
-
if (signal?.aborted) return
|
|
290
|
-
const leftRow = leftArr[i] ?? createNullRow(leftPrefixedCols)
|
|
291
|
-
const rightRow = rightRows[i] ?? createNullRow(rightPrefixedCols)
|
|
292
|
-
yield mergeRows(leftRow, rightRow, leftTable, rightTable)
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Performs a hash join between left and right row sets (streaming).
|
|
298
|
-
* Yields rows as they are found instead of buffering all results.
|
|
299
|
-
*
|
|
300
|
-
* @param {Object} params
|
|
301
|
-
* @param {AsyncIterable<AsyncRow>|AsyncRow[]} params.leftRows - rows from left table (can stream)
|
|
302
|
-
* @param {AsyncRow[]} params.rightRows - rows from right table (must be buffered for hash index)
|
|
303
|
-
* @param {JoinClause} params.join - join specification
|
|
304
|
-
* @param {string} params.leftTable - name of left table (for column prefixing)
|
|
305
|
-
* @param {string} params.rightTable - name of right table (for column prefixing, may be alias)
|
|
306
|
-
* @param {Record<string, AsyncDataSource>} params.tables - all tables for expression evaluation
|
|
307
|
-
* @param {Record<string, UserDefinedFunction>} [params.functions]
|
|
308
|
-
* @param {AbortSignal} [params.signal] - abort signal for cancellation
|
|
309
|
-
* @yields {AsyncRow} joined rows
|
|
310
|
-
*/
|
|
311
|
-
async function* hashJoin({ leftRows, rightRows, join, leftTable, rightTable, tables, functions, signal }) {
|
|
312
|
-
const { joinType, on: onCondition } = join
|
|
313
|
-
|
|
314
|
-
if (!onCondition) {
|
|
315
|
-
throw missingClauseError({
|
|
316
|
-
missing: 'ON condition',
|
|
317
|
-
context: 'JOIN',
|
|
318
|
-
})
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
const keys = extractJoinKeys(onCondition, leftTable, rightTable)
|
|
322
|
-
|
|
323
|
-
// Get column names for NULL row generation (right side is always buffered)
|
|
324
|
-
const rightCols = rightRows.length ? rightRows[0].columns : []
|
|
325
|
-
const rightPrefixedCols = rightCols.flatMap(col =>
|
|
326
|
-
col.includes('.') ? [col] : [`${rightTable}.${col}`, col]
|
|
327
|
-
)
|
|
328
|
-
|
|
329
|
-
// Track left column info - captured from first row during iteration
|
|
330
|
-
/** @type {string[]|null} */
|
|
331
|
-
let leftPrefixedCols = null
|
|
332
|
-
|
|
333
|
-
if (keys) {
|
|
334
|
-
// Hash join: build hash map on right table
|
|
335
|
-
/** @type {Map<string, AsyncRow[]>} */
|
|
336
|
-
const hashMap = new Map()
|
|
337
|
-
|
|
338
|
-
// BUILD PHASE: Index right rows by join key
|
|
339
|
-
// Skip null keys - SQL semantics: NULL != NULL
|
|
340
|
-
for (const rightRow of rightRows) {
|
|
341
|
-
const keyValue = await evaluateExpr({ node: keys.rightKey, row: rightRow, tables, functions })
|
|
342
|
-
if (keyValue == null) continue // NULL keys never match
|
|
343
|
-
const keyStr = stringify(keyValue)
|
|
344
|
-
let bucket = hashMap.get(keyStr)
|
|
345
|
-
if (!bucket) {
|
|
346
|
-
bucket = []
|
|
347
|
-
hashMap.set(keyStr, bucket)
|
|
348
|
-
}
|
|
349
|
-
bucket.push(rightRow)
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// Track which right rows matched (only needed for RIGHT/FULL joins)
|
|
353
|
-
/** @type {Set<AsyncRow>|null} */
|
|
354
|
-
const matchedRightRows = joinType === 'RIGHT' || joinType === 'FULL' ? new Set() : null
|
|
355
|
-
|
|
356
|
-
// PROBE PHASE: Stream through left rows, yield matches immediately
|
|
357
|
-
for await (const leftRow of leftRows) {
|
|
358
|
-
if (signal?.aborted) break
|
|
359
|
-
// Capture left column info from first row (for NULL row generation)
|
|
360
|
-
if (!leftPrefixedCols) {
|
|
361
|
-
leftPrefixedCols = leftRow.columns.flatMap(col =>
|
|
362
|
-
col.includes('.') ? [col] : [`${leftTable}.${col}`, col]
|
|
363
|
-
)
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
const keyValue = await evaluateExpr({ node: keys.leftKey, row: leftRow, tables, functions })
|
|
367
|
-
const keyStr = stringify(keyValue)
|
|
368
|
-
|
|
369
|
-
const matchingRightRows = hashMap.get(keyStr)
|
|
370
|
-
|
|
371
|
-
if (matchingRightRows && matchingRightRows.length > 0) {
|
|
372
|
-
for (const rightRow of matchingRightRows) {
|
|
373
|
-
if (matchedRightRows) matchedRightRows.add(rightRow)
|
|
374
|
-
yield mergeRows(leftRow, rightRow, leftTable, rightTable)
|
|
375
|
-
}
|
|
376
|
-
} else if (joinType === 'LEFT' || joinType === 'FULL') {
|
|
377
|
-
const nullRight = createNullRow(rightPrefixedCols)
|
|
378
|
-
yield mergeRows(leftRow, nullRight, leftTable, rightTable)
|
|
379
|
-
}
|
|
380
|
-
// INNER join with no match: don't yield anything
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// UNMATCHED PHASE: Handle unmatched right rows for RIGHT/FULL joins
|
|
384
|
-
if (matchedRightRows) {
|
|
385
|
-
for (const rightRow of rightRows) {
|
|
386
|
-
if (!matchedRightRows.has(rightRow)) {
|
|
387
|
-
// Use empty array if left table was empty (no rows to derive columns from)
|
|
388
|
-
const nullLeft = createNullRow(leftPrefixedCols || [])
|
|
389
|
-
yield mergeRows(nullLeft, rightRow, leftTable, rightTable)
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
} else {
|
|
394
|
-
// Fallback to nested loop for complex ON conditions
|
|
395
|
-
// Left rows stream through, right rows are iterated for each left row
|
|
396
|
-
/** @type {Set<AsyncRow>|null} */
|
|
397
|
-
const matchedRightRows = joinType === 'RIGHT' || joinType === 'FULL' ? new Set() : null
|
|
398
|
-
|
|
399
|
-
for await (const leftRow of leftRows) {
|
|
400
|
-
if (signal?.aborted) break
|
|
401
|
-
// Capture left column info from first row (for NULL row generation)
|
|
402
|
-
if (!leftPrefixedCols) {
|
|
403
|
-
leftPrefixedCols = leftRow.columns.flatMap(col =>
|
|
404
|
-
col.includes('.') ? [col] : [`${leftTable}.${col}`, col]
|
|
405
|
-
)
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
let hasMatch = false
|
|
409
|
-
|
|
410
|
-
for (const rightRow of rightRows) {
|
|
411
|
-
const tempMerged = mergeRows(leftRow, rightRow, leftTable, rightTable)
|
|
412
|
-
const matches = await evaluateExpr({ node: onCondition, row: tempMerged, tables, functions })
|
|
413
|
-
|
|
414
|
-
if (matches) {
|
|
415
|
-
hasMatch = true
|
|
416
|
-
if (matchedRightRows) matchedRightRows.add(rightRow)
|
|
417
|
-
yield tempMerged
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
if (!hasMatch && (joinType === 'LEFT' || joinType === 'FULL')) {
|
|
422
|
-
const nullRight = createNullRow(rightPrefixedCols)
|
|
423
|
-
yield mergeRows(leftRow, nullRight, leftTable, rightTable)
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// Handle unmatched right rows for RIGHT/FULL joins
|
|
428
|
-
if (matchedRightRows) {
|
|
429
|
-
for (const rightRow of rightRows) {
|
|
430
|
-
if (!matchedRightRows.has(rightRow)) {
|
|
431
|
-
// Use empty array if left table was empty (no rows to derive columns from)
|
|
432
|
-
const nullLeft = createNullRow(leftPrefixedCols || [])
|
|
433
|
-
yield mergeRows(nullLeft, rightRow, leftTable, rightTable)
|
|
434
|
-
}
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
}
|
|
282
|
+
function prefixColumns(cols, table) {
|
|
283
|
+
return cols.flatMap(col => col.includes('.') ? [col] : [`${table}.${col}`, col])
|
|
438
284
|
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { evaluateExpr } from '../expression/evaluate.js'
|
|
2
|
+
import { executePlan } from './execute.js'
|
|
3
|
+
import { compareForTerm } from './utils.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @import { AsyncRow, SqlPrimitive } from '../types.js'
|
|
7
|
+
* @import { ExecuteContext, SortNode } from '../plan/types.js'
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Executes a sort operation (ORDER BY)
|
|
12
|
+
*
|
|
13
|
+
* @param {SortNode} plan
|
|
14
|
+
* @param {ExecuteContext} context
|
|
15
|
+
* @yields {AsyncRow}
|
|
16
|
+
*/
|
|
17
|
+
export async function* executeSort(plan, context) {
|
|
18
|
+
const { tables, functions, signal } = context
|
|
19
|
+
|
|
20
|
+
// Buffer all rows
|
|
21
|
+
/** @type {AsyncRow[]} */
|
|
22
|
+
const rows = []
|
|
23
|
+
for await (const row of executePlan(plan.child, context)) {
|
|
24
|
+
if (signal?.aborted) return
|
|
25
|
+
rows.push(row)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (rows.length === 0) return
|
|
29
|
+
|
|
30
|
+
// Multi-pass lazy sorting
|
|
31
|
+
/** @type {(SqlPrimitive | undefined)[][]} */
|
|
32
|
+
const evaluatedValues = rows.map(() => Array(plan.orderBy.length))
|
|
33
|
+
|
|
34
|
+
/** @type {number[][]} */
|
|
35
|
+
let groups = [rows.map((_, i) => i)]
|
|
36
|
+
|
|
37
|
+
for (let orderByIdx = 0; orderByIdx < plan.orderBy.length; orderByIdx++) {
|
|
38
|
+
const term = plan.orderBy[orderByIdx]
|
|
39
|
+
/** @type {number[][]} */
|
|
40
|
+
const nextGroups = []
|
|
41
|
+
|
|
42
|
+
for (const group of groups) {
|
|
43
|
+
if (group.length <= 1) {
|
|
44
|
+
nextGroups.push(group)
|
|
45
|
+
continue
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Evaluate this column for all rows in the group
|
|
49
|
+
for (const idx of group) {
|
|
50
|
+
if (evaluatedValues[idx][orderByIdx] === undefined) {
|
|
51
|
+
evaluatedValues[idx][orderByIdx] = await evaluateExpr({
|
|
52
|
+
node: term.expr,
|
|
53
|
+
row: rows[idx],
|
|
54
|
+
tables,
|
|
55
|
+
functions,
|
|
56
|
+
aliases: plan.aliases,
|
|
57
|
+
signal,
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Sort the group by this column
|
|
63
|
+
group.sort((aIdx, bIdx) => {
|
|
64
|
+
const av = evaluatedValues[aIdx][orderByIdx]
|
|
65
|
+
const bv = evaluatedValues[bIdx][orderByIdx]
|
|
66
|
+
return compareForTerm(av, bv, term)
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// Split into sub-groups based on ties
|
|
70
|
+
if (orderByIdx < plan.orderBy.length - 1) {
|
|
71
|
+
/** @type {number[]} */
|
|
72
|
+
let currentSubGroup = [group[0]]
|
|
73
|
+
for (let i = 1; i < group.length; i++) {
|
|
74
|
+
const prevIdx = group[i - 1]
|
|
75
|
+
const currIdx = group[i]
|
|
76
|
+
const prevVal = evaluatedValues[prevIdx][orderByIdx]
|
|
77
|
+
const currVal = evaluatedValues[currIdx][orderByIdx]
|
|
78
|
+
|
|
79
|
+
if (compareForTerm(prevVal, currVal, term) === 0) {
|
|
80
|
+
currentSubGroup.push(currIdx)
|
|
81
|
+
} else {
|
|
82
|
+
nextGroups.push(currentSubGroup)
|
|
83
|
+
currentSubGroup = [currIdx]
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
nextGroups.push(currentSubGroup)
|
|
87
|
+
} else {
|
|
88
|
+
nextGroups.push(group)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
groups = nextGroups
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Yield sorted rows
|
|
96
|
+
for (const idx of groups.flat()) {
|
|
97
|
+
yield rows[idx]
|
|
98
|
+
}
|
|
99
|
+
}
|