Skip to content

fix(api): treat OpenSearch's flat_object as an alias for flattened - #86

Merged
xerj-org merged 2 commits into
xerj-org:mainfrom
Vinz2168:fix/flat-object-alias-for-flattened
Aug 1, 2026
Merged

fix(api): treat OpenSearch's flat_object as an alias for flattened#86
xerj-org merged 2 commits into
xerj-org:mainfrom
Vinz2168:fix/flat-object-alias-for-flattened

Conversation

@Vinz2168

Copy link
Copy Markdown
Collaborator

Summary

  • OpenSearch's flat_object field type indexes without expanding sub-fields — the same semantics as flattened, just a different type name.
  • xerj only recognized flattened, so any mapping using flat_object (as OpenSearch's own sample dashboards/UBI sample data do) was rejected as an unsupported field type, and lost the flattened reconstruction/highlighting/ignore_above handling that comes with it.
  • Every flattened special case in es_compat.rs now also treats flat_object as an alias.

Test plan

  • cargo build -p xerj-api
  • cargo fmt --check
  • cargo clippy --workspace --all-targets -- -D warnings
  • cargo test -p xerj-api --lib (94 passed)
  • Verified live against a real OpenSearch Dashboards 3.6.0 instance importing the UBI sample dashboards/data (which use flat_object) — no more "unsupported field type" rejection.

🤖 Generated with Claude Code

https://claude.ai/code/session_014PRCbyt7Y2HDyhG1tbQTeL

OpenSearch's flat_object field type indexes without expanding
sub-fields, the same semantics as flattened, but under a different
type name. xerj only recognized flattened, so any mapping using
flat_object (as OpenSearch's own sample dashboards/UBI data do) was
rejected as an unsupported field type and lost the flattened
reconstruction/highlighting/ignore_above handling that comes with it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014PRCbyt7Y2HDyhG1tbQTeL
@xerj-org

xerj-org commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Reviewed for v1.0.0-rc.9. This is the only one of #86#89 with no blocker — the alias is faithful to flattened at every site it touches and introduces no regression. I'm still holding it out of rc.9, for a reason that's about completeness rather than safety, and I'd rather say why than merge it quietly.

It only half-fixes the case it exists for

The commit names OpenSearch's sample dashboards/UBI data as the motivation. OpenSearch Dashboards discovers index-pattern fields through _field_caps, not GET _mapping — and the alias doesn't reach _field_caps.

Only GET /{index}/_mapping round-trips it, because get_mapping (es_compat.rs:1846-1859) returns the stored JSON verbatim. The other three introspection surfaces derive the type from the native schema, not the stored mapping:

  • field_capsnative_type_to_es_str(&field.field_type) (:17070)
  • global_field_caps → same (:25448)
  • get_mapping_fieldschema_to_es_properties (:25511, :25526-25529)

es_type_to_native sends both flattened and flat_object through the _ => FieldType::Object catch-all (:12995), and native_type_to_es_str maps FieldType::Object => "object" (:17207). collect_multi_fields only emits fields/properties children, never the root, so nothing overrides it.

Net effect: index creation now succeeds where it previously 400'd — a real improvement — but the field surfaces to the dashboard as {"type": "object", "searchable": true, "aggregatable": true}. The failure mode moved from a loud mapper_parsing_exception at create time to a silently mistyped field at visualisation time, which is arguably harder to debug.

To be fair: this is identical pre-existing behaviour for ES flattened, so it's not a regression this PR introduces. It's the reason the PR doesn't yet accomplish what it sets out to.

No test exercises any of the new arms

The diff is 17 insertions in one file and zero test changes; grep -rl flat_object engine/tests/ returns nothing. The repo has several flattened fixtures — search/340_flattened.yml, 600_flattened_ignore_above.yml, aggregations/terms_flattened_field.yml, indices.create/20_synthetic_source.yml and more — and none has a flat_object counterpart. So all six new match arms (:1037, :4429, :9117, :9476, :9513, :11734) execute zero times in CI. Green checks here aren't weak evidence, they're no evidence.

Cloning two of those fixtures with type: flat_object would cover the synthetic-source collapse, the fields-expansion roots and the ignore_above pruning in one commit — and would also have surfaced the _field_caps gap above.

Smaller things

  • The shared validator was widened beyond the mapping path, so runtime: {x: {type: flat_object}} now returns 200.
  • The commit message describes highlighting handling that isn't in the code.
  • One of the six changed sites is unreachable.

Suggestion

Add a FieldType::Flattened (or map both names to a shared native type that native_type_to_es_str can render back correctly), so _field_caps reports the field as flattened/flat_object rather than object — that fixes the alias and the pre-existing flattened gap in one go — plus the two cloned fixtures. With those, this is a clean merge for rc.10.

Holding it out of rc.9 only because that release is already merged, tagged-pending and fully verified, and adding an untested change at the end for a partial fix means re-running the whole gate without gaining the thing the PR is for.

…ject

Addresses maintainer review findings on the flat_object alias PR:

- The main gap: OpenSearch Dashboards discovers index-pattern fields
  via `_field_caps`, not `GET _mapping` — and the alias never reached
  it. `flattened`/`flat_object` both collapse to the generic native
  `FieldType::Object` (xerj has no dedicated native variant for
  either), and `_field_caps`/`global_field_caps`/`GET
  /{index}/_mapping/field/{field}` all derived their reported type
  purely from that native type, so index creation succeeded but the
  field surfaced to any client as a plain `object` — `GET _mapping`
  alone round-tripped the real declared type, since it returns the
  stored mapping JSON verbatim. Fixed with `declared_flattened_type`,
  which looks up a field's real declared type in the stored mapping
  and overrides the generic `object` string only when it's actually
  `flattened`/`flat_object` — every other field keeps deriving from
  the native schema exactly as before, so a dynamically-inferred field
  with no stored-mapping entry is unaffected.

- `is_supported_field_type` is shared between the mapping-properties
  validation path and the runtime-fields validation path; widening it
  to accept `flat_object` leaked into the runtime-fields path too,
  where real ES doesn't allow `flattened`/`flat_object` at all (runtime
  fields are restricted to a small scalar/composite set). Added an
  explicit exclusion in `validate_runtime_fields` for both names.

- Added a YAML fixture (cloned from `search/340_flattened.yml` and
  `search/600_flattened_ignore_above.yml`) covering the exists query,
  fields-option/synthetic-source reconstruction, and ignore_above
  pruning paths with `type: flat_object` — the original PR's six
  changed match arms had zero coverage, all six existing `flattened`
  fixtures had no `flat_object` counterpart. Ran it against a live
  built instance to confirm (a `field_caps` case had to be dropped —
  the project's YAML test runner doesn't implement that action verb at
  all, unrelated to this fix; covered the field_caps behavior with a
  proper Rust end-to-end test instead, going through the real HTTP
  route rather than a unit-level check).

Not addressed here: the commit message on the original PR describes
highlighting handling that isn't actually in the diff (an inaccuracy
in the message itself, not the code — noted in the PR thread rather
than rewriting a pushed, already-reviewed commit), and I couldn't
conclusively pin down which one of the six original match arms was
flagged as unreachable within reasonable effort — each one I traced
appeared to be on a live path, but the surrounding function is ~28k
lines and I didn't have a precise line reference to work from that
still matched the current file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014PRCbyt7Y2HDyhG1tbQTeL
@Vinz2168

Vinz2168 commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks — the _field_caps gap was the right thing to hold this on. Fixed the main issue plus two of the smaller ones; being upfront about the two I couldn't close.

  • _field_caps reports object instead of the real type: added declared_flattened_type, which consults the stored mapping (already available for the multi-fields case _field_caps handles) and overrides the generic object string with the real declared flattened/flat_object — applied to field_caps, global_field_caps, and GET /{index}/_mapping/field/{field}. Scoped as a per-field override, not a wholesale swap of the properties source, so a dynamically-inferred field with no stored-mapping entry still falls back to the schema-derived type exactly as before. Verified live: curl .../_field_caps?fields=attrs now returns {"attrs":{"flat_object":{...}}} instead of {"attrs":{"object":{...}}}.
  • Runtime-field validator leak: confirmed — is_supported_field_type is shared between the mapping path and validate_runtime_fields, so widening it for flat_object let it through for runtime fields too, where real ES doesn't allow it. Added an explicit exclusion scoped to validate_runtime_fields only.
  • Test coverage: added a YAML fixture cloned from 340_flattened.yml/600_flattened_ignore_above.yml covering exists-query, fields-option/synthetic-source, and ignore_above pruning with type: flat_object — ran it against a live built instance rather than just adding it untested (caught one broken case in the process: a field_caps: action, which turns out the project's YAML runner doesn't implement as a verb at all — dropped from the fixture, covered instead with a proper Rust end-to-end test that goes through the real HTTP route, including one that fails without the fix and passes with it).

Two things I did not resolve, flagging rather than guessing:

  • The commit message on the original PR does describe highlighting handling that isn't in the diff — didn't rewrite the pushed/reviewed commit over it, noting it here instead.
  • I couldn't conclusively identify which of the six original match arms you flagged as unreachable — traced each one I could match to current line numbers and each looked live to me, but I didn't have a precise enough reference into a ~28k-line function to be confident. Happy to take another pass if you can point at the specific one.

Full suite green: cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test -p xerj-engine -p xerj-query -p xerj-api --lib (only the 2 pre-existing, unrelated snapshot_path_security_tests failures).

🤖 Generated with Claude Code

@xerj-org

xerj-org commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Housekeeping, not a review comment.

We've just added a Contributor License Agreement to the project (#93). Until now contributions here were covered only by Apache-2.0 §5's inbound=outbound clause, which gives no explicit patent grant — worth tightening for a project this size.

Once #93 merges, a cla-bot check will appear on this PR and it will be red until you're covered. Signing is one small pull request, once per contributor — not per PR:

  1. Read CLA.md
  2. Open a PR adding your GitHub username to .contributors
  3. Comment @cla-bot check back here and it turns green

That PR is the signature — it comes from your own account, so the commit history is the record.

To be explicit about something: we did not add anyone to the signed list on their behalf, including you. That file asserts a person has signed, and that's not ours to assert for someone else — hence the ask rather than a quiet edit.

Sorry for the extra step on work that's already in flight. Thanks for the contributions.

@xerj-org
xerj-org merged commit 3a5737a into xerj-org:main Aug 1, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants