Skip to content

Commit 4acb666

Browse files
ignacio chiazzoignacio chiazzo
authored andcommitted
Use nested queries when doing UPDATE in myslq and GROUP_BY and HAVING clauses are present.
MySQL does not support GROUP_BY and HAVING on UPDATE, we need to use a Subquery.
1 parent d4d16e2 commit 4acb666

File tree

12 files changed

+143
-6
lines changed

12 files changed

+143
-6
lines changed

activerecord/CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,24 @@
1+
* Use subquery for UPDATE with GROUP_BY and HAVING clauses.
2+
3+
Prior to this change, updates with GROUP_BY and HAVING were being ignored, generating a SQL like this:
4+
5+
```sql
6+
UPDATE "posts" SET "flagged" = ? WHERE "posts"."id" IN (
7+
SELECT "posts"."id" FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id"
8+
) [["flagged", "t"]]
9+
```
10+
11+
After this change, GROUP_BY and HAVING clauses are used as a subquery in updates, like this:
12+
13+
```sql
14+
UPDATE "posts" SET "flagged" = ? WHERE "posts"."id" IN (
15+
SELECT "posts"."id" FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id"
16+
GROUP BY posts.id HAVING (count(comments.id) >= 2)
17+
) [["flagged", "t"]]
18+
```
19+
20+
*Ignacio Chiazzo Cardarello*
21+
122
* Add support for setting the filename of the schema or structure dump in the database config.
223

324
Applications may now set their the filename or path of the schema / structure dump file in their database configuration.

activerecord/lib/active_record/relation.rb

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -485,8 +485,9 @@ def update_all(updates)
485485
arel = eager_loading? ? apply_join_dependency.arel : build_arel
486486
arel.source.left = table
487487

488-
stmt = arel.compile_update(values, table[primary_key])
489-
488+
group_values_arel_columns = arel_columns(group_values.uniq)
489+
having_clause_ast = having_clause.ast unless having_clause.empty?
490+
stmt = arel.compile_update(values, table[primary_key], having_clause_ast, group_values_arel_columns)
490491
klass.connection.update(stmt, "#{klass} Update All").tap { reset }
491492
end
492493

activerecord/lib/arel/crud.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,14 +14,22 @@ def create_insert
1414
InsertManager.new
1515
end
1616

17-
def compile_update(values, key = nil)
17+
def compile_update(
18+
values,
19+
key = nil,
20+
having_clause = nil,
21+
group_values_columns = []
22+
)
1823
um = UpdateManager.new(source)
1924
um.set(values)
2025
um.take(limit)
2126
um.offset(offset)
2227
um.order(*orders)
2328
um.wheres = constraints
2429
um.key = key
30+
31+
um.group(group_values_columns) unless group_values_columns.empty?
32+
um.having(having_clause) unless having_clause.nil?
2533
um
2634
end
2735

activerecord/lib/arel/nodes/delete_statement.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
module Arel # :nodoc: all
44
module Nodes
55
class DeleteStatement < Arel::Nodes::Node
6-
attr_accessor :relation, :wheres, :orders, :limit, :offset, :key
6+
attr_accessor :relation, :wheres, :groups, :havings, :orders, :limit, :offset, :key
77

88
def initialize(relation = nil, wheres = [])
99
super()
1010
@relation = relation
1111
@wheres = wheres
12+
@groups = []
13+
@havings = []
1214
@orders = []
1315
@limit = nil
1416
@offset = nil
@@ -30,6 +32,8 @@ def eql?(other)
3032
self.relation == other.relation &&
3133
self.wheres == other.wheres &&
3234
self.orders == other.orders &&
35+
self.groups == other.groups &&
36+
self.havings == other.havings &&
3337
self.limit == other.limit &&
3438
self.offset == other.offset &&
3539
self.key == other.key

activerecord/lib/arel/nodes/update_statement.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
module Arel # :nodoc: all
44
module Nodes
55
class UpdateStatement < Arel::Nodes::Node
6-
attr_accessor :relation, :wheres, :values, :orders, :limit, :offset, :key
6+
attr_accessor :relation, :wheres, :values, :groups, :havings, :orders, :limit, :offset, :key
77

88
def initialize(relation = nil)
99
super()
1010
@relation = relation
1111
@wheres = []
1212
@values = []
13+
@groups = []
14+
@havings = []
1315
@orders = []
1416
@limit = nil
1517
@offset = nil
@@ -31,6 +33,8 @@ def eql?(other)
3133
self.relation == other.relation &&
3234
self.wheres == other.wheres &&
3335
self.values == other.values &&
36+
self.groups == other.groups &&
37+
self.havings == other.havings &&
3438
self.orders == other.orders &&
3539
self.limit == other.limit &&
3640
self.offset == other.offset &&

activerecord/lib/arel/update_manager.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,21 @@ def set(values)
2828
end
2929
self
3030
end
31+
32+
def group(columns)
33+
columns.each do |column|
34+
column = Nodes::SqlLiteral.new(column) if String === column
35+
column = Nodes::SqlLiteral.new(column.to_s) if Symbol === column
36+
37+
@ast.groups.push Nodes::Group.new column
38+
end
39+
40+
self
41+
end
42+
43+
def having(expr)
44+
@ast.havings << expr
45+
self
46+
end
3147
end
3248
end

activerecord/lib/arel/visitors/mysql.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ def visit_Arel_Nodes_NullsFirst(o, collector)
6767
# query. However, this does not allow for LIMIT, OFFSET and ORDER. To support
6868
# these, we must use a subquery.
6969
def prepare_update_statement(o)
70-
if o.offset || has_join_sources?(o) && has_limit_or_offset_or_orders?(o)
70+
if o.offset || has_group_by_and_having?(o) ||
71+
has_join_sources?(o) && has_limit_or_offset_or_orders?(o)
7172
super
7273
else
7374
o

activerecord/lib/arel/visitors/to_sql.rb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -841,6 +841,10 @@ def has_limit_or_offset_or_orders?(o)
841841
o.limit || o.offset || !o.orders.empty?
842842
end
843843

844+
def has_group_by_and_having?(o)
845+
!o.groups.empty? && !o.havings.empty?
846+
end
847+
844848
# The default strategy for an UPDATE with joins is to use a subquery. This doesn't work
845849
# on MySQL (even when aliasing the tables), but MySQL allows using JOIN directly in
846850
# an UPDATE statement, so in the MySQL visitor we redefine this to do that.
@@ -852,6 +856,8 @@ def prepare_update_statement(o)
852856
stmt.orders = []
853857
stmt.wheres = [Nodes::In.new(o.key, [build_subselect(o.key, o)])]
854858
stmt.relation = o.relation.left if has_join_sources?(o)
859+
stmt.groups = o.groups unless o.groups.empty?
860+
stmt.havings = o.havings unless o.havings.empty?
855861
stmt
856862
else
857863
o
@@ -866,6 +872,8 @@ def build_subselect(key, o)
866872
core.froms = o.relation
867873
core.wheres = o.wheres
868874
core.projections = [key]
875+
core.groups = o.groups unless o.groups.empty?
876+
core.havings = o.havings unless o.havings.empty?
869877
stmt.limit = o.limit
870878
stmt.offset = o.offset
871879
stmt.orders = o.orders

activerecord/test/cases/arel/nodes/update_statement_test.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,17 @@
2727
statement1.orders = %w[x y z]
2828
statement1.limit = 42
2929
statement1.key = "zomg"
30+
statement1.groups = ["foo"]
31+
statement1.havings = []
3032
statement2 = Arel::Nodes::UpdateStatement.new
3133
statement2.relation = "zomg"
3234
statement2.wheres = 2
3335
statement2.values = false
3436
statement2.orders = %w[x y z]
3537
statement2.limit = 42
3638
statement2.key = "zomg"
39+
statement2.groups = ["foo"]
40+
statement2.havings = []
3741
array = [statement1, statement2]
3842
assert_equal 1, array.uniq.size
3943
end

activerecord/test/cases/arel/update_manager_test.rb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,57 @@ class UpdateManagerTest < Arel::Spec
2222
assert_match(/LIMIT 10/, um.to_sql)
2323
end
2424

25+
describe "having" do
26+
it "sets having" do
27+
users_table = Table.new(:users)
28+
posts_table = Table.new(:posts)
29+
join_source = Arel::Nodes::InnerJoin.new(users_table, posts_table)
30+
31+
update_manager = Arel::UpdateManager.new
32+
update_manager.table(join_source)
33+
update_manager.group(["posts.id"])
34+
update_manager.having("count(posts.id) >= 2")
35+
36+
assert_equal(["count(posts.id) >= 2"], update_manager.ast.havings)
37+
end
38+
end
39+
40+
describe "group" do
41+
it "adds columns to the AST when group value is a String" do
42+
users_table = Table.new(:users)
43+
posts_table = Table.new(:posts)
44+
join_source = Arel::Nodes::InnerJoin.new(users_table, posts_table)
45+
46+
update_manager = Arel::UpdateManager.new
47+
update_manager.table(join_source)
48+
update_manager.group(["posts.id"])
49+
update_manager.having("count(posts.id) >= 2")
50+
51+
assert_equal(1, update_manager.ast.groups.count)
52+
group_ast = update_manager.ast.groups.first
53+
_(group_ast).must_be_kind_of Nodes::Group
54+
assert_equal("posts.id", group_ast.expr)
55+
assert_equal(["count(posts.id) >= 2"], update_manager.ast.havings)
56+
end
57+
58+
it "adds columns to the AST when group value is a Symbol" do
59+
users_table = Table.new(:users)
60+
posts_table = Table.new(:posts)
61+
join_source = Arel::Nodes::InnerJoin.new(users_table, posts_table)
62+
63+
update_manager = Arel::UpdateManager.new
64+
update_manager.table(join_source)
65+
update_manager.group([:"posts.id"])
66+
update_manager.having("count(posts.id) >= 2")
67+
68+
assert_equal(1, update_manager.ast.groups.count)
69+
group_ast = update_manager.ast.groups.first
70+
_(group_ast).must_be_kind_of Nodes::Group
71+
assert_equal("posts.id", group_ast.expr)
72+
assert_equal(["count(posts.id) >= 2"], update_manager.ast.havings)
73+
end
74+
end
75+
2576
describe "set" do
2677
it "updates with null" do
2778
table = Table.new(:users)

0 commit comments

Comments
 (0)