8000 fix(api): validate composite aggregation definitions like ES by amartya1523 · Pull Request #1854 · xerj-org/xerj · GitHub
[go: up one dir, main page]

Skip to content

fix(api): validate composite aggregation definitions like ES - #1854

Merged
xerj-org merged 1 commit into
xerj-org:mainfrom
amartya1523:fix/composite-agg-validation
Oct 20, 2026
Merged

xerj-org merged 1 commit into
xerj-org:mainfrom
amartya1523:fix/composite-agg-validation

Conversation

@amartya1523
Copy link
Copy Markdown
Contributor

AI-assisted change, run and reviewed by @amartya1523.

Burns down 6 lines of the catch-lenient.txt ratchet from #1583 (151 → 145): every aggregations/composite.yml case except the two date-format ones. Independent of #1824: same pattern, different aggregation, and it touches the same call sites, so whichever merges second needs a trivial rebase.

What this changes, and why

Elasticsearch 8.13 refuses a malformed composite aggregation at parse time with a 400. XERJ answered 200 instead:

  • missing or empty sources → run_composite returned an empty bucket list;
  • two sources with the same name → the later silently overwrote the earlier;
  • missing_order without missing_bucket: true → ignored, though it can never apply;
  • size: 1000000000 → accepted (ES caps it at search.max_buckets);
  • composite under a terms → ran (ES allows only no parent, or nested / reverse_nested / filter chains).

Fix. find_invalid_composite_agg (es_compat.rs) is a request-time walk chained after find_invalid_agg_order_path (#1283) at its four call sites (_search, _msearch, _search/template, _msearch/template). It uses ES 8.13's messages:

Condition Message
sources absent Required [sources]
sources empty / not a list Composite [sources] cannot be null or empty
duplicate source names Composite source names must be unique, found duplicates: [keyword]
missing_order ≠ default without missing_bucket: true missingOrder can only be set if missingBucket is true
size > search.max_buckets (aggs::max_buckets(), the node-wide value) Trying to create too many buckets. Must be less than or equal to: [65536] but was [1000000000]. …
a parent other than nested / reverse_nested / filter [composite] aggregation cannot be used with a parent aggregation of type: [TermsAggregatorFactory]

Evidence

Full ES-YAML suite against a node built from this commit. The runner reported exactly these 6 entries as stale, with 0 failed:

NOTE stale catch-lenient entry, delete it: aggregations/composite.yml · Composite aggregation and array size
NOTE stale catch-lenient entry, delete it: aggregations/composite.yml · Duplicate sources
NOTE stale catch-lenient entry, delete it: aggregations/composite.yml · Invalid Composite aggregation
NOTE stale catch-lenient entry, delete it: aggregations/composite.yml · Missing source
NOTE stale catch-lenient entry, delete it: aggregations/composite.yml · missing_order with missing_bucket = false
NOTE stale catch-lenient entry, delete it: aggregations/composite.yml · missing_order without missing_bucket

With those deleted and enforced:

1402 passed · 0 failed · 10 skipped · 1412 total
145 cases ran catch-lenient

The passing composite.yml cases that nest composite under nested, under a nested > filter > nested chain, and under filter, as well as the missing_bucket: true + missing_order case, all still pass.

Unit tests (es_compat::composite_agg_validation_tests):

test es_compat::composite_agg_validation_tests::valid_composites_pass ... ok
test es_compat::composite_agg_validation_tests::invalid_composites_are_refused_with_es_messages ... ok

Checks

  • cargo fmt --all -- --check (exit 0)
  • Scoped release build: cargo build --release -p xerj-server -p es-yaml-runner
  • cargo test --release -p xerj-api --lib composite_agg_validation (2 passed)
  • ES-YAML suite at 0 failed (above). The node ran with [limits] disk_flood_stage_percent = 0 because this machine's disk is above the 95% flood stage.
  • Docs: none describe the lenient behaviour

Not run: the full cargo test -p xerj-api (disk) and clippy.

Left in the list: Composite aggregation with invalid format / with lossy format. ES raises those while rendering bucket keys (a format whose output cannot be parsed back), so they need bucket data, not a request-time check.

Provenance

  • Written by: AI-assisted, run by @amartya1523, who has reviewed it.
  • Verified (commands run, output observed): everything under Evidence and Checks, on macOS aarch64, rustc 1.99.1.
  • Assumed (not tested): the [XxxAggregatorFactory] name for parents other than terms (only terms → TermsAggregatorFactory is exercised by the YAML). The name is derived by CamelCasing the aggregation type. ES's real class could differ for some types (e.g. date_histogram), which would make the message text inexact but still a 400.

Motivation:
Six `aggregations/composite.yml` cases sit in `catch-lenient.txt`
(xerj-org#1583): Elasticsearch refuses the request with a 400, XERJ answered 200.
`run_composite` returned an empty bucket list for missing or empty
`sources`, let a later source silently overwrite an earlier one of the
same name, ignored a `missing_order` that can never apply, accepted a
page `size` far above `search.max_buckets`, and ran under any parent.

Root cause:
Composite definitions were never validated; `run_composite` and the
columnar `exec_composite` treat every malformed shape as "nothing to
return" or pick one interpretation.

Fix:
`find_invalid_composite_agg` (es_compat.rs), a request-time walk chained
after `find_invalid_agg_order_path` (xerj-org#1283) at its four call sites, with
ES 8.13's messages:
- `sources` absent -> `Required [sources]`; empty -> `Composite [sources]
  cannot be null or empty`;
- duplicate source names -> `Composite source names must be unique,
  found duplicates: [..]`;
- `missing_order` other than `default` without `missing_bucket: true` ->
  `missingOrder can only be set if missingBucket is true`;
- `size` above `search.max_buckets` (`aggs::max_buckets()`, the
  node-wide value) -> ES's too-many-buckets message;
- a parent other than nested / reverse_nested / filter (through any chain
  of them) -> `[composite] aggregation cannot be used with a parent
  aggregation of type: [TermsAggregatorFactory]`.

Evidence:
- Full ES-YAML suite against a node built from this commit: 0 failed,
  and exactly these 6 catch-lenient entries reported stale; with them
  deleted and enforced: `1402 passed · 0 failed · 10 skipped`,
  catch-lenient 151 -> 145.
- Unit tests pin the valid shapes composite.yml relies on (nested /
  filter / reverse_nested parents, `missing_bucket: true` with an order,
  sub-aggregations) and each refusal message.
@cla-bot
cla-bot Bot commented Oct 19, 2026
Copy link
Copy Markdown

Thanks for this contribution — we'd like to merge it.

Before we can, we need a signed Contributor License Agreement from: @amartya1523

It is a one-time thing per contributor, not per pull request. Read CLA.md, then open a small pull request adding your GitHub username to .contributors — that pull request is your signature, since it is made from your own account.

Once it is merged, comment @cla-bot check here and this check will go green. If you believe you are already covered, comment @cla-bot check and it will re-verify.

@xerj-org
Copy link
Copy Markdown
Owner

@cla-bot check

@cla-bot cla-bot Bot added the cla-signed label Oct 20, 2026
@cla-bot
cla-bot Bot commented Oct 20, 2026
Copy link
Copy Markdown

Re-checking the CLA status for every commit author on this pull request…

@xerj-org
xerj-org merged commit b9966fc into xerj-org:main Oct 20, 2026
22 of 23 checks passed
@xerj-org
Copy link
Copy Markdown
Owner

Xerj Merger made the fix: composite aggregation definitions are now validated the way Elasticsearch does it. Missing or duplicate sources, an invalid composite body, a bad array size, and missing_order without missing_bucket are refused with ES-shaped 400s on search, msearch, search templates and msearch templates. That removes 6 lines from catch-lenient.txt. Squash-merged on head b7e0642 after Build + Test passed. Thanks @amartya1523!

amartya1523 added a commit to amartya1523/xerj that referenced this pull request Oct 20, 2026
Motivation:
20 `catch-lenient.txt` lines (xerj-org#1583) are aggregation requests that
Elasticsearch 8.13 refuses with a 400 at parse time and XERJ answered
200, ignoring the bad value or returning an empty or defaulted result:
a negative cardinality `precision_threshold` or unknown
`execution_hint`, `filters` with no filters, a non-object `meta`, an
`ip_prefix` `prefix_length` out of range, a non-positive
`median_absolute_deviation` `compression`, unknown tdigest execution
hints, `percentile_ranks` without usable `values`, a `terms` with
neither `field` nor `script`, `random_sampler` misuse, and `time_series`
under `global` or next to an aggregation incompatible with time-series
execution.

Root cause:
None of these parameters or placements were validated; the executors
treat each malformed shape as "use a default" or "nothing to return".

Fix:
`find_invalid_agg_params` (es_compat.rs), a request-time walk chained
after `find_invalid_agg_order_path` (xerj-org#1283) at its four call sites
(`_search`, `_msearch`, `_search/template`, `_msearch/template`), with
ES 8.13's messages. Only checks that need no mapping are made: e.g.
`avg` on a keyword field (which needs the field type) is left alone.

Evidence:
- Full ES-YAML suite against a node built from this commit: 0 failed,
  and exactly 20 catch-lenient entries reported stale; with them deleted
  and enforced: `1404 passed · 0 failed · 10 skipped` on current main
  (after xerj-org#1854 and xerj-org#1870), catch-lenient 128 -> 108.
- Unit tests pin the valid shapes the suite relies on (the five
  cardinality hints, `default`/`high_accuracy`, random_sampler 0.1 and
  1.0, time_series with date_histogram, terms with only a script, an
  incompatible type in a request without time_series) and each message.
xerj-org pushed a commit that referenced this pull request Oct 20, 2026
#1861)

Motivation:
20 `catch-lenient.txt` lines (#1583) are aggregation requests that
Elasticsearch 8.13 refuses with a 400 at parse time and XERJ answered
200, ignoring the bad value or returning an empty or defaulted result:
a negative cardinality `precision_threshold` or unknown
`execution_hint`, `filters` with no filters, a non-object `meta`, an
`ip_prefix` `prefix_length` out of range, a non-positive
`median_absolute_deviation` `compression`, unknown tdigest execution
hints, `percentile_ranks` without usable `values`, a `terms` with
neither `field` nor `script`, `random_sampler` misuse, and `time_series`
under `global` or next to an aggregation incompatible with time-series
execution.

Root cause:
None of these parameters or placements were validated; the executors
treat each malformed shape as "use a default" or "nothing to return".

Fix:
`find_invalid_agg_params` (es_compat.rs), a request-time walk chained
after `find_invalid_agg_order_path` (#1283) at its four call sites
(`_search`, `_msearch`, `_search/template`, `_msearch/template`), with
ES 8.13's messages. Only checks that need no mapping are made: e.g.
`avg` on a keyword field (which needs the field type) is left alone.

Evidence:
- Full ES-YAML suite against a node built from this commit: 0 failed,
  and exactly 20 catch-lenient entries reported stale; with them deleted
  and enforced: `1404 passed · 0 failed · 10 skipped` on current main
  (after #1854 and #1870), catch-lenient 128 -> 108.
- Unit tests pin the valid shapes the suite relies on (the five
  cardinality hints, `default`/`high_accuracy`, random_sampler 0.1 and
  1.0, time_series with date_histogram, terms with only a script, an
  incompatible type in a request without time_series) and each message.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

0