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.
STDDEV/VARIANCEover 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 anextended_statswith neitherfieldnorscript.Measured
SELECT id, STDDEV(YEAR(createdAt)) AS s FROM t GROUP BY idNo 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 idNeither
fieldnorscript— Elasticsearch rejects it.STDDEV(salary)extended_stats.field: salarySTDDEV(YEAR(createdAt))extended_stats.field: createdAtSTDDEV(DATE_TRUNC(createdAt, MINUTE))extended_stats.field: createdAtSTDDEV(ABS(salary))extended_stats: {}VARIANCE(ABS(salary))extended_stats: {}The whole
extended_statsfamily 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.scalabuildsextendedStatsAgg(name, sourceField).script(s)but the script never reaches the emitted JSON. CompareMAX, which emits bothfieldandscriptfor the same input: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
mainfor both theYEARandDATE_TRUNCforms.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_statsover the wrong field succeeds against a real cluster.