Skip to content

Resolve named windows referenced inside OVER (...) - #26400

Merged
alamb merged 2 commits into
apache:mainfrom
zhuqi-lucas:fix-named-window-frame
Oct 20, 2026
Merged

alamb merged 2 commits into
apache:mainfrom
zhuqi-lucas:fix-named-window-frame

Conversation

@zhuqi-lucas

Copy link
Copy Markdown
Contributor

Which issue does this PR close?

Rationale for this change

OVER (w ROWS BETWEEN ...) silently dropped the named window w. The planner only resolved OVER w; for a name inside the parentheses it ignored WindowSpec::window_name, so the window kept the frame but lost w's PARTITION BY and ORDER BY and produced wrong results without an error.

What changes are included in this PR?

match_window_definitions now resolves a named window referenced inside OVER (...) the way the SQL standard and PostgreSQL do:

  • The window copies the referenced window's PARTITION BY and ORDER BY, and adds its own frame.
  • It may add an ORDER BY only when the referenced window has none.
  • It is an error to add a PARTITION BY, to override an existing ORDER BY, or to copy a window that has a frame.

A window definition can also reference an earlier definition, such as WINDOW w2 AS (w1 ROWS ...). Only earlier definitions can be referenced, so references cannot form a cycle.

What is the testing strategy for this PR?

New cases in window.slt cover inheriting with an added frame, adding an ORDER BY, a definition that references an earlier one, OVER (w), and each error. All of them fail without the fix.

Are there any user-facing changes?

Queries that used OVER (w ...) now return correct results. Queries that relied on the old behavior to override w's PARTITION BY or ORDER BY now get a planning error, as in PostgreSQL.

A window such as OVER (w ROWS BETWEEN ...) referenced the named window w
inside its parentheses, but the planner only resolved OVER w and ignored
the name, so the window lost w's PARTITION BY and ORDER BY and ran over
all rows. Copy the referenced window's PARTITION BY and ORDER BY and
apply the PostgreSQL rules for what may be added, so a window definition
can also reference an earlier one.
Copilot AI balanced review requested due to automatic review settings October 16, 2026 01:27

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions github-actions Bot added sql SQL Planner sqllogictest SQL Logic Tests (.slt) labels Oct 16, 2026
@codecov-commenter

codecov-commenter commented Oct 16, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.18072% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.89%. Comparing base (cdc2fc0) to head (2d171c5).
⚠️ Report is 24 commits behind head on main.

Files with missing lines Patch % Lines
datafusion/sql/src/select.rs 95.18% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #26400      +/-   ##
==========================================
+ Coverage   82.87%   82.89%   +0.01%     
==========================================
  Files        1153     1154       +1     
  Lines      457065   458072    +1007     
  Branches   457065   458072    +1007     
==========================================
+ Hits       378800   379721     +921     
- Misses      55253    55308      +55     
- Partials    23012    23043      +31     

☔ 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.

@zhuqi-lucas
zhuqi-lucas requested a balanced review from Copilot October 16, 2026 05:44

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@jayzhan211 jayzhan211 left a comment

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 @zhuqi-lucas , here are some suggestions

Comment thread datafusion/sql/src/select.rs Outdated
WindowType::WindowSpec(spec.clone())
}
})
if let SQLExpr::Function(f) = expr {

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.

Only select.projection is resolved, so OVER (w ...) in QUALIFY and ORDER BY still drops w silently. That is the same symptom as #26364:

SELECT ticker, ts, price FROM named_window_prices
QUALIFY MAX(price) OVER (w ROWS BETWEEN 1 PRECEDING AND CURRENT ROW) = price
WINDOW w AS (PARTITION BY ticker ORDER BY ts)
ORDER BY ticker, ts;
-- got: BTC 1, BTC 2, DOGE 2   expected: BTC 1, BTC 2, DOGE 1, DOGE 2

ORDER BY ROW_NUMBER() OVER (w ROWS UNBOUNDED PRECEDING) has the same problem. Making the function generic over VisitMut fixes both and removes the SelectItem match and the err option. Verified locally: the QUALIFY and ORDER BY results above are correct, QUALIFY ROW_NUMBER() OVER w = 1 now plans, and the window/qualify slt files pass:

-        query_order_by: Option<OrderBy>,
+        mut query_order_by: Option<OrderBy>,
@@
         self.match_window_definitions(&mut select.projection, &select.named_window)?;
+        self.match_window_definitions(&mut select.qualify, &select.named_window)?;
+        self.match_window_definitions(&mut query_order_by, &select.named_window)?;
fn match_window_definitions<V: VisitMut>(
    &self,
    node: &mut V,
    named_windows: &[NamedWindowDefinition],
) -> Result<()> {
    let named_windows: Vec<(&NamedWindowDefinition, String)> = named_windows
        .iter()
        .map(|w| (w, self.ident_normalizer.normalize(w.0.clone())))
        .collect();
    let flow = visit_expressions_mut(node, |expr| {
        let SQLExpr::Function(f) = expr else {
            return ControlFlow::Continue(());
        };
        let resolved = match &f.over {
            Some(WindowType::NamedWindow(ident)) => {
                self.resolve_named_window(ident, &named_windows)
            }
            Some(WindowType::WindowSpec(spec)) if spec.window_name.is_some() => {
                self.resolve_window_spec(spec, &named_windows)
            }
            _ => return ControlFlow::Continue(()),
        };
        match resolved {
            Ok(spec) => {
                f.over = Some(WindowType::WindowSpec(spec));
                ControlFlow::Continue(())
            }
            Err(e) => ControlFlow::Break(e),
        }
    });
    match flow {
        ControlFlow::Break(e) => Err(e),
        ControlFlow::Continue(()) => Ok(()),
    }
}

Please add a QUALIFY case and an ORDER BY case to window.slt.

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.

visit_expressions_mut also descends into scalar subqueries, so a subquery's OVER (w ...) resolves against the outer WINDOW clause:

SELECT (SELECT MAX(rn) FROM (
  SELECT ROW_NUMBER() OVER (w ROWS UNBOUNDED PRECEDING) AS rn
  FROM named_window_prices WINDOW w AS (PARTITION BY ticker ORDER BY ts)))
FROM named_window_prices WINDOW w AS (ORDER BY ts) LIMIT 1;
-- returns 6, expected 3; without the outer WINDOW: "The window w is not defined!"

OVER w already behaves this way on main, so this is fine to handle in a follow-up. A fix is to use a VisitorMut that counts depth in pre_visit_query / post_visit_query and only rewrites at depth 0, so each subquery resolves its own names in its own select_to_plan.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks @jayzhan211, good catch! Applied in 2d171c5: QUALIFY and ORDER BY are resolved as well, with QUALIFY and ORDER BY cases added to window.slt.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Thanks! Fixed it in this PR rather than a follow-up: the resolver now tracks query depth and only rewrites at depth 0, so each subquery resolves against its own WINDOW clause. Added your case, plus one where the subquery has no WINDOW clause, which now errors like PostgreSQL.

@alamb alamb left a comment

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 @zhuqi-lucas and @jayzhan211

My agent found another issue, which predates this one

@alamb

alamb commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

While reviewing this PR I found a related problem with named windows in HAVING, filed as #26506. It also happens without this PR, so it does not need to block this PR.

@zhuqi-lucas

Copy link
Copy Markdown
Contributor Author

Thanks @jayzhan211 and @alamb for review!

@alamb
alamb added this pull request to the merge queue Oct 20, 2026
Merged via the queue into apache:main with commit 6131a98 Oct 20, 2026
42 checks passed
joroKr21 pushed a commit to coralogix/arrow-datafusion that referenced this pull request Oct 21, 2026
## Which issue does this PR close?

- Closes apache#26506.

## Rationale for this change

A named window in `HAVING` was never resolved, so `MAX(price) OVER w`
was planned as a plain aggregate and the query returned rows instead of
being rejected. An undefined window name in `HAVING` was not reported
either.

## What changes are included in this PR?

Resolve named windows in `HAVING` together with the other clauses. The
query now fails like the inline form (`Window function calls are not
allowed in HAVING`), and an undefined name reports `The window ... is
not defined!`.

This builds on apache#26400, which makes the resolver usable for any clause.
Only the last commit is new; the diff shrinks to it once apache#26400 merges.

## Are these changes tested?

Yes, `window.slt` cases for `HAVING` (inline, named, undefined name) and
`WHERE` (inline, named). The `HAVING` cases fail without the fix.

## Are there any user-facing changes?

Queries that used a named window in `HAVING` now return an error instead
of wrong results.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

sql SQL Planner sqllogictest SQL Logic Tests (.slt) v56.0.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Window referencing a named window with an added frame, OVER (w ROWS ...), silently drops PARTITION BY and ORDER BY

5 participants