8000 fix(api): refuse a reverse_nested path that is not a parent of the scope by amartya1523 · Pull Request #3009 · xerj-org/xerj · GitHub
[go: up one dir, main page]

Skip to content

fix(api): refuse a reverse_nested path that is not a parent of the scope - #3009

Open
amartya1523 wants to merge 2 commits into
xerj-org:mainfrom
amartya1523:fix/reverse-nested-scope
Open

amartya1523 wants to merge 2 commits into
xerj-org:mainfrom
amartya1523:fix/reverse-nested-scope

Conversation

@amartya1523
Copy link
Copy Markdown
Contributor

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

What this changes, and why

A reverse_nested aggregation joins back from the nested documents it sits in to an enclosing level: the root document (no path), or a nested object that contains the current one. In this mapping, courses is nested and courses.sessions is nested inside it:

nested { path: courses }                 <- scope: courses
  reverse_nested { path: courses.sessions }   <- below the scope, not a parent

ES refuses this (ReverseNestedAggregationBuilder, capability reject_invalid_reverse_nesting, 8.16+) with:

Reverse nested path [courses.sessions] is not a parent of the current nested scope [courses]

XERJ ignored path (run_reverse_nested in aggs.rs joins back to the root unconditionally) and answered 200 with a root-document count, which looks like a valid join. One catch-lenient.txt line is this case: aggregations/nested.yml · Illegal reverse nested aggregation to a child nested object.

Fix (es_compat.rs):

  • nested_field_paths collects every field mapped type: nested in the target indices, by full dotted path (courses, courses.sessions). It takes the union over all target indices.

  • find_invalid_reverse_nesting walks the aggregation tree carrying the current nested scope. A nested aggregation sets the scope to its path; a reverse_nested resets it to its own path, or to the root when it has none; every other aggregation passes the scope down unchanged.

    A reverse_nested with a path is refused when all of these hold:

    • there is a current nested scope;
    • the path is a mapped nested field;
    • the path is neither the scope nor one of its parents (the scope isn't path or path.…).

    An unmapped path keeps today's answer; in ES it's an unmapped aggregation, not an error.

  • It runs in search_impl right after find_unsupported_metric_agg_field, with the existing search_shard_validation_error envelope: 400 search_phase_execution_exception, root cause illegal_argument_exception.

No engine change: valid reverse_nested requests run exactly as before.

Evidence

New test module reverse_nesting_tests (es_compat.rs), run against main first:

# main 34bf35b05
test es_compat::reverse_nesting_tests::reverse_nesting_to_a_parent_still_answers ... ok
test es_compat::reverse_nesting_tests::reverse_nesting_below_or_beside_the_scope_is_a_400 ... FAILED
  {"parent_nested":{"nested":{"path":"courses"},"aggs":{"invalid_reverse_nested":{"reverse_nested":{"path":"courses.sessions"}}}}} -> {"took":1, ... 200

# this branch
test es_compat::reverse_nesting_tests::reverse_nesting_below_or_beside_the_scope_is_a_400 ... ok
test es_compat::reverse_nesting_tests::reverse_nesting_to_a_parent_still_answers ... ok
  • Refused (asserts the 400, the outer type, the root-cause type and ES's exact reason):
    • the YAML request (child path)
    • a sibling top-level nested path (nested courses.sessions → reverse_nested staff_list)
  • Still answers:
    • nested courses.sessions → reverse_nested courses (a parent)
    • reverse_nested {} from courses.sessions and from courses (the root)
    • an unmapped path
    • nested courses → filter → nested courses.sessions → reverse_nested {}, the shape of the passing YAML case "Nested then filter then nested then reverse nested"

Checks

  • cargo fmt -p xerj-api -- --check
  • cargo test --profile quick -p xerj-api --lib: 432 passed, 0 failed
  • cargo clippy --profile quick -p xerj-api --lib --tests: no warnings in the changed code
  • YAML: the existing nested.yml case now enforces it; its catch-lenient line is deleted. The other nested.yml cases use reverse_nested: {} or no reverse join, so they're unaffected.
  • Docs: none describe the old behaviour.

Not run locally:

  • The ES-YAML suite against a live node, --release builds and the integration binaries. CI runs these.

Provenance

  • Written by: AI-assisted, run and reviewed by @amartya1523
  • Verified: the test outputs above, on macOS 26 (aarch64), quick profile, based on main 34bf35b05.
  • Assumed:
    • The reason is pinned by the YAML regex; the exception type is inferred from ES's IllegalArgumentException.
    • A reverse_nested path equal to the current scope isn't refused here. It's a no-op join, and I wasn't certain ES refuses it.
    • _msearch and the template paths don't go through search_impl, so they aren't covered.

reverse_nested joins back from the nested documents it sits in to the
root (no path) or to a nested object that contains the current one. ES
refuses a path naming a nested object below or beside the current
scope with "Reverse nested path [p] is not a parent of the current
nested scope [s]". XERJ ignored the path and answered 200 with a count
of root documents, which reads as a valid join.

search_impl now collects the nested field paths of the target indices
and walks the aggregation tree carrying the nested scope (set by nested,
reset by reverse_nested). Only a path that is a mapped nested field is
checked, so an unmapped path keeps today's answer, as in ES.

Retires `aggregations/nested.yml · Illegal reverse nested aggregation to
a child nested object`.
A child path and a sibling path are a 400 with ES's reason; joining back to a parent, to the root, an unmapped path and a reverse_nested below a filter still answer.
@cla-bot cla-bot Bot added the cla-signed label Nov 6, 2026

This branch has not been deployed

No deployments
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.

1 participant

0