squirreling 0.4.0 → 0.4.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
@@ -7,7 +7,7 @@
7
7
  [![minzipped](https://img.shields.io/bundlephobia/minzip/squirreling)](https://www.npmjs.com/package/squirreling)
8
8
  [![workflow status](https://github.com/hyparam/squirreling/actions/workflows/ci.yml/badge.svg)](https://github.com/hyparam/squirreling/actions)
9
9
  [![mit license](https://img.shields.io/badge/License-MIT-orange.svg)](https://opensource.org/licenses/MIT)
10
- ![coverage](https://img.shields.io/badge/Coverage-93-darkred)
10
+ ![coverage](https://img.shields.io/badge/Coverage-95-darkred)
11
11
  [![dependencies](https://img.shields.io/badge/Dependencies-0-blueviolet)](https://www.npmjs.com/package/squirreling?activeTab=dependencies)
12
12
 
13
13
  Squirreling is a streaming async SQL engine for JavaScript. It is designed to provide efficient streaming of results from pluggable backends for highly efficient retrieval of data for browser applications.
@@ -22,8 +22,8 @@ Squirreling is a streaming async SQL engine for JavaScript. It is designed to pr
22
22
  - Constant memory usage for simple queries with LIMIT
23
23
  - Robust error handling and validation designed for LLM tool use
24
24
  - In-memory data option for simple use cases
25
+ - Late materialization for efficiency
25
26
  - Select only
26
- - No joins (yet)
27
27
 
28
28
  ## Usage
29
29
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squirreling",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
4
4
  "description": "Squirreling SQL Engine",
5
5
  "author": "Hyperparam",
6
6
  "homepage": "https://hyperparam.app",
@@ -38,10 +38,10 @@
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "24.10.1",
41
- "@vitest/coverage-v8": "4.0.14",
41
+ "@vitest/coverage-v8": "4.0.15",
42
42
  "eslint": "9.39.1",
43
43
  "eslint-plugin-jsdoc": "61.4.1",
44
44
  "typescript": "5.9.3",
45
- "vitest": "4.0.14"
45
+ "vitest": "4.0.15"
46
46
  }
47
47
  }
@@ -58,7 +58,7 @@ export function cachedDataSource(source) {
58
58
  const cache = new Map()
59
59
  return {
60
60
  /**
61
- * @returns {AsyncGenerator<AsyncRow>}
61
+ * @yields {AsyncRow}
62
62
  */
63
63
  async *getRows() {
64
64
  let index = 0
@@ -1,4 +1,5 @@
1
1
  import { evaluateExpr } from './expression.js'
2
+ import { defaultDerivedAlias } from './utils.js'
2
3
 
3
4
  /**
4
5
  * Evaluates an aggregate function over a set of rows
@@ -72,27 +73,5 @@ export async function evaluateAggregate({ col, rows, tables }) {
72
73
  export function defaultAggregateAlias(col) {
73
74
  const base = col.func.toLowerCase()
74
75
  if (col.arg.kind === 'star') return base + '_all'
75
- return base + '_' + defaultAggregateAliasExpr(col.arg.expr)
76
- }
77
-
78
- /**
79
- * @param {ExprNode} expr
80
- * @returns {string}
81
- */
82
- export function defaultAggregateAliasExpr(expr) {
83
- if (expr.type === 'identifier') {
84
- return expr.name
85
- } else if (expr.type === 'literal') {
86
- return String(expr.value)
87
- } else if (expr.type === 'cast') {
88
- return defaultAggregateAliasExpr(expr.expr) + '_as_' + expr.toType
89
- } else if (expr.type === 'unary') {
90
- return expr.op + '_' + defaultAggregateAliasExpr(expr.argument)
91
- } else if (expr.type === 'binary') {
92
- return defaultAggregateAliasExpr(expr.left) + '_' + expr.op + '_' + defaultAggregateAliasExpr(expr.right)
93
- } else if (expr.type === 'function') {
94
- return expr.name.toLowerCase() + '_' + expr.args.map(defaultAggregateAliasExpr).join('_')
95
- } else {
96
- return 'expr'
97
- }
76
+ return base + '_' + defaultDerivedAlias(col.arg.expr)
98
77
  }
@@ -1,8 +1,10 @@
1
- import { evaluateExpr } from './expression.js'
2
- import { parseSql } from '../parse/parse.js'
3
1
  import { generatorSource, memorySource } from '../backend/dataSource.js'
2
+ import { parseSql } from '../parse/parse.js'
4
3
  import { defaultAggregateAlias, evaluateAggregate } from './aggregates.js'
4
+ import { evaluateExpr } from './expression.js'
5
5
  import { evaluateHavingExpr } from './having.js'
6
+ import { executeJoins } from './join.js'
7
+ import { compareForTerm, defaultDerivedAlias } from './utils.js'
6
8
 
7
9
  /**
8
10
  * @import { AsyncDataSource, ExecuteSqlOptions, ExprNode, OrderByItem, AsyncRow, SelectStatement, SqlPrimitive } from '../types.js'
@@ -12,15 +14,12 @@ import { evaluateHavingExpr } from './having.js'
12
14
  * Executes a SQL SELECT query against named data sources
13
15
  *
14
16
  * @param {ExecuteSqlOptions} options - the execution options
15
- * @returns {AsyncGenerator<AsyncRow>} async generator yielding result rows
17
+ * @yields {AsyncRow} async generator yielding result rows
16
18
  */
17
19
  export async function* executeSql({ tables, query }) {
18
20
  const select = parseSql(query)
19
21
 
20
22
  // Check for unsupported operations
21
- if (select.joins.length) {
22
- throw new Error('JOIN is not supported')
23
- }
24
23
  if (!select.from) {
25
24
  throw new Error('FROM clause is required')
26
25
  }
@@ -44,53 +43,33 @@ export async function* executeSql({ tables, query }) {
44
43
  *
45
44
  * @param {SelectStatement} select
46
45
  * @param {Record<string, AsyncDataSource>} tables
47
- * @returns {AsyncGenerator<AsyncRow>}
46
+ * @yields {AsyncRow}
48
47
  */
49
48
  export async function* executeSelect(select, tables) {
50
49
  /** @type {AsyncDataSource} */
51
50
  let dataSource
52
-
53
- if (typeof select.from === 'string') {
54
- const table = tables[select.from]
55
- if (table === undefined) {
56
- throw new Error(`Table "${select.from}" not found`)
51
+ /** @type {string} */
52
+ let fromTableName
53
+
54
+ if (select.from.kind === 'table') {
55
+ // Use alias for column prefixing, but look up the actual table name
56
+ fromTableName = select.from.alias ?? select.from.table
57
+ dataSource = tables[select.from.table]
58
+ if (dataSource === undefined) {
59
+ throw new Error(`Table "${select.from.table}" not found`)
57
60
  }
58
-
59
- dataSource = table
60
61
  } else {
61
62
  // Nested subquery - recursively resolve
63
+ fromTableName = select.from.alias
62
64
  dataSource = generatorSource(executeSelect(select.from.query, tables))
63
65
  }
64
66
 
65
- yield* evaluateSelectAst(select, dataSource, tables)
66
- }
67
-
68
- /**
69
- * Generates a default alias for a derived column expression
70
- *
71
- * @param {ExprNode} expr - the expression node
72
- * @returns {string} the generated alias
73
- */
74
- function defaultDerivedAlias(expr) {
75
- if (expr.type === 'identifier') {
76
- return expr.name
77
- }
78
- if (expr.type === 'function') {
79
- const base = expr.name.toLowerCase()
80
- // Try to extract column names from identifier arguments
81
- const columnNames = expr.args
82
- .filter(arg => arg.type === 'identifier')
83
- .map(arg => arg.name)
84
- if (columnNames.length > 0) {
85
- return base + '_' + columnNames.join('_')
86
- }
87
- return base
88
- }
89
- if (expr.type === 'cast') return 'cast_expr'
90
- if (expr.type === 'unary' && expr.argument.type === 'identifier') {
91
- return expr.op === '-' ? 'neg_' + expr.argument.name : 'expr'
67
+ // Execute JOINs if present
68
+ if (select.joins.length) {
69
+ dataSource = await executeJoins(dataSource, select.joins, fromTableName, tables)
92
70
  }
93
- return 'expr'
71
+
72
+ yield* evaluateSelectAst(select, dataSource, tables)
94
73
  }
95
74
 
96
75
  /**
@@ -110,31 +89,6 @@ async function stableRowKey(row) {
110
89
  return parts.join('|')
111
90
  }
112
91
 
113
- /**
114
- * Compares two SQL values for sorting
115
- *
116
- * @param {SqlPrimitive} a
117
- * @param {SqlPrimitive} b
118
- * @returns {number} negative if a < b, positive if a > b, 0 if equal
119
- */
120
- function compareValues(a, b) {
121
- if (a === b) return 0
122
- if (a == null) return -1
123
- if (b == null) return 1
124
-
125
- if (typeof a === 'number' && typeof b === 'number') {
126
- if (a < b) return -1
127
- if (a > b) return 1
128
- return 0
129
- }
130
-
131
- const as = String(a)
132
- const bs = String(b)
133
- if (as < bs) return -1
134
- if (as > bs) return 1
135
- return 0
136
- }
137
-
138
92
  /**
139
93
  * Applies DISTINCT filtering to remove duplicate rows
140
94
  *
@@ -156,127 +110,89 @@ async function applyDistinct(rows, distinct) {
156
110
  }
157
111
  return result
158
112
  }
159
-
160
113
  /**
161
- * Applies ORDER BY sorting to RowSource array (before projection)
162
- *
163
- * @param {AsyncRow[]} rows - the input row sources
164
- * @param {OrderByItem[]} orderBy - the sort specifications
165
- * @param {Record<string, AsyncDataSource>} tables
166
- * @returns {Promise<AsyncRow[]>} the sorted row sources
167
- */
168
- async function sortRowSources(rows, orderBy, tables) {
169
- if (!orderBy.length) return rows
170
-
171
- // Pre-evaluate ORDER BY expressions for all rows
172
- /** @type {SqlPrimitive[][]} */
173
- const evaluatedValues = []
174
- for (const row of rows) {
175
- /** @type {SqlPrimitive[]} */
176
- const rowValues = []
177
- for (const term of orderBy) {
178
- const value = await evaluateExpr({ node: term.expr, row, tables })
179
- rowValues.push(value)
180
- }
181
- evaluatedValues.push(rowValues)
182
- }
183
-
184
- // Create index array and sort it
185
- const indices = rows.map((_, i) => i)
186
- indices.sort((aIdx, bIdx) => {
187
- for (let termIdx = 0; termIdx < orderBy.length; termIdx++) {
188
- const term = orderBy[termIdx]
189
- const dir = term.direction
190
- const av = evaluatedValues[aIdx][termIdx]
191
- const bv = evaluatedValues[bIdx][termIdx]
192
-
193
- // Handle NULLS FIRST / NULLS LAST
194
- const aIsNull = av == null
195
- const bIsNull = bv == null
196
-
197
- if (aIsNull || bIsNull) {
198
- if (aIsNull && bIsNull) continue
199
-
200
- const nullsFirst = term.nulls === 'LAST' ? false : true
201
-
202
- if (aIsNull) {
203
- return nullsFirst ? -1 : 1
204
- } else {
205
- return nullsFirst ? 1 : -1
206
- }
207
- }
208
-
209
- const cmp = compareValues(av, bv)
210
- if (cmp !== 0) {
211
- return dir === 'DESC' ? -cmp : cmp
212
- }
213
- }
214
- return 0
215
- })
216
-
217
- // Return sorted rows
218
- return indices.map(i => rows[i])
219
- }
220
-
221
- /**
222
- * Applies ORDER BY sorting to rows
114
+ * Applies ORDER BY sorting to rows using multi-pass lazy evaluation.
115
+ * Secondary ORDER BY columns are only evaluated for rows that tie on
116
+ * previous columns, reducing expensive cell evaluations.
223
117
  *
224
118
  * @param {AsyncRow[]} rows - the input rows
225
119
  * @param {OrderByItem[]} orderBy - the sort specifications
226
120
  * @param {Record<string, AsyncDataSource>} tables
227
121
  * @returns {Promise<AsyncRow[]>} the sorted rows
228
122
  */
229
- async function applyOrderBy(rows, orderBy, tables) {
123
+ async function sortRows(rows, orderBy, tables) {
230
124
  if (!orderBy.length) return rows
231
125
 
232
- // Pre-evaluate ORDER BY expressions for all rows
233
- /** @type {SqlPrimitive[][]} */
234
- const evaluatedValues = []
235
- for (const row of rows) {
236
- /** @type {SqlPrimitive[]} */
237
- const rowValues = []
238
- for (const term of orderBy) {
239
- const value = await evaluateExpr({ node: term.expr, row, tables })
240
- rowValues.push(value)
241
- }
242
- evaluatedValues.push(rowValues)
243
- }
244
-
245
- // Create index array and sort it
246
- const indices = rows.map((_, i) => i)
247
- indices.sort((aIdx, bIdx) => {
248
- for (let termIdx = 0; termIdx < orderBy.length; termIdx++) {
249
- const term = orderBy[termIdx]
250
- const dir = term.direction
251
- const av = evaluatedValues[aIdx][termIdx]
252
- const bv = evaluatedValues[bIdx][termIdx]
126
+ // Cache for evaluated values: evaluatedValues[rowIdx][colIdx]
127
+ /** @type {(SqlPrimitive | undefined)[][]} */
128
+ const evaluatedValues = rows.map(() => Array(orderBy.length))
253
129
 
254
- // Handle NULLS FIRST / NULLS LAST
255
- const aIsNull = av == null
256
- const bIsNull = bv == null
130
+ // Start with all indices in one group
131
+ /** @type {number[][]} */
132
+ let groups = [rows.map((_, i) => i)]
257
133
 
258
- if (aIsNull || bIsNull) {
259
- if (aIsNull && bIsNull) continue
134
+ // Process each ORDER BY column incrementally
135
+ for (let orderByIdx = 0; orderByIdx < orderBy.length; orderByIdx++) {
136
+ const term = orderBy[orderByIdx]
137
+ /** @type {number[][]} */
138
+ const nextGroups = []
260
139
 
261
- const nullsFirst = term.nulls === 'LAST' ? false : true
140
+ for (const group of groups) {
141
+ // Single-element groups don't need sorting or evaluation
142
+ if (group.length <= 1) {
143
+ nextGroups.push(group)
144
+ continue
145
+ }
262
146
 
263
- if (aIsNull) {
264
- return nullsFirst ? -1 : 1
265
- } else {
266
- return nullsFirst ? 1 : -1
147
+ // Evaluate this column for all rows in the group
148
+ for (const idx of group) {
149
+ if (evaluatedValues[idx][orderByIdx] === undefined) {
150
+ evaluatedValues[idx][orderByIdx] = await evaluateExpr({
151
+ node: term.expr,
152
+ row: rows[idx],
153
+ tables,
154
+ })
267
155
  }
268
156
  }
269
157
 
270
- const cmp = compareValues(av, bv)
271
- if (cmp !== 0) {
272
- return dir === 'DESC' ? -cmp : cmp
158
+ // Sort the group by this column
159
+ group.sort((aIdx, bIdx) => {
160
+ const av = evaluatedValues[aIdx][orderByIdx]
161
+ const bv = evaluatedValues[bIdx][orderByIdx]
162
+ return compareForTerm(av, bv, term)
163
+ })
164
+
165
+ // Split into sub-groups based on ties (for next column)
166
+ if (orderByIdx < orderBy.length - 1) {
167
+ /** @type {number[]} */
168
+ let currentSubGroup = [group[0]]
169
+ for (let i = 1; i < group.length; i++) {
170
+ const prevIdx = group[i - 1]
171
+ const currIdx = group[i]
172
+ const prevVal = evaluatedValues[prevIdx][orderByIdx]
173
+ const currVal = evaluatedValues[currIdx][orderByIdx]
174
+
175
+ if (compareForTerm(prevVal, currVal, term) === 0) {
176
+ // Same value, extend current sub-group
177
+ currentSubGroup.push(currIdx)
178
+ } else {
179
+ // Different value, start new sub-group
180
+ nextGroups.push(currentSubGroup)
181
+ currentSubGroup = [currIdx]
182
+ }
183
+ }
184
+ nextGroups.push(currentSubGroup)
185
+ } else {
186
+ // Last column, no need to split
187
+ nextGroups.push(group)
273
188
  }
274
189
  }
275
- return 0
276
- })
277
190
 
278
- // Return sorted rows
279
- return indices.map(i => rows[i])
191
+ groups = nextGroups
192
+ }
193
+
194
+ // Flatten groups to get final sorted indices
195
+ return groups.flat().map(i => rows[i])
280
196
  }
281
197
 
282
198
  /**
@@ -285,7 +201,7 @@ async function applyOrderBy(rows, orderBy, tables) {
285
201
  * @param {SelectStatement} select
286
202
  * @param {AsyncDataSource} dataSource
287
203
  * @param {Record<string, AsyncDataSource>} tables
288
- * @returns {AsyncGenerator<AsyncRow>}
204
+ * @yields {AsyncRow}
289
205
  */
290
206
  async function* evaluateSelectAst(select, dataSource, tables) {
291
207
  // SQL priority: from, where, group by, having, select, order by, offset, limit
@@ -310,7 +226,7 @@ async function* evaluateSelectAst(select, dataSource, tables) {
310
226
  * @param {SelectStatement} select
311
227
  * @param {AsyncDataSource} dataSource
312
228
  * @param {Record<string, AsyncDataSource>} tables
313
- * @returns {AsyncGenerator<AsyncRow>}
229
+ * @yields {AsyncRow}
314
230
  */
315
231
  async function* evaluateStreaming(select, dataSource, tables) {
316
232
  let rowsYielded = 0
@@ -382,7 +298,7 @@ async function* evaluateStreaming(select, dataSource, tables) {
382
298
  * @param {Record<string, AsyncDataSource>} tables
383
299
  * @param {boolean} hasAggregate
384
300
  * @param {boolean} useGrouping
385
- * @returns {AsyncGenerator<AsyncRow>}
301
+ * @yields {AsyncRow}
386
302
  */
387
303
  async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGrouping) {
388
304
  // Step 1: Collect all rows from data source
@@ -487,7 +403,7 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
487
403
  } else {
488
404
  // No grouping, simple projection
489
405
  // Sort before projection so ORDER BY can access columns not in SELECT
490
- const sorted = await sortRowSources(filtered, select.orderBy, tables)
406
+ const sorted = await sortRows(filtered, select.orderBy, tables)
491
407
 
492
408
  // OPTIMIZATION: For non-DISTINCT queries, apply OFFSET/LIMIT before projection
493
409
  // to avoid reading expensive cells for rows that won't be in the final result
@@ -519,7 +435,9 @@ async function* evaluateBuffered(select, dataSource, tables, hasAggregate, useGr
519
435
  projected = await applyDistinct(projected, select.distinct)
520
436
 
521
437
  // Step 5: ORDER BY (final sort for grouped queries)
522
- projected = await applyOrderBy(projected, select.orderBy, tables)
438
+ if (useGrouping) {
439
+ projected = await sortRows(projected, select.orderBy, tables)
440
+ }
523
441
 
524
442
  // Step 6: OFFSET and LIMIT
525
443
  // For non-DISTINCT, non-grouping queries, OFFSET/LIMIT was already applied before projection
@@ -19,15 +19,26 @@ export async function evaluateExpr({ node, row, tables }) {
19
19
  }
20
20
 
21
21
  if (node.type === 'identifier') {
22
- return row[node.name]?.()
22
+ // Try exact match first (handles both qualified and unqualified names)
23
+ if (row[node.name]) {
24
+ return row[node.name]()
25
+ }
26
+ // For qualified names like 'users.id', also try just the column part
27
+ if (node.name.includes('.')) {
28
+ const colName = node.name.split('.').pop()
29
+ if (colName && row[colName]) {
30
+ return row[colName]()
31
+ }
32
+ }
33
+ return undefined
23
34
  }
24
35
 
25
36
  // Scalar subquery - returns a single value
26
37
  if (node.type === 'subquery') {
27
38
  const gen = executeSelect(node.subquery, tables)
28
39
  const first = await gen.next() // Start the generator
29
- gen.return() // Stop further execution
30
- if (first.done) return null
40
+ gen.return(undefined) // Stop further execution
41
+ if (!first.value) return null
31
42
  /** @type {AsyncRow} */
32
43
  const firstRow = first.value
33
44
  const firstKey = Object.keys(firstRow)[0]
@@ -99,21 +110,6 @@ export async function evaluateExpr({ node, row, tables }) {
99
110
  }
100
111
  }
101
112
 
102
- // BETWEEN and NOT BETWEEN
103
- if (node.type === 'between' || node.type === 'not between') {
104
- const expr = await evaluateExpr({ node: node.expr, row, tables })
105
- const lower = await evaluateExpr({ node: node.lower, row, tables })
106
- const upper = await evaluateExpr({ node: node.upper, row, tables })
107
-
108
- // If any value is NULL, return false (SQL behavior)
109
- if (expr == null || lower == null || upper == null) {
110
- return false
111
- }
112
-
113
- const isBetween = expr >= lower && expr <= upper
114
- return node.type === 'between' ? isBetween : !isBetween
115
- }
116
-
117
113
  // Function calls
118
114
  if (node.type === 'function') {
119
115
  const funcName = node.name.toUpperCase()
@@ -189,6 +185,11 @@ export async function evaluateExpr({ node, row, tables }) {
189
185
  return String(str).replaceAll(String(searchStr), String(replaceStr))
190
186
  }
191
187
 
188
+ if (funcName === 'RANDOM' || funcName === 'RAND') {
189
+ if (args.length !== 0) throw new Error(`${funcName} takes no arguments`)
190
+ return Math.random()
191
+ }
192
+
192
193
  throw new Error('Unsupported function ' + funcName)
193
194
  }
194
195
 
@@ -227,16 +228,7 @@ export async function evaluateExpr({ node, row, tables }) {
227
228
  }
228
229
  return false
229
230
  }
230
- if (node.type === 'not in valuelist') {
231
- const exprVal = await evaluateExpr({ node: node.expr, row, tables })
232
- for (const valueNode of node.values) {
233
- const val = await evaluateExpr({ node: valueNode, row, tables })
234
- if (exprVal === val) return false
235
- }
236
- return true
237
- }
238
-
239
- // IN and NOT IN with subqueries
231
+ // IN with subqueries
240
232
  if (node.type === 'in') {
241
233
  const exprVal = await evaluateExpr({ node: node.expr, row, tables })
242
234
  const results = executeSelect(node.subquery, tables)
@@ -249,18 +241,6 @@ export async function evaluateExpr({ node, row, tables }) {
249
241
  }
250
242
  return values.includes(exprVal)
251
243
  }
252
- if (node.type === 'not in') {
253
- const exprVal = await evaluateExpr({ node: node.expr, row, tables })
254
- const results = executeSelect(node.subquery, tables)
255
- /** @type {SqlPrimitive[]} */
256
- const values = []
257
- for await (const resRow of results) {
258
- const firstKey = Object.keys(resRow)[0]
259
- const val = await resRow[firstKey]()
260
- values.push(val)
261
- }
262
- return !values.includes(exprVal)
263
- }
264
244
 
265
245
  // EXISTS and NOT EXISTS with subqueries
266
246
  if (node.type === 'exists') {
@@ -275,7 +255,7 @@ export async function evaluateExpr({ node, row, tables }) {
275
255
  // CASE expressions
276
256
  if (node.type === 'case') {
277
257
  // For simple CASE: evaluate the case expression once
278
- const caseValue = node.caseExpr ? await evaluateExpr({ node: node.caseExpr, row, tables }) : undefined
258
+ const caseValue = node.caseExpr && await evaluateExpr({ node: node.caseExpr, row, tables })
279
259
 
280
260
  // Iterate through WHEN clauses
281
261
  for (const whenClause of node.whenClauses) {
@@ -1,26 +1,9 @@
1
- /**
2
- * @import { AggregateFunc, AsyncDataSource, ExprNode, AsyncRow, SqlPrimitive } from '../types.js'
3
- */
4
-
5
1
  import { isAggregateFunc } from '../validation.js'
6
2
  import { evaluateExpr } from './expression.js'
7
3
 
8
4
  /**
9
- * Creates a context for evaluating HAVING expressions
10
- *
11
- * @param {AsyncRow} resultRow - the aggregated result row
12
- * @param {AsyncRow[]} group - the group of rows
13
- * @returns {AsyncRow} a context row for HAVING evaluation
5
+ * @import { AggregateFunc, AsyncDataSource, ExprNode, AsyncRow, SqlPrimitive } from '../types.js'
14
6
  */
15
- function createHavingContext(resultRow, group) {
16
- // Include the first row of the group (for GROUP BY columns)
17
- const firstRow = group[0]
18
- if (firstRow) {
19
- return { ...firstRow, ...resultRow }
20
- } else {
21
- return resultRow
22
- }
23
- }
24
7
 
25
8
  /**
26
9
  * Evaluates a HAVING expression with support for aggregate functions
@@ -32,7 +15,8 @@ function createHavingContext(resultRow, group) {
32
15
  * @returns {Promise<boolean>} whether the HAVING condition is satisfied
33
16
  */
34
17
  export async function evaluateHavingExpr(expr, row, group, tables) {
35
- const context = createHavingContext(row, group)
18
+ // Having context
19
+ const context = { ...group[0] ?? {}, ...row }
36
20
 
37
21
  // For HAVING, we need special handling of aggregate functions
38
22
  // They need to be re-evaluated against the group
@@ -92,20 +76,6 @@ export async function evaluateHavingExpr(expr, row, group, tables) {
92
76
  }
93
77
  }
94
78
 
95
- if (expr.type === 'between' || expr.type === 'not between') {
96
- const exprVal = await evaluateHavingValue(expr.expr, context, group, tables)
97
- const lower = await evaluateHavingValue(expr.lower, context, group, tables)
98
- const upper = await evaluateHavingValue(expr.upper, context, group, tables)
99
-
100
- // If any value is NULL, return false (SQL behavior)
101
- if (exprVal == null || lower == null || upper == null) {
102
- return false
103
- }
104
-
105
- const isBetween = exprVal >= lower && exprVal <= upper
106
- return expr.type === 'between' ? isBetween : !isBetween
107
- }
108
-
109
79
  // For other expression types, use the context row
110
80
  return Boolean(await evaluateExpr({ node: expr, row: context, tables }))
111
81
  }
@@ -128,7 +98,7 @@ function evaluateHavingValue(expr, context, group, tables) {
128
98
  }
129
99
 
130
100
  // For binary expressions, we need to use evaluateHavingExpr to properly handle aggregates
131
- if (expr.type === 'binary' || expr.type === 'unary' || expr.type === 'between' || expr.type === 'not between') {
101
+ if (expr.type === 'binary' || expr.type === 'unary') {
132
102
  return evaluateHavingExpr(expr, context, group, tables)
133
103
  }
134
104