squirreling 0.1.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/LICENSE +7 -0
- package/README.md +28 -0
- package/package.json +38 -0
- package/src/execute/aggregates.js +73 -0
- package/src/execute/execute.js +282 -0
- package/src/execute/expression.js +177 -0
- package/src/index.d.ts +18 -0
- package/src/index.js +2 -0
- package/src/parse/expression.js +248 -0
- package/src/parse/parse.js +520 -0
- package/src/parse/tokenize.js +332 -0
- package/src/types.d.ts +159 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Squirreling SQL Engine
|
|
2
|
+
|
|
3
|
+
[](https://opensource.org/licenses/MIT)
|
|
4
|
+
|
|
5
|
+
Squirreling is a lightweight SQL engine for JavaScript applications, designed to provide efficient and easy-to-use database functionalities in the browser.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Lightweight and fast
|
|
10
|
+
- Easy to integrate with JavaScript applications
|
|
11
|
+
- Supports standard SQL queries
|
|
12
|
+
- In-memory database for quick data access
|
|
13
|
+
- Robust error handling and validation
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```javascript
|
|
18
|
+
import { executeSql } from 'squirreling'
|
|
19
|
+
|
|
20
|
+
const data = [
|
|
21
|
+
{ id: 1, name: 'Alice' },
|
|
22
|
+
{ id: 2, name: 'Bob' },
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
const result = executeSql(data, 'SELECT UPPER(name) AS name_upper FROM users')
|
|
26
|
+
console.log(result)
|
|
27
|
+
// Output: [ { name_upper: 'ALICE' }, { name_upper: 'BOB' } ]
|
|
28
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "squirreling",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"author": "Hyperparam",
|
|
5
|
+
"homepage": "https://hyperparam.app",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/hyparam/squirrel.git"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"sideEffects": false,
|
|
13
|
+
"types": "src/index.d.ts",
|
|
14
|
+
"main": "src/index.js",
|
|
15
|
+
"export": {
|
|
16
|
+
".": {
|
|
17
|
+
"import": "./src/index.js",
|
|
18
|
+
"types": "./src/index.d.ts"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"src"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"coverage": "vitest run --coverage --coverage.include=src",
|
|
26
|
+
"lint": "eslint",
|
|
27
|
+
"lint:fix": "eslint --fix",
|
|
28
|
+
"test": "vitest run"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "24.10.1",
|
|
32
|
+
"@vitest/coverage-v8": "4.0.13",
|
|
33
|
+
"eslint": "9.39.1",
|
|
34
|
+
"eslint-plugin-jsdoc": "61.4.0",
|
|
35
|
+
"typescript": "5.9.3",
|
|
36
|
+
"vitest": "4.0.13"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* Evaluates an aggregate function over a set of rows
|
|
4
|
+
*
|
|
5
|
+
* @import { AggregateColumn, Row } from '../types.js'
|
|
6
|
+
* @param {AggregateColumn} col - aggregate column definition
|
|
7
|
+
* @param {Row[]} rows - rows to aggregate
|
|
8
|
+
* @returns {number | null} aggregated result
|
|
9
|
+
*/
|
|
10
|
+
export function evaluateAggregate(col, rows) {
|
|
11
|
+
const { arg, func } = col
|
|
12
|
+
|
|
13
|
+
if (func === 'COUNT') {
|
|
14
|
+
if (arg.kind === 'star') return rows.length
|
|
15
|
+
const field = arg.column
|
|
16
|
+
let count = 0
|
|
17
|
+
for (let i = 0; i < rows.length; i += 1) {
|
|
18
|
+
const v = rows[i][field]
|
|
19
|
+
if (v !== null && v !== undefined) {
|
|
20
|
+
count += 1
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return count
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (func === 'SUM' || func === 'AVG' || func === 'MIN' || func === 'MAX') {
|
|
27
|
+
if (arg.kind === 'star') {
|
|
28
|
+
throw new Error(func + '(*) is not supported, use a column name')
|
|
29
|
+
}
|
|
30
|
+
const field = arg.column
|
|
31
|
+
let sum = 0
|
|
32
|
+
let count = 0
|
|
33
|
+
/** @type {number | null} */
|
|
34
|
+
let min = null
|
|
35
|
+
/** @type {number | null} */
|
|
36
|
+
let max = null
|
|
37
|
+
|
|
38
|
+
for (let i = 0; i < rows.length; i += 1) {
|
|
39
|
+
const raw = rows[i][field]
|
|
40
|
+
if (raw == null) continue
|
|
41
|
+
const num = Number(raw)
|
|
42
|
+
if (!Number.isFinite(num)) continue
|
|
43
|
+
|
|
44
|
+
if (count === 0) {
|
|
45
|
+
min = num
|
|
46
|
+
max = num
|
|
47
|
+
} else {
|
|
48
|
+
if (min == null || num < min) min = num
|
|
49
|
+
if (max == null || num > max) max = num
|
|
50
|
+
}
|
|
51
|
+
sum += num
|
|
52
|
+
count += 1
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (func === 'SUM') return sum
|
|
56
|
+
if (func === 'AVG') return count === 0 ? null : sum / count
|
|
57
|
+
if (func === 'MIN') return min
|
|
58
|
+
if (func === 'MAX') return max
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
throw new Error('Unsupported aggregate function ' + func)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Generates a default alias name for an aggregate function
|
|
66
|
+
* @param {AggregateColumn} col - The aggregate column definition
|
|
67
|
+
* @returns {string} The generated alias (e.g., "count_all", "sum_amount")
|
|
68
|
+
*/
|
|
69
|
+
export function defaultAggregateAlias(col) {
|
|
70
|
+
const base = col.func.toLowerCase()
|
|
71
|
+
if (col.arg.kind === 'star') return base + '_all'
|
|
72
|
+
return base + '_' + col.arg.column
|
|
73
|
+
}
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { FunctionColumn, FunctionNode, OrderByItem, Row, SelectStatement, SqlPrimitive } from '../types.js'
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { defaultAggregateAlias, evaluateAggregate } from './aggregates.js'
|
|
6
|
+
import { evaluateExpr } from './expression.js'
|
|
7
|
+
import { parseSql } from '../parse/parse.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Executes a SQL SELECT query against an array of data rows
|
|
11
|
+
* @param {Row[]} rows - The data rows to query
|
|
12
|
+
* @param {string} sql - The SQL query string
|
|
13
|
+
* @returns {Row[]} The result rows matching the query
|
|
14
|
+
*/
|
|
15
|
+
export function executeSql(rows, sql) {
|
|
16
|
+
const select = parseSql(sql)
|
|
17
|
+
return evaluateSelectAst(select, rows)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Generates a default alias name for a string function
|
|
22
|
+
* @param {FunctionColumn} col - The function column definition
|
|
23
|
+
* @returns {string} The generated alias (e.g., "upper_name", "concat_a_b")
|
|
24
|
+
*/
|
|
25
|
+
function defaultFunctionAlias(col) {
|
|
26
|
+
const base = col.func.toLowerCase()
|
|
27
|
+
// Try to extract column names from identifier arguments
|
|
28
|
+
const columnNames = col.args
|
|
29
|
+
.filter(arg => arg.type === 'identifier')
|
|
30
|
+
.map(arg => arg.name)
|
|
31
|
+
if (columnNames.length > 0) {
|
|
32
|
+
return base + '_' + columnNames.join('_')
|
|
33
|
+
}
|
|
34
|
+
return base
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Creates a stable string key for a row to enable deduplication
|
|
39
|
+
* @param {Row} row - The data row
|
|
40
|
+
* @returns {string} A stable string representation of the row
|
|
41
|
+
*/
|
|
42
|
+
function stableRowKey(row) {
|
|
43
|
+
const keys = Object.keys(row).sort()
|
|
44
|
+
/** @type {string[]} */
|
|
45
|
+
const parts = []
|
|
46
|
+
for (const k of keys) {
|
|
47
|
+
const v = row[k]
|
|
48
|
+
parts.push(k + ':' + JSON.stringify(v))
|
|
49
|
+
}
|
|
50
|
+
return parts.join('|')
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Compares two SQL values for sorting
|
|
55
|
+
* @param {SqlPrimitive} a - First value to compare
|
|
56
|
+
* @param {SqlPrimitive} b - Second value to compare
|
|
57
|
+
* @returns {number} Negative if a < b, positive if a > b, 0 if equal
|
|
58
|
+
*/
|
|
59
|
+
function compareValues(a, b) {
|
|
60
|
+
if (a === b) return 0
|
|
61
|
+
if (a == null) return -1
|
|
62
|
+
if (b == null) return 1
|
|
63
|
+
|
|
64
|
+
if (typeof a === 'number' && typeof b === 'number') {
|
|
65
|
+
if (a < b) return -1
|
|
66
|
+
if (a > b) return 1
|
|
67
|
+
return 0
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const as = String(a)
|
|
71
|
+
const bs = String(b)
|
|
72
|
+
if (as < bs) return -1
|
|
73
|
+
if (as > bs) return 1
|
|
74
|
+
return 0
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Applies DISTINCT filtering to remove duplicate rows
|
|
79
|
+
* @param {Row[]} rows - The input rows
|
|
80
|
+
* @param {boolean} distinct - Whether to apply deduplication
|
|
81
|
+
* @returns {Row[]} The deduplicated rows
|
|
82
|
+
*/
|
|
83
|
+
function applyDistinct(rows, distinct) {
|
|
84
|
+
if (!distinct) return rows
|
|
85
|
+
/** @type {Set<string>} */
|
|
86
|
+
const seen = new Set()
|
|
87
|
+
/** @type {Row[]} */
|
|
88
|
+
const result = []
|
|
89
|
+
for (const row of rows) {
|
|
90
|
+
const key = stableRowKey(row)
|
|
91
|
+
if (seen.has(key)) continue
|
|
92
|
+
seen.add(key)
|
|
93
|
+
result.push(row)
|
|
94
|
+
}
|
|
95
|
+
return result
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Applies ORDER BY sorting to rows
|
|
100
|
+
* @param {Row[]} rows - The input rows
|
|
101
|
+
* @param {OrderByItem[]} orderBy - The sort specifications
|
|
102
|
+
* @returns {Row[]} The sorted rows
|
|
103
|
+
*/
|
|
104
|
+
function applyOrderBy(rows, orderBy) {
|
|
105
|
+
if (!orderBy || orderBy.length === 0) return rows
|
|
106
|
+
|
|
107
|
+
const sorted = rows.slice()
|
|
108
|
+
|
|
109
|
+
sorted.sort((a, b) => {
|
|
110
|
+
for (const term of orderBy) {
|
|
111
|
+
const dir = term.direction
|
|
112
|
+
const av = evaluateExpr(term.expr, a)
|
|
113
|
+
const bv = evaluateExpr(term.expr, b)
|
|
114
|
+
const cmp = compareValues(av, bv)
|
|
115
|
+
if (cmp !== 0) {
|
|
116
|
+
return dir === 'DESC' ? -cmp : cmp
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return 0
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
return sorted
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Evaluates a parsed SELECT AST against data rows
|
|
127
|
+
* @param {SelectStatement} select - The parsed SQL AST
|
|
128
|
+
* @param {Row[]} rows - The data rows
|
|
129
|
+
* @returns {Row[]} The filtered, projected, and sorted result rows
|
|
130
|
+
*/
|
|
131
|
+
function evaluateSelectAst(select, rows) {
|
|
132
|
+
// Check for unsupported JOIN operations
|
|
133
|
+
if (select.joins.length) {
|
|
134
|
+
throw new Error('JOIN is not supported')
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// WHERE
|
|
138
|
+
let working = rows
|
|
139
|
+
if (select.where) {
|
|
140
|
+
/** @type {Row[]} */
|
|
141
|
+
const filtered = []
|
|
142
|
+
for (const row of rows) {
|
|
143
|
+
if (evaluateExpr(select.where, row)) {
|
|
144
|
+
filtered.push(row)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
working = filtered
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const hasAggregate = select.columns.some(col => col.kind === 'aggregate')
|
|
151
|
+
const useGrouping = hasAggregate || select.groupBy?.length > 0
|
|
152
|
+
|
|
153
|
+
/** @type {Row[]} */
|
|
154
|
+
const projected = []
|
|
155
|
+
|
|
156
|
+
if (useGrouping) {
|
|
157
|
+
/** @type {Row[][]} */
|
|
158
|
+
const groups = []
|
|
159
|
+
|
|
160
|
+
if (select.groupBy?.length) {
|
|
161
|
+
/** @type {Map<string, Row[]>} */
|
|
162
|
+
const map = new Map()
|
|
163
|
+
for (const row of working) {
|
|
164
|
+
/** @type {string[]} */
|
|
165
|
+
const keyParts = []
|
|
166
|
+
for (const expr of select.groupBy) {
|
|
167
|
+
const v = evaluateExpr(expr, row)
|
|
168
|
+
keyParts.push(JSON.stringify(v))
|
|
169
|
+
}
|
|
170
|
+
const key = keyParts.join('|')
|
|
171
|
+
let group = map.get(key)
|
|
172
|
+
if (!group) {
|
|
173
|
+
group = []
|
|
174
|
+
map.set(key, group)
|
|
175
|
+
groups.push(group)
|
|
176
|
+
}
|
|
177
|
+
group.push(row)
|
|
178
|
+
}
|
|
179
|
+
} else {
|
|
180
|
+
groups.push(working)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const hasStar = select.columns.some(col => col.kind === 'star')
|
|
184
|
+
if (hasStar && hasAggregate) {
|
|
185
|
+
throw new Error('SELECT * with aggregate functions is not supported in this implementation')
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (const group of groups) {
|
|
189
|
+
/** @type {Row} */
|
|
190
|
+
const resultRow = {}
|
|
191
|
+
for (const col of select.columns) {
|
|
192
|
+
if (col.kind === 'star') {
|
|
193
|
+
const firstRow = group[0] || {}
|
|
194
|
+
const keys = Object.keys(firstRow)
|
|
195
|
+
for (const key of keys) {
|
|
196
|
+
resultRow[key] = firstRow[key]
|
|
197
|
+
}
|
|
198
|
+
continue
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (col.kind === 'column') {
|
|
202
|
+
const name = col.column
|
|
203
|
+
const alias = col.alias ?? name
|
|
204
|
+
// Evaluate on first row of group (all rows have same value for GROUP BY columns)
|
|
205
|
+
resultRow[alias] = group.length > 0 ? group[0][name] : undefined
|
|
206
|
+
continue
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (col.kind === 'function') {
|
|
210
|
+
// Evaluate function on the first row of the group
|
|
211
|
+
/** @type {FunctionNode} */
|
|
212
|
+
const funcNode = { type: 'function', name: col.func, args: col.args }
|
|
213
|
+
const alias = col.alias ?? defaultFunctionAlias(col)
|
|
214
|
+
const value = group.length > 0 ? evaluateExpr(funcNode, group[0]) : undefined
|
|
215
|
+
resultRow[alias] = value
|
|
216
|
+
continue
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (col.kind === 'aggregate') {
|
|
220
|
+
const alias = col.alias ?? defaultAggregateAlias(col)
|
|
221
|
+
const value = evaluateAggregate(col, group)
|
|
222
|
+
resultRow[alias] = value
|
|
223
|
+
continue
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (col.kind === 'operation') {
|
|
227
|
+
const alias = col.alias ?? 'expr'
|
|
228
|
+
const value = group.length > 0 ? evaluateExpr(col.expr, group[0]) : undefined
|
|
229
|
+
resultRow[alias] = value
|
|
230
|
+
continue
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
projected.push(resultRow)
|
|
234
|
+
}
|
|
235
|
+
} else {
|
|
236
|
+
for (const row of working) {
|
|
237
|
+
/** @type {Row} */
|
|
238
|
+
const outRow = {}
|
|
239
|
+
for (const col of select.columns) {
|
|
240
|
+
if (col.kind === 'star') {
|
|
241
|
+
const keys = Object.keys(row)
|
|
242
|
+
for (const key of keys) {
|
|
243
|
+
outRow[key] = row[key]
|
|
244
|
+
}
|
|
245
|
+
} else if (col.kind === 'column') {
|
|
246
|
+
const name = col.column
|
|
247
|
+
const alias = col.alias ?? name
|
|
248
|
+
outRow[alias] = row[name]
|
|
249
|
+
} else if (col.kind === 'function') {
|
|
250
|
+
/** @type {FunctionNode} */
|
|
251
|
+
const funcNode = { type: 'function', name: col.func, args: col.args }
|
|
252
|
+
const value = evaluateExpr(funcNode, row)
|
|
253
|
+
const alias = col.alias ?? defaultFunctionAlias(col)
|
|
254
|
+
outRow[alias] = value
|
|
255
|
+
} else if (col.kind === 'operation') {
|
|
256
|
+
const alias = col.alias ?? 'expr'
|
|
257
|
+
const value = evaluateExpr(col.expr, row)
|
|
258
|
+
outRow[alias] = value
|
|
259
|
+
} else if (col.kind === 'aggregate') {
|
|
260
|
+
throw new Error(
|
|
261
|
+
'Aggregate functions require GROUP BY or will act on the whole dataset; add GROUP BY or remove aggregates'
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
projected.push(outRow)
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
let result = projected
|
|
270
|
+
|
|
271
|
+
result = applyDistinct(result, select.distinct)
|
|
272
|
+
result = applyOrderBy(result, select.orderBy)
|
|
273
|
+
|
|
274
|
+
if (typeof select.offset === 'number' && select.offset > 0) {
|
|
275
|
+
result = result.slice(select.offset)
|
|
276
|
+
}
|
|
277
|
+
if (typeof select.limit === 'number') {
|
|
278
|
+
result = result.slice(0, select.limit)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return result
|
|
282
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* Evaluates an expression node against a row of data
|
|
4
|
+
*
|
|
5
|
+
* @import { ExprNode, Row, SqlPrimitive } from '../types.js'
|
|
6
|
+
* @param {ExprNode} node - The expression node to evaluate
|
|
7
|
+
* @param {Row} row - The data row to evaluate against
|
|
8
|
+
* @returns {SqlPrimitive} The result of the evaluation
|
|
9
|
+
*/
|
|
10
|
+
export function evaluateExpr(node, row) {
|
|
11
|
+
if (node.type === 'literal') {
|
|
12
|
+
return node.value
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (node.type === 'identifier') {
|
|
16
|
+
return row[node.name]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (node.type === 'unary') {
|
|
20
|
+
if (node.op === 'NOT') {
|
|
21
|
+
return !evaluateExpr(node.argument, row)
|
|
22
|
+
}
|
|
23
|
+
if (node.op === 'IS NULL') {
|
|
24
|
+
return evaluateExpr(node.argument, row) == null
|
|
25
|
+
}
|
|
26
|
+
if (node.op === 'IS NOT NULL') {
|
|
27
|
+
return evaluateExpr(node.argument, row) != null
|
|
28
|
+
}
|
|
29
|
+
if (node.op === '-') {
|
|
30
|
+
const val = evaluateExpr(node.argument, row)
|
|
31
|
+
if (val == null) return null
|
|
32
|
+
return -Number(val)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (node.type === 'binary') {
|
|
37
|
+
if (node.op === 'AND') {
|
|
38
|
+
const leftVal = evaluateExpr(node.left, row)
|
|
39
|
+
if (!leftVal) return false
|
|
40
|
+
return Boolean(evaluateExpr(node.right, row))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (node.op === 'OR') {
|
|
44
|
+
const leftVal = evaluateExpr(node.left, row)
|
|
45
|
+
if (leftVal) return true
|
|
46
|
+
return Boolean(evaluateExpr(node.right, row))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const left = evaluateExpr(node.left, row)
|
|
50
|
+
const right = evaluateExpr(node.right, row)
|
|
51
|
+
|
|
52
|
+
// In SQL, NULL comparisons with =, !=, <> always return false (unknown)
|
|
53
|
+
// You must use IS NULL or IS NOT NULL to check for NULL
|
|
54
|
+
if (left == null || right == null) {
|
|
55
|
+
if (node.op === '=' || node.op === '!=' || node.op === '<>') {
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (node.op === '=') return left === right
|
|
61
|
+
if (node.op === '!=' || node.op === '<>') return left !== right
|
|
62
|
+
if (node.op === '<') return left < right
|
|
63
|
+
if (node.op === '>') return left > right
|
|
64
|
+
if (node.op === '<=') return left <= right
|
|
65
|
+
if (node.op === '>=') return left >= right
|
|
66
|
+
|
|
67
|
+
if (node.op === 'LIKE') {
|
|
68
|
+
const str = String(left)
|
|
69
|
+
const pattern = String(right)
|
|
70
|
+
// Convert SQL LIKE pattern to regex
|
|
71
|
+
// % matches zero or more characters
|
|
72
|
+
// _ matches exactly one character
|
|
73
|
+
const regexPattern = pattern
|
|
74
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // Escape regex special chars
|
|
75
|
+
.replace(/%/g, '.*') // Replace % with .*
|
|
76
|
+
.replace(/_/g, '.') // Replace _ with .
|
|
77
|
+
const regex = new RegExp('^' + regexPattern + '$', 'i')
|
|
78
|
+
return regex.test(str)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Function calls
|
|
83
|
+
if (node.type === 'function') {
|
|
84
|
+
const funcName = node.name.toUpperCase()
|
|
85
|
+
const args = node.args.map(arg => evaluateExpr(arg, row))
|
|
86
|
+
|
|
87
|
+
if (funcName === 'UPPER') {
|
|
88
|
+
if (args.length !== 1) throw new Error('UPPER requires exactly 1 argument')
|
|
89
|
+
const val = args[0]
|
|
90
|
+
if (val == null) return null
|
|
91
|
+
return String(val).toUpperCase()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (funcName === 'LOWER') {
|
|
95
|
+
if (args.length !== 1) throw new Error('LOWER requires exactly 1 argument')
|
|
96
|
+
const val = args[0]
|
|
97
|
+
if (val == null) return null
|
|
98
|
+
return String(val).toLowerCase()
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (funcName === 'CONCAT') {
|
|
102
|
+
if (args.length < 1) throw new Error('CONCAT requires at least 1 argument')
|
|
103
|
+
// SQL CONCAT returns NULL if any argument is NULL
|
|
104
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
105
|
+
if (args[i] == null) return null
|
|
106
|
+
}
|
|
107
|
+
return args.map(a => String(a)).join('')
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (funcName === 'LENGTH') {
|
|
111
|
+
if (args.length !== 1) throw new Error('LENGTH requires exactly 1 argument')
|
|
112
|
+
const val = args[0]
|
|
113
|
+
if (val == null) return null
|
|
114
|
+
return String(val).length
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (funcName === 'SUBSTRING') {
|
|
118
|
+
if (args.length < 2 || args.length > 3) {
|
|
119
|
+
throw new Error('SUBSTRING requires 2 or 3 arguments')
|
|
120
|
+
}
|
|
121
|
+
const str = args[0]
|
|
122
|
+
if (str == null) return null
|
|
123
|
+
const strVal = String(str)
|
|
124
|
+
const start = Number(args[1])
|
|
125
|
+
if (!Number.isInteger(start) || start < 1) {
|
|
126
|
+
throw new Error('SUBSTRING start position must be a positive integer')
|
|
127
|
+
}
|
|
128
|
+
// SQL uses 1-based indexing
|
|
129
|
+
const startIdx = start - 1
|
|
130
|
+
if (args.length === 3) {
|
|
131
|
+
const len = Number(args[2])
|
|
132
|
+
if (!Number.isInteger(len) || len < 0) {
|
|
133
|
+
throw new Error('SUBSTRING length must be a non-negative integer')
|
|
134
|
+
}
|
|
135
|
+
return strVal.substring(startIdx, startIdx + len)
|
|
136
|
+
}
|
|
137
|
+
return strVal.substring(startIdx)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (funcName === 'TRIM') {
|
|
141
|
+
if (args.length !== 1) throw new Error('TRIM requires exactly 1 argument')
|
|
142
|
+
const val = args[0]
|
|
143
|
+
if (val == null) return null
|
|
144
|
+
return String(val).trim()
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
throw new Error('Unsupported function ' + funcName)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (node.type === 'cast') {
|
|
151
|
+
const val = evaluateExpr(node.expr, row)
|
|
152
|
+
if (val == null) return null
|
|
153
|
+
const toType = node.toType.toUpperCase()
|
|
154
|
+
if (toType === 'INTEGER' || toType === 'INT') {
|
|
155
|
+
const num = Number(val)
|
|
156
|
+
if (isNaN(num)) return null
|
|
157
|
+
return Math.trunc(num)
|
|
158
|
+
}
|
|
159
|
+
if (toType === 'BIGINT') {
|
|
160
|
+
return BigInt(val)
|
|
161
|
+
}
|
|
162
|
+
if (toType === 'FLOAT' || toType === 'REAL' || toType === 'DOUBLE') {
|
|
163
|
+
const num = Number(val)
|
|
164
|
+
if (isNaN(num)) return null
|
|
165
|
+
return num
|
|
166
|
+
}
|
|
167
|
+
if (toType === 'TEXT' || toType === 'STRING') {
|
|
168
|
+
return String(val)
|
|
169
|
+
}
|
|
170
|
+
if (toType === 'BOOLEAN' || toType === 'BOOL') {
|
|
171
|
+
return Boolean(val)
|
|
172
|
+
}
|
|
173
|
+
throw new Error('Unsupported CAST to type ' + node.toType)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
throw new Error('Unknown expression node type ' + node.type)
|
|
177
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Row, SelectStatement } from './types.js'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Executes a SQL SELECT query against an array of data rows
|
|
5
|
+
*
|
|
6
|
+
* @param rows - data rows to query as a list of objects
|
|
7
|
+
* @param sql - SQL query string
|
|
8
|
+
* @returns rows matching the query
|
|
9
|
+
*/
|
|
10
|
+
export function executeSql(rows: Row[], sql: string): Row[]
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Parses a SQL query string into an abstract syntax tree
|
|
14
|
+
*
|
|
15
|
+
* @param sql - SQL query string to parse
|
|
16
|
+
* @returns parsed SQL select statement
|
|
17
|
+
*/
|
|
18
|
+
export function parseSql(sql: string): SelectStatement
|
package/src/index.js
ADDED