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 CHANGED
@@ -1,11 +1,14 @@
1
1
  # Squirreling SQL Engine
2
2
 
3
+ ![squirreling engine](squirreling.jpg)
4
+
3
5
  [![npm](https://img.shields.io/npm/v/squirreling)](https://www.npmjs.com/package/squirreling)
4
6
  [![downloads](https://img.shields.io/npm/dt/squirreling)](https://www.npmjs.com/package/squirreling)
5
7
  [![minzipped](https://img.shields.io/bundlephobia/minzip/squirreling)](https://www.npmjs.com/package/squirreling)
6
8
  [![workflow status](https://github.com/hyparam/squirreling/actions/workflows/ci.yml/badge.svg)](https://github.com/hyparam/squirreling/actions)
7
9
  [![mit license](https://img.shields.io/badge/License-MIT-orange.svg)](https://opensource.org/licenses/MIT)
8
10
  [![dependencies](https://img.shields.io/badge/Dependencies-0-blueviolet)](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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "author": "Hyperparam",
5
5
  "homepage": "https://hyperparam.app",
6
6
  "license": "MIT",
@@ -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
+ }
@@ -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
- const arg = parseExpression(c)
46
- args.push(arg)
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)
@@ -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 type ExprNode = LiteralNode | IdentifierNode | UnaryNode | BinaryNode | FunctionNode | CastNode
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'