squirreling 0.10.1 → 0.10.2
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 +5 -5
- package/src/execute/execute.js +56 -8
- package/src/execute/utils.js +14 -14
- package/src/expression/evaluate.js +18 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.2",
|
|
4
4
|
"description": "Squirreling Async SQL Engine",
|
|
5
5
|
"author": "Hyperparam",
|
|
6
6
|
"homepage": "https://hyperparam.app",
|
|
@@ -39,11 +39,11 @@
|
|
|
39
39
|
"test": "vitest run"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
42
|
-
"@types/node": "25.
|
|
43
|
-
"@vitest/coverage-v8": "4.0
|
|
42
|
+
"@types/node": "25.5.0",
|
|
43
|
+
"@vitest/coverage-v8": "4.1.0",
|
|
44
44
|
"eslint": "9.39.2",
|
|
45
|
-
"eslint-plugin-jsdoc": "62.
|
|
45
|
+
"eslint-plugin-jsdoc": "62.8.0",
|
|
46
46
|
"typescript": "5.9.3",
|
|
47
|
-
"vitest": "4.0
|
|
47
|
+
"vitest": "4.1.0"
|
|
48
48
|
}
|
|
49
49
|
}
|
package/src/execute/execute.js
CHANGED
|
@@ -119,7 +119,7 @@ async function* executeScan(plan, context) {
|
|
|
119
119
|
|
|
120
120
|
// Apply WHERE if data source did not
|
|
121
121
|
if (!appliedWhere && plan.hints.where) {
|
|
122
|
-
result = filterRows(result, plan.hints.where, context)
|
|
122
|
+
result = filterRows(result, plan.hints.where, context, plan.hints.limit)
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
// Apply LIMIT/OFFSET if data source did not
|
|
@@ -174,15 +174,42 @@ async function* executeCount(plan, { tables, signal }) {
|
|
|
174
174
|
* @param {AsyncIterable<AsyncRow>} rows
|
|
175
175
|
* @param {ExprNode} condition
|
|
176
176
|
* @param {ExecuteContext} context
|
|
177
|
+
* @param {number} [limit] - downstream LIMIT hint for chunk sizing
|
|
177
178
|
* @yields {AsyncRow}
|
|
178
179
|
*/
|
|
179
|
-
async function* filterRows(rows, condition, context) {
|
|
180
|
+
async function* filterRows(rows, condition, context, limit) {
|
|
181
|
+
const MAX_CHUNK = 256
|
|
182
|
+
let chunkSize = limit ?? Infinity
|
|
180
183
|
let rowIndex = 0
|
|
184
|
+
|
|
185
|
+
/** @type {{ row: AsyncRow, rowIndex: number }[]} */
|
|
186
|
+
let buffer = []
|
|
187
|
+
|
|
181
188
|
for await (const row of rows) {
|
|
182
189
|
if (context.signal?.aborted) return
|
|
183
190
|
rowIndex++
|
|
184
|
-
|
|
185
|
-
|
|
191
|
+
buffer.push({ row, rowIndex })
|
|
192
|
+
|
|
193
|
+
if (buffer.length >= chunkSize) {
|
|
194
|
+
const results = await Promise.all(buffer.map(b =>
|
|
195
|
+
evaluateExpr({ node: condition, row: b.row, rowIndex: b.rowIndex, context })
|
|
196
|
+
))
|
|
197
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
198
|
+
if (results[i]) yield buffer[i].row
|
|
199
|
+
}
|
|
200
|
+
buffer = []
|
|
201
|
+
chunkSize = Math.min(chunkSize * 2, MAX_CHUNK)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Flush remaining rows
|
|
206
|
+
if (buffer.length > 0) {
|
|
207
|
+
const results = await Promise.all(buffer.map(b =>
|
|
208
|
+
evaluateExpr({ node: condition, row: b.row, rowIndex: b.rowIndex, context })
|
|
209
|
+
))
|
|
210
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
211
|
+
if (results[i]) yield buffer[i].row
|
|
212
|
+
}
|
|
186
213
|
}
|
|
187
214
|
}
|
|
188
215
|
|
|
@@ -275,17 +302,38 @@ async function* executeProject(plan, context) {
|
|
|
275
302
|
*/
|
|
276
303
|
async function* executeDistinct(plan, context) {
|
|
277
304
|
const { signal } = context
|
|
305
|
+
const MAX_CHUNK = 256
|
|
278
306
|
|
|
279
307
|
/** @type {Set<string>} */
|
|
280
308
|
const seen = new Set()
|
|
281
309
|
|
|
310
|
+
/** @type {AsyncRow[]} */
|
|
311
|
+
let buffer = []
|
|
312
|
+
|
|
282
313
|
for await (const row of executePlan({ plan: plan.child, context })) {
|
|
283
314
|
if (signal?.aborted) return
|
|
315
|
+
buffer.push(row)
|
|
284
316
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
317
|
+
if (buffer.length >= MAX_CHUNK) {
|
|
318
|
+
const keys = await Promise.all(buffer.map(r => stableRowKey(r.cells)))
|
|
319
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
320
|
+
if (!seen.has(keys[i])) {
|
|
321
|
+
seen.add(keys[i])
|
|
322
|
+
yield buffer[i]
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
buffer = []
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Flush remaining
|
|
330
|
+
if (buffer.length > 0) {
|
|
331
|
+
const keys = await Promise.all(buffer.map(r => stableRowKey(r.cells)))
|
|
332
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
333
|
+
if (!seen.has(keys[i])) {
|
|
334
|
+
seen.add(keys[i])
|
|
335
|
+
yield buffer[i]
|
|
336
|
+
}
|
|
289
337
|
}
|
|
290
338
|
}
|
|
291
339
|
}
|
package/src/execute/utils.js
CHANGED
|
@@ -44,17 +44,22 @@ export function compareForTerm(a, b, term) {
|
|
|
44
44
|
* @returns {Promise<Record<string, SqlPrimitive>[]>} array of all yielded values
|
|
45
45
|
*/
|
|
46
46
|
export async function collect(asyncRows) {
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
// Collect all rows first, then materialize cells concurrently
|
|
48
|
+
// This enables dataloader-style batching of cell accessors
|
|
49
|
+
/** @type {AsyncRow[]} */
|
|
50
|
+
const rows = []
|
|
49
51
|
for await (const asyncRow of asyncRows) {
|
|
52
|
+
rows.push(asyncRow)
|
|
53
|
+
}
|
|
54
|
+
return Promise.all(rows.map(async asyncRow => {
|
|
55
|
+
const values = await Promise.all(asyncRow.columns.map(k => asyncRow.cells[k]()))
|
|
50
56
|
/** @type {Record<string, SqlPrimitive>} */
|
|
51
57
|
const item = {}
|
|
52
|
-
for (
|
|
53
|
-
item[
|
|
58
|
+
for (let i = 0; i < asyncRow.columns.length; i++) {
|
|
59
|
+
item[asyncRow.columns[i]] = values[i]
|
|
54
60
|
}
|
|
55
|
-
|
|
56
|
-
}
|
|
57
|
-
return results
|
|
61
|
+
return item
|
|
62
|
+
}))
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
/**
|
|
@@ -79,11 +84,6 @@ export function stringify(value) {
|
|
|
79
84
|
*/
|
|
80
85
|
export async function stableRowKey(cells) {
|
|
81
86
|
const keys = Object.keys(cells).sort()
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
for (const k of keys) {
|
|
85
|
-
const v = await cells[k]()
|
|
86
|
-
parts.push(k + ':' + stringify(v))
|
|
87
|
-
}
|
|
88
|
-
return parts.join('|')
|
|
87
|
+
const values = await Promise.all(keys.map(k => cells[k]()))
|
|
88
|
+
return keys.map((k, i) => k + ':' + stringify(values[i])).join('|')
|
|
89
89
|
}
|
|
@@ -118,11 +118,10 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
118
118
|
// Apply FILTER clause if present
|
|
119
119
|
let filteredRows = rows
|
|
120
120
|
if (node.filter) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
121
|
+
const passes = await Promise.all(rows.map(row =>
|
|
122
|
+
evaluateExpr({ node: node.filter, row, context })
|
|
123
|
+
))
|
|
124
|
+
filteredRows = rows.filter((_, i) => passes[i])
|
|
126
125
|
}
|
|
127
126
|
|
|
128
127
|
const argNode = node.args[0]
|
|
@@ -132,23 +131,27 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
132
131
|
return filteredRows.length
|
|
133
132
|
}
|
|
134
133
|
|
|
134
|
+
const values = await Promise.all(filteredRows.map(row =>
|
|
135
|
+
evaluateExpr({ node: argNode, row, context })
|
|
136
|
+
))
|
|
135
137
|
if (node.distinct) {
|
|
136
138
|
const seen = new Set()
|
|
137
|
-
for (const
|
|
138
|
-
const v = await evaluateExpr({ node: argNode, row, context })
|
|
139
|
+
for (const v of values) {
|
|
139
140
|
if (v != null) seen.add(v)
|
|
140
141
|
}
|
|
141
142
|
return seen.size
|
|
142
143
|
}
|
|
143
144
|
let count = 0
|
|
144
|
-
for (const
|
|
145
|
-
const v = await evaluateExpr({ node: argNode, row, context })
|
|
145
|
+
for (const v of values) {
|
|
146
146
|
if (v != null) count++
|
|
147
147
|
}
|
|
148
148
|
return count
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
if (funcName === 'SUM' || funcName === 'AVG' || funcName === 'MIN' || funcName === 'MAX') {
|
|
152
|
+
const rawValues = await Promise.all(filteredRows.map(row =>
|
|
153
|
+
evaluateExpr({ node: argNode, row, context })
|
|
154
|
+
))
|
|
152
155
|
let sum = 0
|
|
153
156
|
let count = 0
|
|
154
157
|
/** @type {number | null} */
|
|
@@ -156,8 +159,7 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
156
159
|
/** @type {number | null} */
|
|
157
160
|
let max = null
|
|
158
161
|
|
|
159
|
-
for (const
|
|
160
|
-
const raw = await evaluateExpr({ node: argNode, row, context })
|
|
162
|
+
for (const raw of rawValues) {
|
|
161
163
|
if (raw == null) continue
|
|
162
164
|
const num = Number(raw)
|
|
163
165
|
if (!Number.isFinite(num)) continue
|
|
@@ -180,9 +182,12 @@ export async function evaluateExpr({ node, row, rowIndex, rows, context }) {
|
|
|
180
182
|
}
|
|
181
183
|
|
|
182
184
|
if (funcName === 'STDDEV_SAMP' || funcName === 'STDDEV_POP') {
|
|
185
|
+
const rawValues = await Promise.all(filteredRows.map(row =>
|
|
186
|
+
evaluateExpr({ node: argNode, row, context })
|
|
187
|
+
))
|
|
188
|
+
/** @type {number[]} */
|
|
183
189
|
const values = []
|
|
184
|
-
for (const
|
|
185
|
-
const raw = await evaluateExpr({ node: argNode, row, context })
|
|
190
|
+
for (const raw of rawValues) {
|
|
186
191
|
if (raw == null) continue
|
|
187
192
|
const num = Number(raw)
|
|
188
193
|
if (!Number.isFinite(num)) continue
|