Repository navigation
Conversation
jayzhan211
left a comment
There was a problem hiding this comment.
@abp002 thanks for tracking this down! The fix works, and on current main it also fixes the window case and #27287. I left three small asks inline: run the promotion in EquivalenceClass::push/extend so classes built through EquivalenceGroup::new or extend get it too, and add tests for the window shape and for the merge/push arms of add_equal_conditions.
| /// of this class. Otherwise, with `a + b` as the canonical expression of a | ||
| /// class that also holds `a`, normalization would replace the `a` inside | ||
| /// `a + b` and turn `a + b` into `(a + b) + b`. | ||
| fn promote_sub_expressions(&mut self) { |
There was a problem hiding this comment.
@abp002 thanks for the fix! Classes built through EquivalenceGroup::new or merged by EquivalenceGroup::extend (via bridge_classes) skip this promotion, so EquivalenceGroup::new([EquivalenceClass::new([a + b, a])]) still normalizes a to (a + b) + b. Both are public. Could you run the promotion inside EquivalenceClass::push and extend instead? Then every way of building a class gets it, and the four calls in add_equal_conditions go away:
self.exprs.insert(expr);
+ self.promote_sub_expressions();
}
@@
pub fn extend(&mut self, other: Self) {
self.exprs.extend(other.exprs);
+ self.promote_sub_expressions();
@@
self.classes[first_idx].extend(other_class);
- self.classes[first_idx].promote_sub_expressions();
@@
self.classes[group_idx].push(right);
- self.classes[group_idx].promote_sub_expressions();
@@
self.classes[group_idx].push(left);
- self.classes[group_idx].promote_sub_expressions();
@@
- let mut class = EquivalenceClass::new([left, right]);
- class.promote_sub_expressions();
+ let class = EquivalenceClass::new([left, right]);With this change on current main, the physical-expr unit tests and the full sqllogictest suite pass.
Unit test for both paths (fails on the current PR, passes with the change)
#[test]
fn test_sub_expression_is_canonical_on_every_path() -> Result<()> {
let schema = abc_schema();
let (a, b, c) = (col("a", &schema)?, col("b", &schema)?, col("c", &schema)?);
let a_plus_b = binary(Arc::clone(&a), Operator::Plus, b, &schema)?;
// A class built directly, with the larger expression first
let group = EquivalenceGroup::new([EquivalenceClass::new([
Arc::clone(&a_plus_b),
Arc::clone(&a),
])]);
assert!(group.normalize_expr(Arc::clone(&a_plus_b)).eq(&a));
// Two classes bridged by `extend` through the shared `c`
let mut group = EquivalenceGroup::default();
group.add_equal_conditions(Arc::clone(&a_plus_b), Arc::clone(&c));
let mut other = EquivalenceGroup::default();
other.add_equal_conditions(c, Arc::clone(&a));
group.extend(other);
assert!(group.normalize_expr(a_plus_b).eq(&a));
Ok(())
}| statement ok | ||
| DROP TABLE eq_in; |
There was a problem hiding this comment.
@abp002 the same bug also breaks a window on main, without a sort-merge join: PARTITION BY a + b ORDER BY a under WHERE a + b = a fails at runtime with Expects PARTITION BY expression to be ordered. This PR fixes it, but nothing tests that path. Could you add it?
| statement ok | |
| DROP TABLE eq_in; | |
| statement ok | |
| DROP TABLE eq_in; | |
| # With a + b = a, the window's PARTITION BY a + b is the same key as the | |
| # ORDER BY a below it | |
| statement ok | |
| CREATE TABLE eq_sub (a INT, b INT) AS VALUES (1, 0), (2, 3), (1, 0), (5, 0); | |
| query II | |
| SELECT a, sum(b) OVER (PARTITION BY a + b ORDER BY a) FROM eq_sub WHERE a + b = a ORDER BY a; | |
| ---- | |
| 1 0 | |
| 1 0 | |
| 5 0 | |
| statement ok | |
| DROP TABLE eq_sub; |
| 1 0 1 0 | ||
|
|
||
| statement ok | ||
| DROP TABLE smj_self_eq; |
There was a problem hiding this comment.
Removing the promotion after the merge (extend) or after push(right) in add_equal_conditions still passes the tests here, because the issue query only reaches the new-class arm. These two queries reach the other arms. Both fail with SanityCheckPlan on main, and each catches one of those removals:
| DROP TABLE smj_self_eq; | |
| DROP TABLE smj_self_eq; | |
| statement ok | |
| CREATE TABLE smj_self_eq_cd (a INT, b INT, c INT, d INT) AS VALUES (1, 0, 1, 1), (2, 3, 5, 2); | |
| # a joins the class of a + b after the class exists | |
| query IIII | |
| SELECT x.a, x.b, y.a, y.b FROM smj_self_eq_cd x JOIN smj_self_eq_cd y ON x.a = y.a AND x.b = y.b WHERE x.a + x.b = x.c AND x.c = x.a; | |
| ---- | |
| 1 0 1 0 | |
| # a reaches the class of a + b when two classes merge | |
| query IIII | |
| SELECT x.a, x.b, y.a, y.b FROM smj_self_eq_cd x JOIN smj_self_eq_cd y ON x.a = y.a AND x.b = y.b WHERE x.a + x.b = x.c AND x.a = x.d AND x.d = x.c; | |
| ---- | |
| 1 0 1 0 | |
| statement ok | |
| DROP TABLE smj_self_eq_cd; |
| statement ok | ||
| DROP TABLE eq_t; | ||
|
|
||
| # The two IN lists hold the same values in a different order, so they compare |
There was a problem hiding this comment.
Optional cleanup: this test doesn't reach the loop bound in promote_sub_expressions. With the bound replaced by loop, the query still finishes, because neither IN list is inside the other. A promoted expression is always strictly inside the previous canonical expression, so it can't cycle. I'd drop this test, or change its comment so it doesn't claim to cover termination.
…dow and the other arms
7b2d9b2 to
a2bc6db
Compare
|
Thanks for the review, and for the tests. I moved the promotion into I also dropped the Rebased on main. |
jayzhan211
left a comment
There was a problem hiding this comment.
@abp002 thanks for the updates, all four look good. The new queries fail on main and pass here, and the full sqllogictest suite passes. I left one optional test suggestion inline.
| fn promote_sub_expressions(&mut self) { | ||
| // Each promoted expression is inside the previous canonical | ||
| // expression, so there are at most as many rounds as members. | ||
| for _ in 0..self.exprs.len() { |
There was a problem hiding this comment.
Optional: no test needs a second round of this loop. With for _ in 0..1 every test still passes. A second round happens when extend brings in a + b and a behind an unrelated canonical expression. This test passes here and fails with one round:
#[test]
fn test_sub_expression_promotion_takes_several_rounds() -> Result<()> {
let schema = abc_schema();
let (a, b, c) = (col("a", &schema)?, col("b", &schema)?, col("c", &schema)?);
let a_plus_b = binary(Arc::clone(&a), Operator::Plus, Arc::clone(&b), &schema)?;
let a_plus_b_plus_c = binary(
Arc::clone(&a_plus_b),
Operator::Plus,
Arc::clone(&c),
&schema,
)?;
// `b * c` is canonical in `other`, so `a + b` stays ahead of `a` there
let b_times_c = binary(b, Operator::Multiply, c, &schema)?;
let other = EquivalenceClass::new([b_times_c, a_plus_b, Arc::clone(&a)]);
let mut class = EquivalenceClass::new([a_plus_b_plus_c]);
class.extend(other);
assert!(class.canonical_expr().unwrap().eq(&a));
Ok(())
}
Which issue does this PR close?
a + b = a#26623.Rationale for this change
A sort-merge join on
(a, b)fails withSanityCheckPlanwhen a filter below it equates an expression with one of its own sub-expressions, such asx.a + x.b = x.a.The filter puts
a + bandain one equivalence class, witha + bas the canonical expression because it comes first.normalize_exprrewrites bottom-up, so it replaces theainsidea + bwitha + band turnsa + binto(a + b) + b. The join requires[a, b], which normalizes to[a + b, b], while the sort below it is on[a + b, b], which normalizes to[(a + b) + b, b], so the sort no longer satisfies the join. Written asx.a = x.a + x.b, the query works, becauseais the canonical expression.What changes are included in this PR?
When
EquivalenceGroup::add_equal_conditionsadds to a class, a member that is a sub-expression of the canonical expression now becomes the canonical expression.normalize_expris unchanged, so classes without such a pair normalize as before.What is the testing strategy for this PR?
The query from the issue is added to
sort_merge_join.slt. It fails without the fix.equivalence_properties.sltgets a plan thatmainalready gets right and that a fix innormalize_expritself would break: witha = b AND c = a + 1aboveORDER BY a + 1 LIMIT 100, the outerORDER BY a + 1needs no sort.It also gets a query with two
INlists that hold the same values in a different order. They compare equal but hash differently, so both stay in the class, and the promotion must still stop.Are there any user-facing changes?
No API changes. Queries like the one in the issue now plan instead of failing in
SanityCheckPlan.