Skip to content

Commit 58a4fbf

Browse files
committed
Support joins in update_all
Ref rails/rails#53950
1 parent 43737b8 commit 58a4fbf

File tree

2 files changed

+118
-22
lines changed

2 files changed

+118
-22
lines changed

lib/arel/visitors/sqlserver.rb

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,77 @@ def visit_Arel_Nodes_Concat(o, collector)
2929
visit o.right, collector
3030
end
3131

32+
# def visit_Arel_Nodes_UpdateStatement(o, collector)
33+
# if has_join_and_composite_primary_key?(o)
34+
# update_statement_using_join(o, collector)
35+
# else
36+
# o.limit = Nodes::Limit.new(9_223_372_036_854_775_807) if o.orders.any? && o.limit.nil?
37+
#
38+
# super
39+
# end
40+
# end
41+
3242
def visit_Arel_Nodes_UpdateStatement(o, collector)
33-
if has_join_and_composite_primary_key?(o)
34-
update_statement_using_join(o, collector)
43+
collector.retryable = false
44+
o = prepare_update_statement(o)
45+
46+
collector << "UPDATE "
47+
48+
# UPDATE with JOIN is in the form of:
49+
#
50+
# UPDATE t1
51+
# SET ..
52+
# FROM t2
53+
# WHERE t1.join_id = t2.join_id
54+
#
55+
# Or if more than one join is present:
56+
#
57+
# UPDATE t1
58+
# SET ..
59+
# FROM t2
60+
# JOIN t3 ON t2.join_id = t3.join_id
61+
# WHERE t1.join_id = t2.join_id
62+
if has_join_sources?(o)
63+
visit o.relation.left, collector
64+
collect_nodes_for o.values, collector, " SET "
65+
collector << " FROM "
66+
first_join, *remaining_joins = o.relation.right
67+
visit first_join.left, collector
68+
69+
if remaining_joins && !remaining_joins.empty?
70+
collector << " "
71+
remaining_joins.each do |join|
72+
visit join, collector
73+
end
74+
end
75+
76+
collect_nodes_for [first_join.right.expr] + o.wheres, collector, " WHERE ", " AND "
77+
else
78+
collector = visit o.relation, collector
79+
collect_nodes_for o.values, collector, " SET "
80+
collect_nodes_for o.wheres, collector, " WHERE ", " AND "
81+
end
82+
83+
collect_nodes_for o.orders, collector, " ORDER BY "
84+
maybe_visit o.limit, collector
85+
end
86+
87+
# In the simple case, PostgreSQL allows us to place FROM or JOINs directly into the UPDATE
88+
# query. However, this does not allow for LIMIT, OFFSET and ORDER. To support
89+
# these, we must use a subquery.
90+
def prepare_update_statement(o)
91+
92+
93+
if has_join_sources?(o) && !has_limit_or_offset_or_orders?(o) && !has_group_by_and_having?(o)
94+
o
3595
else
3696
o.limit = Nodes::Limit.new(9_223_372_036_854_775_807) if o.orders.any? && o.limit.nil?
3797

3898
super
3999
end
40100
end
41101

102+
42103
def visit_Arel_Nodes_DeleteStatement(o, collector)
43104
if has_join_and_composite_primary_key?(o)
44105
delete_statement_using_join(o, collector)
@@ -61,16 +122,16 @@ def delete_statement_using_join(o, collector)
61122
collect_nodes_for o.wheres, collector, " WHERE ", " AND "
62123
end
63124

64-
def update_statement_using_join(o, collector)
65-
collector.retryable = false
66-
67-
collector << "UPDATE "
68-
visit o.relation.left, collector
69-
collect_nodes_for o.values, collector, " SET "
70-
collector << " FROM "
71-
visit o.relation, collector
72-
collect_nodes_for o.wheres, collector, " WHERE ", " AND "
73-
end
125+
# def update_statement_using_join(o, collector)
126+
# collector.retryable = false
127+
#
128+
# collector << "UPDATE "
129+
# visit o.relation.left, collector
130+
# collect_nodes_for o.values, collector, " SET "
131+
# collector << " FROM "
132+
# visit o.relation, collector
133+
# collect_nodes_for o.wheres, collector, " WHERE ", " AND "
134+
# end
74135

75136
def visit_Arel_Nodes_Lock(o, collector)
76137
o.expr = Arel.sql("WITH(UPDLOCK)") if o.expr.to_s =~ /FOR UPDATE/

test/cases/coerced_tests.rb

Lines changed: 45 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1303,18 +1303,25 @@ def test_update_coerced
13031303

13041304
require "models/author"
13051305
class UpdateAllTest < ActiveRecord::TestCase
1306-
# Rails test required updating a identity column.
1306+
# Regular expression slightly different.
13071307
coerce_tests! :test_update_all_doesnt_ignore_order
13081308
def test_update_all_doesnt_ignore_order_coerced
1309-
david, mary = authors(:david), authors(:mary)
1310-
_(david.id).must_equal 1
1311-
_(mary.id).must_equal 2
1312-
_(david.name).wont_equal mary.name
1313-
assert_queries_match(/UPDATE.*\(SELECT \[authors\].\[id\] FROM \[authors\].*ORDER BY \[authors\].\[id\]/i) do
1314-
Author.where("[id] > 1").order(:id).update_all(name: "Test")
1309+
assert_equal authors(:david).id + 1, authors(:mary).id # make sure there is going to be a duplicate PK error
1310+
test_update_with_order_succeeds = lambda do |order|
1311+
Author.order(order).update_all("id = id + 1")
1312+
rescue ActiveRecord::ActiveRecordError
1313+
false
1314+
end
1315+
1316+
if test_update_with_order_succeeds.call("id DESC")
1317+
# test that this wasn't a fluke and using an incorrect order results in an exception
1318+
assert_not test_update_with_order_succeeds.call("id ASC")
1319+
else
1320+
# test that we're failing because the current Arel's engine doesn't support UPDATE ORDER BY queries is using subselects instead
1321+
assert_queries_match(/\AUPDATE .+ \(SELECT .* ORDER BY id DESC.*\)/i) do
1322+
test_update_with_order_succeeds.call("id DESC")
1323+
end
13151324
end
1316-
_(david.reload.name).must_equal "David"
1317-
_(mary.reload.name).must_equal "Test"
13181325
end
13191326

13201327
# SELECT columns must be in the GROUP clause.
@@ -1332,6 +1339,34 @@ def test_update_all_with_group_by_coerced
13321339
post = Post.select(:id, :title).group(:title).joins(:comments).group("posts.id").having("count(comments.id) < #{minimum_comments_count}").first
13331340
assert_not_equal "ig", post.title
13341341
end
1342+
1343+
# TODO
1344+
coerce_tests! :test_dynamic_update_all_with_one_joined_table
1345+
def test_dynamic_update_all_with_one_joined_table_coerced
1346+
update_fragment = "name = pets.name"
1347+
1348+
toys = Toy.joins(:pet)
1349+
assert_equal 3, toys.count
1350+
assert_equal 3, toys.update_all(update_fragment)
1351+
1352+
toys.each do |toy|
1353+
assert_equal toy.pet.name, toy.name
1354+
end
1355+
end
1356+
1357+
# TODO
1358+
coerce_tests! :test_dynamic_update_all_with_two_joined_table
1359+
def test_dynamic_update_all_with_two_joined_table_coerced
1360+
update_fragment = "name = owners.name"
1361+
1362+
toys = Toy.joins(pet: [:owner])
1363+
assert_equal 3, toys.count
1364+
assert_equal 3, toys.update_all(update_fragment)
1365+
1366+
toys.each do |toy|
1367+
assert_equal toy.pet.owner.name, toy.name
1368+
end
1369+
end
13351370
end
13361371

13371372
class DeleteAllTest < ActiveRecord::TestCase
@@ -1971,7 +2006,7 @@ def with_marshable_time_defaults
19712006
# Revert changes
19722007
@connection.change_column_default(:sst_datatypes, :datetime, current_default) if current_default.present?
19732008
end
1974-
2009+
19752010
# We need to give the full paths for this to work.
19762011
undef_method :schema_dump_5_1_path
19772012
def schema_dump_5_1_path

0 commit comments

Comments
 (0)