Skip to content

Commit f314bae

Browse files
authored
Merge pull request rails#43580 from ignacio-chiazzo/ignacio/delete_all
Use nested queries when doing DELETE and GROUP_BY and HAVINAG clauses…
2 parents 33ca392 + 989d53b commit f314bae

File tree

5 files changed

+53
-6
lines changed

5 files changed

+53
-6
lines changed

activerecord/CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
* Use subquery for DELETE with GROUP_BY and HAVING clauses.
2+
3+
Prior to this change, deletes with GROUP_BY and HAVING were returning an error.
4+
5+
After this change, GROUP_BY and HAVING are valid clauses in DELETE queries, generating the following query:
6+
7+
```sql
8+
DELETE FROM "posts" WHERE "posts"."id" IN (
9+
SELECT "posts"."id" FROM "posts" INNER JOIN "comments" ON "comments"."post_id" = "posts"."id" GROUP BY "posts"."id" HAVING (count(comments.id) >= 2))
10+
) [["flagged", "t"]]
11+
```
12+
13+
*Ignacio Chiazzo Cardarello*
14+
115
* Use subquery for UPDATE with GROUP_BY and HAVING clauses.
216

317
Prior to this change, updates with GROUP_BY and HAVING were being ignored, generating a SQL like this:

activerecord/lib/active_record/relation.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class Relation
1111
:reverse_order, :distinct, :create_with, :skip_query_cache]
1212

1313
CLAUSE_METHODS = [:where, :having, :from]
14-
INVALID_METHODS_FOR_DELETE_ALL = [:distinct, :group, :having]
14+
INVALID_METHODS_FOR_DELETE_ALL = [:distinct]
1515

1616
VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS + CLAUSE_METHODS
1717

@@ -616,7 +616,9 @@ def delete_all
616616
arel = eager_loading? ? apply_join_dependency.arel : build_arel
617617
arel.source.left = table
618618

619-
stmt = arel.compile_delete(table[primary_key])
619+
group_values_arel_columns = arel_columns(group_values.uniq)
620+
having_clause_ast = having_clause.ast unless having_clause.empty?
621+
stmt = arel.compile_delete(table[primary_key], having_clause_ast, group_values_arel_columns)
620622

621623
klass.connection.delete(stmt, "#{klass} Delete All").tap { reset }
622624
end

activerecord/lib/arel/crud.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,15 @@ def compile_update(
3333
um
3434
end
3535

36-
def compile_delete(key = nil)
36+
def compile_delete(key = nil, having_clause = nil, group_values_columns = [])
3737
dm = DeleteManager.new(source)
3838
dm.take(limit)
3939
dm.offset(offset)
4040
dm.order(*orders)
4141
dm.wheres = constraints
4242
dm.key = key
43+
dm.group(group_values_columns) unless group_values_columns.empty?
44+
dm.having(having_clause) unless having_clause.nil?
4345
dm
4446
end
4547
end

activerecord/lib/arel/delete_manager.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,21 @@ def from(relation)
1212
@ast.relation = relation
1313
self
1414
end
15+
16+
def group(columns)
17+
columns.each do |column|
18+
column = Nodes::SqlLiteral.new(column) if String === column
19+
column = Nodes::SqlLiteral.new(column.to_s) if Symbol === column
20+
21+
@ast.groups.push Nodes::Group.new column
22+
end
23+
24+
self
25+
end
26+
27+
def having(expr)
28+
@ast.havings << expr
29+
self
30+
end
1531
end
1632
end

activerecord/test/cases/relation/delete_all_test.rb

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,10 @@
55
require "models/post"
66
require "models/pet"
77
require "models/toy"
8+
require "models/comment"
89

910
class DeleteAllTest < ActiveRecord::TestCase
10-
fixtures :authors, :author_addresses, :posts, :pets, :toys
11+
fixtures :authors, :author_addresses, :comments, :posts, :pets, :toys
1112

1213
def test_destroy_all
1314
davids = Author.where(name: "David")
@@ -53,10 +54,22 @@ def test_delete_all_loaded
5354
assert_predicate davids, :loaded?
5455
end
5556

57+
def test_delete_all_with_group_by_and_having
58+
minimum_comments_count = 2
59+
posts_to_be_deleted = Post.most_commented(minimum_comments_count).all.to_a
60+
assert_operator posts_to_be_deleted.length, :>, 0
61+
62+
assert_difference("Post.count", -posts_to_be_deleted.length) do
63+
Post.most_commented(minimum_comments_count).delete_all
64+
end
65+
66+
posts_to_be_deleted.each do |deleted_post|
67+
assert_raise(ActiveRecord::RecordNotFound) { deleted_post.reload }
68+
end
69+
end
70+
5671
def test_delete_all_with_unpermitted_relation_raises_error
5772
assert_raises(ActiveRecord::ActiveRecordError) { Author.distinct.delete_all }
58-
assert_raises(ActiveRecord::ActiveRecordError) { Author.group(:name).delete_all }
59-
assert_raises(ActiveRecord::ActiveRecordError) { Author.having("SUM(id) < 3").delete_all }
6073
end
6174

6275
def test_delete_all_with_joins_and_where_part_is_hash

0 commit comments

Comments
 (0)