Skip to content

Commit c24efdb

Browse files
committed
When skipping duplicates in bulk insert on MySQL, avoid assigning id when not specified
If `id` is an `AUTONUMBER` column, then my former strategy here of assigning `no_op_column` to an arbitrary column would fail in this specific scenario: 1. `model.columns.first` is an AUTONUMBER column 2. `model.columns.first` is not assigned in the insert attributes I added three tests: the first test covers the actual error; the second test documents that this _isn't_ a problem when a value is given for the AUTONUMBER column and the third test ensures that this no-op strategy isn't secretly doing an UPSERT.
1 parent bf1494a commit c24efdb

File tree

3 files changed

+42
-4
lines changed

3 files changed

+42
-4
lines changed

activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -516,8 +516,8 @@ def build_insert_sql(insert) # :nodoc:
516516
sql = +"INSERT #{insert.into} #{insert.values_list}"
517517

518518
if insert.skip_duplicates?
519-
any_column = quote_column_name(insert.model.columns.first.name)
520-
sql << " ON DUPLICATE KEY UPDATE #{any_column}=#{any_column}"
519+
no_op_column = quote_column_name(insert.keys.first)
520+
sql << " ON DUPLICATE KEY UPDATE #{no_op_column}=#{no_op_column}"
521521
elsif insert.update_duplicates?
522522
sql << " ON DUPLICATE KEY UPDATE "
523523
sql << insert.updatable_columns.map { |column| "#{column}=VALUES(#{column})" }.join(",")

activerecord/lib/active_record/insert_all.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ def verify_attributes(attributes)
111111
class Builder
112112
attr_reader :model
113113

114-
delegate :skip_duplicates?, :update_duplicates?, to: :insert_all
114+
delegate :skip_duplicates?, :update_duplicates?, :keys, to: :insert_all
115115

116116
def initialize(insert_all)
117117
@insert_all, @model, @connection = insert_all, insert_all.model, insert_all.connection
@@ -122,7 +122,7 @@ def into
122122
end
123123

124124
def values_list
125-
types = extract_types_from_columns_on(model.table_name, keys: insert_all.keys)
125+
types = extract_types_from_columns_on(model.table_name, keys: keys)
126126

127127
values_list = insert_all.map_key_with_value do |key, value|
128128
bind = Relation::QueryAttribute.new(key, value, types[key])

activerecord/test/cases/insert_all_test.rb

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,44 @@ def test_insert_all_can_skip_duplicate_records
104104
end
105105
end
106106

107+
def test_insert_all_with_skip_duplicates_and_autonumber_id_not_given
108+
skip unless supports_insert_on_duplicate_skip?
109+
110+
assert_difference "Book.count", 1 do
111+
# These two books are duplicates according to an index on %i[author_id name]
112+
# but their IDs are not specified so they will be assigned different IDs
113+
# by autonumber. We will get an exception from MySQL if we attempt to skip
114+
# one of these records by assigning its ID.
115+
Book.insert_all [
116+
{ author_id: 8, name: "Refactoring" },
117+
{ author_id: 8, name: "Refactoring" }
118+
]
119+
end
120+
end
121+
122+
def test_insert_all_with_skip_duplicates_and_autonumber_id_given
123+
skip unless supports_insert_on_duplicate_skip?
124+
125+
assert_difference "Book.count", 1 do
126+
Book.insert_all [
127+
{ id: 200, author_id: 8, name: "Refactoring" },
128+
{ id: 201, author_id: 8, name: "Refactoring" }
129+
]
130+
end
131+
end
132+
133+
def test_skip_duplicates_strategy_does_not_secretly_upsert
134+
skip unless supports_insert_on_duplicate_skip?
135+
136+
book = Book.create!(author_id: 8, name: "Refactoring", format: "EXPECTED")
137+
138+
assert_no_difference "Book.count" do
139+
Book.insert(author_id: 8, name: "Refactoring", format: "UNEXPECTED")
140+
end
141+
142+
assert_equal "EXPECTED", book.reload.format
143+
end
144+
107145
def test_insert_all_will_raise_if_duplicates_are_skipped_only_for_a_certain_conflict_target
108146
skip unless supports_insert_on_duplicate_skip? && supports_insert_conflict_target?
109147

0 commit comments

Comments
 (0)