Skip to content

ESQL: Fix incorrectly optimized fork with nullify unmapped_fields#143030

Merged
elasticsearchmachine merged 17 commits intoelastic:mainfrom
kanoshiou:fork-nullify-optimization
Mar 12, 2026
Merged

ESQL: Fix incorrectly optimized fork with nullify unmapped_fields#143030
elasticsearchmachine merged 17 commits intoelastic:mainfrom
kanoshiou:fork-nullify-optimization

Conversation

@kanoshiou
Copy link
Copy Markdown
Contributor

@kanoshiou kanoshiou commented Feb 25, 2026

This PR fixes a bug where Fork.withSubPlans() incorrectly reassigned new NameIds to its output attributes, breaking references in the upper plan. This issue specifically manifests when using FORK alongside the SET unmapped_fields="nullify" mode.

By design, a FORK assigns new NameIds to its output attributes via refreshOutput() to decouple them from the internal branches. This isolation is necessary to prevent unintended side effects during plan optimizations, such as aggressive constant folding leaking across branches.

However, the previous implementation unconditionally re-minted these NameIds every time withSubPlans() was called. Because of this, any node sitting above the FORK (like EVAL or STATS) that already held a reference to the initial NameIds would suddenly point to a nonexistent ID. Downstream analysis rules would then fail to resolve these orphaned references, causing the plan execution to fail with an "optimized incorrectly due to missing references" error.

Fixes #142762

@elasticsearchmachine elasticsearchmachine added needs:triage Requires assignment of a team area label v9.4.0 external-contributor Pull request authored by a developer outside the Elasticsearch team labels Feb 25, 2026
@alex-spies alex-spies added the :Analytics/ES|QL AKA ESQL label Feb 25, 2026
@elasticsearchmachine elasticsearchmachine added Team:Analytics Meta label for analytical engine team (ESQL/Aggs/Geo) and removed needs:triage Requires assignment of a team area label labels Feb 25, 2026
@elasticsearchmachine
Copy link
Copy Markdown
Collaborator

Pinging @elastic/es-analytical-engine (Team:Analytics)

# Conflicts:
#	x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java
@astefan astefan added the >bug label Feb 27, 2026
@astefan
Copy link
Copy Markdown
Contributor

astefan commented Feb 27, 2026

buildkite test this

@kanoshiou
Copy link
Copy Markdown
Contributor Author

@astefan, I can not reproduce the failure, and it seems not related to this PR.

@astefan astefan self-assigned this Mar 4, 2026
@astefan
Copy link
Copy Markdown
Contributor

astefan commented Mar 6, 2026

buildkite test this

@astefan astefan requested review from alex-spies and bpintea March 6, 2026 11:29
@alex-spies
Copy link
Copy Markdown
Contributor

alex-spies commented Mar 6, 2026

Heya, in #141340, I'm adding a test that is failing due to this in GenerativeForkIT (build scan).

I'll have to mute it in the other PR and it'd be super nice to unmute as part of this PR. In any case, will put the mute into muted-tests.yml and point to the corresponding issue this fixes, #142762.

Update: muting in c85e691

@astefan
Copy link
Copy Markdown
Contributor

astefan commented Mar 11, 2026

buildkite test this

@astefan
Copy link
Copy Markdown
Contributor

astefan commented Mar 11, 2026

@alex-spies I've unmuted that test. From my local tests, it didn't complain. We'll see what CI says

Copy link
Copy Markdown
Contributor

@alex-spies alex-spies left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot @kanoshiou and @astefan !

The fix for Bug 2 was independently discovered by @idegtiarenko while fixing #141870. Apologies that I didn't spot that we're fixing the same issue in both PRs.

The fix for Bug 1 (keeping name ids for existing fork attributes while refreshing) looks super good to me. Thanks a lot!

Comment on lines +97 to +107
Set<String> childOutputNames = new HashSet<>();
for (LogicalPlan child : plan.children()) {
for (Attribute attr : child.output()) {
childOutputNames.add(attr.name());
}
}
unresolved.removeIf(ua -> childOutputNames.contains(ua.name()));

if (unresolved.isEmpty()) {
return plan;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I agree that this is the right solution!

And I'm super sorry I didn't notice this earlier @astefan . Bug 2 from the PR description is indeed the same problem that @idegtiarenko fixed in #142300, and you two ended up implementing the very same solution.

The silver lining is that we very much agree on the solution. The added tests in this PR are great, too, as they highlight different queries where ImplicitCasting can introduce an intermediate step between ResolveRefs and ResolveUnmapped.

Comment on lines +671 to +673
required_capability: optional_fields_nullify_tech_preview
required_capability: fork_v9
required_capability: fix_fork_unmapped_nullify
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think we need fork_v9 to exclude this from CsvTests, but we should only require 1 of optional_fields_nullify_tech_preview and fix_fork_unmapped_nullify, no?

Also applies below.

Comment on lines +2269 to +2272
* Bug 2: After {@code ImplicitCasting} runs, the plan may remain unresolved because it requires a
* subsequent {@code ResolveRefs} pass to fully resolve. However, {@code ResolveUnmapped} runs before
* that second {@code ResolveRefs} pass and mistakenly treats those still-unresolved attributes as
* user-introduced unmapped fields, incorrectly nullifying valid references.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: unfortunately, this got stale. Bug 2 is already fixed.

FIX_FULL_TEXT_FUNCTIONS_ON_RENAMED_FIELDS,

/**
* Fixes two independent analysis bugs in {@code FORK} with {@code unmapped_fields="nullify"}.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

super nit: references to the to-be-closed issues (#142762
and #142543) are always nice in capabilities.

* matches one in {@code existingOutput}. Genuinely new attributes get fresh NameIds.
*/
public static List<Attribute> toReferenceAttributes(List<? extends NamedExpression> named) {
public static List<Attribute> toReferenceAttributes(List<? extends NamedExpression> named, List<Attribute> existingOutput) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: method name doesn't really say what this does, now. Maybe toReferenceAttributesPreservingIds?

@astefan astefan added auto-merge-without-approval Automatically merge pull request when CI checks pass (NB doesn't wait for reviews!) auto-backport Automatically create backport pull requests when merged labels Mar 12, 2026
# Conflicts:
#	x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/rules/ResolveUnmapped.java
#	x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerUnmappedTests.java
@kanoshiou
Copy link
Copy Markdown
Contributor Author

@astefan conflicts resolved

@astefan
Copy link
Copy Markdown
Contributor

astefan commented Mar 12, 2026

buildkite test this

@astefan
Copy link
Copy Markdown
Contributor

astefan commented Mar 12, 2026

buildkite test this

@astefan
Copy link
Copy Markdown
Contributor

astefan commented Mar 12, 2026

buildkite test this

Copy link
Copy Markdown
Contributor

@bpintea bpintea left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a note. But if the CI is happy, LGTM.
Also, would update the PR description:

Because NameId equality drives attribute identity across the whole plan tree

That's not quite correct -- see comment.


protected List<Attribute> refreshedOutput() {
return toReferenceAttributes(outputUnion(children()));
return toReferenceAttributesPreservingIds(outputUnion(children()), this.output());
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this no longer refreshes the IDs. Meaning that some assumptions are either broken, or they were incorrect to have, or they're no longer valid (in the meantime).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bpintea can you expand on this, please? What use cases would this impact/break/change? (are we missing tests?)

Comment on lines -108 to -109
// We don't want to keep the same attributes that are outputted by the FORK branches.
// Keeping the same attributes can have unintended side effects when applying optimizations like constant folding.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be removed. The attribute IDs from within the branches are not kept / reused atop Fork.

@kanoshiou
Copy link
Copy Markdown
Contributor Author

Thanks for the review, @bpintea! I've updated the PR description to reflect your feedback.

@elasticsearchmachine elasticsearchmachine merged commit 5fb7136 into elastic:main Mar 12, 2026
38 checks passed
@elasticsearchmachine
Copy link
Copy Markdown
Collaborator

💔 Backport failed

Status Branch Result
9.3 Commit could not be cherrypicked due to conflicts

You can use sqren/backport to manually backport by running backport --upstream elastic/elasticsearch --pr 143030

@kanoshiou
Copy link
Copy Markdown
Contributor Author

I can help with the backport.

@kanoshiou
Copy link
Copy Markdown
Contributor Author

To backport this PR, I think we need to wait for #144097 to be merged first.

szybia added a commit to szybia/elasticsearch that referenced this pull request Mar 12, 2026
…elocations

* upstream/main: (49 commits)
  CCS logging fixes (elastic#144070)
  Improve CPS cluster exclusion handling (elastic#143488)
  Remove snapshot condition now that node_reduce phase is in non-snapshot builds (elastic#144090)
  Drop deprecation warnings when updating a mapping in the cluster state applier (elastic#143884) (elastic#144040)
  Add ensureGreenAndNoInitializingShards helper (elastic#144044)
  Removed unnecessary applies_to blocks from deprecated query (elastic#144096)
  [CPS] Use single CrossProjectModeDecider instance (elastic#144030)
  Fix ESQL TS requests with LIMIT 0 (elastic#144031)
  ESQL: Remove `create` methods in aggs (elastic#144098)
  ES|QL: Refactor ChangeLimitOperator (elastic#144017)
  Add Paginated Hit Source Tests (elastic#142592)
  Fix test failure not preferred (elastic#144019)
  Remove serialization logic from EIS authorization response (elastic#144021)
  ESQL: CSV schema inference and parsing enhancements (elastic#144050)
  ESQL: Fix incorrectly optimized fork with nullify unmapped_fields (elastic#143030)
  Fix MMR release test using subqueries (elastic#144087)
  Refactoring `UserAgentPlugin` (elastic#140712)
  Drop non-finite samples in Prometheus remote write (elastic#144055)
  [TEST] Wait for internal inference indices to be created in authorization IT (elastic#143885)
  Disable ndjson datasource QA tests in release-tests (elastic#143992)
  ...
@alex-spies
Copy link
Copy Markdown
Contributor

To backport this PR, I think we need to wait for #144097 to be merged first.

Thanks @kanoshiou !

It's merged!

We may still see conflicts because #143399 was not backported. I don't think it should, though, as this one required a new transport version, increasing the cost of backports a bit.

@kanoshiou
Copy link
Copy Markdown
Contributor Author

💚 All backports created successfully

Status Branch Result
9.3

Questions ?

Please refer to the Backport tool documentation

@kanoshiou
Copy link
Copy Markdown
Contributor Author

💚 All backports created successfully

Status Branch Result
9.3

Questions ?

Please refer to the Backport tool documentation

kanoshiou added a commit to kanoshiou/elasticsearch that referenced this pull request Mar 13, 2026
…astic#143030)

This PR fixes a bug where `Fork.withSubPlans()` incorrectly reassigned
new `NameId`s to its output attributes, breaking references in the upper
plan. This issue specifically manifests when using `FORK` alongside the
`SET unmapped_fields="nullify"` mode.

By design, a `FORK` assigns new `NameId`s to its output attributes via
`refreshOutput()` to decouple them from the internal branches. This
isolation is necessary to prevent unintended side effects during plan
optimizations, such as aggressive constant folding leaking across
branches.

However, the previous implementation unconditionally re-minted these
`NameId`s every time `withSubPlans()` was called. Because of this, any
node sitting above the `FORK` (like `EVAL` or `STATS`) that already held
a reference to the initial `NameId`s would suddenly point to a
nonexistent ID. Downstream analysis rules would then fail to resolve
these orphaned references, causing the plan execution to fail with an
*"optimized incorrectly due to missing references"* error.

Fixes elastic#142762

(cherry picked from commit 5fb7136)

# Conflicts:
#	muted-tests.yml
#	x-pack/plugin/esql/qa/server/src/main/java/org/elasticsearch/xpack/esql/qa/rest/generative/GenerativeRestTest.java
#	x-pack/plugin/esql/qa/testFixtures/src/main/resources/unmapped-nullify.csv-spec
#	x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/action/EsqlCapabilities.java
#	x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/analysis/rules/ResolveUnmapped.java
#	x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/analysis/AnalyzerUnmappedTests.java
astefan pushed a commit to astefan/elasticsearch that referenced this pull request Mar 17, 2026
…astic#143030)

This PR fixes a bug where `Fork.withSubPlans()` incorrectly reassigned
new `NameId`s to its output attributes, breaking references in the upper
plan. This issue specifically manifests when using `FORK` alongside the
`SET unmapped_fields="nullify"` mode.

By design, a `FORK` assigns new `NameId`s to its output attributes via
`refreshOutput()` to decouple them from the internal branches. This
isolation is necessary to prevent unintended side effects during plan
optimizations, such as aggressive constant folding leaking across
branches.

However, the previous implementation unconditionally re-minted these
`NameId`s every time `withSubPlans()` was called. Because of this, any
node sitting above the `FORK` (like `EVAL` or `STATS`) that already held
a reference to the initial `NameId`s would suddenly point to a
nonexistent ID. Downstream analysis rules would then fail to resolve
these orphaned references, causing the plan execution to fail with an
*"optimized incorrectly due to missing references"* error.

Fixes elastic#142762

(cherry picked from commit 5fb7136)
elasticsearchmachine pushed a commit that referenced this pull request Mar 18, 2026
…43030) (#144386)

This PR fixes a bug where `Fork.withSubPlans()` incorrectly reassigned
new `NameId`s to its output attributes, breaking references in the upper
plan. This issue specifically manifests when using `FORK` alongside the
`SET unmapped_fields="nullify"` mode.

By design, a `FORK` assigns new `NameId`s to its output attributes via
`refreshOutput()` to decouple them from the internal branches. This
isolation is necessary to prevent unintended side effects during plan
optimizations, such as aggressive constant folding leaking across
branches.

However, the previous implementation unconditionally re-minted these
`NameId`s every time `withSubPlans()` was called. Because of this, any
node sitting above the `FORK` (like `EVAL` or `STATS`) that already held
a reference to the initial `NameId`s would suddenly point to a
nonexistent ID. Downstream analysis rules would then fail to resolve
these orphaned references, causing the plan execution to fail with an
*"optimized incorrectly due to missing references"* error.

Fixes #142762

(cherry picked from commit 5fb7136)

Co-authored-by: kanoshiou <uiaao@tuta.io>
michalborek pushed a commit to michalborek/elasticsearch that referenced this pull request Mar 23, 2026
…astic#143030)

This PR fixes a bug where `Fork.withSubPlans()` incorrectly reassigned
new `NameId`s to its output attributes, breaking references in the upper
plan. This issue specifically manifests when using `FORK` alongside the
`SET unmapped_fields="nullify"` mode.

By design, a `FORK` assigns new `NameId`s to its output attributes via
`refreshOutput()` to decouple them from the internal branches. This
isolation is necessary to prevent unintended side effects during plan
optimizations, such as aggressive constant folding leaking across
branches. 

However, the previous implementation unconditionally re-minted these
`NameId`s every time `withSubPlans()` was called. Because of this, any
node sitting above the `FORK` (like `EVAL` or `STATS`) that already held
a reference to the initial `NameId`s would suddenly point to a
nonexistent ID. Downstream analysis rules would then fail to resolve
these orphaned references, causing the plan execution to fail with an
*"optimized incorrectly due to missing references"* error.

Fixes elastic#142762
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

:Analytics/ES|QL AKA ESQL auto-backport Automatically create backport pull requests when merged auto-merge-without-approval Automatically merge pull request when CI checks pass (NB doesn't wait for reviews!) >bug external-contributor Pull request authored by a developer outside the Elasticsearch team Team:Analytics Meta label for analytical engine team (ESQL/Aggs/Geo) v9.3.3 v9.4.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ESQL: incorrectly optimized fork with nullify unmapped_fields

6 participants