Skip to content

Commit 85c7530

Browse files
committed
Arel: wrap SELECT statements in parentheses when generating SQL for UNION
This commit fixes generated SQL for `UNION` and `UNION ALL` involving `LIMIT` or `ORDER BY` by wrapping `SELECT` statements appearing in an `UNION` or `UNION ALL` in parentheses.
1 parent 93df871 commit 85c7530

File tree

5 files changed

+36
-19
lines changed

5 files changed

+36
-19
lines changed

activerecord/lib/arel/visitors/postgresql.rb

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ def visit_Arel_Nodes_GroupingSet(o, collector)
6363

6464
def visit_Arel_Nodes_Lateral(o, collector)
6565
collector << "LATERAL "
66-
grouping_parentheses o, collector
66+
grouping_parentheses o.expr, collector
6767
end
6868

6969
def visit_Arel_Nodes_IsNotDistinctFrom(o, collector)
@@ -83,17 +83,6 @@ def visit_Arel_Nodes_IsDistinctFrom(o, collector)
8383

8484
def bind_block; BIND_BLOCK; end
8585

86-
# Used by Lateral visitor to enclose select queries in parentheses
87-
def grouping_parentheses(o, collector)
88-
if o.expr.is_a? Nodes::SelectStatement
89-
collector << "("
90-
visit o.expr, collector
91-
collector << ")"
92-
else
93-
visit o.expr, collector
94-
end
95-
end
96-
9786
# Utilized by GroupingSet, Cube & RollUp visitors to
9887
# handle grouping aggregation semantics
9988
def grouping_array_or_grouping_element(o, collector)

activerecord/lib/arel/visitors/to_sql.rb

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -965,18 +965,30 @@ def infix_value_with_paren(o, collector, value, suppress_parens = false)
965965
collector = if o.left.class == o.class
966966
infix_value_with_paren(o.left, collector, value, true)
967967
else
968-
visit o.left, collector
968+
grouping_parentheses o.left, collector
969969
end
970970
collector << value
971971
collector = if o.right.class == o.class
972972
infix_value_with_paren(o.right, collector, value, true)
973973
else
974-
visit o.right, collector
974+
grouping_parentheses o.right, collector
975975
end
976976
collector << " )" unless suppress_parens
977977
collector
978978
end
979979

980+
# Used by some visitors to enclose select queries in parentheses
981+
def grouping_parentheses(o, collector)
982+
if o.is_a?(Nodes::SelectStatement)
983+
collector << "("
984+
visit o, collector
985+
collector << ")"
986+
collector
987+
else
988+
visit o, collector
989+
end
990+
end
991+
980992
def aggregate(name, o, collector)
981993
collector << "#{name}("
982994
if o.distinct

activerecord/test/cases/arel/attributes/attribute_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -963,7 +963,7 @@ class AttributeTest < Arel::Spec
963963
union = mgr1.union(mgr2)
964964
node = relation[:id].in(union)
965965
_(node.to_sql).must_be_like %{
966-
"users"."id" IN (( SELECT "users"."id" FROM "users" UNION SELECT "users"."id" FROM "users" ))
966+
"users"."id" IN (( (SELECT "users"."id" FROM "users") UNION (SELECT "users"."id" FROM "users") ))
967967
}
968968
end
969969

activerecord/test/cases/arel/select_manager_test.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -253,15 +253,15 @@ def test_join_sources
253253

254254
# maybe FIXME: decide when wrapper parens are needed
255255
_(node.to_sql).must_be_like %{
256-
( SELECT * FROM "users" WHERE "users"."age" < 18 UNION SELECT * FROM "users" WHERE "users"."age" > 99 )
256+
( (SELECT * FROM "users" WHERE "users"."age" < 18) UNION (SELECT * FROM "users" WHERE "users"."age" > 99) )
257257
}
258258
end
259259

260260
it "should union all" do
261261
node = @m1.union :all, @m2
262262

263263
_(node.to_sql).must_be_like %{
264-
( SELECT * FROM "users" WHERE "users"."age" < 18 UNION ALL SELECT * FROM "users" WHERE "users"."age" > 99 )
264+
( (SELECT * FROM "users" WHERE "users"."age" < 18) UNION ALL (SELECT * FROM "users" WHERE "users"."age" > 99) )
265265
}
266266
end
267267
end
@@ -354,9 +354,9 @@ def test_join_sources
354354
sql = manager.to_sql
355355
_(sql).must_be_like %{
356356
WITH RECURSIVE "replies" AS (
357-
SELECT "comments"."id", "comments"."parent_id" FROM "comments" WHERE "comments"."id" = 42
357+
(SELECT "comments"."id", "comments"."parent_id" FROM "comments" WHERE "comments"."id" = 42)
358358
UNION
359-
SELECT "comments"."id", "comments"."parent_id" FROM "comments" INNER JOIN "replies" ON "comments"."parent_id" = "replies"."id"
359+
(SELECT "comments"."id", "comments"."parent_id" FROM "comments" INNER JOIN "replies" ON "comments"."parent_id" = "replies"."id")
360360
)
361361
SELECT * FROM "replies"
362362
}

activerecord/test/cases/arel/visitors/to_sql_test.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -635,6 +635,14 @@ def dispatch
635635
node = Nodes::Union.new Arel.sql("topleft"), subnode
636636
assert_equal("( topleft UNION left UNION right )", compile(node))
637637
end
638+
639+
it "encloses SELECT statements with parentheses" do
640+
table = Table.new(:users)
641+
left = table.where(table[:name].eq(0)).take(1).ast
642+
right = table.where(table[:name].eq(1)).take(1).ast
643+
node = Nodes::Union.new left, right
644+
assert_match(/LIMIT 1\) UNION \(/, compile(node))
645+
end
638646
end
639647

640648
describe "Nodes::UnionAll" do
@@ -646,6 +654,14 @@ def dispatch
646654
node = Nodes::UnionAll.new Arel.sql("topleft"), subnode
647655
assert_equal("( topleft UNION ALL left UNION ALL right )", compile(node))
648656
end
657+
658+
it "encloses SELECT statements with parentheses" do
659+
table = Table.new(:users)
660+
left = table.where(table[:name].eq(0)).take(1).ast
661+
right = table.where(table[:name].eq(1)).take(1).ast
662+
node = Nodes::UnionAll.new left, right
663+
assert_match(/LIMIT 1\) UNION ALL \(/, compile(node))
664+
end
649665
end
650666

651667
describe "Nodes::NotIn" do

0 commit comments

Comments
 (0)