squirreling 0.7.7 → 0.7.9
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 +1 -1
- package/package.json +2 -2
- package/src/execute/expression.js +40 -11
- package/src/execute/having.js +46 -12
- package/src/execute/math.js +15 -0
- package/src/parse/functions.js +23 -2
- package/src/parse/parse.js +1 -2
- package/src/parse/tokenize.js +1 -0
- package/src/types.d.ts +4 -1
- package/src/validation.js +6 -2
- package/src/validationErrors.js +1 -0
package/README.md
CHANGED
|
@@ -99,7 +99,7 @@ Squirreling mostly follows the SQL standard. The following features are supporte
|
|
|
99
99
|
|
|
100
100
|
- Aggregate: `COUNT`, `SUM`, `AVG`, `MIN`, `MAX`, `JSON_ARRAYAGG`
|
|
101
101
|
- String: `CONCAT`, `SUBSTRING`, `REPLACE`, `LENGTH`, `UPPER`, `LOWER`, `TRIM`, `LEFT`, `RIGHT`, `INSTR`
|
|
102
|
-
- Math: `ABS`, `CEIL`, `FLOOR`, `ROUND`, `MOD`, `RAND`, `RANDOM`, `LN`, `LOG10`, `EXP`, `POWER`, `SQRT`
|
|
102
|
+
- Math: `ABS`, `SIGN`, `CEIL`, `FLOOR`, `ROUND`, `MOD`, `RAND`, `RANDOM`, `LN`, `LOG10`, `EXP`, `POWER`, `SQRT`
|
|
103
103
|
- Trig: `SIN`, `COS`, `TAN`, `COT`, `ASIN`, `ACOS`, `ATAN`, `ATAN2`, `DEGREES`, `RADIANS`, `PI`
|
|
104
104
|
- Date: `CURRENT_DATE`, `CURRENT_TIME`, `CURRENT_TIMESTAMP`, `INTERVAL`
|
|
105
105
|
- Json: `JSON_VALUE`, `JSON_QUERY`, `JSON_OBJECT`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "squirreling",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.9",
|
|
4
4
|
"description": "Squirreling SQL Engine",
|
|
5
5
|
"author": "Hyperparam",
|
|
6
6
|
"homepage": "https://hyperparam.app",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"test": "vitest run"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
|
-
"@types/node": "25.0.
|
|
40
|
+
"@types/node": "25.0.9",
|
|
41
41
|
"@vitest/coverage-v8": "4.0.17",
|
|
42
42
|
"eslint": "9.39.2",
|
|
43
43
|
"eslint-plugin-jsdoc": "62.0.0",
|
|
@@ -123,10 +123,20 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
123
123
|
})
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
// Apply FILTER clause if present
|
|
127
|
+
let filteredRows = rows
|
|
128
|
+
if (node.filter) {
|
|
129
|
+
filteredRows = []
|
|
130
|
+
for (const row of rows) {
|
|
131
|
+
const passes = await evaluateExpr({ node: node.filter, row, tables, functions })
|
|
132
|
+
if (passes) filteredRows.push(row)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
126
136
|
// Check for star argument (COUNT(*))
|
|
127
137
|
if (node.args.length === 1 && node.args[0].type === 'identifier' && node.args[0].name === '*') {
|
|
128
138
|
if (funcName === 'COUNT') {
|
|
129
|
-
return
|
|
139
|
+
return filteredRows.length
|
|
130
140
|
}
|
|
131
141
|
throw aggregateError({
|
|
132
142
|
funcName,
|
|
@@ -139,15 +149,15 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
139
149
|
if (funcName === 'COUNT') {
|
|
140
150
|
if (node.distinct) {
|
|
141
151
|
const seen = new Set()
|
|
142
|
-
for (const
|
|
143
|
-
const v = await evaluateExpr({ node: argNode, row
|
|
152
|
+
for (const row of filteredRows) {
|
|
153
|
+
const v = await evaluateExpr({ node: argNode, row, tables, functions })
|
|
144
154
|
if (v != null) seen.add(v)
|
|
145
155
|
}
|
|
146
156
|
return seen.size
|
|
147
157
|
}
|
|
148
158
|
let count = 0
|
|
149
|
-
for (const
|
|
150
|
-
const v = await evaluateExpr({ node: argNode, row
|
|
159
|
+
for (const row of filteredRows) {
|
|
160
|
+
const v = await evaluateExpr({ node: argNode, row, tables, functions })
|
|
151
161
|
if (v != null) count++
|
|
152
162
|
}
|
|
153
163
|
return count
|
|
@@ -161,8 +171,8 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
161
171
|
/** @type {number | null} */
|
|
162
172
|
let max = null
|
|
163
173
|
|
|
164
|
-
for (const
|
|
165
|
-
const raw = await evaluateExpr({ node: argNode, row
|
|
174
|
+
for (const row of filteredRows) {
|
|
175
|
+
const raw = await evaluateExpr({ node: argNode, row, tables, functions })
|
|
166
176
|
if (raw == null) continue
|
|
167
177
|
const num = Number(raw)
|
|
168
178
|
if (!Number.isFinite(num)) continue
|
|
@@ -184,13 +194,32 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
184
194
|
if (funcName === 'MAX') return max
|
|
185
195
|
}
|
|
186
196
|
|
|
197
|
+
if (funcName === 'STDDEV_SAMP' || funcName === 'STDDEV_POP') {
|
|
198
|
+
const values = []
|
|
199
|
+
for (const row of filteredRows) {
|
|
200
|
+
const raw = await evaluateExpr({ node: argNode, row, tables, functions })
|
|
201
|
+
if (raw == null) continue
|
|
202
|
+
const num = Number(raw)
|
|
203
|
+
if (!Number.isFinite(num)) continue
|
|
204
|
+
values.push(num)
|
|
205
|
+
}
|
|
206
|
+
const n = values.length
|
|
207
|
+
if (n === 0) return null
|
|
208
|
+
if (funcName === 'STDDEV_SAMP' && n === 1) return null
|
|
209
|
+
|
|
210
|
+
const mean = values.reduce((a, b) => a + b, 0) / n
|
|
211
|
+
const squaredDiffs = values.reduce((acc, val) => acc + (val - mean) ** 2, 0)
|
|
212
|
+
const divisor = funcName === 'STDDEV_SAMP' ? n - 1 : n
|
|
213
|
+
return Math.sqrt(squaredDiffs / divisor)
|
|
214
|
+
}
|
|
215
|
+
|
|
187
216
|
if (funcName === 'JSON_ARRAYAGG') {
|
|
188
217
|
/** @type {SqlPrimitive[]} */
|
|
189
218
|
const values = []
|
|
190
219
|
if (node.distinct) {
|
|
191
220
|
const seen = new Set()
|
|
192
|
-
for (const
|
|
193
|
-
const v = await evaluateExpr({ node: argNode, row
|
|
221
|
+
for (const row of filteredRows) {
|
|
222
|
+
const v = await evaluateExpr({ node: argNode, row, tables, functions })
|
|
194
223
|
const key = stringify(v)
|
|
195
224
|
if (!seen.has(key)) {
|
|
196
225
|
seen.add(key)
|
|
@@ -198,8 +227,8 @@ export async function evaluateExpr({ node, row, tables, functions, rowIndex, row
|
|
|
198
227
|
}
|
|
199
228
|
}
|
|
200
229
|
} else {
|
|
201
|
-
for (const
|
|
202
|
-
const v = await evaluateExpr({ node: argNode, row
|
|
230
|
+
for (const row of filteredRows) {
|
|
231
|
+
const v = await evaluateExpr({ node: argNode, row, tables, functions })
|
|
203
232
|
values.push(v)
|
|
204
233
|
}
|
|
205
234
|
}
|
package/src/execute/having.js
CHANGED
|
@@ -28,7 +28,7 @@ export async function evaluateHavingExpr({ expr, row, group, tables, functions }
|
|
|
28
28
|
const funcName = expr.name.toUpperCase()
|
|
29
29
|
if (isAggregateFunc(funcName)) {
|
|
30
30
|
// Evaluate aggregate function on the group
|
|
31
|
-
return Boolean(await evaluateAggregateFunction({ funcName, args: expr.args, group, tables, functions }))
|
|
31
|
+
return Boolean(await evaluateAggregateFunction({ funcName, args: expr.args, filter: expr.filter, group, tables, functions }))
|
|
32
32
|
}
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -78,7 +78,7 @@ function evaluateHavingValue({ expr, context, group, tables, functions }) {
|
|
|
78
78
|
if (expr.type === 'function') {
|
|
79
79
|
const funcName = expr.name.toUpperCase()
|
|
80
80
|
if (isAggregateFunc(funcName)) {
|
|
81
|
-
return evaluateAggregateFunction({ funcName, args: expr.args, group, tables, functions })
|
|
81
|
+
return evaluateAggregateFunction({ funcName, args: expr.args, filter: expr.filter, group, tables, functions })
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
@@ -96,19 +96,30 @@ function evaluateHavingValue({ expr, context, group, tables, functions }) {
|
|
|
96
96
|
* @param {Object} options
|
|
97
97
|
* @param {AggregateFunc} options.funcName - aggregate function name
|
|
98
98
|
* @param {ExprNode[]} options.args - function arguments
|
|
99
|
+
* @param {ExprNode} [options.filter] - optional FILTER clause expression
|
|
99
100
|
* @param {AsyncRow[]} options.group - the group of rows
|
|
100
101
|
* @param {Record<string, AsyncDataSource>} options.tables
|
|
101
102
|
* @param {Record<string, UserDefinedFunction>} [options.functions]
|
|
102
103
|
* @returns {Promise<SqlPrimitive>} the aggregate result
|
|
103
104
|
*/
|
|
104
|
-
async function evaluateAggregateFunction({ funcName, args, group, tables, functions }) {
|
|
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
|
+
|
|
105
116
|
if (funcName === 'COUNT') {
|
|
106
117
|
if (args.length === 1 && args[0].type === 'identifier' && args[0].name === '*') {
|
|
107
|
-
return
|
|
118
|
+
return filteredGroup.length
|
|
108
119
|
}
|
|
109
120
|
// COUNT(column) - count non-null values
|
|
110
121
|
let count = 0
|
|
111
|
-
for (const row of
|
|
122
|
+
for (const row of filteredGroup) {
|
|
112
123
|
const val = await evaluateExpr({ node: args[0], row, tables, functions })
|
|
113
124
|
if (val != null) count++
|
|
114
125
|
}
|
|
@@ -117,17 +128,21 @@ async function evaluateAggregateFunction({ funcName, args, group, tables, functi
|
|
|
117
128
|
|
|
118
129
|
if (funcName === 'SUM') {
|
|
119
130
|
let sum = 0
|
|
120
|
-
|
|
131
|
+
let hasValue = false
|
|
132
|
+
for (const row of filteredGroup) {
|
|
121
133
|
const val = await evaluateExpr({ node: args[0], row, tables, functions })
|
|
122
|
-
if (val != null)
|
|
134
|
+
if (val != null) {
|
|
135
|
+
sum += Number(val)
|
|
136
|
+
hasValue = true
|
|
137
|
+
}
|
|
123
138
|
}
|
|
124
|
-
return sum
|
|
139
|
+
return hasValue ? sum : null
|
|
125
140
|
}
|
|
126
141
|
|
|
127
142
|
if (funcName === 'AVG') {
|
|
128
143
|
let sum = 0
|
|
129
144
|
let count = 0
|
|
130
|
-
for (const row of
|
|
145
|
+
for (const row of filteredGroup) {
|
|
131
146
|
const val = await evaluateExpr({ node: args[0], row, tables, functions })
|
|
132
147
|
if (val != null) {
|
|
133
148
|
sum += Number(val)
|
|
@@ -139,7 +154,7 @@ async function evaluateAggregateFunction({ funcName, args, group, tables, functi
|
|
|
139
154
|
|
|
140
155
|
if (funcName === 'MIN') {
|
|
141
156
|
let min = null
|
|
142
|
-
for (const row of
|
|
157
|
+
for (const row of filteredGroup) {
|
|
143
158
|
const val = await evaluateExpr({ node: args[0], row, tables, functions })
|
|
144
159
|
if (val != null && (min == null || val < min)) {
|
|
145
160
|
min = val
|
|
@@ -150,7 +165,7 @@ async function evaluateAggregateFunction({ funcName, args, group, tables, functi
|
|
|
150
165
|
|
|
151
166
|
if (funcName === 'MAX') {
|
|
152
167
|
let max = null
|
|
153
|
-
for (const row of
|
|
168
|
+
for (const row of filteredGroup) {
|
|
154
169
|
const val = await evaluateExpr({ node: args[0], row, tables, functions })
|
|
155
170
|
if (val != null && (max == null || val > max)) {
|
|
156
171
|
max = val
|
|
@@ -159,10 +174,29 @@ async function evaluateAggregateFunction({ funcName, args, group, tables, functi
|
|
|
159
174
|
return max
|
|
160
175
|
}
|
|
161
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
|
+
|
|
162
196
|
throw unknownFunctionError({
|
|
163
197
|
funcName,
|
|
164
198
|
positionStart: 0,
|
|
165
199
|
positionEnd: 0,
|
|
166
|
-
validFunctions: 'COUNT, SUM, AVG, MIN, MAX',
|
|
200
|
+
validFunctions: 'COUNT, SUM, AVG, MIN, MAX, STDDEV_SAMP, STDDEV_POP',
|
|
167
201
|
})
|
|
168
202
|
}
|
package/src/execute/math.js
CHANGED
|
@@ -23,12 +23,27 @@ export function evaluateMathFunc({ funcName, args }) {
|
|
|
23
23
|
return Math.ceil(Number(val))
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
if (funcName === 'ROUND') {
|
|
27
|
+
const val = args[0]
|
|
28
|
+
if (val == null) return null
|
|
29
|
+
const decimals = args[1] ?? 0
|
|
30
|
+
if (decimals == null) return null
|
|
31
|
+
const multiplier = 10 ** Number(decimals)
|
|
32
|
+
return Math.round(Number(val) * multiplier) / multiplier
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
if (funcName === 'ABS') {
|
|
27
36
|
const val = args[0]
|
|
28
37
|
if (val == null) return null
|
|
29
38
|
return Math.abs(Number(val))
|
|
30
39
|
}
|
|
31
40
|
|
|
41
|
+
if (funcName === 'SIGN') {
|
|
42
|
+
const val = args[0]
|
|
43
|
+
if (val == null) return null
|
|
44
|
+
return Math.sign(Number(val))
|
|
45
|
+
}
|
|
46
|
+
|
|
32
47
|
if (funcName === 'MOD') {
|
|
33
48
|
const dividend = args[0]
|
|
34
49
|
const divisor = args[1]
|
package/src/parse/functions.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { argCountParseError } from '../parseErrors.js'
|
|
2
|
-
import { validateFunctionArgCount } from '../validation.js'
|
|
1
|
+
import { argCountParseError, syntaxError } from '../parseErrors.js'
|
|
2
|
+
import { isAggregateFunc, validateFunctionArgCount } from '../validation.js'
|
|
3
3
|
import { parseExpression } from './expression.js'
|
|
4
4
|
import { consume, current, expect, lastPosition, match } from './state.js'
|
|
5
5
|
|
|
@@ -52,6 +52,26 @@ export function parseFunctionCall(state, funcName, positionStart) {
|
|
|
52
52
|
|
|
53
53
|
expect(state, 'paren', ')')
|
|
54
54
|
|
|
55
|
+
// Check for FILTER clause (only valid for aggregate functions)
|
|
56
|
+
/** @type {ExprNode | undefined} */
|
|
57
|
+
let filter
|
|
58
|
+
if (current(state).type === 'keyword' && current(state).value === 'FILTER') {
|
|
59
|
+
const funcNameUpper = funcName.toUpperCase()
|
|
60
|
+
if (!isAggregateFunc(funcNameUpper)) {
|
|
61
|
+
throw syntaxError({
|
|
62
|
+
expected: 'aggregate function for FILTER clause',
|
|
63
|
+
received: `FILTER on non-aggregate function "${funcName}"`,
|
|
64
|
+
positionStart: current(state).positionStart,
|
|
65
|
+
positionEnd: current(state).positionEnd,
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
consume(state) // FILTER
|
|
69
|
+
expect(state, 'paren', '(')
|
|
70
|
+
expect(state, 'keyword', 'WHERE')
|
|
71
|
+
filter = parseExpression(state)
|
|
72
|
+
expect(state, 'paren', ')')
|
|
73
|
+
}
|
|
74
|
+
|
|
55
75
|
// Validate argument count at parse time
|
|
56
76
|
const funcNameUpper = funcName.toUpperCase()
|
|
57
77
|
const validation = validateFunctionArgCount(funcNameUpper, args.length, state.functions)
|
|
@@ -70,6 +90,7 @@ export function parseFunctionCall(state, funcName, positionStart) {
|
|
|
70
90
|
name: funcName,
|
|
71
91
|
args,
|
|
72
92
|
distinct: distinct || undefined,
|
|
93
|
+
filter,
|
|
73
94
|
positionStart,
|
|
74
95
|
positionEnd: lastPosition(state),
|
|
75
96
|
}
|
package/src/parse/parse.js
CHANGED
|
@@ -214,8 +214,7 @@ function parseFromSubquery(state) {
|
|
|
214
214
|
expect(state, 'paren', '(')
|
|
215
215
|
const query = parseSelectInternal(state)
|
|
216
216
|
expect(state, 'paren', ')')
|
|
217
|
-
|
|
218
|
-
const alias = expectIdentifier(state).value
|
|
217
|
+
const alias = parseTableAlias(state)
|
|
219
218
|
return { kind: 'subquery', query, alias }
|
|
220
219
|
}
|
|
221
220
|
|
package/src/parse/tokenize.js
CHANGED
package/src/types.d.ts
CHANGED
|
@@ -141,6 +141,7 @@ export interface FunctionNode extends ExprNodeBase {
|
|
|
141
141
|
name: string
|
|
142
142
|
args: ExprNode[]
|
|
143
143
|
distinct?: boolean
|
|
144
|
+
filter?: ExprNode
|
|
144
145
|
}
|
|
145
146
|
|
|
146
147
|
export interface CastNode extends ExprNodeBase {
|
|
@@ -211,13 +212,15 @@ export interface StarColumn {
|
|
|
211
212
|
alias?: string
|
|
212
213
|
}
|
|
213
214
|
|
|
214
|
-
export type AggregateFunc = 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX' | 'JSON_ARRAYAGG'
|
|
215
|
+
export type AggregateFunc = 'COUNT' | 'SUM' | 'AVG' | 'MIN' | 'MAX' | 'JSON_ARRAYAGG' | 'STDDEV_SAMP' | 'STDDEV_POP'
|
|
215
216
|
|
|
216
217
|
export type MathFunc =
|
|
217
218
|
| 'FLOOR'
|
|
218
219
|
| 'CEIL'
|
|
219
220
|
| 'CEILING'
|
|
221
|
+
| 'ROUND'
|
|
220
222
|
| 'ABS'
|
|
223
|
+
| 'SIGN'
|
|
221
224
|
| 'MOD'
|
|
222
225
|
| 'EXP'
|
|
223
226
|
| 'LN'
|
package/src/validation.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* @returns {name is AggregateFunc}
|
|
6
6
|
*/
|
|
7
7
|
export function isAggregateFunc(name) {
|
|
8
|
-
return ['COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'JSON_ARRAYAGG'].includes(name)
|
|
8
|
+
return ['COUNT', 'SUM', 'AVG', 'MIN', 'MAX', 'JSON_ARRAYAGG', 'STDDEV_SAMP', 'STDDEV_POP'].includes(name)
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
/**
|
|
@@ -22,7 +22,7 @@ export function isRegexpFunc(name) {
|
|
|
22
22
|
*/
|
|
23
23
|
export function isMathFunc(name) {
|
|
24
24
|
return [
|
|
25
|
-
'FLOOR', 'CEIL', 'CEILING', 'ABS', 'MOD', 'EXP', 'LN', 'LOG10', 'POWER', 'SQRT',
|
|
25
|
+
'FLOOR', 'CEIL', 'CEILING', 'ROUND', 'ABS', 'SIGN', 'MOD', 'EXP', 'LN', 'LOG10', 'POWER', 'SQRT',
|
|
26
26
|
'SIN', 'COS', 'TAN', 'COT', 'ASIN', 'ACOS', 'ATAN', 'ATAN2', 'DEGREES', 'RADIANS', 'PI',
|
|
27
27
|
'RAND', 'RANDOM',
|
|
28
28
|
].includes(name)
|
|
@@ -88,7 +88,9 @@ export const FUNCTION_ARG_COUNTS = {
|
|
|
88
88
|
FLOOR: { min: 1, max: 1 },
|
|
89
89
|
CEIL: { min: 1, max: 1 },
|
|
90
90
|
CEILING: { min: 1, max: 1 },
|
|
91
|
+
ROUND: { min: 1, max: 2 },
|
|
91
92
|
ABS: { min: 1, max: 1 },
|
|
93
|
+
SIGN: { min: 1, max: 1 },
|
|
92
94
|
MOD: { min: 2, max: 2 },
|
|
93
95
|
EXP: { min: 1, max: 1 },
|
|
94
96
|
LN: { min: 1, max: 1 },
|
|
@@ -122,6 +124,8 @@ export const FUNCTION_ARG_COUNTS = {
|
|
|
122
124
|
AVG: { min: 1, max: 1 },
|
|
123
125
|
MIN: { min: 1, max: 1 },
|
|
124
126
|
MAX: { min: 1, max: 1 },
|
|
127
|
+
STDDEV_SAMP: { min: 1, max: 1 },
|
|
128
|
+
STDDEV_POP: { min: 1, max: 1 },
|
|
125
129
|
}
|
|
126
130
|
|
|
127
131
|
/**
|