Skip to content

Commit f3c4d33

Browse files
authored
Support joins in update_all (#1293)
1 parent 43737b8 commit f3c4d33

File tree

2 files changed

+67
-23
lines changed

2 files changed

+67
-23
lines changed

lib/arel/visitors/sqlserver.rb

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

32+
# Same as SQLite and PostgreSQL.
3233
def visit_Arel_Nodes_UpdateStatement(o, collector)
33-
if has_join_and_composite_primary_key?(o)
34-
update_statement_using_join(o, collector)
34+
collector.retryable = false
35+
o = prepare_update_statement(o)
36+
37+
collector << "UPDATE "
38+
39+
# UPDATE with JOIN is in the form of:
40+
#
41+
# UPDATE t1
42+
# SET ..
43+
# FROM t2
44+
# WHERE t1.join_id = t2.join_id
45+
#
46+
# Or if more than one join is present:
47+
#
48+
# UPDATE t1
49+
# SET ..
50+
# FROM t2
51+
# JOIN t3 ON t2.join_id = t3.join_id
52+
# WHERE t1.join_id = t2.join_id
53+
if has_join_sources?(o)
54+
visit o.relation.left, collector
55+
collect_nodes_for o.values, collector, " SET "
56+
collector << " FROM "
57+
first_join, *remaining_joins = o.relation.right
58+
visit first_join.left, collector
59+
60+
if remaining_joins && !remaining_joins.empty?
61+
collector << " "
62+
remaining_joins.each do |join|
63+
visit join, collector
64+
end
65+
end
66+
67+
collect_nodes_for [first_join.right.expr] + o.wheres, collector, " WHERE ", " AND "
68+
else
69+
collector = visit o.relation, collector
70+
collect_nodes_for o.values, collector, " SET "
71+
collect_nodes_for o.wheres, collector, " WHERE ", " AND "
72+
end
73+
74+
collect_nodes_for o.orders, collector, " ORDER BY "
75+
maybe_visit o.limit, collector
76+
end
77+
78+
# Same as PostgreSQL except we need to add limit if using subquery.
79+
def prepare_update_statement(o)
80+
if has_join_sources?(o) && !has_limit_or_offset_or_orders?(o) && !has_group_by_and_having?(o)
81+
o
3582
else
3683
o.limit = Nodes::Limit.new(9_223_372_036_854_775_807) if o.orders.any? && o.limit.nil?
3784

3885
super
3986
end
4087
end
4188

89+
4290
def visit_Arel_Nodes_DeleteStatement(o, collector)
4391
if has_join_and_composite_primary_key?(o)
4492
delete_statement_using_join(o, collector)
@@ -61,17 +109,6 @@ def delete_statement_using_join(o, collector)
61109
collect_nodes_for o.wheres, collector, " WHERE ", " AND "
62110
end
63111

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
74-
75112
def visit_Arel_Nodes_Lock(o, collector)
76113
o.expr = Arel.sql("WITH(UPDLOCK)") if o.expr.to_s =~ /FOR UPDATE/
77114
collector << " "

test/cases/coerced_tests.rb

Lines changed: 17 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.
@@ -1971,7 +1978,7 @@ def with_marshable_time_defaults
19711978
# Revert changes
19721979
@connection.change_column_default(:sst_datatypes, :datetime, current_default) if current_default.present?
19731980
end
1974-
1981+
19751982
# We need to give the full paths for this to work.
19761983
undef_method :schema_dump_5_1_path
19771984
def schema_dump_5_1_path

0 commit comments

Comments
 (0)