squirreling 0.7.9 → 0.8.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.
@@ -1,202 +0,0 @@
1
- import { unknownFunctionError } from '../parseErrors.js'
2
- import { isAggregateFunc } from '../validation.js'
3
- import { evaluateExpr } from './expression.js'
4
- import { applyBinaryOp } from './utils.js'
5
-
6
- /**
7
- * @import { AggregateFunc, AsyncDataSource, ExprNode, AsyncRow, SqlPrimitive, UserDefinedFunction } from '../types.js'
8
- */
9
-
10
- /**
11
- * Evaluates a HAVING expression with support for aggregate functions
12
- *
13
- * @param {Object} options
14
- * @param {ExprNode} options.expr - the HAVING expression
15
- * @param {AsyncRow} options.row - the aggregated result row
16
- * @param {AsyncRow[]} options.group - the group of rows for re-evaluating aggregates
17
- * @param {Record<string, AsyncDataSource>} options.tables
18
- * @param {Record<string, UserDefinedFunction>} [options.functions]
19
- * @returns {Promise<boolean>} whether the HAVING condition is satisfied
20
- */
21
- export async function evaluateHavingExpr({ expr, row, group, tables, functions }) {
22
- // Having context
23
- const context = { ...group[0], ...row }
24
-
25
- // For HAVING, we need special handling of aggregate functions
26
- // They need to be re-evaluated against the group
27
- if (expr.type === 'function') {
28
- const funcName = expr.name.toUpperCase()
29
- if (isAggregateFunc(funcName)) {
30
- // Evaluate aggregate function on the group
31
- return Boolean(await evaluateAggregateFunction({ funcName, args: expr.args, filter: expr.filter, group, tables, functions }))
32
- }
33
- }
34
-
35
- if (expr.type === 'binary') {
36
- const left = await evaluateHavingValue({ expr: expr.left, context, group, tables, functions })
37
-
38
- // Short-circuit evaluation for AND and OR
39
- if (expr.op === 'AND') {
40
- if (!left) return false
41
- }
42
- if (expr.op === 'OR') {
43
- if (left) return true
44
- }
45
-
46
- const right = await evaluateHavingValue({ expr: expr.right, context, group, tables, functions })
47
- return Boolean(applyBinaryOp(expr.op, left, right))
48
- }
49
-
50
- if (expr.type === 'unary') {
51
- if (expr.op === 'NOT') {
52
- return !await evaluateHavingExpr({ expr: expr.argument, row: context, group, tables, functions })
53
- }
54
- if (expr.op === 'IS NULL') {
55
- return await evaluateHavingValue({ expr: expr.argument, context, group, tables, functions }) == null
56
- }
57
- if (expr.op === 'IS NOT NULL') {
58
- return await evaluateHavingValue({ expr: expr.argument, context, group, tables, functions }) != null
59
- }
60
- }
61
-
62
- // For other expression types, use the context row
63
- return Boolean(await evaluateExpr({ node: expr, row: context, tables, functions }))
64
- }
65
-
66
- /**
67
- * Evaluates a value in a HAVING expression
68
- *
69
- * @param {Object} options
70
- * @param {ExprNode} options.expr
71
- * @param {AsyncRow} options.context - the context row
72
- * @param {AsyncRow[]} options.group - the group of rows
73
- * @param {Record<string, AsyncDataSource>} options.tables
74
- * @param {Record<string, UserDefinedFunction>} [options.functions]
75
- * @returns {Promise<SqlPrimitive>} the evaluated value
76
- */
77
- function evaluateHavingValue({ expr, context, group, tables, functions }) {
78
- if (expr.type === 'function') {
79
- const funcName = expr.name.toUpperCase()
80
- if (isAggregateFunc(funcName)) {
81
- return evaluateAggregateFunction({ funcName, args: expr.args, filter: expr.filter, group, tables, functions })
82
- }
83
- }
84
-
85
- // For binary expressions, we need to use evaluateHavingExpr to properly handle aggregates
86
- if (expr.type === 'binary' || expr.type === 'unary') {
87
- return evaluateHavingExpr({ expr, row: context, group, tables, functions })
88
- }
89
-
90
- return evaluateExpr({ node: expr, row: context, tables, functions })
91
- }
92
-
93
- /**
94
- * Evaluates an aggregate function on a group
95
- *
96
- * @param {Object} options
97
- * @param {AggregateFunc} options.funcName - aggregate function name
98
- * @param {ExprNode[]} options.args - function arguments
99
- * @param {ExprNode} [options.filter] - optional FILTER clause expression
100
- * @param {AsyncRow[]} options.group - the group of rows
101
- * @param {Record<string, AsyncDataSource>} options.tables
102
- * @param {Record<string, UserDefinedFunction>} [options.functions]
103
- * @returns {Promise<SqlPrimitive>} the aggregate result
104
- */
105
- async function evaluateAggregateFunction({ funcName, args, filter, group, tables, functions }) {
106
- // Apply FILTER clause if present
107
- let filteredGroup = group
108
- if (filter) {
109
- filteredGroup = []
110
- for (const row of group) {
111
- const passes = await evaluateExpr({ node: filter, row, tables, functions })
112
- if (passes) filteredGroup.push(row)
113
- }
114
- }
115
-
116
- if (funcName === 'COUNT') {
117
- if (args.length === 1 && args[0].type === 'identifier' && args[0].name === '*') {
118
- return filteredGroup.length
119
- }
120
- // COUNT(column) - count non-null values
121
- let count = 0
122
- for (const row of filteredGroup) {
123
- const val = await evaluateExpr({ node: args[0], row, tables, functions })
124
- if (val != null) count++
125
- }
126
- return count
127
- }
128
-
129
- if (funcName === 'SUM') {
130
- let sum = 0
131
- let hasValue = false
132
- for (const row of filteredGroup) {
133
- const val = await evaluateExpr({ node: args[0], row, tables, functions })
134
- if (val != null) {
135
- sum += Number(val)
136
- hasValue = true
137
- }
138
- }
139
- return hasValue ? sum : null
140
- }
141
-
142
- if (funcName === 'AVG') {
143
- let sum = 0
144
- let count = 0
145
- for (const row of filteredGroup) {
146
- const val = await evaluateExpr({ node: args[0], row, tables, functions })
147
- if (val != null) {
148
- sum += Number(val)
149
- count++
150
- }
151
- }
152
- return count > 0 ? sum / count : null
153
- }
154
-
155
- if (funcName === 'MIN') {
156
- let min = null
157
- for (const row of filteredGroup) {
158
- const val = await evaluateExpr({ node: args[0], row, tables, functions })
159
- if (val != null && (min == null || val < min)) {
160
- min = val
161
- }
162
- }
163
- return min
164
- }
165
-
166
- if (funcName === 'MAX') {
167
- let max = null
168
- for (const row of filteredGroup) {
169
- const val = await evaluateExpr({ node: args[0], row, tables, functions })
170
- if (val != null && (max == null || val > max)) {
171
- max = val
172
- }
173
- }
174
- return max
175
- }
176
-
177
- if (funcName === 'STDDEV_SAMP' || funcName === 'STDDEV_POP') {
178
- const values = []
179
- for (const row of filteredGroup) {
180
- const val = await evaluateExpr({ node: args[0], row, tables, functions })
181
- if (val == null) continue
182
- const num = Number(val)
183
- if (!Number.isFinite(num)) continue
184
- values.push(num)
185
- }
186
- const n = values.length
187
- if (n === 0) return null
188
- if (funcName === 'STDDEV_SAMP' && n === 1) return null
189
-
190
- const mean = values.reduce((a, b) => a + b, 0) / n
191
- const squaredDiffs = values.reduce((acc, val) => acc + (val - mean) ** 2, 0)
192
- const divisor = funcName === 'STDDEV_SAMP' ? n - 1 : n
193
- return Math.sqrt(squaredDiffs / divisor)
194
- }
195
-
196
- throw unknownFunctionError({
197
- funcName,
198
- positionStart: 0,
199
- positionEnd: 0,
200
- validFunctions: 'COUNT, SUM, AVG, MIN, MAX, STDDEV_SAMP, STDDEV_POP',
201
- })
202
- }
@@ -1,63 +0,0 @@
1
- import { tableNotFoundError } from '../executionErrors.js'
2
- import { generatorSource } from '../backend/dataSource.js'
3
-
4
- /**
5
- * @import { AsyncDataSource, CTEDefinition, UserDefinedFunction, WithClause } from '../types.js'
6
- */
7
-
8
- /**
9
- * Gets CTEs defined before the target CTE (excluding the target itself).
10
- * Enforces SQL scoping rules: each CTE can only reference CTEs defined before it.
11
- *
12
- * @param {CTEDefinition[]} allCtes - all CTE definitions in order
13
- * @param {string} targetCteName - the CTE name (case-insensitive)
14
- * @returns {WithClause} CTEs available to the target
15
- */
16
- export function getCtesDefinedBefore(allCtes, targetCteName) {
17
- const available = []
18
- for (const cte of allCtes) {
19
- if (cte.name.toLowerCase() === targetCteName) break
20
- available.push(cte)
21
- }
22
- return { ctes: available }
23
- }
24
-
25
- /**
26
- * Resolves a table name to an AsyncDataSource, checking CTEs first
27
- *
28
- * @param {string} tableName - the table name to resolve
29
- * @param {Record<string, AsyncDataSource>} tables - regular tables
30
- * @param {import('../types.js').WithClause} [withClause] - WITH clause containing CTE definitions
31
- * @param {Function} [executeSelectFn] - function to execute SELECT for CTEs
32
- * @param {Record<string, UserDefinedFunction>} [functions]
33
- * @param {AbortSignal} [signal]
34
- * @returns {AsyncDataSource}
35
- */
36
- export function resolveTableSource(tableName, tables, withClause, executeSelectFn, functions, signal) {
37
- // Check CTEs first (case-insensitive) - only build map when CTE is actually found
38
- if (withClause && executeSelectFn) {
39
- const lowerName = tableName.toLowerCase()
40
- const cte = withClause.ctes.find(c => c.name.toLowerCase() === lowerName)
41
-
42
- if (cte) {
43
- // CTE reference: wrap in generatorSource, re-execute each time (streaming)
44
- // Pass only CTEs defined before this one to prevent self-reference
45
- const availableCtes = getCtesDefinedBefore(withClause.ctes, lowerName)
46
-
47
- return generatorSource(executeSelectFn({
48
- select: cte.query,
49
- tables,
50
- withClause: availableCtes,
51
- functions,
52
- signal,
53
- }))
54
- }
55
- }
56
-
57
- // Regular table lookup
58
- const tableSource = tables[tableName]
59
- if (tableSource === undefined) {
60
- throw tableNotFoundError({ tableName })
61
- }
62
- return tableSource
63
- }