fix: correct LEAD/LAG IGNORE NULLS evaluation and limit pushdown - #25472
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #25472 +/- ##
==========================================
+ Coverage 81.91% 82.42% +0.50%
==========================================
Files 1135 1138 +3
Lines 427416 435491 +8075
Branches 427416 435491 +8075
==========================================
+ Hits 350128 358934 +8806
+ Misses 56368 54847 -1521
- Partials 20920 21710 +790 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
jayzhan211
left a comment
There was a problem hiding this comment.
Thanks @lyne7-sc , there are 2 suggestions
|
|
||
| fn limit_effect(&self) -> LimitEffect { | ||
| self.fun.inner().limit_effect(self.args.as_slice()) | ||
| if self.ignore_nulls { |
There was a problem hiding this comment.
Blanket Unknown also disables pushdown for IGNORE NULLS functions that never look ahead (positive LAG, first_value/last_value/nth_value within a bounded ROWS frame). LAG(v, 1) IGNORE NULLS OVER (ORDER BY id ROWS ...) LIMIT 1 now plans a full SortExec instead of TopK(fetch=1). Only a row-count effect is invalidated by skipping NULLs:
fn limit_effect(&self) -> LimitEffect {
- if self.ignore_nulls {
+ match self.fun.inner().limit_effect(self.args.as_slice()) {
// The function's offset counts non-null values, so it cannot bound
// the number of input rows needed when NULLs are skipped.
- LimitEffect::Unknown
- } else {
- self.fun.inner().limit_effect(self.args.as_slice())
+ LimitEffect::Relative(_) | LimitEffect::Absolute(_) if self.ignore_nulls => {
+ LimitEffect::Unknown
+ }
+ effect => effect,
}
}Needs the WindowShift::limit_effect change from the other comment so negative-offset LAG reports Relative. With both applied locally, all window*.slt pass and TopK is retained.
There was a problem hiding this comment.
Updated this to preserve None and only convert Relative/Absolute to Unknown for IGNORE NULLS.
There was a problem hiding this comment.
limit_effect returns None for every Lag, but LAG(v, -n) looks ahead like LEAD(v, n). The new "negative-offset LAG" test only covers IGNORE NULLS; without it the result is still wrong (pre-existing, fine as a follow-up, but it's the same function):
SELECT id, LAG(v, -1) OVER (ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
FROM (VALUES (1, 10), (2, 20), (3, 30), (4, 40)) AS t(id, v)
ORDER BY id LIMIT 1;
-- returns 1 NULL, expected 1 20Fix:
fn limit_effect(&self, args: &[Arc<dyn PhysicalExpr>]) -> LimitEffect {
- if self.kind == WindowShiftKind::Lag {
- return LimitEffect::None;
- }
- match args {
+ let amount = match args {
[_, expr, ..] => {
let Some(lit) = expr.downcast_ref::<expressions::Literal>() else {
return LimitEffect::Unknown;
};
let ScalarValue::Int64(Some(amount)) = lit.value() else {
return LimitEffect::Unknown; // we should only get int64 from the parser
};
- LimitEffect::Relative((*amount).max(0) as usize)
+ *amount
}
- [_] => LimitEffect::Relative(1), // default value
- _ => LimitEffect::Unknown, // invalid arguments
+ [_] => 1, // default value
+ _ => return LimitEffect::Unknown, // invalid arguments
+ };
+ // LAG(n) looks ahead like LEAD(-n)
+ let lookahead = match self.kind {
+ WindowShiftKind::Lag => amount.saturating_neg(),
+ WindowShiftKind::Lead => amount,
+ };
+ if lookahead > 0 {
+ LimitEffect::Relative(offset_magnitude(lookahead))
+ } else {
+ LimitEffect::None
}
}There was a problem hiding this comment.
Fixed this here as well. I reused the existing WindowShiftKind::shift_offset helper.
|
Thanks for the detailed review @jayzhan211. I've addressed both comments and added regression slt coverage. |
|
Thank you for opening this pull request! Reviewer note: cargo-semver-checks reported the current version number is not SemVer-compatible with the changes in this pull request (compared against the base branch). Details |
jayzhan211
left a comment
There was a problem hiding this comment.
Thanks @lyne7-sc LGTM!
…pache#25472) ## Which issue does this PR close? <!-- We generally require a GitHub issue to be filed for all bug fixes and enhancements and this helps us generate change logs for our releases. You can link an issue to this PR using the GitHub syntax. For example `Closes apache#123` indicates that this PR will close issue apache#123. --> - Closes apache#25471. ## Rationale for this change <!-- Why are you proposing this change? If this is already explained clearly in the issue then this section is not needed. Explaining clearly why changes are proposed helps reviewers understand your changes and offer better suggestions for fixes. Please explain the problem you are trying to solve in terms of the user-visible behavior, rather than the implementation. For example, "The code in `foo.rs` doesn't handle nulls" is a symptom of the implementation. "COUNT(DISTINCT) returns wrong results when the column contains nulls" is the user-visible problem. --> LEAD and negative-offset LAG with IGNORE NULLS can return NULL or a default value even when the requested non-null row exists. This happens when stateful evaluation fails to refill a partially populated lookahead cache, or when window LIMIT pushdown truncates input before the target row. ## What changes are included in this PR? <!-- There is no need to duplicate the description in the issue here, but it is sometimes worth providing a summary of the individual changes in this PR. --> - Resume scanning after the last cached non-null row until enough candidates are available or the range ends. - Return `LimitEffect::Unknown` for IGNORE NULLS window UDF expressions, since non-null offsets cannot bound the number of required input rows. ## What is the testing strategy for this PR? <!-- We typically require tests for all PRs in order to: 1. Prevent the code from being accidentally broken by subsequent changes 2. Serve as another way to document the expected behavior of the code Briefly describe how this PR is tested, and point to the specific tests you added. For example: 'This new feature is covered by the `sqllogictest` cases added in `foo.slt`'. If this PR does not add tests, explain why. For example, if the change is already covered by existing tests, please mention it. You should also check the `codecov` bot reply on this PR to confirm the changed code is exercised. --> Added regression tests in `window.slt` for LEAD/LAG IGNORE NULLS across NULL gaps and batch boundaries, and in `window_limits.slt` for LIMIT pushdown correctness. ## Are there any user-facing changes? <!-- If there are user-facing changes then we may require documentation to be updated before approving the PR. If there are any breaking changes to public APIs, please add the `api change` label. --> Affected IGNORE NULLS window queries now return the correct results. No public API changes.
Which issue does this PR close?
LEAD/LAG IGNORE NULLSreturns incorrect results acrossNULLgaps and withLIMIT#25471.Rationale for this change
LEAD and negative-offset LAG with IGNORE NULLS can return NULL or a default value even when the requested non-null row exists.
This happens when stateful evaluation fails to refill a partially populated lookahead cache, or when window LIMIT pushdown truncates input before the target row.
What changes are included in this PR?
LimitEffect::Unknownfor IGNORE NULLS window UDF expressions, since non-null offsets cannot bound the number of required input rows.What is the testing strategy for this PR?
Added regression tests in
window.sltfor LEAD/LAG IGNORE NULLS across NULL gaps and batch boundaries, and inwindow_limits.sltfor LIMIT pushdown correctness.Are there any user-facing changes?
Affected IGNORE NULLS window queries now return the correct results. No public API changes.