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

  1. Re-create the metric properly through the constructor with an AGGREGATE_* constant instead of rehydrating/mutating one.
  2. Clear stale caches (dashboard/report caches, config cache) after upgrading or renaming aggregate constants.
  3. If extending the builder, override getAggregateSql() and add your template to the match (plus your own validation upstream) rather than passing unknown strings.
  4. 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

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


AI-assisted analysis of octobercms/october@b608633a7e (2026-08-21). Data as JSON: /api/errors/22f770ccaec25516. Report an issue: GitHub.