Avoid duplicate clauses when using #or #29950
Conversation
Thanks for the pull request, and welcome! The Rails team is excited to review your changes, and you should hear from @pixeltrix (or someone else) soon. If any changes to this PR are deemed necessary, please add them as extra commits. This ensures that the reviewer can see what has changed since they last reviewed the code. Due to the way GitHub handles out-of-date commits, this should also make it reasonably obvious what issues have or haven't been addressed. Large or tricky changes may require several passes of review and changes. This repository is being automatically checked for code quality issues using Code Climate. You can see results for this analysis in the PR status below. Newly introduced issues should be fixed before a Pull Request is considered ready to review. Please see the contribution instructions for more information. |
Condenses the clauses that are common to both sides of the OR and put them outside, before the OR This fix the current behavior where the number of conditions is exponential based on the number of times #or is used.
actual = (actual + wcs[1]).or(actual + wcs[2]) | ||
expected = expected + wcs[1].or(wcs[2]) | ||
|
||
actual = (actual + wcs[3] + wcs[4]).or(actual + wcs[5] + wcs[6]) |
lugray
Jul 28, 2017
Contributor
Is this addtional manipulation really necessary for the test?
Is this addtional manipulation really necessary for the test?
expected = wcs[0] | ||
|
||
actual = (actual + wcs[1]).or(actual + wcs[2]) | ||
expected = expected + wcs[1].or(wcs[2]) |
lugray
Jul 28, 2017
Contributor
The redefinition makes this harder to understand. Why not instead:
common = wcs[0]
actual = (common + wcs[1]).or(common + wcs[2])
expected = common + wcs[1].or(wcs[2])
The redefinition makes this harder to understand. Why not instead:
common = wcs[0]
actual = (common + wcs[1]).or(common + wcs[2])
expected = common + wcs[1].or(wcs[2])
common = self - left | ||
right = other - common | ||
|
||
return common if left.empty? || right.empty? |
sgrif
Jul 28, 2017
Contributor
I'd prefer to avoid postfix conditionals (and early returns for that matter) if possible. How about this instead?
if left.empty? || right.empty?
common
else
or_clause = WhereClause.new(
[left.ast.or(right.ast)],
)
common + or_clause
end
I'd prefer to avoid postfix conditionals (and early returns for that matter) if possible. How about this instead?
if left.empty? || right.empty?
common
else
or_clause = WhereClause.new(
[left.ast.or(right.ast)],
)
common + or_clause
end
@@ -181,6 +181,39 @@ class WhereClauseTest < ActiveRecord::TestCase | |||
assert_equal WhereClause.empty, WhereClause.empty.or(where_clause) | |||
end | |||
|
|||
test "or places common conditions before the OR" do | |||
wcs = (0..6).map do |i| | |||
WhereClause.new([table["col_#{i}"].eq(bind_param(i))]) |
sgrif
Jul 28, 2017
Contributor
This sort of thing makes this test really difficult to read. How about something like this?
a = WhereClause.new(
[table["id"].eq(bind_param(1)), table["name"].eq(bind_param("Sean"))],
)
b = WhereClause.new(
[table["id"].eq(bind_param(1)) ,table["hair_color"].eq(bind_param("black"))],
)
common = WhereClause.new(
[table["id"].eq(bind_param(1))],
)
or_clause = WhereClause.new(
[table["name"].eq(bind_param("Sean")).or(table["hair_color"].eq(bind_param("black")))],
)
assert_equal common + or_clause, a.or(b)
This sort of thing makes this test really difficult to read. How about something like this?
a = WhereClause.new(
[table["id"].eq(bind_param(1)), table["name"].eq(bind_param("Sean"))],
)
b = WhereClause.new(
[table["id"].eq(bind_param(1)) ,table["hair_color"].eq(bind_param("black"))],
)
common = WhereClause.new(
[table["id"].eq(bind_param(1))],
)
or_clause = WhereClause.new(
[table["name"].eq(bind_param("Sean")).or(table["hair_color"].eq(bind_param("black")))],
)
assert_equal common + or_clause, a.or(b)
MaxLap
Jul 28, 2017
Author
Contributor
Just tried, that doesn't work because WhereClause#or isn't call for both sides, and WhereClause#or always adds a And node around the predicates, even if there only one.
I'll tweak the test.
Just tried, that doesn't work because WhereClause#or isn't call for both sides, and WhereClause#or always adds a And node around the predicates, even if there only one.
I'll tweak the test.
extra = WhereClause.new([table["extra"].eq(bind_param("pluto"))]) | ||
|
||
actual_extra_only_on_left = (common + extra).or(common) | ||
actual_extra_only_on_right = (common).or(common + extra) |
sgrif
Jul 28, 2017
Contributor
I also find this really hard to read. Can we either just test this once, or just write out the actual conditions that we're testing?
I also find this really hard to read. Can we either just test this once, or just write out the actual conditions that we're testing?
Personally, I thought that using variables and then doing + to concatenate conditions together helped with readability. But i'll change it to be more basic and will split the tests into smaller ones. |
I did the changes and added another test. Thnx for the reviews |
Attempt number 2 for this change. A refactor of WhereClause by @sgrif made this change a lot simpler. Initial proposal at #29805.
Right now, when using
#or
, nothing is done to try to avoid duplicated conditions. As a result, query length increases at an exponential rate. This makes it really hard to parse for humans, making debugging / understanding the query log really hard.To show the scale of the problem: