import { SqlEvaluationError } from './errors.js'; /** * Aggregate functions, keyed by uppercase name. Each one receives the argument * value from every row of a group, in row order, and folds them into a single * value. Following SQL, nulls are skipped rather than propagated, and an * all-null (or empty) group aggregates to `null` - except COUNT, which counts. * * `COUNT(*)` passes a non-null placeholder for each row (see the `Star` argument * handling in query.js), so it counts rows where `COUNT(expr)` counts non-nulls. * * These names are reserved by the query layer: unlike the builtins in * functions.js they cannot be overridden via `{ functions }`, since shadowing * one would silently turn a grouped query into a non-grouped one. * @type {Record any>} */ export const aggregates = { COUNT: (values) => values.reduce((n, v) => (v === null ? n : n + 1), 0), SUM: (values) => { const nums = numbersOf('SUM', values); return nums.length === 0 ? null : nums.reduce((a, b) => a + b, 0); }, AVG: (values) => { const nums = numbersOf('AVG', values); return nums.length === 0 ? null : nums.reduce((a, b) => a + b, 0) / nums.length; }, MIN: (values) => extreme('MIN', values, -1), MAX: (values) => extreme('MAX', values, 1), ARRAY_AGG: (values) => values, }; /** @param {string} name */ export function isAggregateName(name) { return Object.prototype.hasOwnProperty.call(aggregates, name.toUpperCase()); } /** @param {string} name @param {any[]} values */ function numbersOf(name, values) { /** @type {number[]} */ const out = []; for (const v of values) { if (v === null) continue; if (typeof v !== 'number') { throw new SqlEvaluationError(`${name} requires numbers, got ${typeName(v)}`); } out.push(v); } return out; } /** * @param {string} name * @param {any[]} values * @param {-1|1} sign - -1 keeps the smallest value, 1 the largest */ function extreme(name, values, sign) { /** @type {number|string|null} */ let best = null; for (const v of values) { if (v === null) continue; if (typeof v !== 'number' && typeof v !== 'string') { throw new SqlEvaluationError(`${name} requires numbers or strings, got ${typeName(v)}`); } if (best === null) { best = v; continue; } if (typeof v !== typeof best) { throw new SqlEvaluationError(`${name} cannot mix numbers and strings`); } if (sign < 0 ? v < best : v > best) best = v; } return best; } /** @param {any} v */ function typeName(v) { if (v === null) return 'null'; if (Array.isArray(v)) return 'array'; return typeof v; }