Skip to content

Commit 3a8668b

Browse files
boblailkaspth
authored andcommitted
Touch updated_at when upsert_all modifies a record
- When a user passes `updated_at` to `upsert_all`, the given value is used. - When a user omits `updated_at`, `upsert_all` touches the timestamp if (but only if) any upserted values differ. Preserve Rails' ability to generate intelligent cache keys for ActiveRecord when using `upsert_all` frequently to sync imported data.
1 parent 31c0dbf commit 3a8668b

File tree

6 files changed

+59
-0
lines changed

6 files changed

+59
-0
lines changed

activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,7 @@ def build_insert_sql(insert) # :nodoc:
510510
sql << " ON DUPLICATE KEY UPDATE #{no_op_column}=#{no_op_column}"
511511
elsif insert.update_duplicates?
512512
sql << " ON DUPLICATE KEY UPDATE "
513+
sql << insert.touch_model_timestamps_unless { |column| "#{column}<=>VALUES(#{column})" }
513514
sql << insert.updatable_columns.map { |column| "#{column}=VALUES(#{column})" }.join(",")
514515
end
515516

activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,7 @@ def build_insert_sql(insert) # :nodoc:
450450
sql << " ON CONFLICT #{insert.conflict_target} DO NOTHING"
451451
elsif insert.update_duplicates?
452452
sql << " ON CONFLICT #{insert.conflict_target} DO UPDATE SET "
453+
sql << insert.touch_model_timestamps_unless { |column| "#{insert.model.quoted_table_name}.#{column} IS NOT DISTINCT FROM excluded.#{column}" }
453454
sql << insert.updatable_columns.map { |column| "#{column}=excluded.#{column}" }.join(",")
454455
end
455456

activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,7 @@ def build_insert_sql(insert) # :nodoc:
319319
sql << " ON CONFLICT #{insert.conflict_target} DO NOTHING"
320320
elsif insert.update_duplicates?
321321
sql << " ON CONFLICT #{insert.conflict_target} DO UPDATE SET "
322+
sql << insert.touch_model_timestamps_unless { |column| "#{column} IS excluded.#{column}" }
322323
sql << insert.updatable_columns.map { |column| "#{column}=excluded.#{column}" }.join(",")
323324
end
324325

activerecord/lib/active_record/insert_all.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,21 @@ def updatable_columns
153153
quote_columns(insert_all.updatable_columns)
154154
end
155155

156+
def touch_model_timestamps_unless(&block)
157+
model.send(:timestamp_attributes_for_update_in_model).map do |column_name|
158+
if touch_timestamp_attribute?(column_name)
159+
"#{column_name}=(CASE WHEN (#{updatable_columns.map(&block).join(" AND ")}) THEN #{model.quoted_table_name}.#{column_name} ELSE CURRENT_TIMESTAMP END),"
160+
end
161+
end.compact.join
162+
end
163+
156164
private
157165
attr_reader :connection, :insert_all
158166

167+
def touch_timestamp_attribute?(column_name)
168+
update_duplicates? && !insert_all.updatable_columns.include?(column_name)
169+
end
170+
159171
def columns_list
160172
format_columns(insert_all.keys)
161173
end

activerecord/test/cases/insert_all_test.rb

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,46 @@ def test_upsert_all_does_not_perform_an_upsert_if_a_partial_index_doesnt_apply
273273
assert_equal ["Out of the Silent Planet", "Perelandra"], Book.where(isbn: "1974522598").order(:name).pluck(:name)
274274
end
275275

276+
def test_upsert_all_does_not_touch_updated_at_when_values_do_not_change
277+
skip unless supports_insert_on_duplicate_update?
278+
279+
updated_at = Time.now.utc - 5.years
280+
Book.insert_all [{ id: 101, name: "Out of the Silent Planet", published_on: Date.new(1938, 4, 1), updated_at: updated_at }]
281+
Book.upsert_all [{ id: 101, name: "Out of the Silent Planet", published_on: Date.new(1938, 4, 1) }]
282+
283+
assert_in_delta updated_at, Book.find(101).updated_at, 1
284+
end
285+
286+
def test_upsert_all_touches_updated_at_and_updated_on_when_values_change
287+
skip unless supports_insert_on_duplicate_update?
288+
289+
Book.insert_all [{ id: 101, name: "Out of the Silent Planet", published_on: Date.new(1938, 4, 1), updated_at: 5.years.ago, updated_on: 5.years.ago }]
290+
Book.upsert_all [{ id: 101, name: "Out of the Silent Planet", published_on: Date.new(1938, 4, 8) }]
291+
292+
assert_equal Time.now.year, Book.find(101).updated_at.year
293+
assert_equal Time.now.year, Book.find(101).updated_on.year
294+
end
295+
296+
def test_upsert_all_uses_given_updated_at_over_implicit_updated_at
297+
skip unless supports_insert_on_duplicate_update?
298+
299+
updated_at = Time.now.utc - 1.year
300+
Book.insert_all [{ id: 101, name: "Out of the Silent Planet", published_on: Date.new(1938, 4, 1), updated_at: 5.years.ago }]
301+
Book.upsert_all [{ id: 101, name: "Out of the Silent Planet", published_on: Date.new(1938, 4, 8), updated_at: updated_at }]
302+
303+
assert_in_delta updated_at, Book.find(101).updated_at, 1
304+
end
305+
306+
def test_upsert_all_uses_given_updated_on_over_implicit_updated_on
307+
skip unless supports_insert_on_duplicate_update?
308+
309+
updated_on = Time.now.utc.to_date - 30
310+
Book.insert_all [{ id: 101, name: "Out of the Silent Planet", published_on: Date.new(1938, 4, 1), updated_on: 5.years.ago }]
311+
Book.upsert_all [{ id: 101, name: "Out of the Silent Planet", published_on: Date.new(1938, 4, 8), updated_on: updated_on }]
312+
313+
assert_equal updated_on, Book.find(101).updated_on
314+
end
315+
276316
def test_insert_all_raises_on_unknown_attribute
277317
assert_raise ActiveRecord::UnknownAttributeError do
278318
Book.insert_all! [{ unknown_attribute: "Test" }]

activerecord/test/schema/schema.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@
119119
t.datetime :published_on
120120
t.index [:author_id, :name], unique: true
121121
t.index :isbn, where: "published_on IS NOT NULL", unique: true
122+
123+
t.datetime :created_at
124+
t.datetime :updated_at
125+
t.date :updated_on
122126
end
123127

124128
create_table :booleans, force: true do |t|

0 commit comments

Comments
 (0)