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 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)
@@ -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.6",
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.6",
41
- "@vitest/coverage-v8": "4.0.16",
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.16"
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 === val) return true
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 === value) return true
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 === whenValue
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 })
@@ -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]
@@ -214,8 +214,7 @@ function parseFromSubquery(state) {
214
214
  expect(state, 'paren', '(')
215
215
  const query = parseSelectInternal(state)
216
216
  expect(state, 'paren', ')')
217
- expect(state, 'keyword', 'AS')
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
@@ -217,7 +217,9 @@ export type MathFunc =
217
217
  | 'FLOOR'
218
218
  | 'CEIL'
219
219
  | 'CEILING'
220
+ | 'ROUND'
220
221
  | 'ABS'
222
+ | 'SIGN'
221
223
  | 'MOD'
222
224
  | 'EXP'
223
225
  | 'LN'
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 },
@@ -34,6 +34,7 @@ export const FUNCTION_SIGNATURES = {
34
34
  FLOOR: 'number',
35
35
  CEIL: 'number',
36
36
  CEILING: 'number',
37
+ ROUND: 'number[, decimals]',
37
38
  ABS: 'number',
38
39
  MOD: 'dividend, divisor',
39
40
  EXP: 'number',