8000 fix(api): reject invalid aggregation parameters and placements like ES by amartya1523 · Pull Request #1861 · xerj-org/xerj · GitHub
[go: up one dir, main page]

Skip to content

fix(api): reject invalid aggregation parameters and placements like ES - #1861

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

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

Conversation

@amartya1523
Copy link
Copy Markdown
Contributor

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

Burns down 20 lines of the catch-lenient.txt ratchet from #1583 (151 → 131). These are aggregation requests Elasticsearch 8.13 refuses at parse time with a 400, where XERJ answered 200 and ignored the bad value, or returned an empty/defaulted result.

It is independent of #1824 (pipeline aggregations), #1854 (composite) and #1859 (search limits): different checks, same pattern. All four edit catch-lenient.txt, and #1824 / #1854 also chain at the same four call sites, so whichever merges later needs a trivial rebase.

What this changes, and why

find_invalid_agg_params (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:

Check Message
meta not an object Expected [START_OBJECT] under [meta], but got a [VALUE_NULL] in [the_filter]
cardinality precision_threshold < 0 [precisionThreshold] must be greater than or equal to 0. Found [-1] in [distinct_int]
cardinality unknown execution_hint Invalid execution mode for cardinality aggregation. Got [bogus]expected one of [...]
filters with no filters [filters] cannot be empty.
ip_prefix prefix_length outside [0, 32] / [0, 128] [prefix_length] must be in range [0, 32] while value is [44]
median_absolute_deviation compression ≤ 0 [compression] must be greater than 0. Found [0.0] in [mad]
unknown tdigest / MAD execution_hint Invalid execution_hint [no_such_hint], valid values are [default, high_accuracy]
tdigest compression < 0 [compression] must be greater than or equal to 0. Found [-1.0]
percentile_ranks values missing / null / empty / non-numeric 400 ([values] must not be null…)
terms with neither field nor script Required one of fields [field, script], but none were specified.
random_sampler probability outside (0, 0.5] ∪ {1.0} [probability] must be between 0.0 and 0.5 or exactly 1.0, was [1.5] / … greater than 0.0 …
random_sampler with no sub-aggs / with a parent / with a sampler or time_series below it … must have sub-aggregations / … cannot have a parent aggregation / … does not support sampling [time_series] aggregation [ts]
time_series under global Time series aggregations cannot be used inside global aggregation.
composite / auto_date_histogram / variable_width_histogram / rare_terms in a request with time_series [by_time] aggregation is incompatible with time series execution mode

Deliberately not checked here: anything that needs the field's mapped type (avg on a keyword field, regex include/exclude on an ip field, percentiles on a string field). Those stay in the list for a mapping-aware follow-up.

Evidence

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

  • cardinality_metric: Invalid Precision test, invalid execution hint throws
  • filters_bucket: Bad params, null meta
  • ip_prefix: Invalid IPv4 prefix, Invalid IPv6 prefix
  • median_absolute_deviation_metric: bad arguments, with execution hint
  • percentile_ranks_hdr_metric: invalid params
  • percentile_ranks_tdigest_metric: invalid params, with execution hint
  • percentiles_tdigest_metric: Execution hint
  • random_sampler: Test random_sampler aggregation with poor settings
  • terms: No field or script
  • time_series: Composite …, Global …, Sampler …, auto_date_histogram …, rare_terms …, variable_width_histogram …

With them deleted and enforced:

$ es-yaml-runner --dir tests/es-compat-yaml/yaml
1402 passed · 0 failed · 10 skipped · 1412 total
131 cases ran catch-lenient (an expected error XERJ does not raise yet; see catch-lenient.txt)

Unit tests (es_compat::agg_param_validation_tests):

test es_compat::agg_param_validation_tests::valid_aggregations_pass ... ok
test es_compat::agg_param_validation_tests::invalid_parameters_are_refused_with_es_messages ... ok

valid_aggregations_pass pins the shapes the suite relies on:

  • a cardinality hint (the five valid hints are listed in the validator);
  • high_accuracy / default;
  • random_sampler at 0.1 and 1.0;
  • time_series with date_histogram;
  • terms with only a script;
  • meta objects;
  • composite and rare_terms in a request without time_series.

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 agg_param_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.

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 exact wording of the percentile_ranks values messages. The YAML only checks for a 400 (bad_request), and I followed ES's [values] must not be null phrasing without checking it against a live ES.

@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
Copy link
Copy Markdown
Owner

Hi @amartya1523, #1854 (composite aggregation validation) and #1870 just landed on main, and this branch now conflicts with main in engine/crates/xerj-api/src/es_compat.rs (the search, msearch and search-template validation paths that #1854 also touched) and in catch-lenient.txt. The code conflicts are too big to resolve safely without you, so 8000 could you rebase onto main? Once Build + Test is green on the new head, I'll squash-merge it.

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.
@amartya1523
Copy link
Copy Markdown
Contributor Author

Rebased onto current main (after #1854 and #1870), as requested, and fixed the CI failure.

  • Format + Clippy failed on clippy::collapsible_match in the time_series arm of find_invalid_agg_params; it is now a match guard ("time_series" if ancestors.contains(&"global") => …), exactly what clippy suggested. That was the only error clippy reported for the crate.
  • The conflicts were mechanical (fix(api): validate composite aggregation definitions like ES #1854 added its validator at the same insertion point and the same four call sites), so I re-applied this change on top of main rather than hand-merging hunks: find_invalid_agg_params sits after find_invalid_composite_agg and is chained after it at all four call sites, and the same 20 catch-lenient.txt lines are removed from the current file. Validator logic and tests are otherwise unchanged.

Re-verified on the new head:

$ cargo fmt --all -- --check        # exit 0
$ es-yaml-runner --dir tests/es-compat-yaml/yaml
1404 passed · 0 failed · 10 skipped · 1414 total
108 cases ran catch-lenient

(main alone has 128; the 20 this PR removes are enforced strictly and pass.) Note: #1824 chains at the same call sites, so whichever of the two merges second will need the same small rebase; happy to do it.

@amartya1523
amartya1523 force-pushed the fix/agg-param-validation branch from 87b93c2 to 601f4b8 Compare October 20, 2026 06:30
@xerj-org
xerj-org merged commit 2ffdcc7 into xerj-org:main Oct 20, 2026
21 of 23 checks passed
@xerj-org
Copy link
Copy Markdown
Owner

Xerj Merger made the fix: reviewed the full diff. find_invalid_agg_params is a request-time walk that refuses the 20 aggregation shapes ES 8.13 rejects at parse time, using ES's messages. It is chained after the order-path and composite validators at all four call sites, and the matching 20 catch-lenient entries are gone. It doesn't try the mapping-dependent checks, by design. Thanks @amartya1523. Verified locally at 601f4b8, merged with main 5585819 (in a combined tree with #1910, #1886, #1700 and #1746): cargo fmt --all --check, cargo clippy --workspace --all-targets -D warnings and cargo test --profile ci-test --workspace are all green. That run was ahead of the CI backlog, on the owner's instruction.

xerj-org added a commit to amartya1523/xerj that referenced this pull request Oct 20, 2026
Resolve conflicts with xerj-org#1861 (find_invalid_agg_params): keep both request-time validators, chain find_invalid_pipeline_agg after find_invalid_agg_params at all four call sites, and drop this PR's 25 catch-lenient lines from main's list.
xerj-org pushed a commit that referenced this pull request Nov 4, 2026
…#2913)

* fix(api): reject avg/sum/min/max/stats on a non-numeric field like ES

Motivation:
`{"aggs": {"a": {"avg": {"field": "string_field"}}}}` on a keyword field
answered 200 with `"value": null` (`sum` answered `-0.0`), so a metric
pointed at the wrong field looked like an empty index. ES 8.13 fails
every shard with `Field [string_field] of type [keyword] is not
supported for aggregation [avg]` (ValuesSourceRegistry#getAggregator)
and answers 400. Four catch-lenient lines are this case
(`{avg,max,min,sum}_metric.yml · Aggregating wrong datatype test`).

Root cause:
`find_invalid_agg_params` (#1861) deliberately made only mapping-free
checks; nothing on the search path compared a metric's `field` with
the mapped type.

Fix:
`find_unsupported_metric_agg_field`, called in `search_impl` after the
field-alias rewrite, walks the aggregation tree (sub-aggregations
included) and resolves each `avg`/`sum`/`min`/`max`/`stats`/
`extended_stats` field through the participating indices' mappings,
following object `properties` and multi-field `fields` (`name.raw`).
It refuses only types ES never accepts for these aggregations (keyword,
constant_keyword, wildcard, ip, geo_point, version), and only when
every target index maps the field that way: an unmapped field, or one
that is numeric in at least one target, still answers 200 as ES does
(a partial-shard success there). The error uses the existing
`search_shard_validation_error` envelope (search_phase_execution_exception
wrapping illegal_argument_exception).

Evidence:
- ES-YAML suite against a node built from this change (with its two
  sibling scroll fixes): `1453 passed · 0 failed · 10 skipped`, no stale
  catch-lenient entries; the four lines above are deleted.

* test(api): pin the metric aggregation field-type check

Runs through the ES router: avg on a keyword, max on an ip, sum on a
keyword multi-field, stats through a field alias and min under a terms
bucket each answer 400 with ES's reason; avg on a numeric field, on an
unmapped field, across a keyword/long pair of indices, and value_count
on a keyword still answer 200.

On main the first test fails with the 200 it used to return:
  {"a":{"avg":{"field":"s"}}} -> {..."aggregations":{"a":{"value":null}}}
  left: 200  right: 400
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