Skip to content

STDDEV / VARIANCE over a transform silently compute the statistic on the raw field (or emit an empty extended_stats) #222

Description

@fupelaqu

STDDEV / VARIANCE over anything other than a bare column silently ignore the transform. The aggregation is computed over the raw field, so the query returns a plausible number that answers a different question — or, when there is no raw field to fall back on, emits an extended_stats with neither field nor script.

Measured

SELECT id, STDDEV(YEAR(createdAt)) AS s FROM t GROUP BY id

"s": { "extended_stats": { "field": "createdAt" } }

No script. This is the standard deviation of the raw timestamps, not of the extracted years.

SELECT id, STDDEV(ABS(salary)) AS s FROM t GROUP BY id

"s": { "extended_stats": {} }

Neither field nor script — Elasticsearch rejects it.

SQL Emitted Verdict
STDDEV(salary) extended_stats.field: salary ✅ correct
STDDEV(YEAR(createdAt)) extended_stats.field: createdAt ❌ wrong statistic, silent
STDDEV(DATE_TRUNC(createdAt, MINUTE)) extended_stats.field: createdAt ❌ wrong statistic, silent
STDDEV(ABS(salary)) extended_stats: {} ❌ ES rejects
VARIANCE(ABS(salary)) extended_stats: {} ❌ ES rejects

The whole extended_stats family is affected — STDDEV, STDDEV_POP, STDDEV_SAMP, VARIANCE, VAR_POP, VAR_SAMP — since they share one emission path.

The silent variant is the dangerous one: nothing errors, and the number is the right shape, so it survives review.

Cause

ElasticAggregation.scala builds extendedStatsAgg(name, sourceField).script(s) but the script never reaches the emitted JSON. Compare MAX, which emits both field and script for the same input:

"lastSeen": { "max": { "field": "createdAt", "script": { "source": "… .get(ChronoField.YEAR)" } } }

Pre-existing

Verified byte-identical before and after PR #221 (which touches this area) by re-running the same probe against the pre-fix sources — this is not a regression from that change. Reproduces on main for both the YEAR and DATE_TRUNC forms.

Found while reviewing #220 / PR #221: the parenthesis fix newly routes STDDEV(YEAR(x)) through this path, so the shape is now reachable where it previously failed to parse.

Suggested check

A regression test must assert the emitted JSON contains the script, not merely that the query executes — an extended_stats over the wrong field succeeds against a real cluster.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions