squirreling 0.11.5 → 0.12.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 +7 -4
- package/package.json +1 -1
- package/src/backend/dataSource.js +37 -6
- package/src/execute/aggregates.js +89 -72
- package/src/execute/execute.js +314 -224
- package/src/execute/join.js +174 -152
- package/src/execute/sort.js +71 -64
- package/src/execute/utils.js +40 -5
- package/src/expression/evaluate.js +7 -7
- package/src/index.d.ts +5 -4
- package/src/types.d.ts +12 -2
package/README.md
CHANGED
|
@@ -34,20 +34,23 @@ const users = [
|
|
|
34
34
|
]
|
|
35
35
|
|
|
36
36
|
// Squirreling return types
|
|
37
|
+
interface QueryResults {
|
|
38
|
+
rows: () => AsyncGenerator<AsyncRow>
|
|
39
|
+
}
|
|
37
40
|
interface AsyncRow {
|
|
38
41
|
columns: string[]
|
|
39
42
|
cells: Record<string, AsyncCell>
|
|
40
43
|
}
|
|
41
44
|
type AsyncCell = () => Promise<SqlPrimitive>
|
|
42
45
|
|
|
43
|
-
// Returns
|
|
44
|
-
const
|
|
46
|
+
// Returns a QueryResults object with streaming rows
|
|
47
|
+
const { rows } = executeSql({
|
|
45
48
|
tables: { users },
|
|
46
49
|
query: 'SELECT * FROM users',
|
|
47
50
|
})
|
|
48
51
|
|
|
49
52
|
// Process rows as they arrive (streaming)
|
|
50
|
-
for await (const { cells } of
|
|
53
|
+
for await (const { cells } of rows()) {
|
|
51
54
|
console.log(`User id=${await cells.id()}, name=${await cells.name()}`)
|
|
52
55
|
}
|
|
53
56
|
```
|
|
@@ -105,7 +108,7 @@ interface ScanOptions {
|
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
interface ScanResults {
|
|
108
|
-
rows: AsyncIterable<AsyncRow> // async iterable of rows
|
|
111
|
+
rows(): AsyncIterable<AsyncRow> // async iterable of rows
|
|
109
112
|
appliedWhere: boolean // WHERE filter applied at scan time?
|
|
110
113
|
appliedLimitOffset: boolean // LIMIT and OFFSET applied at scan time?
|
|
111
114
|
}
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @import { AsyncCells, AsyncDataSource, AsyncRow, SqlPrimitive } from '../types.js'
|
|
2
|
+
* @import { AsyncCells, AsyncDataSource, AsyncRow, ScanColumnOptions, SqlPrimitive } from '../types.js'
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -55,12 +55,12 @@ export function memorySource({ data, columns }) {
|
|
|
55
55
|
const start = !where ? offset ?? 0 : 0
|
|
56
56
|
const end = !where && limit !== undefined ? start + limit : data.length
|
|
57
57
|
return {
|
|
58
|
-
|
|
58
|
+
async *rows() {
|
|
59
59
|
for (let i = start; i < end && i < data.length; i++) {
|
|
60
60
|
if (signal?.aborted) break
|
|
61
61
|
yield asyncRow(data[i], scanColumns ?? columns)
|
|
62
62
|
}
|
|
63
|
-
}
|
|
63
|
+
},
|
|
64
64
|
appliedWhere: false,
|
|
65
65
|
appliedLimitOffset: !where,
|
|
66
66
|
}
|
|
@@ -92,9 +92,9 @@ export function cachedDataSource(source) {
|
|
|
92
92
|
const indexOffset = appliedLimitOffset && options.offset ? options.offset : 0
|
|
93
93
|
|
|
94
94
|
return {
|
|
95
|
-
|
|
95
|
+
async *rows() {
|
|
96
96
|
let index = 0
|
|
97
|
-
for await (const row of rows) {
|
|
97
|
+
for await (const row of rows()) {
|
|
98
98
|
if (options.signal?.aborted) break
|
|
99
99
|
const rowIndex = index + indexOffset
|
|
100
100
|
/** @type {AsyncCells} */
|
|
@@ -115,10 +115,41 @@ export function cachedDataSource(source) {
|
|
|
115
115
|
yield { columns: row.columns, cells }
|
|
116
116
|
index++
|
|
117
117
|
}
|
|
118
|
-
}
|
|
118
|
+
},
|
|
119
119
|
appliedWhere,
|
|
120
120
|
appliedLimitOffset,
|
|
121
121
|
}
|
|
122
122
|
},
|
|
123
|
+
...source.scanColumn && {
|
|
124
|
+
/**
|
|
125
|
+
* @param {ScanColumnOptions} options
|
|
126
|
+
* @returns {AsyncIterable<ArrayLike<SqlPrimitive>>}
|
|
127
|
+
*/
|
|
128
|
+
scanColumn(options) {
|
|
129
|
+
const inner = source.scanColumn(options)
|
|
130
|
+
const indexOffset = options.offset ?? 0
|
|
131
|
+
return (async function* () {
|
|
132
|
+
let chunkStart = 0
|
|
133
|
+
for await (const chunk of inner) {
|
|
134
|
+
if (options.signal?.aborted) break
|
|
135
|
+
/** @type {SqlPrimitive[]} */
|
|
136
|
+
const cached = new Array(chunk.length)
|
|
137
|
+
for (let i = 0; i < chunk.length; i++) {
|
|
138
|
+
const cacheKey = `${chunkStart + i + indexOffset}:${options.column}`
|
|
139
|
+
const existing = cache.get(cacheKey)
|
|
140
|
+
if (existing) {
|
|
141
|
+
cached[i] = await existing
|
|
142
|
+
} else {
|
|
143
|
+
const value = chunk[i]
|
|
144
|
+
cache.set(cacheKey, Promise.resolve(value))
|
|
145
|
+
cached[i] = value
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
yield cached
|
|
149
|
+
chunkStart += chunk.length
|
|
150
|
+
}
|
|
151
|
+
})()
|
|
152
|
+
},
|
|
153
|
+
},
|
|
123
154
|
}
|
|
124
155
|
}
|
|
@@ -4,7 +4,7 @@ import { executePlan } from './execute.js'
|
|
|
4
4
|
import { keyify } from './utils.js'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* @import { AsyncCells, AsyncDataSource, AsyncRow, DerivedColumn, ExecuteContext, SelectColumn, SqlPrimitive } from '../types.js'
|
|
7
|
+
* @import { AsyncCells, AsyncDataSource, AsyncRow, DerivedColumn, ExecuteContext, QueryResults, SelectColumn, SqlPrimitive } from '../types.js'
|
|
8
8
|
* @import { HashAggregateNode, ScalarAggregateNode } from '../plan/types.js'
|
|
9
9
|
*/
|
|
10
10
|
|
|
@@ -55,52 +55,58 @@ function projectAggregateColumns(selectColumns, group, context) {
|
|
|
55
55
|
*
|
|
56
56
|
* @param {HashAggregateNode} plan
|
|
57
57
|
* @param {ExecuteContext} context
|
|
58
|
-
* @
|
|
58
|
+
* @returns {QueryResults}
|
|
59
59
|
*/
|
|
60
|
-
export
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
60
|
+
export function executeHashAggregate(plan, context) {
|
|
61
|
+
const child = executePlan({ plan: plan.child, context })
|
|
62
|
+
return {
|
|
63
|
+
maxRows: child.maxRows,
|
|
64
|
+
async *rows () {
|
|
65
|
+
// Collect all rows
|
|
66
|
+
/** @type {AsyncRow[]} */
|
|
67
|
+
const allRows = []
|
|
68
|
+
for await (const row of child.rows()) {
|
|
69
|
+
if (context.signal?.aborted) return
|
|
70
|
+
allRows.push(row)
|
|
71
|
+
}
|
|
68
72
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
// Group rows by GROUP BY keys
|
|
74
|
+
/** @type {Map<any, AsyncRow[]>} */
|
|
75
|
+
const groups = new Map()
|
|
72
76
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
77
|
+
for (const row of allRows) {
|
|
78
|
+
const key = keyify(...await Promise.all(plan.groupBy.map(expr => evaluateExpr({ node: expr, row, context }))))
|
|
79
|
+
let group = groups.get(key)
|
|
80
|
+
if (!group) {
|
|
81
|
+
group = []
|
|
82
|
+
groups.set(key, group)
|
|
83
|
+
}
|
|
84
|
+
group.push(row)
|
|
85
|
+
}
|
|
82
86
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
87
|
+
// Yield one row per group
|
|
88
|
+
for (const group of groups.values()) {
|
|
89
|
+
const asyncRow = projectAggregateColumns(plan.columns, group, context)
|
|
86
90
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
91
|
+
// Apply HAVING filter
|
|
92
|
+
if (plan.having) {
|
|
93
|
+
/** @type {AsyncRow} */
|
|
94
|
+
const havingRow = {
|
|
95
|
+
columns: [...group[0].columns, ...asyncRow.columns],
|
|
96
|
+
cells: { ...group[0].cells, ...asyncRow.cells },
|
|
97
|
+
}
|
|
98
|
+
const passes = await evaluateExpr({
|
|
99
|
+
node: plan.having,
|
|
100
|
+
row: havingRow,
|
|
101
|
+
rows: group,
|
|
102
|
+
context,
|
|
103
|
+
})
|
|
104
|
+
if (!passes) continue
|
|
105
|
+
}
|
|
102
106
|
|
|
103
|
-
|
|
107
|
+
yield asyncRow
|
|
108
|
+
}
|
|
109
|
+
},
|
|
104
110
|
}
|
|
105
111
|
}
|
|
106
112
|
|
|
@@ -109,43 +115,54 @@ export async function* executeHashAggregate(plan, context) {
|
|
|
109
115
|
*
|
|
110
116
|
* @param {ScalarAggregateNode} plan
|
|
111
117
|
* @param {ExecuteContext} context
|
|
112
|
-
* @
|
|
118
|
+
* @returns {QueryResults}
|
|
113
119
|
*/
|
|
114
|
-
export
|
|
120
|
+
export function executeScalarAggregate(plan, context) {
|
|
115
121
|
// Fast path: use scanColumn when available
|
|
116
122
|
const fast = tryColumnScanAggregate(plan, context)
|
|
117
123
|
if (fast) {
|
|
118
|
-
|
|
119
|
-
|
|
124
|
+
return {
|
|
125
|
+
numRows: 1,
|
|
126
|
+
maxRows: 1,
|
|
127
|
+
rows: fast,
|
|
128
|
+
}
|
|
120
129
|
}
|
|
121
130
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
131
|
+
const child = executePlan({ plan: plan.child, context })
|
|
132
|
+
return {
|
|
133
|
+
numRows: plan.having ? undefined : 1,
|
|
134
|
+
maxRows: 1,
|
|
135
|
+
async *rows () {
|
|
136
|
+
// Collect all rows into single group
|
|
137
|
+
/** @type {AsyncRow[]} */
|
|
138
|
+
const group = []
|
|
139
|
+
for await (const row of child.rows()) {
|
|
140
|
+
if (context.signal?.aborted) return
|
|
141
|
+
group.push(row)
|
|
142
|
+
}
|
|
129
143
|
|
|
130
|
-
|
|
144
|
+
const asyncRow = projectAggregateColumns(plan.columns, group, context)
|
|
131
145
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
146
|
+
// Apply HAVING filter
|
|
147
|
+
if (plan.having) {
|
|
148
|
+
const baseRow = group[0] ?? { columns: [], cells: {} }
|
|
149
|
+
/** @type {AsyncRow} */
|
|
150
|
+
const havingRow = {
|
|
151
|
+
columns: [...baseRow.columns, ...asyncRow.columns],
|
|
152
|
+
cells: { ...baseRow.cells, ...asyncRow.cells },
|
|
153
|
+
}
|
|
154
|
+
const passes = await evaluateExpr({
|
|
155
|
+
node: plan.having,
|
|
156
|
+
row: havingRow,
|
|
157
|
+
rows: group,
|
|
158
|
+
context,
|
|
159
|
+
})
|
|
160
|
+
if (!passes) return
|
|
161
|
+
}
|
|
147
162
|
|
|
148
|
-
|
|
163
|
+
yield asyncRow
|
|
164
|
+
},
|
|
165
|
+
}
|
|
149
166
|
}
|
|
150
167
|
|
|
151
168
|
/**
|
|
@@ -163,7 +180,7 @@ export async function* executeScalarAggregate(plan, context) {
|
|
|
163
180
|
*
|
|
164
181
|
* @param {ScalarAggregateNode} plan
|
|
165
182
|
* @param {ExecuteContext} context
|
|
166
|
-
* @returns {AsyncGenerator<AsyncRow> | undefined}
|
|
183
|
+
* @returns {(() => AsyncGenerator<AsyncRow>) | undefined}
|
|
167
184
|
*/
|
|
168
185
|
function tryColumnScanAggregate(plan, { tables, signal }) {
|
|
169
186
|
// No HAVING support in fast path
|
|
@@ -188,7 +205,7 @@ function tryColumnScanAggregate(plan, { tables, signal }) {
|
|
|
188
205
|
specs.push(spec)
|
|
189
206
|
}
|
|
190
207
|
|
|
191
|
-
return
|
|
208
|
+
return async function* () {
|
|
192
209
|
/** @type {string[]} */
|
|
193
210
|
const columns = []
|
|
194
211
|
/** @type {AsyncCells} */
|
|
@@ -200,7 +217,7 @@ function tryColumnScanAggregate(plan, { tables, signal }) {
|
|
|
200
217
|
}
|
|
201
218
|
|
|
202
219
|
yield { columns, cells }
|
|
203
|
-
}
|
|
220
|
+
}
|
|
204
221
|
}
|
|
205
222
|
|
|
206
223
|
/**
|