squirreling 0.1.1 → 0.1.2
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 +3 -0
- package/package.json +1 -1
- package/src/execute/execute.js +12 -0
- package/src/execute/expression.js +15 -0
- package/src/execute/having.js +193 -0
- package/src/parse/expression.js +42 -2
- package/src/parse/parse.js +7 -0
- package/src/types.d.ts +16 -1
package/README.md
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
# Squirreling SQL Engine
|
|
2
2
|
|
|
3
|
+

|
|
4
|
+
|
|
3
5
|
[](https://www.npmjs.com/package/squirreling)
|
|
4
6
|
[](https://www.npmjs.com/package/squirreling)
|
|
5
7
|
[](https://www.npmjs.com/package/squirreling)
|
|
6
8
|
[](https://github.com/hyparam/squirreling/actions)
|
|
7
9
|
[](https://opensource.org/licenses/MIT)
|
|
8
10
|
[](https://www.npmjs.com/package/squirreling?activeTab=dependencies)
|
|
11
|
+
|
|
9
12
|
Squirreling is a lightweight SQL engine for JavaScript applications, designed to provide efficient and easy-to-use database functionalities in the browser.
|
|
10
13
|
|
|
11
14
|
## Features
|
package/package.json
CHANGED
package/src/execute/execute.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { defaultAggregateAlias, evaluateAggregate } from './aggregates.js'
|
|
6
6
|
import { evaluateExpr } from './expression.js'
|
|
7
|
+
import { createHavingContext, evaluateHavingExpr } from './having.js'
|
|
7
8
|
import { parseSql } from '../parse/parse.js'
|
|
8
9
|
|
|
9
10
|
/**
|
|
@@ -230,6 +231,17 @@ function evaluateSelectAst(select, rows) {
|
|
|
230
231
|
continue
|
|
231
232
|
}
|
|
232
233
|
}
|
|
234
|
+
|
|
235
|
+
// Apply HAVING filter before adding to projected results
|
|
236
|
+
if (select.having) {
|
|
237
|
+
// For HAVING, we need to evaluate aggregates in the context of the group
|
|
238
|
+
// Create a special row context that includes both the group data and aggregate values
|
|
239
|
+
const havingContext = createHavingContext(resultRow, group)
|
|
240
|
+
if (!evaluateHavingExpr(select.having, havingContext, group)) {
|
|
241
|
+
continue
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
233
245
|
projected.push(resultRow)
|
|
234
246
|
}
|
|
235
247
|
} else {
|
|
@@ -79,6 +79,21 @@ export function evaluateExpr(node, row) {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
// BETWEEN and NOT BETWEEN
|
|
83
|
+
if (node.type === 'between' || node.type === 'not between') {
|
|
84
|
+
const expr = evaluateExpr(node.expr, row)
|
|
85
|
+
const lower = evaluateExpr(node.lower, row)
|
|
86
|
+
const upper = evaluateExpr(node.upper, row)
|
|
87
|
+
|
|
88
|
+
// If any value is NULL, return false (SQL behavior)
|
|
89
|
+
if (expr == null || lower == null || upper == null) {
|
|
90
|
+
return false
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const isBetween = expr >= lower && expr <= upper
|
|
94
|
+
return node.type === 'between' ? isBetween : !isBetween
|
|
95
|
+
}
|
|
96
|
+
|
|
82
97
|
// Function calls
|
|
83
98
|
if (node.type === 'function') {
|
|
84
99
|
const funcName = node.name.toUpperCase()
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { ExprNode, Row, SqlPrimitive } from '../types.js'
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { evaluateExpr } from './expression.js'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Creates a context for evaluating HAVING expressions
|
|
9
|
+
* @param {Row} resultRow - The aggregated result row
|
|
10
|
+
* @param {Row[]} group - The group of rows
|
|
11
|
+
* @returns {Row} A context row for HAVING evaluation
|
|
12
|
+
*/
|
|
13
|
+
export function createHavingContext(resultRow, group) {
|
|
14
|
+
// Include the first row of the group (for GROUP BY columns)
|
|
15
|
+
const firstRow = group[0] || {}
|
|
16
|
+
// Merge with result row (which has aggregates computed)
|
|
17
|
+
return { ...firstRow, ...resultRow }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Evaluates a HAVING expression with support for aggregate functions
|
|
22
|
+
* @param {ExprNode} expr - The HAVING expression
|
|
23
|
+
* @param {Row} context - The context row with aggregated values
|
|
24
|
+
* @param {Row[]} group - The group of rows for re-evaluating aggregates
|
|
25
|
+
* @returns {boolean} Whether the HAVING condition is satisfied
|
|
26
|
+
*/
|
|
27
|
+
export function evaluateHavingExpr(expr, context, group) {
|
|
28
|
+
// For HAVING, we need special handling of aggregate functions
|
|
29
|
+
// They need to be re-evaluated against the group
|
|
30
|
+
if (expr.type === 'function') {
|
|
31
|
+
const funcName = expr.name.toUpperCase()
|
|
32
|
+
if (['COUNT', 'SUM', 'AVG', 'MIN', 'MAX'].includes(funcName)) {
|
|
33
|
+
// Evaluate aggregate function on the group
|
|
34
|
+
return Boolean(evaluateAggregateFunction(funcName, expr.args, group))
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (expr.type === 'binary') {
|
|
39
|
+
const left = evaluateHavingValue(expr.left, context, group)
|
|
40
|
+
const right = evaluateHavingValue(expr.right, context, group)
|
|
41
|
+
|
|
42
|
+
if (expr.op === 'AND') {
|
|
43
|
+
return Boolean(left && right)
|
|
44
|
+
}
|
|
45
|
+
if (expr.op === 'OR') {
|
|
46
|
+
return Boolean(left || right)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Handle NULL comparisons
|
|
50
|
+
if (left == null || right == null) {
|
|
51
|
+
if (expr.op === '=' || expr.op === '!=' || expr.op === '<>') {
|
|
52
|
+
return false
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (expr.op === '=') return left === right
|
|
57
|
+
if (expr.op === '!=' || expr.op === '<>') return left !== right
|
|
58
|
+
if (expr.op === '<') return left < right
|
|
59
|
+
if (expr.op === '>') return left > right
|
|
60
|
+
if (expr.op === '<=') return left <= right
|
|
61
|
+
if (expr.op === '>=') return left >= right
|
|
62
|
+
if (expr.op === 'LIKE') {
|
|
63
|
+
const str = String(left)
|
|
64
|
+
const pattern = String(right)
|
|
65
|
+
const regexPattern = pattern
|
|
66
|
+
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
67
|
+
.replace(/%/g, '.*')
|
|
68
|
+
.replace(/_/g, '.')
|
|
69
|
+
const regex = new RegExp('^' + regexPattern + '$', 'i')
|
|
70
|
+
return regex.test(str)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (expr.type === 'unary') {
|
|
75
|
+
if (expr.op === 'NOT') {
|
|
76
|
+
return !evaluateHavingExpr(expr.argument, context, group)
|
|
77
|
+
}
|
|
78
|
+
if (expr.op === 'IS NULL') {
|
|
79
|
+
return evaluateHavingValue(expr.argument, context, group) == null
|
|
80
|
+
}
|
|
81
|
+
if (expr.op === 'IS NOT NULL') {
|
|
82
|
+
return evaluateHavingValue(expr.argument, context, group) != null
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (expr.type === 'between' || expr.type === 'not between') {
|
|
87
|
+
const exprVal = evaluateHavingValue(expr.expr, context, group)
|
|
88
|
+
const lower = evaluateHavingValue(expr.lower, context, group)
|
|
89
|
+
const upper = evaluateHavingValue(expr.upper, context, group)
|
|
90
|
+
|
|
91
|
+
// If any value is NULL, return false (SQL behavior)
|
|
92
|
+
if (exprVal == null || lower == null || upper == null) {
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const isBetween = exprVal >= lower && exprVal <= upper
|
|
97
|
+
return expr.type === 'between' ? isBetween : !isBetween
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// For other expression types, use the context row
|
|
101
|
+
return Boolean(evaluateExpr(expr, context))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Evaluates a value in a HAVING expression
|
|
106
|
+
* @param {ExprNode} expr - The expression
|
|
107
|
+
* @param {Row} context - The context row
|
|
108
|
+
* @param {Row[]} group - The group of rows
|
|
109
|
+
* @returns {SqlPrimitive} The evaluated value
|
|
110
|
+
*/
|
|
111
|
+
function evaluateHavingValue(expr, context, group) {
|
|
112
|
+
if (expr.type === 'function') {
|
|
113
|
+
const funcName = expr.name.toUpperCase()
|
|
114
|
+
if (['COUNT', 'SUM', 'AVG', 'MIN', 'MAX'].includes(funcName)) {
|
|
115
|
+
return evaluateAggregateFunction(funcName, expr.args, group)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// For binary expressions, we need to use evaluateHavingExpr to properly handle aggregates
|
|
120
|
+
if (expr.type === 'binary' || expr.type === 'unary' || expr.type === 'between' || expr.type === 'not between') {
|
|
121
|
+
return evaluateHavingExpr(expr, context, group)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return evaluateExpr(expr, context)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Evaluates an aggregate function on a group
|
|
129
|
+
* @param {string} funcName - The aggregate function name
|
|
130
|
+
* @param {ExprNode[]} args - The function arguments
|
|
131
|
+
* @param {Row[]} group - The group of rows
|
|
132
|
+
* @returns {SqlPrimitive} The aggregate result
|
|
133
|
+
*/
|
|
134
|
+
function evaluateAggregateFunction(funcName, args, group) {
|
|
135
|
+
if (funcName === 'COUNT') {
|
|
136
|
+
if (args.length === 1 && args[0].type === 'identifier' && args[0].name === '*') {
|
|
137
|
+
return group.length
|
|
138
|
+
}
|
|
139
|
+
// COUNT(column) - count non-null values
|
|
140
|
+
let count = 0
|
|
141
|
+
for (const row of group) {
|
|
142
|
+
const val = evaluateExpr(args[0], row)
|
|
143
|
+
if (val != null) count++
|
|
144
|
+
}
|
|
145
|
+
return count
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (funcName === 'SUM') {
|
|
149
|
+
let sum = 0
|
|
150
|
+
for (const row of group) {
|
|
151
|
+
const val = evaluateExpr(args[0], row)
|
|
152
|
+
if (val != null) sum += Number(val)
|
|
153
|
+
}
|
|
154
|
+
return sum
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (funcName === 'AVG') {
|
|
158
|
+
let sum = 0
|
|
159
|
+
let count = 0
|
|
160
|
+
for (const row of group) {
|
|
161
|
+
const val = evaluateExpr(args[0], row)
|
|
162
|
+
if (val != null) {
|
|
163
|
+
sum += Number(val)
|
|
164
|
+
count++
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return count > 0 ? sum / count : null
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (funcName === 'MIN') {
|
|
171
|
+
let min = null
|
|
172
|
+
for (const row of group) {
|
|
173
|
+
const val = evaluateExpr(args[0], row)
|
|
174
|
+
if (val != null && (min == null || val < min)) {
|
|
175
|
+
min = val
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return min
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (funcName === 'MAX') {
|
|
182
|
+
let max = null
|
|
183
|
+
for (const row of group) {
|
|
184
|
+
const val = evaluateExpr(args[0], row)
|
|
185
|
+
if (val != null && (max == null || val > max)) {
|
|
186
|
+
max = val
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return max
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
throw new Error('Unsupported aggregate function: ' + funcName)
|
|
193
|
+
}
|
package/src/parse/expression.js
CHANGED
|
@@ -42,8 +42,17 @@ export function parsePrimary(c) {
|
|
|
42
42
|
|
|
43
43
|
if (c.current().type !== 'paren' || c.current().value !== ')') {
|
|
44
44
|
while (true) {
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
// Handle COUNT(*) - treat * as a special identifier
|
|
46
|
+
if (c.current().type === 'operator' && c.current().value === '*') {
|
|
47
|
+
c.consume()
|
|
48
|
+
args.push({
|
|
49
|
+
type: 'identifier',
|
|
50
|
+
name: '*',
|
|
51
|
+
})
|
|
52
|
+
} else {
|
|
53
|
+
const arg = parseExpression(c)
|
|
54
|
+
args.push(arg)
|
|
55
|
+
}
|
|
47
56
|
if (!c.match('comma')) break
|
|
48
57
|
}
|
|
49
58
|
}
|
|
@@ -217,6 +226,37 @@ function parseComparison(c) {
|
|
|
217
226
|
}
|
|
218
227
|
}
|
|
219
228
|
|
|
229
|
+
// [NOT] BETWEEN
|
|
230
|
+
if (tok.type === 'keyword' && tok.value === 'NOT') {
|
|
231
|
+
const nextTok = c.peek(1)
|
|
232
|
+
if (nextTok.type === 'keyword' && nextTok.value === 'BETWEEN') {
|
|
233
|
+
c.consume() // NOT
|
|
234
|
+
c.consume() // BETWEEN
|
|
235
|
+
const lower = parsePrimary(c)
|
|
236
|
+
c.expect('keyword', 'AND')
|
|
237
|
+
const upper = parsePrimary(c)
|
|
238
|
+
return {
|
|
239
|
+
type: 'not between',
|
|
240
|
+
expr: left,
|
|
241
|
+
lower,
|
|
242
|
+
upper,
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (tok.type === 'keyword' && tok.value === 'BETWEEN') {
|
|
248
|
+
c.consume()
|
|
249
|
+
const lower = parsePrimary(c)
|
|
250
|
+
c.expect('keyword', 'AND')
|
|
251
|
+
const upper = parsePrimary(c)
|
|
252
|
+
return {
|
|
253
|
+
type: 'between',
|
|
254
|
+
expr: left,
|
|
255
|
+
lower,
|
|
256
|
+
upper,
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
220
260
|
if (tok.type === 'operator' && isComparisonOperator(tok.value)) {
|
|
221
261
|
c.consume()
|
|
222
262
|
const right = parsePrimary(c)
|
package/src/parse/parse.js
CHANGED
|
@@ -407,6 +407,8 @@ function parseSelectInternal(state) {
|
|
|
407
407
|
let where
|
|
408
408
|
/** @type {ExprNode[]} */
|
|
409
409
|
const groupBy = []
|
|
410
|
+
/** @type {ExprNode | undefined} */
|
|
411
|
+
let having
|
|
410
412
|
/** @type {OrderByItem[]} */
|
|
411
413
|
const orderBy = []
|
|
412
414
|
/** @type {number | undefined} */
|
|
@@ -429,6 +431,10 @@ function parseSelectInternal(state) {
|
|
|
429
431
|
}
|
|
430
432
|
}
|
|
431
433
|
|
|
434
|
+
if (match(state, 'keyword', 'HAVING')) {
|
|
435
|
+
having = parseExpression(cursor)
|
|
436
|
+
}
|
|
437
|
+
|
|
432
438
|
if (match(state, 'keyword', 'ORDER')) {
|
|
433
439
|
expect(state, 'keyword', 'BY')
|
|
434
440
|
while (true) {
|
|
@@ -497,6 +503,7 @@ function parseSelectInternal(state) {
|
|
|
497
503
|
joins,
|
|
498
504
|
where,
|
|
499
505
|
groupBy,
|
|
506
|
+
having,
|
|
500
507
|
orderBy,
|
|
501
508
|
limit,
|
|
502
509
|
offset,
|
package/src/types.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface SelectStatement {
|
|
|
9
9
|
joins: JoinClause[]
|
|
10
10
|
where?: ExprNode
|
|
11
11
|
groupBy: ExprNode[]
|
|
12
|
+
having?: ExprNode
|
|
12
13
|
orderBy: OrderByItem[]
|
|
13
14
|
limit?: number
|
|
14
15
|
offset?: number
|
|
@@ -81,7 +82,21 @@ export interface CastNode {
|
|
|
81
82
|
toType: string
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
export
|
|
85
|
+
export interface BetweenNode {
|
|
86
|
+
type: 'between' | 'not between'
|
|
87
|
+
expr: ExprNode
|
|
88
|
+
lower: ExprNode
|
|
89
|
+
upper: ExprNode
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export type ExprNode =
|
|
93
|
+
| LiteralNode
|
|
94
|
+
| IdentifierNode
|
|
95
|
+
| UnaryNode
|
|
96
|
+
| BinaryNode
|
|
97
|
+
| FunctionNode
|
|
98
|
+
| CastNode
|
|
99
|
+
| BetweenNode
|
|
85
100
|
|
|
86
101
|
export interface StarColumn {
|
|
87
102
|
kind: 'star'
|