Repository navigation
25672: fix: drop the redundant Limit above a Sort in the same PushDownLimit visit - #376
martin-augment wants to merge 3 commits into
Conversation
…visit For Limit(skip=0, fetch=n) over a Sort without a fetch, PushDownLimit set Sort.fetch = n but kept the Limit, and removed it only on its next visit in the next optimizer pass. Every ORDER BY ... LIMIT n query therefore needed one more logical optimizer pass. Drop the Limit in the same visit, and apply the TopK-through-join pushdown to the Sort there, because the Sort is now the visited node and is not visited again in this pass.
PushDownLimit now removes the Limit above a Sort in the same visit that gives the Sort its fetch, so a rule that runs later in the pass sees the Sort with a fetch, not Limit over Sort.
… is dropped When the Sort already has an equal or smaller fetch, PushDownLimit also drops the Limit and returns the Sort, so the Sort's own visit is skipped in that pass. Both skip = 0 paths now share one branch that calls push_topk_through_join. The result is always marked transformed, because the Limit is removed even when the TopK does not move. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Advanced Run ID: 📒 Files selected for processing (3)
Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review. Summary
TestsTest execution results were not provided. WalkthroughFor a zero-skip Priority: ⬇️ Low Merge Risk: ⚪ Minimal · up to The change preserves the query result limit while enabling the intended one-pass TopK optimization. No actionable merge risk remains beyond normal checks. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Code Review: PushDownLimit Sort/Fetch Optimization
Summary
The changes refactor rewrite_limit to drop a redundant Limit(skip=0) over a Sort in the same pass, and to apply the TopK-through-join pushdown immediately when the Sort replaces the Limit as the visited node. The test changes reflect the new single-pass convergence behavior.
Overall the logic appears sound, but there are several points worth scrutinizing.
Issues & Observations
1. result.transformed = true unconditionally overrides the inner result (medium)
let mut result = push_topk_through_join(sort)?;
result.transformed = true;
Ok(result)This forcibly marks the plan as transformed regardless of what push_topk_through_join returned. The comment justifies this ("the Limit is gone"), which is correct — the Limit node is removed in this branch. However:
- It silently discards any
transformedsemantics frompush_topk_through_join. If that helper ever returnsTransformed::nofor a legitimate reason (e.g., a future guard), this code would still claim a change. Considerresult.transformed |= truesemantics being explicit, or better, assert/document that the removal ofLimitis the sole source of truth. - More importantly, verify that
push_topk_through_joindoes not itself rely ontransformedbeing accurate for its own recursion/termination. Overriding it could mask a bug.
2. Behavior change: Limit is now dropped even when skip > 0 is false but fetch semantics differ (verify)
In the old code, the Limit was only dropped when new_fetch != sort.fetch (i.e., the fetch actually changed). Now, with skip == 0, the Limit is dropped unconditionally, even when new_fetch == sort.fetch.
This is the intended change, but double-check the case where sort.fetch is None and fetch is None... actually fetch here comes from the Limit, which always has a fetch in this branch. Still, confirm:
- When
sort.fetch == Some(f)andlimit.fetch == Some(f)withskip == 0: dropping theLimitis correct becauseSortalready caps atf. ✅ - When
sort.fetch == Some(smaller)andlimit.fetch == Some(larger):new_fetch = min(...) = smaller,Limitdropped. ✅ (covered by new test..._smaller_fetch...)
The logic looks correct, but the asymmetry between the skip > 0 and skip == 0 branches is now significant and deserves a comment explaining why skip == 0 can drop unconditionally while skip > 0 cannot.
3. expr.len() == 1 guard in user_defined_plan.rs — silent fallthrough (low)
if let LogicalPlan::Sort(Sort { expr, input, fetch: Some(fetch) }) = &plan
&& expr.len() == 1
{
return Ok(Transformed::yes(...));
}If a Sort has fetch: Some(_) but expr.len() != 1, the code falls through to the Limit handling below, which will not match a Sort and return Transformed::no(plan). That's fine, but the multi-column case is silently ignored — consistent with the documented "does not handle many edge cases" note. No action required, but the comment could explicitly mention the single-column restriction.
4. Test limit_push_down_sort_reports_transformed asserts matches!(result.data, LogicalPlan::Sort(_)) (low)
This is a good assertion, but it only checks the top-level node type. Consider also asserting the fetch value on the resulting Sort to lock in the min behavior, e.g. for sort_fetch = Some(5) the result should be fetch = Some(5) (not Some(10)). The current test would pass even if the fetch were incorrectly set to the Limit's value.
5. Pass-count test brittleness (low)
let optimizer_ctx = OptimizerContext::new().with_max_passes(3);
...
assert_eq!(passes, 2);This asserts an exact pass count. If the optimizer's convergence detection changes (e.g., a future rule reports transformed differently), this test breaks even though the final plan is correct. The companion snapshot tests already validate the output; consider whether the exact pass count is a meaningful contract or an over-specification. At minimum, the comment should note this is testing convergence, not a hard API guarantee.
6. topk_pushed_through_limit_over_sort_with_same_fetch_in_one_pass — snapshot shows duplicated Sort (informational)
The snapshot:
Sort: t1.b ASC NULLS LAST, fetch=3
Left Join: t1.a = t2.a
Sort: t1.b ASC NULLS LAST, fetch=3
TableScan: t1
TableScan: t2
The outer Sort and the inner Sort on t1 both carry fetch=3. This is expected (TopK pushed to the join input, outer Sort retained), but it's worth confirming this is the desired final form and not an artifact of the single-pass change. The previous two-pass test presumably produced the same shape; if so, fine.
7. Naming: push_topk_through_join called on a Sort with no join (low)
In the skip == 0 branch, push_topk_through_join(sort) is called even when the Sort's input is not a join. The function name implies join-specific behavior; calling it unconditionally is fine if it's a no-op for non-join inputs, but the comment "The Sort then replaces the Limit as the visited node ... so apply the TopK pushdown here" could be clearer that this is a general TopK pushdown entry point, not join-only.
Recommendations
- Clarify the
transformedoverride — add a comment or restructure so it's obvious theLimitremoval is the reason, and confirmpush_topk_through_joindoesn't depend on its owntransformedflag. - Add a comment explaining why
skip == 0permits unconditionalLimitremoval whileskip > 0does not. - Strengthen
limit_push_down_sort_reports_transformedto assert the resultingSort'sfetchvalue. - Reconsider the exact pass-count assertion or document it as a convergence contract.
- Verify the duplicated-
Sortsnapshot is the intended steady state (compare against the prior two-pass output).
No blocking correctness issues found; the core logic change (dropping the redundant Limit in the same pass and reporting transformed) appears correct and is well-covered by the new tests.
ReviewTraced through Correctness
Test coverage — solid. The new tests cover: single-pass convergence ( Style — consistent with the surrounding code; the Performance — net positive: this removes one full optimizer pass for a very common query shape ( Security — none; this is a pure logical-plan rewrite with no I/O, no unsafe code, and no external input handling. Nothing blocking. I wasn't able to run |
🤖 Augment PR SummarySummary: This PR removes a redundant zero-offset Changes:
🤖 Was this summary useful? React with 👍 or 👎 |
25672: To review by AI