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 CHANGED
@@ -10,22 +10,13 @@
10
10
  ![coverage](https://img.shields.io/badge/Coverage-95-darkred)
11
11
  [![dependencies](https://img.shields.io/badge/Dependencies-0-blueviolet)](https://www.npmjs.com/package/squirreling?activeTab=dependencies)
12
12
 
13
- Squirreling is a streaming async SQL engine built for the web. It is designed to query over various data sources and provide efficient streaming of results. 100% JavaScript with zero dependencies.
14
-
15
- ## Features
16
-
17
- - Lightweight and fast
18
- - Easy to integrate with frontend applications
19
- - Lets you move query execution closer to your users
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
- ## Supported SQL Features
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.5",
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.3",
41
- "@vitest/coverage-v8": "4.0.16",
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.16"
45
+ "vitest": "4.0.17"
46
46
  }
47
47
  }
@@ -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, CTEDefinition, ExecuteSqlOptions, OrderByItem, QueryHints, SelectStatement, SqlPrimitive, UserDefinedFunction, WithClause } from '../types.js'
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
- const sorted = await sortRows({ rows: filtered, orderBy: select.orderBy, tables, functions })
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 === val) return true
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 === value) return true
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 === whenValue
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 })
@@ -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] ?? {}, ...row }
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
@@ -1,5 +1,5 @@
1
1
  /**
2
- * @import {AsyncRow, BinaryOp, ExprNode, OrderByItem, SqlPrimitive} from '../types.js'
2
+ * @import { AsyncRow, BinaryOp, ExprNode, OrderByItem, SqlPrimitive } from '../types.js'
3
3
  */
4
4
 
5
5
  /**
package/src/validation.js CHANGED
@@ -1,6 +1,6 @@
1
1
 
2
2
  /**
3
- * @import {AggregateFunc, BinaryOp, ComparisonOp, IntervalUnit, MathFunc, StringFunc, UserDefinedFunction} from './types.js'
3
+ * @import { AggregateFunc, BinaryOp, IntervalUnit, MathFunc, StringFunc, UserDefinedFunction } from './types.js'
4
4
  * @param {string} name
5
5
  * @returns {name is AggregateFunc}
6
6
  */