8000 fix(api): reject misplaced pipeline aggregations and bad buckets_paths like ES by amartya1523 · Pull Request #1824 · xerj-org/xerj · GitHub
[go: up one dir, main page]

Skip to content

fix(api): reject misplaced pipeline aggregations and bad buckets_paths like ES - #1824

Merged
xerj-org merged 2 commits into
xerj-org:mainfrom
amartya1523:fix/pipeline-agg-validation
Oct 20, 2026
Merged

xerj-org merged 2 commits into
xerj-org:mainfrom
amartya1523:fix/pipeline-agg-validation

Conversation

@amartya1523
@amartya1523 amartya1523 commented Oct 19, 2026 •
Copy link
Copy Markdown
Contributor

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

Burns down 25 lines of the catch-lenient.txt ratchet from #1583 (151 → 126). All 25 are one class: pipeline aggregations that are misplaced or point at aggregations that do not exist.

What this changes, and why

Elasticsearch 8.13 refuses these requests with a 400. XERJ answered 200 with a null (or silently skipped) pipeline value, so a typo in a buckets_path, or a derivative under a terms, looked like "no data".

Root cause. Pipeline aggregations become sentinels during execution and are resolved afterwards:

  • a missing path target degrades to {"value": null} (resolve_pipeline_agg_with_gap, resolve_bucket_script);
  • a parent-only pipeline whose parent is not a bucket list is never applied (apply_bucket_pipeline_ops).

Nothing validated placement or paths, and the executor has no error channel for it.

Fix. find_invalid_pipeline_agg (es_compat.rs) is a request-time walk of the aggregation tree, placed next to find_invalid_agg_order_path (#1283). It is chained after that check at the same four call sites (_search via build_search_request, _msearch, _search/template, _msearch/template), so the answer does not depend on which executor would run. It applies ES 8.13's rules and messages:

Rule ES message
derivative / cumulative_sum / serial_diff / moving_fn / cumulative_cardinality / moving_percentiles at top level {type} aggregation [{name}] must have a histogram, date_histogram or auto_date_histogram as parent but doesn't have a parent
… under any other parent {type} aggregation [{name}] must have a histogram, date_histogram or auto_date_histogram as parent
bucket_script / bucket_selector / bucket_sort at top level {type} aggregation [{name}] must be declared inside of another aggregation
… under a single-bucket parent (filter, global, nested, …) Expected a multi bucket aggregation but got [InternalFilter] for aggregation [{name}]
bucket_sort with no sort, size or from [{name}] is configured to perform nothing. Please set either of [sort, size, from] to use bucket_sort
moving_fn window ≤ 0 [window] must be a positive, non-zero integer.
a buckets_path (or bucket_sort sort key) naming nothing Validation Failed: 1: No aggregation found for path [p]; / No aggregation [x] found for path [p];

Path checking is one-sided on purpose. A path is reported only when no reading of it can name an existing aggregation. These are left to the executor as before:

  • value keys (p.99, p[99.9], s.avg);
  • dotted aggregation names (v.v);
  • _count, _key and _bucket_count;
  • references to sibling pipelines;
  • anything past a metric aggregation.

A false positive would turn a working query into a 400, which is worse than the gap.

Evidence

Full ES-YAML suite against a node built from this commit. The runner flagged exactly 25 catch-lenient entries as stale (NOTE stale catch-lenient entry, delete it: …) and 0 failed. Those 25 lines are deleted. With the ratchet now enforcing them:

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

(151 before.) The 25 cases: bad path in avg_bucket / max_bucket / min_bucket / sum_bucket / percentiles_bucket / cumulative_sum / derivative / moving_fn; invalid parent aggregation in bucket_script / bucket_selector / bucket_sort / cumulative_sum / derivative / moving_fn / serial_diff; top level fails in bucket_script / bucket_selector / bucket_sort; invalid path in bucket_script / bucket_selector; invalid path fails and no action fails in bucket_sort; Not under date_histo in derivative / moving_fn; Bad window in moving_fn.

Unit tests (es_compat::pipeline_agg_validation_tests):

test es_compat::pipeline_agg_validation_tests::valid_pipelines_pass ... ok
test es_compat::pipeline_agg_validation_tests::misplaced_and_mispathed_pipelines_are_refused_with_es_messages ... ok

valid_pipelines_pass pins the shapes that must keep working:

  • a pipeline reading another pipeline;
  • p.99 and p[99.9] value keys, s.avg, and the dotted agg name v.v.value;
  • _count;
  • object buckets_paths;
  • bucket_sort with [{b: {order: desc}}, "_key"] + size, or from alone.

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 pipeline_agg_validation (2 passed)
  • ES-YAML conformance 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.
  • Conformance coverage: the 25 existing upstream YAML cases, now strict instead of lenient
  • Docs: none describe the lenient behaviour

Not run: the full cargo test -p xerj-api (release/fat-LTO test binaries do not fit this machine's free disk; CI runs them) and clippy.

Out of scope, left in the list:

  • bucket_script / bucket_selector · bad script: these need a Painless-style runtime NullPointerException.
  • pipeline.yml · Max pipeline on terms agg: the expected message names the ES result class (LongTerms), which depends on the field mapping.
  • The two pipeline.yml multi-value-metric cases: ES raises them at reduce time, so on an empty index ES answers 200; a static check would be stricter than ES.

Engine unit test bucket_script_missing_buckets_path_target_stays_an_explicit_null (aggs.rs) is untouched: this PR does not change xerj-engine, so the executor keeps its lenient behaviour, and the API now refuses such requests before they reach it.

Provenance

@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 could you rebase onto main? Once Build + Test is green on the new head, I'll squash-merge it.

…s like ES

Motivation:
`catch-lenient.txt` (xerj-org#1583) records ES-YAML cases where Elasticsearch
returns an error and XERJ does not. 25 of them are one class: pipeline
aggregations that are misplaced or point at aggregations that do not
exist. ES 8.13 refuses those requests with a 400; XERJ answered 200 with
a `null` (or a silently skipped) pipeline value, so a typo in a
`buckets_path`, or a `derivative` under a `terms`, looked like "no data".

Root cause:
Pipeline aggregations become sentinels during execution and are
resolved afterwards; a missing path target degrades to `{"value": null}`
(`resolve_pipeline_agg_with_gap`, `resolve_bucket_script`) and a
parent-only pipeline whose parent is not a bucket list is simply never
applied (`apply_bucket_pipeline_ops`). Nothing validated placement or
paths, and the executor has no error channel for it.

Fix:
`find_invalid_pipeline_agg` (es_compat.rs), a request-time walk of the
aggregation tree next to `find_invalid_agg_order_path` (xerj-org#1283) and
chained after it at the same four call sites (`_search` via
`build_search_request`, `_msearch`, `_search/template`,
`_msearch/template`). So the answer does not depend on which executor
would run. ES 8.13's rules and messages:
- derivative / cumulative_sum / serial_diff / moving_fn /
  cumulative_cardinality / moving_percentiles need a histogram,
  date_histogram or auto_date_histogram parent;
- bucket_script / bucket_selector / bucket_sort must be declared inside
  another aggregation, and that parent must be multi-bucket (single-bucket
  parents named by ES's internal class: InternalFilter, ...);
- bucket_sort must have a sort, size or from; moving_fn's window > 0;
- every buckets_path (and bucket_sort sort key) must name aggregations
  that exist from the level the pipeline is declared at:
  "No aggregation found for path [p]" / "No aggregation [x] found for
  path [p]", wrapped "Validation Failed: 1: ...;" as ES does.
Path checking reports a path only when no reading of it can name an
existing aggregation: value keys (`p.99`, `p[99.9]`, `s.avg`), dotted
aggregation names (`v.v`), `_count`/`_key`/`_bucket_count`, references to
sibling pipelines and anything past a metric aggregation are left to the
executor as before.

Evidence:
- Full ES-YAML suite against a node built from this commit: 0 failed,
  and the runner flagged exactly 25 catch-lenient entries as stale (now
  met); those 25 lines are deleted, and with the ratchet enforcing them
  the suite is `1402 passed · 0 failed · 10 skipped`, catch-lenient
  151 -> 126.
- Unit tests: valid shapes (pipelines on pipelines, value/bracket keys,
  dotted names, `_count`, bucket_sort with only `from`) are not refused;
  each rule produces ES's message.
@amartya1523
Copy link
Copy Markdown
Contributor Author

Rebased onto current main (after #1854 and #1870), as requested.

The conflicts were mechanical (both PRs added a request-time validator at the same insertion point and chained it at the same four call sites), so I re-applied this change on top of main rather than hand-merging hunks. find_invalid_pipeline_agg now sits after find_invalid_composite_agg and is chained after it at all four call sites (build_search_request, _msearch, _search/template, _msearch/template); the same 25 catch-lenient.txt lines are removed from the current file. The validator and its tests are 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
103 cases ran catch-lenient

(main alone has 128 catch-lenient cases; the 25 this PR removes are enforced strictly and pass.) The earlier cargo-audit failure on this PR was the zip advisory, which main has since fixed (#1869).

@amartya1523
amartya1523 force-pushed the fix/pipeline-agg-validation branch from 5dc67ee to 23a89f1 Compare October 20, 2026 06:01
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
xerj-org merged commit 9c92296 into xerj-org:main Oct 20, 2026
23 checks passed
@xerj-org
Copy link
Copy Markdown
Owner

Xerj Merger made the fix: reviewed the full diff: pipeline-aggregation placement and buckets_path / bucket_sort paths are now checked at request time with ES 8.13's messages, at all four search call sites. The path check is deliberately one-sided, so value keys and dotted names are left alone, and the 25 catch-lenient lines are gone. I merged main into the branch (7c7c4c9) to resolve the conflict with #1861: both validators are kept and chained. verified locally at 425dc3fdae (main 764e7a9 + all six batch-B PRs; fmt, clippy -D warnings, full workspace test compile, all lib unit tests and xerj-api integration tests) and at 9a466a4871 (main 5585819 + the same PRs; full cargo test --profile ci-test --workspace, 5676 passed, 0 failed) ahead of the CI backlog on owner's instruction.

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