Skip to content

Introduce ViewUnionAll to differentiate view-produced unions from subquery unions#143564

Merged
craigtaverner merged 20 commits intoelastic:mainfrom
quackaplop:view_union_all_fork
Mar 23, 2026
Merged

Introduce ViewUnionAll to differentiate view-produced unions from subquery unions#143564
craigtaverner merged 20 commits intoelastic:mainfrom
quackaplop:view_union_all_fork

Conversation

@quackaplop
Copy link
Copy Markdown
Contributor

@quackaplop quackaplop commented Mar 4, 2026

Summary

  • Creates ViewUnionAll extends UnionAll as a type marker for view-produced unions
  • ViewResolver now only skips ViewUnionAll, allowing plain UnionAll from subqueries to be processed normally for view resolution
  • The Fork handler uses replaceSubPlans() to preserve subclass types instead of constructing new Fork(...) directly, fixing a pre-existing type-erasure bug
  • createTopPlan() produces ViewUnionAll instead of UnionAll

Relates to esql-planning#181 (under "Loose ends and TODOs")

Test plan

  • Unit tests for ViewUnionAll copy methods (replaceChildren, replaceSubPlans, replaceSubPlansAndOutput preserve type)
  • Behavioral test verifying views inside user-written subqueries are resolved
  • CSV-spec tests combining views with subqueries (viewInSubquery, viewInSubqueryMultipleRows)
  • Existing view tests updated to assert ViewUnionAll
  • Existing subquery parser tests pass unchanged
  • Optimizer PushDownFilterAndLimitIntoUnionAllTests pass unchanged
  • CI integration tests (CsvIT, EsqlSpecIT)

Post-review changes

  • Re-do the ViewResolver changes after Views Security PR is merged
  • Change to >non-issue and remove changelog file
  • Add view name to ViewUnionAll (and add tests for name management)
  • Remove name from Subquery
  • Test subquery inside view definition

quackaplop added a commit to quackaplop/elasticsearch that referenced this pull request Mar 4, 2026
@quackaplop quackaplop marked this pull request as ready for review March 4, 2026 16:11
@quackaplop quackaplop requested a review from craigtaverner March 4, 2026 16:11
@elasticsearchmachine elasticsearchmachine added the needs:triage Requires assignment of a team area label label Mar 4, 2026
Copy link
Copy Markdown
Contributor

@craigtaverner craigtaverner left a comment

Choose a reason for hiding this comment

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

If we can name the subqueries, we will complete the requirements. This could be done by enhancing UnionAll (discuss with @Fang), or adding names here. If we add names here we could also consider directly extending Fork, instead of UnionAll. Some more thinking might be required for that option.

Also, this PR needs to rebase on the security PR at #141050, or wait for that to merge and rebase on main. There will be conflicts.

// --- Behavioral test: views inside subqueries ---

public void testViewInsideSubqueryIsResolved() {
assumeTrue("Requires subquery in FROM command support", EsqlCapabilities.Cap.SUBQUERY_IN_FROM_COMMAND.isEnabled());
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.

We could use VIEWS_WITH_BRANCHING here, since that depends on subqueries in from, and will also only turn on when both subqueries and views are out of snapshot.

@craigtaverner craigtaverner added >non-issue Team:Analytics Meta label for analytical engine team (ESQL/Aggs/Geo) :Analytics/ES|QL AKA ESQL and removed needs:triage Requires assignment of a team area label labels Mar 4, 2026
@elasticsearchmachine
Copy link
Copy Markdown
Collaborator

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

@@ -0,0 +1,6 @@
area: ES|QL
issues:
- 143564
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.

PR cannot refer to itself as an issue

- 143564
pr: 143564
summary: Introduce ViewUnionAll to differentiate view-produced unions from subquery unions
type: bug
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.

For in-progress development we would rather mark this as non-issue to prevent an explosion of changelog entries for as-yet-unreleased work.

…query unions

ViewResolver and the parser's subquery syntax both produce UnionAll nodes.
Previously, ViewResolver skipped all UnionAll nodes assuming they were from
view resolution, and the Fork handler lost the UnionAll type by rebuilding
as plain Fork.

This change:
- Creates ViewUnionAll (extends UnionAll) as a type marker for view-produced unions
- ViewResolver now only skips ViewUnionAll, allowing plain UnionAll from subqueries
  to be processed normally
- The Fork handler uses replaceSubPlans() to preserve subclass types instead of
  constructing new Fork directly
- createTopPlan() produces ViewUnionAll instead of UnionAll
Move ViewUnionAll unit tests from InMemoryViewServiceTests into a
dedicated test file following the codebase convention of per-node
test classes. Adds equals/hashCode and type-inequality tests.
The testAllCommandsWhitelistedOrBlacklisted test scans the classpath for
all LogicalPlan subclasses and requires each to be listed. ViewUnionAll
was missing, causing CI part-3 to fail.
Copy link
Copy Markdown
Member

@fang-xing-esql fang-xing-esql left a comment

Choose a reason for hiding this comment

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

LGTM! I left a minor comment.

otherPlans.add(new Subquery(ur.source(), lp.plan, lp.name));
assert otherPlans.containsKey(lp.name) == false;
// TODO: Consider dropping Subquery nodes entirely
otherPlans.put(lp.name, new Subquery(ur.source(), lp.plan, lp.name));
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It seems like the view name is saved in the hash map, shall we just use the subquery constructor without name here?

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.

Yes, definitely. I had thought to make that change together with removing Subquery entirely (another PR), but could do it here too!

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.

I've removed it here, and will remove Subquery as part of another PR.

12 | false
;

// Testing views used inside subqueries
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.

Could also test subqueries inside views

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.

Done inside the InMemoryViewServiceTests

Test views inside subqueries and subqueries inside views
Copy link
Copy Markdown
Contributor

@craigtaverner craigtaverner left a comment

Choose a reason for hiding this comment

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

All previous comments have been taken care of now.

12 | false
;

// Testing views used inside subqueries
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.

Done inside the InMemoryViewServiceTests

listener.delegateFailureAndWrap((l, rewritten) -> listener.onResponse(new ViewResolutionResult(rewritten, viewQueries)))
);
replaceViews(plan, parser, new LinkedHashSet<>(), viewQueries, 0, listener.delegateFailureAndWrap((l, rewritten) -> {
LogicalPlan postProcessed = rewriteUnionAllsWithNamedSubqueries(rewritten);
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.

Perhaps add comments describing which use cases need this post-view-resolution update

@craigtaverner craigtaverner added the auto-merge-without-approval Automatically merge pull request when CI checks pass (NB doesn't wait for reviews!) label Mar 17, 2026
@craigtaverner craigtaverner merged commit dcf6860 into elastic:main Mar 23, 2026
35 of 36 checks passed
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-merge-without-approval Automatically merge pull request when CI checks pass (NB doesn't wait for reviews!) >non-issue Team:Analytics Meta label for analytical engine team (ESQL/Aggs/Geo) v9.4.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants