squirreling 0.4.7 → 0.5.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 +1 -0
- package/package.json +3 -3
- package/src/execute/aggregates.js +12 -5
- package/src/execute/date.js +57 -0
- package/src/execute/execute.js +20 -7
- package/src/execute/expression.js +268 -40
- package/src/execute/having.js +7 -1
- package/src/execute/join.js +9 -4
- package/src/execute/math.js +165 -0
- package/src/execute/utils.js +3 -0
- package/src/executionErrors.js +62 -0
- package/src/index.js +1 -0
- package/src/parse/comparison.js +41 -7
- package/src/parse/expression.js +121 -10
- package/src/parse/parse.js +6 -1
- package/src/parse/state.js +16 -4
- package/src/parse/tokenize.js +113 -48
- package/src/parseErrors.js +117 -0
- package/src/types.d.ts +58 -14
- package/src/validation.js +23 -1
- package/src/validationErrors.js +127 -0
package/README.md
CHANGED
|
@@ -76,5 +76,6 @@ console.log(allUsers)
|
|
|
76
76
|
- `GROUP BY` and `HAVING` clauses
|
|
77
77
|
- Aggregate functions: `COUNT`, `SUM`, `AVG`, `MIN`, `MAX`, `JSON_ARRAYAGG`
|
|
78
78
|
- String functions: `CONCAT`, `SUBSTRING`, `LENGTH`, `UPPER`, `LOWER`
|
|
79
|
+
- Date functions: `CURRENT_DATE`, `CURRENT_TIME`, `CURRENT_TIMESTAMP`, `INTERVAL`
|
|
79
80
|
- Json functions: `JSON_VALUE`, `JSON_QUERY`, `JSON_OBJECT`
|
|
80
81
|
- 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.5.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,3 +1,5 @@
|
|
|
1
|
+
import { unknownFunctionError } from '../parseErrors.js'
|
|
2
|
+
import { aggregateError } from '../validationErrors.js'
|
|
1
3
|
import { evaluateExpr } from './expression.js'
|
|
2
4
|
import { defaultDerivedAlias, stringify } from './utils.js'
|
|
3
5
|
|
|
@@ -20,7 +22,7 @@ export async function evaluateAggregate({ col, rows, tables }) {
|
|
|
20
22
|
const seen = new Set()
|
|
21
23
|
for (const row of rows) {
|
|
22
24
|
const v = await evaluateExpr({ node: arg.expr, row, tables })
|
|
23
|
-
if (v
|
|
25
|
+
if (v != null) {
|
|
24
26
|
seen.add(v)
|
|
25
27
|
}
|
|
26
28
|
}
|
|
@@ -29,7 +31,7 @@ export async function evaluateAggregate({ col, rows, tables }) {
|
|
|
29
31
|
let count = 0
|
|
30
32
|
for (const row of rows) {
|
|
31
33
|
const v = await evaluateExpr({ node: arg.expr, row, tables })
|
|
32
|
-
if (v
|
|
34
|
+
if (v != null) {
|
|
33
35
|
count += 1
|
|
34
36
|
}
|
|
35
37
|
}
|
|
@@ -38,7 +40,7 @@ export async function evaluateAggregate({ col, rows, tables }) {
|
|
|
38
40
|
|
|
39
41
|
if (func === 'SUM' || func === 'AVG' || func === 'MIN' || func === 'MAX') {
|
|
40
42
|
if (arg.kind === 'star') {
|
|
41
|
-
throw
|
|
43
|
+
throw aggregateError({ funcName: func, issue: '(*) is not supported, use a column name' })
|
|
42
44
|
}
|
|
43
45
|
let sum = 0
|
|
44
46
|
let count = 0
|
|
@@ -72,7 +74,7 @@ export async function evaluateAggregate({ col, rows, tables }) {
|
|
|
72
74
|
|
|
73
75
|
if (func === 'JSON_ARRAYAGG') {
|
|
74
76
|
if (arg.kind === 'star') {
|
|
75
|
-
throw
|
|
77
|
+
throw aggregateError({ funcName: 'JSON_ARRAYAGG', issue: '(*) is not supported, use a column name or expression' })
|
|
76
78
|
}
|
|
77
79
|
/** @type {SqlPrimitive[]} */
|
|
78
80
|
const values = []
|
|
@@ -95,7 +97,12 @@ export async function evaluateAggregate({ col, rows, tables }) {
|
|
|
95
97
|
return values
|
|
96
98
|
}
|
|
97
99
|
|
|
98
|
-
throw
|
|
100
|
+
throw unknownFunctionError({
|
|
101
|
+
funcName: func,
|
|
102
|
+
positionStart: 0,
|
|
103
|
+
positionEnd: 0,
|
|
104
|
+
validFunctions: 'COUNT, SUM, AVG, MIN, MAX, JSON_ARRAYAGG',
|
|
105
|
+
})
|
|
99
106
|
}
|
|
100
107
|
|
|
101
108
|
/**
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { SqlPrimitive, IntervalUnit } from '../types.js'
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {SqlPrimitive} val
|
|
7
|
+
* @returns {Date | null}
|
|
8
|
+
*/
|
|
9
|
+
function toDate(val) {
|
|
10
|
+
if (val instanceof Date) return val
|
|
11
|
+
const dateOrTime = /^\d{4}-\d{2}-\d{2}(T\d{2}:\d{2}:\d{2})?/
|
|
12
|
+
if (typeof val === 'string' && dateOrTime.test(val)) {
|
|
13
|
+
const date = new Date(val)
|
|
14
|
+
if (!isNaN(date.getTime())) {
|
|
15
|
+
return date
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Apply an interval to a date
|
|
23
|
+
* @param {SqlPrimitive} dateVal
|
|
24
|
+
* @param {number} value
|
|
25
|
+
* @param {IntervalUnit} unit
|
|
26
|
+
* @param {'+' | '-'} op
|
|
27
|
+
* @returns {string | null}
|
|
28
|
+
*/
|
|
29
|
+
export function applyIntervalToDate(dateVal, value, unit, op) {
|
|
30
|
+
const date = toDate(dateVal)
|
|
31
|
+
if (date == null) return null
|
|
32
|
+
|
|
33
|
+
const multiplier = op === '+' ? 1 : -1
|
|
34
|
+
const adjusted = value * multiplier
|
|
35
|
+
|
|
36
|
+
if (unit === 'SECOND') {
|
|
37
|
+
date.setUTCSeconds(date.getUTCSeconds() + adjusted)
|
|
38
|
+
} else if (unit === 'MINUTE') {
|
|
39
|
+
date.setUTCMinutes(date.getUTCMinutes() + adjusted)
|
|
40
|
+
} else if (unit === 'HOUR') {
|
|
41
|
+
date.setUTCHours(date.getUTCHours() + adjusted)
|
|
42
|
+
} else if (unit === 'DAY') {
|
|
43
|
+
date.setUTCDate(date.getUTCDate() + adjusted)
|
|
44
|
+
} else if (unit === 'MONTH') {
|
|
45
|
+
date.setUTCMonth(date.getUTCMonth() + adjusted)
|
|
46
|
+
} else if (unit === 'YEAR') {
|
|
47
|
+
date.setUTCFullYear(date.getUTCFullYear() + adjusted)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Return in same format as input
|
|
51
|
+
if (dateVal instanceof Date) return date.toISOString()
|
|
52
|
+
if (String(dateVal).includes('T')) {
|
|
53
|
+
return date.toISOString()
|
|
54
|
+
} else {
|
|
55
|
+
return date.toISOString().split('T')[0]
|
|
56
|
+
}
|
|
57
|
+
}
|
package/src/execute/execute.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { missingClauseError } from '../parseErrors.js'
|
|
2
|
+
import { tableNotFoundError, unsupportedOperationError } from '../executionErrors.js'
|
|
1
3
|
import { generatorSource, memorySource } from '../backend/dataSource.js'
|
|
2
4
|
import { parseSql } from '../parse/parse.js'
|
|
3
5
|
import { defaultAggregateAlias, evaluateAggregate } from './aggregates.js'
|
|
@@ -22,7 +24,10 @@ export async function* executeSql({ tables, query }) {
|
|
|
22
24
|
|
|
23
25
|
// Check for unsupported operations
|
|
24
26
|
if (!select.from) {
|
|
25
|
-
throw
|
|
27
|
+
throw missingClauseError({
|
|
28
|
+
missing: 'FROM clause',
|
|
29
|
+
context: 'SELECT statement',
|
|
30
|
+
})
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
// Normalize tables: convert arrays to AsyncDataSource
|
|
@@ -57,7 +62,7 @@ export async function* executeSelect(select, tables) {
|
|
|
57
62
|
fromTableName = select.from.alias ?? select.from.table
|
|
58
63
|
dataSource = tables[select.from.table]
|
|
59
64
|
if (dataSource === undefined) {
|
|
60
|
-
throw
|
|
65
|
+
throw tableNotFoundError({ tableName: select.from.table })
|
|
61
66
|
}
|
|
62
67
|
} else {
|
|
63
68
|
// Nested subquery - recursively resolve
|
|
@@ -232,6 +237,7 @@ async function* evaluateSelectAst(select, dataSource, tables) {
|
|
|
232
237
|
async function* evaluateStreaming(select, dataSource, tables) {
|
|
233
238
|
let rowsYielded = 0
|
|
234
239
|
let rowsSkipped = 0
|
|
240
|
+
let rowIndex = 0
|
|
235
241
|
const offset = select.offset ?? 0
|
|
236
242
|
const limit = select.limit ?? Infinity
|
|
237
243
|
if (limit <= 0) return
|
|
@@ -250,9 +256,10 @@ async function* evaluateStreaming(select, dataSource, tables) {
|
|
|
250
256
|
}
|
|
251
257
|
|
|
252
258
|
for await (const row of dataSource.getRows(hints)) {
|
|
259
|
+
rowIndex++
|
|
253
260
|
// WHERE filter
|
|
254
261
|
if (select.where) {
|
|
255
|
-
const pass = await evaluateExpr({ node: select.where, row, tables })
|
|
262
|
+
const pass = await evaluateExpr({ node: select.where, row, tables, rowIndex })
|
|
256
263
|
if (!pass) continue
|
|
257
264
|
}
|
|
258
265
|
|
|
@@ -265,6 +272,7 @@ async function* evaluateStreaming(select, dataSource, tables) {
|
|
|
265
272
|
// SELECT projection
|
|
266
273
|
/** @type {AsyncRow} */
|
|
267
274
|
const outRow = {}
|
|
275
|
+
const currentRowIndex = rowIndex
|
|
268
276
|
for (const col of select.columns) {
|
|
269
277
|
if (col.kind === 'star') {
|
|
270
278
|
for (const [key, cell] of Object.entries(row)) {
|
|
@@ -272,7 +280,7 @@ async function* evaluateStreaming(select, dataSource, tables) {
|
|
|
272
280
|
}
|
|
273
281
|
} else if (col.kind === 'derived') {
|
|
274
282
|
const alias = col.alias ?? defaultDerivedAlias(col.expr)
|
|
275
|
-
outRow[alias] = () => evaluateExpr({ node: col.expr, row, tables })
|
|
283
|
+
outRow[alias] = () => evaluateExpr({ node: col.expr, row, tables, rowIndex: currentRowIndex })
|
|
276
284
|
} else if (col.kind === 'aggregate') {
|
|
277
285
|
throw new Error(
|
|
278
286
|
'Aggregate functions require GROUP BY or will act on the whole dataset; add GROUP BY or remove aggregates'
|
|
@@ -330,9 +338,11 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
|
|
|
330
338
|
/** @type {AsyncRow[]} */
|
|
331
339
|
const filtered = []
|
|
332
340
|
|
|
333
|
-
for (
|
|
341
|
+
for (let i = 0; i < working.length; i++) {
|
|
342
|
+
const row = working[i]
|
|
343
|
+
const rowIndex = i + 1 // 1-based
|
|
334
344
|
if (select.where) {
|
|
335
|
-
const passes = await evaluateExpr({ node: select.where, row, tables })
|
|
345
|
+
const passes = await evaluateExpr({ node: select.where, row, tables, rowIndex })
|
|
336
346
|
|
|
337
347
|
if (!passes) {
|
|
338
348
|
continue
|
|
@@ -375,7 +385,10 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
|
|
|
375
385
|
|
|
376
386
|
const hasStar = select.columns.some(col => col.kind === 'star')
|
|
377
387
|
if (hasStar && hasAggregate) {
|
|
378
|
-
throw
|
|
388
|
+
throw unsupportedOperationError({
|
|
389
|
+
operation: 'SELECT * with aggregate functions is not supported',
|
|
390
|
+
hint: 'Replace * with specific column names when using aggregate functions.',
|
|
391
|
+
})
|
|
379
392
|
}
|
|
380
393
|
|
|
381
394
|
for (const group of groups) {
|