squirreling 0.11.5 → 0.12.1
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 +10 -4
- package/package.json +4 -4
- package/src/backend/dataSource.js +37 -6
- package/src/execute/aggregates.js +94 -73
- package/src/execute/execute.js +351 -224
- package/src/execute/join.js +193 -152
- package/src/execute/sort.js +72 -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,26 @@ const users = [
|
|
|
34
34
|
]
|
|
35
35
|
|
|
36
36
|
// Squirreling return types
|
|
37
|
+
interface QueryResults {
|
|
38
|
+
columns: string[]
|
|
39
|
+
numRows?: number
|
|
40
|
+
maxRows?: number
|
|
41
|
+
rows(): AsyncGenerator<AsyncRow>
|
|
42
|
+
}
|
|
37
43
|
interface AsyncRow {
|
|
38
44
|
columns: string[]
|
|
39
45
|
cells: Record<string, AsyncCell>
|
|
40
46
|
}
|
|
41
47
|
type AsyncCell = () => Promise<SqlPrimitive>
|
|
42
48
|
|
|
43
|
-
// Returns
|
|
44
|
-
const
|
|
49
|
+
// Returns a QueryResults object with streaming rows
|
|
50
|
+
const { rows } = executeSql({
|
|
45
51
|
tables: { users },
|
|
46
52
|
query: 'SELECT * FROM users',
|
|
47
53
|
})
|
|
48
54
|
|
|
49
55
|
// Process rows as they arrive (streaming)
|
|
50
|
-
for await (const { cells } of
|
|
56
|
+
for await (const { cells } of rows()) {
|
|
51
57
|
console.log(`User id=${await cells.id()}, name=${await cells.name()}`)
|
|
52
58
|
}
|
|
53
59
|
```
|
|
@@ -105,7 +111,7 @@ interface ScanOptions {
|
|
|
105
111
|
}
|
|
106
112
|
|
|
107
113
|
interface ScanResults {
|
|
108
|
-
rows: AsyncIterable<AsyncRow> // async iterable of rows
|
|
114
|
+
rows(): AsyncIterable<AsyncRow> // async iterable of rows
|
|
109
115
|
appliedWhere: boolean // WHERE filter applied at scan time?
|
|
110
116
|
appliedLimitOffset: boolean // LIMIT and OFFSET applied at scan time?
|
|
111
117
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.1",
|
|
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.5.
|
|
43
|
-
"@vitest/coverage-v8": "4.1.
|
|
42
|
+
"@types/node": "25.5.2",
|
|
43
|
+
"@vitest/coverage-v8": "4.1.3",
|
|
44
44
|
"eslint": "9.39.2",
|
|
45
45
|
"eslint-plugin-jsdoc": "62.9.0",
|
|
46
46
|
"typescript": "6.0.2",
|
|
47
|
-
"vitest": "4.1.
|
|
47
|
+
"vitest": "4.1.3"
|
|
48
48
|
}
|
|
49
49
|
}
|
|
@@ -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
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { derivedAlias } from '../expression/alias.js'
|
|
2
2
|
import { evaluateExpr } from '../expression/evaluate.js'
|
|
3
|
-
import { executePlan } from './execute.js'
|
|
3
|
+
import { executePlan, selectColumnNames } 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,59 @@ 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
|
+
columns: selectColumnNames(plan.columns, child.columns),
|
|
64
|
+
maxRows: child.maxRows,
|
|
65
|
+
async *rows () {
|
|
66
|
+
// Collect all rows
|
|
67
|
+
/** @type {AsyncRow[]} */
|
|
68
|
+
const allRows = []
|
|
69
|
+
for await (const row of child.rows()) {
|
|
70
|
+
if (context.signal?.aborted) return
|
|
71
|
+
allRows.push(row)
|
|
72
|
+
}
|
|
68
73
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
74
|
+
// Group rows by GROUP BY keys
|
|
75
|
+
/** @type {Map<any, AsyncRow[]>} */
|
|
76
|
+
const groups = new Map()
|
|
72
77
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
for (const row of allRows) {
|
|
79
|
+
const key = keyify(...await Promise.all(plan.groupBy.map(expr => evaluateExpr({ node: expr, row, context }))))
|
|
80
|
+
let group = groups.get(key)
|
|
81
|
+
if (!group) {
|
|
82
|
+
group = []
|
|
83
|
+
groups.set(key, group)
|
|
84
|
+
}
|
|
85
|
+
group.push(row)
|
|
86
|
+
}
|
|
82
87
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
88
|
+
// Yield one row per group
|
|
89
|
+
for (const group of groups.values()) {
|
|
90
|
+
const asyncRow = projectAggregateColumns(plan.columns, group, context)
|
|
86
91
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
92
|
+
// Apply HAVING filter
|
|
93
|
+
if (plan.having) {
|
|
94
|
+
/** @type {AsyncRow} */
|
|
95
|
+
const havingRow = {
|
|
96
|
+
columns: [...group[0].columns, ...asyncRow.columns],
|
|
97
|
+
cells: { ...group[0].cells, ...asyncRow.cells },
|
|
98
|
+
}
|
|
99
|
+
const passes = await evaluateExpr({
|
|
100
|
+
node: plan.having,
|
|
101
|
+
row: havingRow,
|
|
102
|
+
rows: group,
|
|
103
|
+
context,
|
|
104
|
+
})
|
|
105
|
+
if (!passes) continue
|
|
106
|
+
}
|
|
102
107
|
|
|
103
|
-
|
|
108
|
+
yield asyncRow
|
|
109
|
+
}
|
|
110
|
+
},
|
|
104
111
|
}
|
|
105
112
|
}
|
|
106
113
|
|
|
@@ -109,43 +116,57 @@ export async function* executeHashAggregate(plan, context) {
|
|
|
109
116
|
*
|
|
110
117
|
* @param {ScalarAggregateNode} plan
|
|
111
118
|
* @param {ExecuteContext} context
|
|
112
|
-
* @
|
|
119
|
+
* @returns {QueryResults}
|
|
113
120
|
*/
|
|
114
|
-
export
|
|
121
|
+
export function executeScalarAggregate(plan, context) {
|
|
115
122
|
// Fast path: use scanColumn when available
|
|
123
|
+
const scalarColumns = selectColumnNames(plan.columns, [])
|
|
116
124
|
const fast = tryColumnScanAggregate(plan, context)
|
|
117
125
|
if (fast) {
|
|
118
|
-
|
|
119
|
-
|
|
126
|
+
return {
|
|
127
|
+
columns: scalarColumns,
|
|
128
|
+
numRows: 1,
|
|
129
|
+
maxRows: 1,
|
|
130
|
+
rows: fast,
|
|
131
|
+
}
|
|
120
132
|
}
|
|
121
133
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
134
|
+
const child = executePlan({ plan: plan.child, context })
|
|
135
|
+
return {
|
|
136
|
+
columns: selectColumnNames(plan.columns, child.columns),
|
|
137
|
+
numRows: plan.having ? undefined : 1,
|
|
138
|
+
maxRows: 1,
|
|
139
|
+
async *rows () {
|
|
140
|
+
// Collect all rows into single group
|
|
141
|
+
/** @type {AsyncRow[]} */
|
|
142
|
+
const group = []
|
|
143
|
+
for await (const row of child.rows()) {
|
|
144
|
+
if (context.signal?.aborted) return
|
|
145
|
+
group.push(row)
|
|
146
|
+
}
|
|
129
147
|
|
|
130
|
-
|
|
148
|
+
const asyncRow = projectAggregateColumns(plan.columns, group, context)
|
|
131
149
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
150
|
+
// Apply HAVING filter
|
|
151
|
+
if (plan.having) {
|
|
152
|
+
const baseRow = group[0] ?? { columns: [], cells: {} }
|
|
153
|
+
/** @type {AsyncRow} */
|
|
154
|
+
const havingRow = {
|
|
155
|
+
columns: [...baseRow.columns, ...asyncRow.columns],
|
|
156
|
+
cells: { ...baseRow.cells, ...asyncRow.cells },
|
|
157
|
+
}
|
|
158
|
+
const passes = await evaluateExpr({
|
|
159
|
+
node: plan.having,
|
|
160
|
+
row: havingRow,
|
|
161
|
+
rows: group,
|
|
162
|
+
context,
|
|
163
|
+
})
|
|
164
|
+
if (!passes) return
|
|
165
|
+
}
|
|
147
166
|
|
|
148
|
-
|
|
167
|
+
yield asyncRow
|
|
168
|
+
},
|
|
169
|
+
}
|
|
149
170
|
}
|
|
150
171
|
|
|
151
172
|
/**
|
|
@@ -163,7 +184,7 @@ export async function* executeScalarAggregate(plan, context) {
|
|
|
163
184
|
*
|
|
164
185
|
* @param {ScalarAggregateNode} plan
|
|
165
186
|
* @param {ExecuteContext} context
|
|
166
|
-
* @returns {AsyncGenerator<AsyncRow> | undefined}
|
|
187
|
+
* @returns {(() => AsyncGenerator<AsyncRow>) | undefined}
|
|
167
188
|
*/
|
|
168
189
|
function tryColumnScanAggregate(plan, { tables, signal }) {
|
|
169
190
|
// No HAVING support in fast path
|
|
@@ -188,7 +209,7 @@ function tryColumnScanAggregate(plan, { tables, signal }) {
|
|
|
188
209
|
specs.push(spec)
|
|
189
210
|
}
|
|
190
211
|
|
|
191
|
-
return
|
|
212
|
+
return async function* () {
|
|
192
213
|
/** @type {string[]} */
|
|
193
214
|
const columns = []
|
|
194
215
|
/** @type {AsyncCells} */
|
|
@@ -200,7 +221,7 @@ function tryColumnScanAggregate(plan, { tables, signal }) {
|
|
|
200
221
|
}
|
|
201
222
|
|
|
202
223
|
yield { columns, cells }
|
|
203
|
-
}
|
|
224
|
+
}
|
|
204
225
|
}
|
|
205
226
|
|
|
206
227
|
/**
|