squirreling 0.4.8 → 0.6.0
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 +5 -1
- package/package.json +3 -3
- package/src/backend/dataSource.js +15 -14
- package/src/execute/aggregates.js +10 -4
- package/src/execute/execute.js +58 -40
- package/src/execute/expression.js +212 -54
- package/src/execute/having.js +7 -2
- package/src/execute/join.js +35 -34
- package/src/execute/math.js +340 -0
- package/src/execute/utils.js +2 -2
- package/src/executionErrors.js +63 -0
- package/src/index.js +1 -0
- package/src/parse/comparison.js +41 -8
- package/src/parse/expression.js +53 -13
- package/src/parse/state.js +13 -3
- package/src/parse/tokenize.js +34 -21
- package/src/parseErrors.js +118 -0
- package/src/types.d.ts +55 -16
- package/src/validation.js +14 -1
- package/src/validationErrors.js +138 -0
- package/src/errors.js +0 -230
package/README.md
CHANGED
|
@@ -40,7 +40,10 @@ const users = [
|
|
|
40
40
|
// ...more rows
|
|
41
41
|
]
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
interface AsyncRow {
|
|
44
|
+
columns: string[]
|
|
45
|
+
cells: Record<string, AsyncCell>
|
|
46
|
+
}
|
|
44
47
|
type AsyncCell = () => Promise<SqlPrimitive>
|
|
45
48
|
|
|
46
49
|
// Returns an async iterable of rows with async cells
|
|
@@ -76,6 +79,7 @@ console.log(allUsers)
|
|
|
76
79
|
- `GROUP BY` and `HAVING` clauses
|
|
77
80
|
- Aggregate functions: `COUNT`, `SUM`, `AVG`, `MIN`, `MAX`, `JSON_ARRAYAGG`
|
|
78
81
|
- String functions: `CONCAT`, `SUBSTRING`, `LENGTH`, `UPPER`, `LOWER`
|
|
82
|
+
- Math functions: `ABS`, `CEIL`, `FLOOR`, `ROUND`, `MOD`, `RAND`, `RANDOM`, `LN`, `LOG10`, `EXP`, `POWER`, `SQRT`, `SIN`, `COS`, `TAN`, `COT`, `ASIN`, `ACOS`, `ATAN`, `ATAN2`, `DEGREES`, `RADIANS`, `PI`
|
|
79
83
|
- Date functions: `CURRENT_DATE`, `CURRENT_TIME`, `CURRENT_TIMESTAMP`, `INTERVAL`
|
|
80
84
|
- Json functions: `JSON_VALUE`, `JSON_QUERY`, `JSON_OBJECT`
|
|
81
85
|
- Basic expressions and arithmetic operations
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Squirreling SQL Engine",
|
|
5
5
|
"author": "Hyperparam",
|
|
6
6
|
"homepage": "https://hyperparam.app",
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
"test": "vitest run"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@types/node": "24.10.
|
|
40
|
+
"@types/node": "24.10.2",
|
|
41
41
|
"@vitest/coverage-v8": "4.0.15",
|
|
42
42
|
"eslint": "9.39.1",
|
|
43
|
-
"eslint-plugin-jsdoc": "61.
|
|
43
|
+
"eslint-plugin-jsdoc": "61.5.0",
|
|
44
44
|
"typescript": "5.9.3",
|
|
45
45
|
"vitest": "4.0.15"
|
|
46
46
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @import { AsyncDataSource, AsyncRow, SqlPrimitive } from '../types.js'
|
|
2
|
+
* @import { AsyncCell, AsyncCells, AsyncDataSource, AsyncRow, SqlPrimitive } from '../types.js'
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
export function generatorSource(gen) {
|
|
13
13
|
return {
|
|
14
|
-
async *
|
|
14
|
+
async *scan() {
|
|
15
15
|
yield* gen
|
|
16
16
|
},
|
|
17
17
|
}
|
|
@@ -24,12 +24,12 @@ export function generatorSource(gen) {
|
|
|
24
24
|
* @returns {AsyncRow} a row accessor interface
|
|
25
25
|
*/
|
|
26
26
|
function asyncRow(obj) {
|
|
27
|
-
/** @type {
|
|
28
|
-
const
|
|
27
|
+
/** @type {AsyncCells} */
|
|
28
|
+
const cells = {}
|
|
29
29
|
for (const [key, value] of Object.entries(obj)) {
|
|
30
|
-
|
|
30
|
+
cells[key] = () => Promise.resolve(value)
|
|
31
31
|
}
|
|
32
|
-
return
|
|
32
|
+
return { columns: Object.keys(obj), cells }
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
/**
|
|
@@ -40,7 +40,7 @@ function asyncRow(obj) {
|
|
|
40
40
|
*/
|
|
41
41
|
export function memorySource(data) {
|
|
42
42
|
return {
|
|
43
|
-
async *
|
|
43
|
+
async *scan() {
|
|
44
44
|
for (const item of data) {
|
|
45
45
|
yield asyncRow(item)
|
|
46
46
|
}
|
|
@@ -60,15 +60,16 @@ export function cachedDataSource(source) {
|
|
|
60
60
|
/**
|
|
61
61
|
* @yields {AsyncRow}
|
|
62
62
|
*/
|
|
63
|
-
async *
|
|
63
|
+
async *scan() {
|
|
64
64
|
let index = 0
|
|
65
|
-
for await (const row of source.
|
|
65
|
+
for await (const row of source.scan()) {
|
|
66
66
|
const rowIndex = index
|
|
67
|
-
/** @type {
|
|
68
|
-
const
|
|
69
|
-
for (const
|
|
67
|
+
/** @type {AsyncCells} */
|
|
68
|
+
const cells = {}
|
|
69
|
+
for (const key of row.columns) {
|
|
70
|
+
const cell = row.cells[key]
|
|
70
71
|
// Wrap the cell to cache accesses
|
|
71
|
-
|
|
72
|
+
cells[key] = () => {
|
|
72
73
|
const cacheKey = `${rowIndex}:${key}`
|
|
73
74
|
let value = cache.get(cacheKey)
|
|
74
75
|
if (!value) {
|
|
@@ -78,7 +79,7 @@ export function cachedDataSource(source) {
|
|
|
78
79
|
return value
|
|
79
80
|
}
|
|
80
81
|
}
|
|
81
|
-
yield
|
|
82
|
+
yield { columns: row.columns, cells }
|
|
82
83
|
index++
|
|
83
84
|
}
|
|
84
85
|
},
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { unknownFunctionError } from '../parseErrors.js'
|
|
2
|
+
import { aggregateError } from '../validationErrors.js'
|
|
2
3
|
import { evaluateExpr } from './expression.js'
|
|
3
4
|
import { defaultDerivedAlias, stringify } from './utils.js'
|
|
4
5
|
|
|
@@ -39,7 +40,7 @@ export async function evaluateAggregate({ col, rows, tables }) {
|
|
|
39
40
|
|
|
40
41
|
if (func === 'SUM' || func === 'AVG' || func === 'MIN' || func === 'MAX') {
|
|
41
42
|
if (arg.kind === 'star') {
|
|
42
|
-
throw aggregateError(func, '(*) is not supported, use a column name')
|
|
43
|
+
throw aggregateError({ funcName: func, issue: '(*) is not supported, use a column name' })
|
|
43
44
|
}
|
|
44
45
|
let sum = 0
|
|
45
46
|
let count = 0
|
|
@@ -73,7 +74,7 @@ export async function evaluateAggregate({ col, rows, tables }) {
|
|
|
73
74
|
|
|
74
75
|
if (func === 'JSON_ARRAYAGG') {
|
|
75
76
|
if (arg.kind === 'star') {
|
|
76
|
-
throw aggregateError('JSON_ARRAYAGG', '(*) is not supported, use a column name or expression')
|
|
77
|
+
throw aggregateError({ funcName: 'JSON_ARRAYAGG', issue: '(*) is not supported, use a column name or expression' })
|
|
77
78
|
}
|
|
78
79
|
/** @type {SqlPrimitive[]} */
|
|
79
80
|
const values = []
|
|
@@ -96,7 +97,12 @@ export async function evaluateAggregate({ col, rows, tables }) {
|
|
|
96
97
|
return values
|
|
97
98
|
}
|
|
98
99
|
|
|
99
|
-
throw unknownFunctionError(
|
|
100
|
+
throw unknownFunctionError({
|
|
101
|
+
funcName: func,
|
|
102
|
+
positionStart: 0,
|
|
103
|
+
positionEnd: 0,
|
|
104
|
+
validFunctions: 'COUNT, SUM, AVG, MIN, MAX, JSON_ARRAYAGG',
|
|
105
|
+
})
|
|
100
106
|
}
|
|
101
107
|
|
|
102
108
|
/**
|
package/src/execute/execute.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { missingClauseError
|
|
1
|
+
import { missingClauseError } from '../parseErrors.js'
|
|
2
|
+
import { tableNotFoundError, unsupportedOperationError } from '../executionErrors.js'
|
|
2
3
|
import { generatorSource, memorySource } from '../backend/dataSource.js'
|
|
3
4
|
import { parseSql } from '../parse/parse.js'
|
|
4
5
|
import { defaultAggregateAlias, evaluateAggregate } from './aggregates.js'
|
|
@@ -9,7 +10,7 @@ import { executeJoins } from './join.js'
|
|
|
9
10
|
import { compareForTerm, defaultDerivedAlias, stringify } from './utils.js'
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
|
-
* @import { AsyncDataSource, AsyncRow, ExecuteSqlOptions, OrderByItem, QueryHints, SelectStatement, SqlPrimitive } from '../types.js'
|
|
13
|
+
* @import { AsyncCells, AsyncDataSource, AsyncRow, ExecuteSqlOptions, OrderByItem, QueryHints, SelectStatement, SqlPrimitive } from '../types.js'
|
|
13
14
|
*/
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -19,7 +20,7 @@ import { compareForTerm, defaultDerivedAlias, stringify } from './utils.js'
|
|
|
19
20
|
* @yields {AsyncRow} async generator yielding result rows
|
|
20
21
|
*/
|
|
21
22
|
export async function* executeSql({ tables, query }) {
|
|
22
|
-
const select = parseSql(query)
|
|
23
|
+
const select = typeof query === 'string' ? parseSql(query) : query
|
|
23
24
|
|
|
24
25
|
// Check for unsupported operations
|
|
25
26
|
if (!select.from) {
|
|
@@ -61,7 +62,7 @@ export async function* executeSelect(select, tables) {
|
|
|
61
62
|
fromTableName = select.from.alias ?? select.from.table
|
|
62
63
|
dataSource = tables[select.from.table]
|
|
63
64
|
if (dataSource === undefined) {
|
|
64
|
-
throw tableNotFoundError(select.from.table)
|
|
65
|
+
throw tableNotFoundError({ tableName: select.from.table })
|
|
65
66
|
}
|
|
66
67
|
} else {
|
|
67
68
|
// Nested subquery - recursively resolve
|
|
@@ -80,15 +81,15 @@ export async function* executeSelect(select, tables) {
|
|
|
80
81
|
/**
|
|
81
82
|
* Creates a stable string key for a row to enable deduplication
|
|
82
83
|
*
|
|
83
|
-
* @param {
|
|
84
|
+
* @param {AsyncCells} cells
|
|
84
85
|
* @returns {Promise<string>} a stable string representation of the row
|
|
85
86
|
*/
|
|
86
|
-
async function stableRowKey(
|
|
87
|
-
const keys = Object.keys(
|
|
87
|
+
async function stableRowKey(cells) {
|
|
88
|
+
const keys = Object.keys(cells).sort()
|
|
88
89
|
/** @type {string[]} */
|
|
89
90
|
const parts = []
|
|
90
91
|
for (const k of keys) {
|
|
91
|
-
const v = await
|
|
92
|
+
const v = await cells[k]()
|
|
92
93
|
parts.push(k + ':' + stringify(v))
|
|
93
94
|
}
|
|
94
95
|
return parts.join('|')
|
|
@@ -108,7 +109,7 @@ async function applyDistinct(rows, distinct) {
|
|
|
108
109
|
/** @type {AsyncRow[]} */
|
|
109
110
|
const result = []
|
|
110
111
|
for (const row of rows) {
|
|
111
|
-
const key = await stableRowKey(row)
|
|
112
|
+
const key = await stableRowKey(row.cells)
|
|
112
113
|
if (seen.has(key)) continue
|
|
113
114
|
seen.add(key)
|
|
114
115
|
result.push(row)
|
|
@@ -236,6 +237,7 @@ async function* evaluateSelectAst(select, dataSource, tables) {
|
|
|
236
237
|
async function* evaluateStreaming(select, dataSource, tables) {
|
|
237
238
|
let rowsYielded = 0
|
|
238
239
|
let rowsSkipped = 0
|
|
240
|
+
let rowIndex = 0
|
|
239
241
|
const offset = select.offset ?? 0
|
|
240
242
|
const limit = select.limit ?? Infinity
|
|
241
243
|
if (limit <= 0) return
|
|
@@ -253,10 +255,11 @@ async function* evaluateStreaming(select, dataSource, tables) {
|
|
|
253
255
|
offset: select.offset,
|
|
254
256
|
}
|
|
255
257
|
|
|
256
|
-
for await (const row of dataSource.
|
|
258
|
+
for await (const row of dataSource.scan(hints)) {
|
|
259
|
+
rowIndex++
|
|
257
260
|
// WHERE filter
|
|
258
261
|
if (select.where) {
|
|
259
|
-
const pass = await evaluateExpr({ node: select.where, row, tables })
|
|
262
|
+
const pass = await evaluateExpr({ node: select.where, row, tables, rowIndex })
|
|
260
263
|
if (!pass) continue
|
|
261
264
|
}
|
|
262
265
|
|
|
@@ -267,16 +270,21 @@ async function* evaluateStreaming(select, dataSource, tables) {
|
|
|
267
270
|
}
|
|
268
271
|
|
|
269
272
|
// SELECT projection
|
|
270
|
-
/** @type {
|
|
271
|
-
const
|
|
273
|
+
/** @type {string[]} */
|
|
274
|
+
const columns = []
|
|
275
|
+
/** @type {AsyncCells} */
|
|
276
|
+
const cells = {}
|
|
277
|
+
const currentRowIndex = rowIndex
|
|
272
278
|
for (const col of select.columns) {
|
|
273
279
|
if (col.kind === 'star') {
|
|
274
|
-
for (const
|
|
275
|
-
|
|
280
|
+
for (const key of row.columns) {
|
|
281
|
+
columns.push(key)
|
|
282
|
+
cells[key] = row.cells[key]
|
|
276
283
|
}
|
|
277
284
|
} else if (col.kind === 'derived') {
|
|
278
285
|
const alias = col.alias ?? defaultDerivedAlias(col.expr)
|
|
279
|
-
|
|
286
|
+
columns.push(alias)
|
|
287
|
+
cells[alias] = () => evaluateExpr({ node: col.expr, row, tables, rowIndex: currentRowIndex })
|
|
280
288
|
} else if (col.kind === 'aggregate') {
|
|
281
289
|
throw new Error(
|
|
282
290
|
'Aggregate functions require GROUP BY or will act on the whole dataset; add GROUP BY or remove aggregates'
|
|
@@ -286,7 +294,7 @@ async function* evaluateStreaming(select, dataSource, tables) {
|
|
|
286
294
|
|
|
287
295
|
// DISTINCT: skip duplicate rows
|
|
288
296
|
if (seen) {
|
|
289
|
-
const key = await stableRowKey(
|
|
297
|
+
const key = await stableRowKey(cells)
|
|
290
298
|
if (seen.has(key)) continue
|
|
291
299
|
seen.add(key)
|
|
292
300
|
// OFFSET applies to distinct rows
|
|
@@ -296,7 +304,7 @@ async function* evaluateStreaming(select, dataSource, tables) {
|
|
|
296
304
|
}
|
|
297
305
|
}
|
|
298
306
|
|
|
299
|
-
yield
|
|
307
|
+
yield { columns, cells }
|
|
300
308
|
rowsYielded++
|
|
301
309
|
if (rowsYielded >= limit) {
|
|
302
310
|
break
|
|
@@ -326,7 +334,7 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
|
|
|
326
334
|
// Step 1: Collect all rows from data source
|
|
327
335
|
/** @type {AsyncRow[]} */
|
|
328
336
|
const working = []
|
|
329
|
-
for await (const row of dataSource.
|
|
337
|
+
for await (const row of dataSource.scan(hints)) {
|
|
330
338
|
working.push(row)
|
|
331
339
|
}
|
|
332
340
|
|
|
@@ -334,9 +342,11 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
|
|
|
334
342
|
/** @type {AsyncRow[]} */
|
|
335
343
|
const filtered = []
|
|
336
344
|
|
|
337
|
-
for (
|
|
345
|
+
for (let i = 0; i < working.length; i++) {
|
|
346
|
+
const row = working[i]
|
|
347
|
+
const rowIndex = i + 1 // 1-based
|
|
338
348
|
if (select.where) {
|
|
339
|
-
const passes = await evaluateExpr({ node: select.where, row, tables })
|
|
349
|
+
const passes = await evaluateExpr({ node: select.where, row, tables, rowIndex })
|
|
340
350
|
|
|
341
351
|
if (!passes) {
|
|
342
352
|
continue
|
|
@@ -379,21 +389,23 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
|
|
|
379
389
|
|
|
380
390
|
const hasStar = select.columns.some(col => col.kind === 'star')
|
|
381
391
|
if (hasStar && hasAggregate) {
|
|
382
|
-
throw unsupportedOperationError(
|
|
383
|
-
'SELECT * with aggregate functions is not supported',
|
|
384
|
-
'Replace * with specific column names when using aggregate functions.'
|
|
385
|
-
)
|
|
392
|
+
throw unsupportedOperationError({
|
|
393
|
+
operation: 'SELECT * with aggregate functions is not supported',
|
|
394
|
+
hint: 'Replace * with specific column names when using aggregate functions.',
|
|
395
|
+
})
|
|
386
396
|
}
|
|
387
397
|
|
|
388
398
|
for (const group of groups) {
|
|
389
|
-
|
|
390
|
-
|
|
399
|
+
const columns = []
|
|
400
|
+
/** @type {AsyncCells} */
|
|
401
|
+
const cells = {}
|
|
391
402
|
for (const col of select.columns) {
|
|
392
403
|
if (col.kind === 'star') {
|
|
393
404
|
const firstRow = group[0]
|
|
394
405
|
if (firstRow) {
|
|
395
|
-
for (const
|
|
396
|
-
|
|
406
|
+
for (const key of firstRow.columns) {
|
|
407
|
+
columns.push(key)
|
|
408
|
+
cells[key] = firstRow.cells[key]
|
|
397
409
|
}
|
|
398
410
|
}
|
|
399
411
|
continue
|
|
@@ -401,29 +413,32 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
|
|
|
401
413
|
|
|
402
414
|
if (col.kind === 'derived') {
|
|
403
415
|
const alias = col.alias ?? defaultDerivedAlias(col.expr)
|
|
416
|
+
columns.push(alias)
|
|
404
417
|
if (group.length > 0) {
|
|
405
|
-
|
|
418
|
+
cells[alias] = () => evaluateExpr({ node: col.expr, row: group[0], tables })
|
|
406
419
|
} else {
|
|
407
|
-
delete
|
|
420
|
+
delete cells[alias]
|
|
408
421
|
}
|
|
409
422
|
continue
|
|
410
423
|
}
|
|
411
424
|
|
|
412
425
|
if (col.kind === 'aggregate') {
|
|
413
426
|
const alias = col.alias ?? defaultAggregateAlias(col)
|
|
414
|
-
|
|
427
|
+
columns.push(alias)
|
|
428
|
+
cells[alias] = () => evaluateAggregate({ col, rows: group, tables })
|
|
415
429
|
continue
|
|
416
430
|
}
|
|
417
431
|
}
|
|
432
|
+
const asyncRow = { columns, cells }
|
|
418
433
|
|
|
419
434
|
// Apply HAVING filter before adding to projected results
|
|
420
435
|
if (select.having) {
|
|
421
|
-
if (!await evaluateHavingExpr(select.having,
|
|
436
|
+
if (!await evaluateHavingExpr(select.having, asyncRow, group, tables)) {
|
|
422
437
|
continue
|
|
423
438
|
}
|
|
424
439
|
}
|
|
425
440
|
|
|
426
|
-
projected.push(
|
|
441
|
+
projected.push(asyncRow)
|
|
427
442
|
}
|
|
428
443
|
} else {
|
|
429
444
|
// No grouping, simple projection
|
|
@@ -440,19 +455,22 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
|
|
|
440
455
|
}
|
|
441
456
|
|
|
442
457
|
for (const row of rowsToProject) {
|
|
443
|
-
|
|
444
|
-
|
|
458
|
+
const columns = []
|
|
459
|
+
/** @type {AsyncCells} */
|
|
460
|
+
const cells = {}
|
|
445
461
|
for (const col of select.columns) {
|
|
446
462
|
if (col.kind === 'star') {
|
|
447
|
-
for (const
|
|
448
|
-
|
|
463
|
+
for (const key of row.columns) {
|
|
464
|
+
columns.push(key)
|
|
465
|
+
cells[key] = row.cells[key]
|
|
449
466
|
}
|
|
450
467
|
} else if (col.kind === 'derived') {
|
|
451
468
|
const alias = col.alias ?? defaultDerivedAlias(col.expr)
|
|
452
|
-
|
|
469
|
+
columns.push(alias)
|
|
470
|
+
cells[alias] = () => evaluateExpr({ node: col.expr, row, tables })
|
|
453
471
|
}
|
|
454
472
|
}
|
|
455
|
-
projected.push(
|
|
473
|
+
projected.push({ columns, cells })
|
|
456
474
|
}
|
|
457
475
|
}
|
|
458
476
|
|