Repository navigation
fix: do not prune ORDER BY keys using nullable UNIQUE constraints - #23918
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #23918 +/- ##
==========================================
- Coverage 82.58% 82.58% -0.01%
==========================================
Files 1142 1142
Lines 440930 440985 +55
Branches 440930 440985 +55
==========================================
+ Hits 364123 364168 +45
+ Misses 54804 54803 -1
- Partials 22003 22014 +11 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
There are a bunch of related bugs / issues here:
As well as the issue this PR targets, #23818. Based on #23636 (review) , I would guess there might also be an issue with window queries. Probably makes sense to look at this whole area holistically. I see that @alamb and @LDM-A have also been working on related tickets, so we should probably make sure we coordinate. |
Thanks for the heads up. I thought with the tests added in #23821 I can implement a fix but did not see the discussion in #23820. I'll follow it and can also close this PR if it will implemented in a combined fashion |
I have not had too much chance to look into the #23820 ticket after my initial weekend. However @alamb mentioned a refactor of the functional dependency file to take care off all the free functions in it. Maybe since there is multiple PRs coming making changes we can do a refactor then add in the changes. Rather than merging some now then refactoring those with the refactor and finishing some off |
| .strip_backtrace(); | ||
|
|
||
| insta::assert_snapshot!(e, @r#"Error during planning: Extension planner for NoOp created an ExecutionPlan with mismatched schema. LogicalPlan schema: DFSchema { inner: Schema { fields: [Field { name: "a", data_type: Int32 }], metadata: {} }, field_qualifiers: [None], functional_dependencies: FunctionalDependencies { deps: [] } }, ExecutionPlan schema: Schema { fields: [Field { name: "b", data_type: Int32 }], metadata: {} }"#); | ||
| insta::assert_snapshot!(e, @r#"Error during planning: Extension planner for NoOp created an ExecutionPlan with mismatched schema. LogicalPlan schema: DFSchema { inner: Schema { fields: [Field { name: "a", data_type: Int32 }], metadata: {} }, field_qualifiers: [None], functional_dependencies: FunctionalDependencies { deps: [], null_equalities: [] } }, ExecutionPlan schema: Schema { fields: [Field { name: "b", data_type: Int32 }], metadata: {} }"#); |
There was a problem hiding this comment.
For this as well if we make NullEquality part of FunctionalDependence instead of FunctionalDependencies it would create an easier to read error in my opinion
There was a problem hiding this comment.
I've moved it int other FunctionalDependence struct which also simplified the PR imo
|
@alamb can you take a look into this when you've time? I think there needs to be choice of incorporating edit: just moved field into the FunctionalDependence struct |
|
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 @buraksenn , here are some suggestions
| /// nullable `UNIQUE` constraint permits multiple NULL rows that may differ. | ||
| /// [`NullEquality::NullEqualsNull`] means it also holds when NULL | ||
| /// determinant values are treated as equal; e.g. a `GROUP BY` key. | ||
| pub null_equality: NullEquality, |
There was a problem hiding this comment.
FunctionalDependence is a public struct with all-public fields, so adding pub null_equality breaks downstream struct literals and exhaustive destructuring. You had to change the patterns in this file for the same reason. The PR description says "No API changes": please add the api change label and a short note in docs/source/library-user-guide/upgrading/56.0.0.md, for example:
+### `FunctionalDependence` has a new `null_equality` field
+
+`FunctionalDependence` now records the NULL semantics its dependency holds under.
+Build it with `FunctionalDependence::new(..)` and, when needed,
+`.with_null_equality(NullEquality::NullEqualsNull)` instead of a struct literal.| // Survivors become nullable, and the new NULLs are not equal to one another: | ||
| self.deps.iter_mut().for_each(|item| { | ||
| item.nullable = true; | ||
| item.null_equality = NullEquality::NullEqualsNothing; |
There was a problem hiding this comment.
Only nullable = false deps survive downgrade_dependencies. For those, any new NULL in the source comes from a padded row, and a padded row is NULL in every column on that side, so the targets match. Resetting to NullEqualsNothing stops sort pruning that main did correctly. SELECT l.k, p.a, p.b FROM l LEFT JOIN p ON l.k = p.a ORDER BY p.a, p.b (p.a PRIMARY KEY) now keeps p.b, where main plans Sort: p.a.
- // Survivors become nullable, and the new NULLs are not equal to one another:
+ // Survivors had a non-null determinant, so every new NULL comes from a
+ // padded row whose targets are all NULL too:
self.deps.iter_mut().for_each(|item| {
item.nullable = true;
- item.null_equality = NullEquality::NullEqualsNothing;
+ item.null_equality = NullEquality::NullEqualsNull;
});Please add a LEFT JOIN + PK case to functional_dependencies.slt.
|
|
||
| // The following simple comparison is working well because | ||
| // GROUP BY expressions come here as a prefix. | ||
| item.source_indices.iter().all(|idx| idx < &count) |
There was a problem hiding this comment.
The whole-GROUP-BY-key dep is skipped whenever some dep's source is a subset of the key, even when that dep can't be used across NULLs. With u(x INT UNIQUE, y), SELECT x, y, c FROM (SELECT x, y, count(*) c FROM u GROUP BY x, y) ORDER BY x, y, c now keeps c, even though (x, y) is unique after grouping. Fine to handle in a follow-up.
item.source_indices.iter().all(|idx| idx < &count)
+ && item.is_valid_across_nulls(aggr_schema)
}) {I tested both fixes locally before reverting them. With them, functional_dependencies.slt passes, the plans become Sort: p.a and Sort: x, y, and the results with DESC tie-breakers are still correct.
|
Thanks for the review @jayzhan211. I've applied the reviews and adjusted PR description. I can't add api-change label since I dont have permission though |
Which issue does this PR close?
Rationale for this change
Please check the issue for details, but the main idea is that a nullable
UNIQUEcolumn permits multiple NULL rows, so it does not determine later sort keys across them and they must not be pruned.What changes are included in this PR?
FunctionalDependencegets anull_equalityfield that records the NULL semantics under which the dependency holds. The default isNullEqualsNothing.NOT NULL.NullEqualsNull, because padded rows are NULL in every column on that side.Are these changes tested?
Yes. Regression tests are added in
functional_dependencies.sltfor nullableUNIQUE,NOT NULL UNIQUE, LEFT JOIN with a primary key, and GROUP BY over a nullableUNIQUEcolumn.Are there any user-facing changes?
Yes: incorrect results are fixed. There is also an API change: the public
FunctionalDependencestruct has a newnull_equalityfield, so code that builds it with a struct literal must switch toFunctionalDependence::new(..). This is documented in the 56.0.0 upgrade guide.