squirreling 0.7.5 → 0.7.7
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 +29 -17
- package/package.json +4 -4
- package/src/execute/execute.js +14 -3
- package/src/execute/expression.js +20 -14
- package/src/execute/having.js +1 -1
- package/src/execute/utils.js +1 -1
- package/src/validation.js +1 -1
package/README.md
CHANGED
|
@@ -10,22 +10,13 @@
|
|
|
10
10
|

|
|
11
11
|
[](https://www.npmjs.com/package/squirreling?activeTab=dependencies)
|
|
12
12
|
|
|
13
|
-
Squirreling is a streaming async SQL engine
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
-
|
|
18
|
-
|
|
19
|
-
-
|
|
20
|
-
- Supports standard SQL queries
|
|
21
|
-
- Async streaming for large datasets
|
|
22
|
-
- Native javascript Promises, AsyncGenerators, AbortSignals
|
|
23
|
-
- Async user-defined functions (UDFs)
|
|
24
|
-
- Constant memory usage for simple queries with LIMIT
|
|
25
|
-
- Robust error handling and validation designed for LLM tool use
|
|
26
|
-
- In-memory data option for simple use cases
|
|
27
|
-
- Late materialization for efficiency
|
|
28
|
-
- Select only
|
|
13
|
+
Squirreling is a streaming async SQL engine in pure JavaScript. Built for the browser from the ground up: streaming input and output, pluggable data sources, and lazy async cell evaluation. This makes Squirreling ideal for querying data from network sources, APIs, or LLMs where latency and cost matter.
|
|
14
|
+
|
|
15
|
+
- **Standard SQL**: Full SQL support for querying data (read-only)
|
|
16
|
+
- **Async UDFs**: User-defined functions can call APIs or models
|
|
17
|
+
- **Tiny**: 13 kb bundle, zero dependencies, instant startup
|
|
18
|
+
|
|
19
|
+
The key idea is **cell-level lazy evaluation**: rows are native AsyncGenerators and cells are async thunks `() => Promise<T>`. This means expensive operations only execute for cells that actually appear in your query results. Unlike WebAssembly databases, Squirreling is fully async with true streaming during network fetches.
|
|
29
20
|
|
|
30
21
|
## Usage
|
|
31
22
|
|
|
@@ -75,7 +66,28 @@ console.log(`Collected rows:`, rows)
|
|
|
75
66
|
// Collected rows: [ { active: true, cnt: 2 }, { active: false, cnt: 1 } ]
|
|
76
67
|
```
|
|
77
68
|
|
|
78
|
-
|
|
69
|
+
### User-Defined Functions
|
|
70
|
+
|
|
71
|
+
Pass custom functions via the `functions` option. UDFs can be sync or async, making them ideal for calling APIs, models, or other external services:
|
|
72
|
+
|
|
73
|
+
```javascript
|
|
74
|
+
const rows = await collect(executeSql({
|
|
75
|
+
tables: { products },
|
|
76
|
+
query: 'SELECT name,AI_SCORE(description) AS score FROM products',
|
|
77
|
+
functions: {
|
|
78
|
+
AI_SCORE: {
|
|
79
|
+
apply: async (text) => completions(`Rate the following product description from 1 to 10: ${text}`),
|
|
80
|
+
arguments: { min: 1, max: 1 },
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
}))
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Because Squirreling uses lazy cell evaluation, the `AI_SCORE` function only executes for cells that are actually materialized. Combined with `LIMIT` or `WHERE`, you can efficiently query expensive operations.
|
|
87
|
+
|
|
88
|
+
## Supported SQL Syntax
|
|
89
|
+
|
|
90
|
+
Squirreling mostly follows the SQL standard. The following features are supported:
|
|
79
91
|
|
|
80
92
|
- `SELECT` statements with `WHERE`, `ORDER BY`, `LIMIT`, `OFFSET`
|
|
81
93
|
- `WITH` clause for Common Table Expressions (CTEs)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.7",
|
|
4
4
|
"description": "Squirreling SQL Engine",
|
|
5
5
|
"author": "Hyperparam",
|
|
6
6
|
"homepage": "https://hyperparam.app",
|
|
@@ -37,11 +37,11 @@
|
|
|
37
37
|
"test": "vitest run"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@types/node": "25.0.
|
|
41
|
-
"@vitest/coverage-v8": "4.0.
|
|
40
|
+
"@types/node": "25.0.7",
|
|
41
|
+
"@vitest/coverage-v8": "4.0.17",
|
|
42
42
|
"eslint": "9.39.2",
|
|
43
43
|
"eslint-plugin-jsdoc": "62.0.0",
|
|
44
44
|
"typescript": "5.9.3",
|
|
45
|
-
"vitest": "4.0.
|
|
45
|
+
"vitest": "4.0.17"
|
|
46
46
|
}
|
|
47
47
|
}
|
package/src/execute/execute.js
CHANGED
|
@@ -10,7 +10,7 @@ import { resolveTableSource } from './tableSource.js'
|
|
|
10
10
|
import { compareForTerm, defaultDerivedAlias, stringify } from './utils.js'
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
|
-
* @import { AsyncCells, AsyncDataSource, AsyncRow,
|
|
13
|
+
* @import { AsyncCells, AsyncDataSource, AsyncRow, ExecuteSqlOptions, ExprNode, OrderByItem, QueryHints, SelectColumn, SelectStatement, SqlPrimitive, UserDefinedFunction, WithClause } from '../types.js'
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
/**
|
|
@@ -143,9 +143,10 @@ async function applyDistinct(rows, distinct) {
|
|
|
143
143
|
* @param {OrderByItem[]} options.orderBy - the sort specifications
|
|
144
144
|
* @param {Record<string, AsyncDataSource>} options.tables
|
|
145
145
|
* @param {Record<string, UserDefinedFunction>} [options.functions]
|
|
146
|
+
* @param {Map<string, ExprNode>} [options.aliases] - SELECT column aliases for ORDER BY resolution
|
|
146
147
|
* @returns {Promise<AsyncRow[]>} the sorted rows
|
|
147
148
|
*/
|
|
148
|
-
async function sortRows({ rows, orderBy, tables, functions }) {
|
|
149
|
+
async function sortRows({ rows, orderBy, tables, functions, aliases }) {
|
|
149
150
|
if (!orderBy.length) return rows
|
|
150
151
|
|
|
151
152
|
// Cache for evaluated values: evaluatedValues[rowIdx][colIdx]
|
|
@@ -177,6 +178,7 @@ async function sortRows({ rows, orderBy, tables, functions }) {
|
|
|
177
178
|
row: rows[idx],
|
|
178
179
|
tables,
|
|
179
180
|
functions,
|
|
181
|
+
aliases,
|
|
180
182
|
})
|
|
181
183
|
}
|
|
182
184
|
}
|
|
@@ -459,7 +461,16 @@ async function* evaluateBuffered({ select, dataSource, tables, functions, hasAgg
|
|
|
459
461
|
} else {
|
|
460
462
|
// No grouping, simple projection
|
|
461
463
|
// Sort before projection so ORDER BY can access columns not in SELECT
|
|
462
|
-
|
|
464
|
+
|
|
465
|
+
// Pass aliases so ORDER BY can reference SELECT column aliases
|
|
466
|
+
/** @type {Map<string, ExprNode>} */
|
|
467
|
+
const aliases = new Map()
|
|
468
|
+
for (const col of select.columns) {
|
|
469
|
+
if (col.kind === 'derived' && col.alias) {
|
|
470
|
+
aliases.set(col.alias, col.expr)
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const sorted = await sortRows({ rows: filtered, orderBy: select.orderBy, tables, functions, aliases })
|
|
463
474
|
|
|
464
475
|
// OPTIMIZATION: For non-DISTINCT queries, apply OFFSET/LIMIT before projection
|
|
465
476
|
// to avoid reading expensive cells for rows that won't be in the final result
|
|
@@ -23,9 +23,10 @@ import { applyBinaryOp, stringify } from './utils.js'
|
|
|
23
23
|
* @param {Record<string, UserDefinedFunction>} [params.functions] - User-defined functions
|
|
24
24
|
* @param {number} [params.rowIndex] - 1-based row index for error reporting
|
|
25
25
|
* @param {AsyncRow[]} [params.rows] - Group of rows for aggregate functions
|
|
26
|
+
* @param {Map<string, ExprNode>} [params.aliases] - SELECT column aliases for ORDER BY resolution
|
|
26
27
|
* @returns {Promise<SqlPrimitive>} The result of the evaluation
|
|
27
28
|
*/
|
|
28
|
-
export async function evaluateExpr({ node, row, tables, functions, rowIndex, rows }) {
|
|
29
|
+
export async function evaluateExpr({ node, row, tables, functions, rowIndex, rows, aliases }) {
|
|
29
30
|
if (node.type === 'literal') {
|
|
30
31
|
return node.value
|
|
31
32
|
}
|
|
@@ -42,6 +43,11 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
42
43
|
return row.cells[colName]()
|
|
43
44
|
}
|
|
44
45
|
}
|
|
46
|
+
// Check if it's a SELECT alias (for ORDER BY)
|
|
47
|
+
if (aliases?.has(node.name)) {
|
|
48
|
+
return evaluateExpr({ node: aliases.get(node.name), row, tables, functions, rowIndex, rows, aliases })
|
|
49
|
+
}
|
|
50
|
+
// Unknown identifier
|
|
45
51
|
throw columnNotFoundError({
|
|
46
52
|
columnName: node.name,
|
|
47
53
|
availableColumns: Object.keys(row.cells),
|
|
@@ -63,16 +69,16 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
63
69
|
// Unary operators
|
|
64
70
|
if (node.type === 'unary') {
|
|
65
71
|
if (node.op === 'NOT') {
|
|
66
|
-
return !await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows })
|
|
72
|
+
return !await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows, aliases })
|
|
67
73
|
}
|
|
68
74
|
if (node.op === 'IS NULL') {
|
|
69
|
-
return await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows }) == null
|
|
75
|
+
return await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows, aliases }) == null
|
|
70
76
|
}
|
|
71
77
|
if (node.op === 'IS NOT NULL') {
|
|
72
|
-
return await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows }) != null
|
|
78
|
+
return await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows, aliases }) != null
|
|
73
79
|
}
|
|
74
80
|
if (node.op === '-') {
|
|
75
|
-
const val = await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows })
|
|
81
|
+
const val = await evaluateExpr({ node: node.argument, row, tables, functions, rowIndex, rows, aliases })
|
|
76
82
|
if (val == null) return null
|
|
77
83
|
return -val
|
|
78
84
|
}
|
|
@@ -82,15 +88,15 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
82
88
|
if (node.type === 'binary') {
|
|
83
89
|
// Handle date +/- interval at AST level
|
|
84
90
|
if ((node.op === '+' || node.op === '-') && node.right.type === 'interval') {
|
|
85
|
-
const dateVal = await evaluateExpr({ node: node.left, row, tables, functions, rowIndex, rows })
|
|
91
|
+
const dateVal = await evaluateExpr({ node: node.left, row, tables, functions, rowIndex, rows, aliases })
|
|
86
92
|
return applyIntervalToDate(dateVal, node.right.value, node.right.unit, node.op)
|
|
87
93
|
}
|
|
88
94
|
if (node.op === '+' && node.left.type === 'interval') {
|
|
89
|
-
const dateVal = await evaluateExpr({ node: node.right, row, tables, functions, rowIndex, rows })
|
|
95
|
+
const dateVal = await evaluateExpr({ node: node.right, row, tables, functions, rowIndex, rows, aliases })
|
|
90
96
|
return applyIntervalToDate(dateVal, node.left.value, node.left.unit, '+')
|
|
91
97
|
}
|
|
92
98
|
|
|
93
|
-
const left = await evaluateExpr({ node: node.left, row, tables, functions, rowIndex, rows })
|
|
99
|
+
const left = await evaluateExpr({ node: node.left, row, tables, functions, rowIndex, rows, aliases })
|
|
94
100
|
|
|
95
101
|
// Short-circuit evaluation for AND and OR
|
|
96
102
|
if (node.op === 'AND') {
|
|
@@ -100,7 +106,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
100
106
|
if (left) return true
|
|
101
107
|
}
|
|
102
108
|
|
|
103
|
-
const right = await evaluateExpr({ node: node.right, row, tables, functions, rowIndex, rows })
|
|
109
|
+
const right = await evaluateExpr({ node: node.right, row, tables, functions, rowIndex, rows, aliases })
|
|
104
110
|
return applyBinaryOp(node.op, left, right)
|
|
105
111
|
}
|
|
106
112
|
|
|
@@ -172,7 +178,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
172
178
|
count++
|
|
173
179
|
}
|
|
174
180
|
|
|
175
|
-
if (funcName === 'SUM') return sum
|
|
181
|
+
if (funcName === 'SUM') return count === 0 ? null : sum
|
|
176
182
|
if (funcName === 'AVG') return count === 0 ? null : sum / count
|
|
177
183
|
if (funcName === 'MIN') return min
|
|
178
184
|
if (funcName === 'MAX') return max
|
|
@@ -202,7 +208,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
202
208
|
}
|
|
203
209
|
|
|
204
210
|
/** @type {SqlPrimitive[]} */
|
|
205
|
-
const args = await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, tables, functions, rowIndex, rows })))
|
|
211
|
+
const args = await Promise.all(node.args.map(arg => evaluateExpr({ node: arg, row, tables, functions, rowIndex, rows, aliases })))
|
|
206
212
|
|
|
207
213
|
if (isStringFunc(funcName)) {
|
|
208
214
|
return evaluateStringFunc({
|
|
@@ -397,7 +403,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
397
403
|
const exprVal = await evaluateExpr({ node: node.expr, row, tables, functions, rowIndex, rows })
|
|
398
404
|
for (const valueNode of node.values) {
|
|
399
405
|
const val = await evaluateExpr({ node: valueNode, row, tables, functions, rowIndex, rows })
|
|
400
|
-
if (exprVal
|
|
406
|
+
if (exprVal == val) return true
|
|
401
407
|
}
|
|
402
408
|
return false
|
|
403
409
|
}
|
|
@@ -407,7 +413,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
407
413
|
const results = executeSelect({ select: node.subquery, tables })
|
|
408
414
|
for await (const resRow of results) {
|
|
409
415
|
const value = await resRow.cells[resRow.columns[0]]()
|
|
410
|
-
if (exprVal
|
|
416
|
+
if (exprVal == value) return true
|
|
411
417
|
}
|
|
412
418
|
return false
|
|
413
419
|
}
|
|
@@ -433,7 +439,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
433
439
|
if (caseValue !== undefined) {
|
|
434
440
|
// Simple CASE: compare caseValue with condition
|
|
435
441
|
const whenValue = await evaluateExpr({ node: whenClause.condition, row, tables, functions, rowIndex, rows })
|
|
436
|
-
conditionResult = caseValue
|
|
442
|
+
conditionResult = caseValue == whenValue
|
|
437
443
|
} else {
|
|
438
444
|
// Searched CASE: evaluate condition as boolean
|
|
439
445
|
conditionResult = await evaluateExpr({ node: whenClause.condition, row, tables, functions, rowIndex, rows })
|
package/src/execute/having.js
CHANGED
|
@@ -20,7 +20,7 @@ import { applyBinaryOp } from './utils.js'
|
|
|
20
20
|
*/
|
|
21
21
|
export async function evaluateHavingExpr({ expr, row, group, tables, functions }) {
|
|
22
22
|
// Having context
|
|
23
|
-
const context = { ...group[0]
|
|
23
|
+
const context = { ...group[0], ...row }
|
|
24
24
|
|
|
25
25
|
// For HAVING, we need special handling of aggregate functions
|
|
26
26
|
// They need to be re-evaluated against the group
|
package/src/execute/utils.js
CHANGED
package/src/validation.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
|
|
2
2
|
/**
|
|
3
|
-
* @import {AggregateFunc, BinaryOp,
|
|
3
|
+
* @import { AggregateFunc, BinaryOp, IntervalUnit, MathFunc, StringFunc, UserDefinedFunction } from './types.js'
|
|
4
4
|
* @param {string} name
|
|
5
5
|
* @returns {name is AggregateFunc}
|
|
6
6
|
*/
|