squirreling 0.7.6 → 0.7.8
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 +30 -18
- package/package.json +4 -4
- package/src/execute/expression.js +4 -4
- package/src/execute/math.js +15 -0
- package/src/parse/parse.js +1 -2
- package/src/types.d.ts +2 -0
- package/src/validation.js +3 -1
- package/src/validationErrors.js +1 -0
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)
|
|
@@ -87,7 +99,7 @@ console.log(`Collected rows:`, rows)
|
|
|
87
99
|
|
|
88
100
|
- Aggregate: `COUNT`, `SUM`, `AVG`, `MIN`, `MAX`, `JSON_ARRAYAGG`
|
|
89
101
|
- String: `CONCAT`, `SUBSTRING`, `REPLACE`, `LENGTH`, `UPPER`, `LOWER`, `TRIM`, `LEFT`, `RIGHT`, `INSTR`
|
|
90
|
-
- Math: `ABS`, `CEIL`, `FLOOR`, `ROUND`, `MOD`, `RAND`, `RANDOM`, `LN`, `LOG10`, `EXP`, `POWER`, `SQRT`
|
|
102
|
+
- Math: `ABS`, `SIGN`, `CEIL`, `FLOOR`, `ROUND`, `MOD`, `RAND`, `RANDOM`, `LN`, `LOG10`, `EXP`, `POWER`, `SQRT`
|
|
91
103
|
- Trig: `SIN`, `COS`, `TAN`, `COT`, `ASIN`, `ACOS`, `ATAN`, `ATAN2`, `DEGREES`, `RADIANS`, `PI`
|
|
92
104
|
- Date: `CURRENT_DATE`, `CURRENT_TIME`, `CURRENT_TIMESTAMP`, `INTERVAL`
|
|
93
105
|
- Json: `JSON_VALUE`, `JSON_QUERY`, `JSON_OBJECT`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.8",
|
|
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.9",
|
|
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
|
}
|
|
@@ -178,7 +178,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
178
178
|
count++
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
if (funcName === 'SUM') return sum
|
|
181
|
+
if (funcName === 'SUM') return count === 0 ? null : sum
|
|
182
182
|
if (funcName === 'AVG') return count === 0 ? null : sum / count
|
|
183
183
|
if (funcName === 'MIN') return min
|
|
184
184
|
if (funcName === 'MAX') return max
|
|
@@ -403,7 +403,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
403
403
|
const exprVal = await evaluateExpr({ node: node.expr, row, tables, functions, rowIndex, rows })
|
|
404
404
|
for (const valueNode of node.values) {
|
|
405
405
|
const val = await evaluateExpr({ node: valueNode, row, tables, functions, rowIndex, rows })
|
|
406
|
-
if (exprVal
|
|
406
|
+
if (exprVal == val) return true
|
|
407
407
|
}
|
|
408
408
|
return false
|
|
409
409
|
}
|
|
@@ -413,7 +413,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
413
413
|
const results = executeSelect({ select: node.subquery, tables })
|
|
414
414
|
for await (const resRow of results) {
|
|
415
415
|
const value = await resRow.cells[resRow.columns[0]]()
|
|
416
|
-
if (exprVal
|
|
416
|
+
if (exprVal == value) return true
|
|
417
417
|
}
|
|
418
418
|
return false
|
|
419
419
|
}
|
|
@@ -439,7 +439,7 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
439
439
|
if (caseValue !== undefined) {
|
|
440
440
|
// Simple CASE: compare caseValue with condition
|
|
441
441
|
const whenValue = await evaluateExpr({ node: whenClause.condition, row, tables, functions, rowIndex, rows })
|
|
442
|
-
conditionResult = caseValue
|
|
442
|
+
conditionResult = caseValue == whenValue
|
|
443
443
|
} else {
|
|
444
444
|
// Searched CASE: evaluate condition as boolean
|
|
445
445
|
conditionResult = await evaluateExpr({ node: whenClause.condition, row, tables, functions, rowIndex, rows })
|
package/src/execute/math.js
CHANGED
|
@@ -23,12 +23,27 @@ export function evaluateMathFunc({ funcName, args }) {
|
|
|
23
23
|
return Math.ceil(Number(val))
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
if (funcName === 'ROUND') {
|
|
27
|
+
const val = args[0]
|
|
28
|
+
if (val == null) return null
|
|
29
|
+
const decimals = args[1] ?? 0
|
|
30
|
+
if (decimals == null) return null
|
|
31
|
+
const multiplier = 10 ** Number(decimals)
|
|
32
|
+
return Math.round(Number(val) * multiplier) / multiplier
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
if (funcName === 'ABS') {
|
|
27
36
|
const val = args[0]
|
|
28
37
|
if (val == null) return null
|
|
29
38
|
return Math.abs(Number(val))
|
|
30
39
|
}
|
|
31
40
|
|
|
41
|
+
if (funcName === 'SIGN') {
|
|
42
|
+
const val = args[0]
|
|
43
|
+
if (val == null) return null
|
|
44
|
+
return Math.sign(Number(val))
|
|
45
|
+
}
|
|
46
|
+
|
|
32
47
|
if (funcName === 'MOD') {
|
|
33
48
|
const dividend = args[0]
|
|
34
49
|
const divisor = args[1]
|
package/src/parse/parse.js
CHANGED
|
@@ -214,8 +214,7 @@ function parseFromSubquery(state) {
|
|
|
214
214
|
expect(state, 'paren', '(')
|
|
215
215
|
const query = parseSelectInternal(state)
|
|
216
216
|
expect(state, 'paren', ')')
|
|
217
|
-
|
|
218
|
-
const alias = expectIdentifier(state).value
|
|
217
|
+
const alias = parseTableAlias(state)
|
|
219
218
|
return { kind: 'subquery', query, alias }
|
|
220
219
|
}
|
|
221
220
|
|
package/src/types.d.ts
CHANGED
package/src/validation.js
CHANGED
|
@@ -22,7 +22,7 @@ export function isRegexpFunc(name) {
|
|
|
22
22
|
*/
|
|
23
23
|
export function isMathFunc(name) {
|
|
24
24
|
return [
|
|
25
|
-
'FLOOR', 'CEIL', 'CEILING', 'ABS', 'MOD', 'EXP', 'LN', 'LOG10', 'POWER', 'SQRT',
|
|
25
|
+
'FLOOR', 'CEIL', 'CEILING', 'ROUND', 'ABS', 'SIGN', 'MOD', 'EXP', 'LN', 'LOG10', 'POWER', 'SQRT',
|
|
26
26
|
'SIN', 'COS', 'TAN', 'COT', 'ASIN', 'ACOS', 'ATAN', 'ATAN2', 'DEGREES', 'RADIANS', 'PI',
|
|
27
27
|
'RAND', 'RANDOM',
|
|
28
28
|
].includes(name)
|
|
@@ -88,7 +88,9 @@ export const FUNCTION_ARG_COUNTS = {
|
|
|
88
88
|
FLOOR: { min: 1, max: 1 },
|
|
89
89
|
CEIL: { min: 1, max: 1 },
|
|
90
90
|
CEILING: { min: 1, max: 1 },
|
|
91
|
+
ROUND: { min: 1, max: 2 },
|
|
91
92
|
ABS: { min: 1, max: 1 },
|
|
93
|
+
SIGN: { min: 1, max: 1 },
|
|
92
94
|
MOD: { min: 2, max: 2 },
|
|
93
95
|
EXP: { min: 1, max: 1 },
|
|
94
96
|
LN: { min: 1, max: 1 },
|