Skip to content

fix: Capture global ORDER BY requirement under ScalarSubqueryExec root#23146

Closed
sgrebnov wants to merge 5 commits into
apache:mainfrom
spiceai:sgrebnov/scalar-subquery-output-requirements
Closed

fix: Capture global ORDER BY requirement under ScalarSubqueryExec root#23146
sgrebnov wants to merge 5 commits into
apache:mainfrom
spiceai:sgrebnov/scalar-subquery-output-requirements

Conversation

@sgrebnov

@sgrebnov sgrebnov commented Jun 24, 2026

Copy link
Copy Markdown
Member

Which issue does this PR close?

After the DataFusion 53 → 54 upgrade, queries with a ScalarSubquery stopped
respecting the global ORDER BY and returned unordered results.

Rationale for this change

In add mode, OutputRequirements walks the plan via require_top_ordering_helper to capture the global ORDER BY as an OutputRequirementExec. The helper bails on any node with children.len() != 1. ScalarSubqueryExec has multiple children (child 0 = order-transparent main input, the rest = uncorrelated subquery plans),
so when it is the root the rule stops immediately and stamps an empty OutputRequirementExec(order_by=[], dist_by=Unspecified) at the top — losing the ordering requirement.

ScalarSubqueryExec is order-transparent on child 0 (maintains_input_order()[0]== true, no required input ordering), so the search should descend through it like any other single-child order-preserving operator.

What changes are included in this PR?

require_top_ordering_helper now special-cases ScalarSubqueryExec: it descends through child 0 to find and wrap the top SortExec / SortPreservingMergeExec, reattaching the subquery children unchanged.

Are these changes tested?

Yes — two tests in datafusion/core/tests/physical_optimizer/output_requirements.rs:

  1. Rule level (require_top_ordering_descends_through_scalar_subquery): asserts the OutputRequirementExec carrying the ordering is placed below the ScalarSubqueryExec; without the fix it lands empty at the root.

  2. End to end (scalar_subquery_root_preserves_global_ordering_end_to_end): runs the full default optimizer pipeline over a ScalarSubqueryExec root whose ordering comes from a SortPreservingMergeExec over a two-partition ordered source (the shape federated/custom planners produce, with no SortExec backstop), then executes the plan and checks the rows. Verified by toggling the fix:

    executed output
    without fix 1, 3, 5, 7, 2, 4, 6, 8 — merge dropped, rows partition-interleaved
    with fix 1, 2, 3, 4, 5, 6, 7, 8 — globally ordered

Existing subquery.slt / TPC-H snapshots are unchanged. I was unable to construct a SQL query that triggers the bug — over built-in sources the global ORDER BY is always preserved regardless of the missing requirement. It only surfaces when a custom physical planner supplies a plan shaped like the end-to-end test: a SortPreservingMergeExec over already-sorted partitions, with no SortExec above it.

Are there any user-facing changes?

No.

require_top_ordering_helper bailed on ScalarSubqueryExec because it has
multiple children (main input + subquery plans), so the rule stamped an
empty OutputRequirementExec (order_by=[], dist_by=Unspecified) at the root
and never recorded the query's global ORDER BY requirement.

Descend through child 0 (the order-transparent main input:
maintains_input_order()[0] == true, no required input ordering) to find
and protect the top SortExec, reattaching the subquery children unchanged.
@github-actions github-actions Bot added optimizer Optimizer rules core Core DataFusion crate labels Jun 24, 2026
@sgrebnov sgrebnov changed the title Capture global ORDER BY requirement under ScalarSubqueryExec root WIP Capture global ORDER BY requirement under ScalarSubqueryExec root Jun 29, 2026
@sgrebnov sgrebnov changed the title WIP Capture global ORDER BY requirement under ScalarSubqueryExec root fix: Capture global ORDER BY requirement under ScalarSubqueryExec root Jul 12, 2026
@sgrebnov
sgrebnov marked this pull request as ready for review July 12, 2026 20:33
@sgrebnov
sgrebnov marked this pull request as draft July 12, 2026 20:49
…bqueryExec root

EXPLAIN VERBOSE pins the 'physical_plan after OutputRequirements' step:
the OutputRequirementExec carrying the query's global ORDER BY is placed
below the ScalarSubqueryExec root. Without the fix the rule stamps an
empty order_by=[], dist_by=Unspecified requirement at the root and this
snapshot fails.
@github-actions github-actions Bot added the sqllogictest SQL Logic Tests (.slt) label Jul 12, 2026
Drop the pass-pinning EXPLAIN VERBOSE block from subquery.slt: SQL over
built-in sources cannot produce wrong results from this defect (the top
SortExec and the requirement-independent merge backstops in
EnsureRequirements always restore the global order), so the slt could
only ever assert on intermediate pass output while pinning the full pass
list.

Instead add full_pipeline_keeps_row_order_under_scalar_subquery_root:
it runs the complete default physical optimizer pipeline over a
ScalarSubqueryExec root whose ordering comes from a
SortPreservingMergeExec over a two-partition ordered source (the shape
federated planners produce, with no SortExec backstop) and executes the
plan. Without the fix the empty root requirement lets the pipeline drop
the merge and the rows come back partition-interleaved.
@github-actions github-actions Bot removed the sqllogictest SQL Logic Tests (.slt) label Jul 12, 2026
…ing tests

Rename the tests to name the unit each one exercises: the rule traversal
(require_top_ordering_descends_through_scalar_subquery) and the end-to-end
ordering guarantee (scalar_subquery_root_preserves_global_ordering_end_to_end).

Rebuild the end-to-end test around record_batch!, drop the redundant
plan-shape assertion and the unneeded optimizer config knobs (verified: the
executed-row-order check alone still catches the regression), and describe the
expected behavior rather than the failure mode.

Replace unwrap() with descriptive expect().
@sgrebnov sgrebnov closed this Jul 17, 2026
@codecov-commenter

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 88.88889% with 1 line in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@f87e817). Learn more about missing BASE report.

Files with missing lines Patch % Lines
...sion/physical-optimizer/src/output_requirements.rs 88.88% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main   #23146   +/-   ##
=======================================
  Coverage        ?   80.66%           
=======================================
  Files           ?     1087           
  Lines           ?   367420           
  Branches        ?   367420           
=======================================
  Hits            ?   296364           
  Misses          ?    53397           
  Partials        ?    17659           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

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

Labels

core Core DataFusion crate optimizer Optimizer rules

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants