Skip to content

HAVING / ORDER BY on an aggregate over a transform emit a malformed aggregation (double transform + doc[] in bucket_selector; empty agg name) #223

Description

@fupelaqu

When HAVING or ORDER BY names an aggregate whose argument is a transform — MAX(YEAR(x)), MAX(DATE_TRUNC(x, MINUTE)), MAX(ABS(x)) — the emitted aggregation is malformed. Two distinct symptoms, one root: an aggregate over a transform has no plain field to key a bucket path on.

Symptom 1 — HAVING: the transform is applied twice, inside a script that cannot run there

SELECT id, COUNT(x) AS c FROM t GROUP BY id HAVING MAX(YEAR(createdAt)) > 2020

"having_filter": {
  "bucket_selector": {
    "buckets_path": { "createdAt": "createdAt" },
    "script": { "source": "def left = (doc['createdAt'].size() == 0 ? null : doc['createdAt'].value.toInstant().atZone(ZoneId.of('Z')).get(ChronoField.YEAR)).get(ChronoField.YEAR); left == null ? false : (left > 2020)" }
  }
}

Two problems in one script:

  • .get(ChronoField.YEAR) appears twice — once inside the parenthesised doc[…] expression and once applied to its result.
  • it reads doc[…], which a bucket_selector script cannot do; that context exposes params only, populated from buckets_path. The declared buckets_path entry (createdAt) is never referenced by the script.

The correct form is the one the bare-column case already emits: "script": {"source": "params.salary > 2020"}.

Identical doubling with DATE_TRUNC (.truncatedTo(ChronoUnit.MINUTES) twice), so it is not specific to the extractors.

Symptom 2 — ORDER BY: the aggregation is named ""

SELECT id, COUNT(x) AS c FROM t GROUP BY id ORDER BY MAX(ABS(salary)) DESC

"terms": { "field": "id", "size": 65536, "min_doc_count": 1, "order": { "": "desc" } },
"aggs": { "c": { }, "": { "max": { "script": { … Math.abs … } } } }

The sub-aggregation key and the order key are both the empty string. The name derives from the identifier's path, which is empty when the aggregate is script-backed rather than field-backed.

Controls

SQL Result
HAVING MAX(salary) > 2020 "script": "params.salary > 2020"
ORDER BY MAX(salary) DESC "order": {"salary": "desc"}, agg named salary
HAVING MAX(YEAR(createdAt)) > 2020 ❌ double transform + doc[…]
HAVING MAX(DATE_TRUNC(createdAt, MINUTE)) > 2020 ❌ same
ORDER BY MAX(ABS(salary)) DESC ❌ agg named ""

Pre-existing

Verified byte-identical before and after PR #221 by re-running the same probe against the pre-fix sources — not a regression from that change. Same family as #54 (buckets_path keying for un-aliased aggregates).

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

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