octobercms/october · error · SystemException
Invalid aggregate function:
Error message
Invalid aggregate function:
What it means
ReportQueryBuilder::getAggregateSql(string $function) maps a metric's aggregate function to its SQL template via a match expression; any function string outside the eight AGGREGATE_* values hits the default arm and throws SystemException('Invalid aggregate function: <function>'). Under normal flow this is unreachable because ReportMetric's constructor already whitelists the function — hitting it means a metric reached the builder without constructor validation, or the function was mutated/constructed bypassing the setters.
Source
Thrown at modules/dashboard/classes/ReportQueryBuilder.php:870
/**
* getAggregateSql returns SQL template for aggregate function
*
* @param string $function
* @return string
*/
protected function getAggregateSql(string $function): string
{
return match ($function) {
ReportMetric::AGGREGATE_AVG => 'avg(%1$s)',
ReportMetric::AGGREGATE_COUNT => 'count(%1$s)',
ReportMetric::AGGREGATE_MAX => 'max(%1$s)',
ReportMetric::AGGREGATE_MIN => 'min(%1$s)',
ReportMetric::AGGREGATE_SUM => 'sum(%1$s)',
ReportMetric::AGGREGATE_COUNT_DISTINCT => 'count(distinct %1$s)',
ReportMetric::AGGREGATE_COUNT_DISTINCT_NOT_NULL => 'count(distinct case when %1$s is not null then %1$s end)',
ReportMetric::AGGREGATE_NONE => '%1$s',
default => throw new SystemException('Invalid aggregate function: ' . $function)
};
}
/**
* applyDateFilters applies date range or timestamp filtering
*
* @param QueryBuilder $query
*/
protected function applyDateFilters(QueryBuilder $query): void
{
if ($this->dateColumn && $this->dateStart !== null) {
$query->whereBetween($this->dateColumn, [
$this->dateStart->startOfDay()->toDateTimeString(),
$this->dateEnd->endOfDay()->toDateTimeString()
]);
}
if ($this->timestampColumn && $this->startTimestamp !== null) {View on GitHub (pinned to b608633a7e)
Solutions
- Re-create the metric properly through the constructor with an AGGREGATE_* constant instead of rehydrating/mutating one.
- Clear stale caches (dashboard/report caches, config cache) after upgrading or renaming aggregate constants.
- If extending the builder, override getAggregateSql() and add your template to the match (plus your own validation upstream) rather than passing unknown strings.
- Log the offending function value from the exception message to identify which metric code carries it.
Example fix
// before (extending builder with custom aggregate)
$this->getAggregateSql('median'); // throws
// after
protected function getAggregateSql(string $function): string
{
return match ($function) {
'median' => 'percentile_cont(0.5) within group (order by %1$s)',
default => parent::getAggregateSql($function),
};
} Defensive patterns
Strategy: validation
Validate before calling
// Only feed metrics to the builder that were built via the constructor
foreach ($metrics as $metric) {
if (!$metric instanceof ReportMetric) {
throw new InvalidArgumentException('All metrics must be ReportMetric instances.');
}
}
$builder->applyMetrics($metrics); Type guard
function hasValidAggregate(ReportMetric $metric): bool
{
return in_array($metric->getAggregateFunction(), [
ReportMetric::AGGREGATE_SUM, ReportMetric::AGGREGATE_AVG,
ReportMetric::AGGREGATE_MIN, ReportMetric::AGGREGATE_MAX,
ReportMetric::AGGREGATE_COUNT, ReportMetric::AGGREGATE_NONE,
ReportMetric::AGGREGATE_COUNT_DISTINCT, ReportMetric::AGGREGATE_COUNT_DISTINCT_NOT_NULL,
], true);
} Try / catch
try {
$rows = $builder->buildQuery()->get();
} catch (SystemException $e) {
if (str_contains($e->getMessage(), 'Invalid aggregate function')) {
// a cached/serialized metric is stale — rebuild metrics from definitions and retry once
$metrics = $dataSource->getAvailableMetrics(true); // force fresh
$builder->applyMetrics($metrics);
$rows = $builder->buildQuery()->get();
} else {
throw $e;
}
} Prevention
- Never rehydrate ReportMetric objects from cache without revalidating the aggregate function.
- Clear report/dashboard caches after upgrading modules that rename AGGREGATE constants.
- Extend getAggregateSql() in a subclass if you genuinely need custom aggregates, and validate upstream.
When it happens
Trigger: Subclass or test constructing a half-initialized ReportMetric (e.g. via reflection or by unserializing cached data) and passing it to applyMetrics(); a metric object whose $aggregateFunction property was changed after construction; calling getAggregateSql() directly with an arbitrary string like 'median' while extending the builder.
Common situations: Cached/serialized metrics restored from an old cache entry created before a constant rename; custom ReportQueryBuilder subclass that injects its own function names; version skew between a cached dashboard definition and updated AGGREGATE constants.
Related errors
- Invalid filter attribute type
- Invalid filter operation:
- Invalid order rule type
- The aggregate function cannot be empty.
- The aggregate function is not supported:
AI-assisted analysis of octobercms/october@b608633a7e (2026-08-21).
Data as JSON: /api/errors/22f770ccaec25516.
Report an issue: GitHub.