Skip to content

Commit 58ecdf9

Browse files
authored
Merge pull request rails#54962 from Shopify/insert-all-default-primary-key
Set default for primary keys in `insert_all`/`upsert_all`
2 parents b61318b + 78f06aa commit 58ecdf9

File tree

6 files changed

+57
-22
lines changed

6 files changed

+57
-22
lines changed

activerecord/CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
* Set default for primary keys in `insert_all`/`upsert_all`.
2+
3+
Previously in Postgres, updating and inserting new records in one upsert wasn't possible
4+
due to null primary key values. `nil` primary key values passed into `insert_all`/`upsert_all`
5+
are now implicitly set to the default insert value specified by adapter.
6+
7+
*Jenny Shen*
8+
19
* Add a load hook `active_record_database_configurations` for `ActiveRecord::DatabaseConfigurations`
210

311
*Mike Dalessio*

activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -550,7 +550,14 @@ def internal_exec_query(...) # :nodoc:
550550
cast_result(internal_execute(...))
551551
end
552552

553+
def default_insert_value(column) # :nodoc:
554+
DEFAULT_INSERT_VALUE
555+
end
556+
553557
private
558+
DEFAULT_INSERT_VALUE = Arel.sql("DEFAULT").freeze
559+
private_constant :DEFAULT_INSERT_VALUE
560+
554561
# Lowest level way to execute a query. Doesn't check for illegal writes, doesn't annotate queries, yields a native result object.
555562
def raw_execute(sql, name = nil, binds = [], prepare: false, async: false, allow_retry: false, materialize_transactions: true, batch: false)
556563
type_casted_binds = type_casted_binds(binds)
@@ -607,13 +614,6 @@ def execute_batch(statements, name = nil, **kwargs)
607614
end
608615
end
609616

610-
DEFAULT_INSERT_VALUE = Arel.sql("DEFAULT").freeze
611-
private_constant :DEFAULT_INSERT_VALUE
612-
613-
def default_insert_value(column)
614-
DEFAULT_INSERT_VALUE
615-
end
616-
617617
def build_fixture_sql(fixtures, table_name)
618618
columns = schema_cache.columns_hash(table_name).reject { |_, column| supports_virtual_columns? && column.virtual? }
619619

activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,16 +45,16 @@ def build_explain_clause(options = [])
4545
end
4646
end
4747

48+
def default_insert_value(column) # :nodoc:
49+
super unless column.auto_increment?
50+
end
51+
4852
private
4953
# https://mariadb.com/kb/en/analyze-statement/
5054
def analyze_without_explain?
5155
mariadb? && database_version >= "10.1.0"
5256
end
5357

54-
def default_insert_value(column)
55-
super unless column.auto_increment?
56-
end
57-
5858
def returning_column_values(result)
5959
if supports_insert_returning?
6060
result.rows.first

activerecord/lib/active_record/connection_adapters/sqlite3/database_statements.rb

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,14 @@ def reset_isolation_level # :nodoc:
6161
@previous_read_uncommitted = nil
6262
end
6363

64+
def default_insert_value(column) # :nodoc:
65+
if column.default_function
66+
Arel.sql(column.default_function)
67+
else
68+
column.default
69+
end
70+
end
71+
6472
private
6573
def internal_begin_transaction(mode, isolation)
6674
if isolation
@@ -136,14 +144,6 @@ def build_truncate_statement(table_name)
136144
def returning_column_values(result)
137145
result.rows.first
138146
end
139-
140-
def default_insert_value(column)
141-
if column.default_function
142-
Arel.sql(column.default_function)
143-
else
144-
column.default
145-
end
146-
end
147147
end
148148
end
149149
end

activerecord/lib/active_record/insert_all.rb

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ def timestamps_for_create
225225
class Builder # :nodoc:
226226
attr_reader :model
227227

228-
delegate :skip_duplicates?, :update_duplicates?, :keys, :keys_including_timestamps, :record_timestamps?, to: :insert_all
228+
delegate :skip_duplicates?, :update_duplicates?, :keys, :keys_including_timestamps, :record_timestamps?, :primary_keys, to: :insert_all
229229

230230
def initialize(insert_all)
231231
@insert_all, @model, @connection = insert_all, insert_all.model, insert_all.connection
@@ -239,8 +239,13 @@ def values_list
239239
types = extract_types_from_columns_on(model.table_name, keys: keys_including_timestamps)
240240

241241
values_list = insert_all.map_key_with_value do |key, value|
242-
next value if Arel::Nodes::SqlLiteral === value
243-
ActiveModel::Type::SerializeCastValue.serialize(type = types[key], type.cast(value))
242+
if Arel::Nodes::SqlLiteral === value
243+
value
244+
elsif primary_keys.include?(key) && value.nil?
245+
connection.default_insert_value(column_from_key(key))
246+
else
247+
ActiveModel::Type::SerializeCastValue.serialize(type = types[key], type.cast(value))
248+
end
244249
end
245250

246251
connection.visitor.compile(Arel::Nodes::ValuesList.new(values_list))
@@ -323,6 +328,10 @@ def quote_columns(columns)
323328
def quote_column(column)
324329
connection.quote_column_name(column)
325330
end
331+
332+
def column_from_key(key)
333+
model.schema_cache.columns_hash(model.table_name)[key]
334+
end
326335
end
327336
end
328337
end

activerecord/test/cases/insert_all_test.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,24 @@ def test_upsert_all_updates_existing_record_by_primary_key
409409
assert_equal "New edition", Book.find(1).name
410410
end
411411

412+
def test_upsert_all_implicitly_sets_primary_keys_when_nil
413+
assert_difference "Book.count", 2 do
414+
Book.upsert_all [
415+
{ id: 1, name: "New edition" },
416+
{ id: nil, name: "New edition 2" },
417+
{ id: nil, name: "New edition 3" },
418+
]
419+
end
420+
421+
assert_equal "New edition", Book.find(1).name
422+
end
423+
424+
def test_insert_all_implicitly_sets_primary_keys_when_nil
425+
assert_difference "Book.count", 1 do
426+
Book.insert_all [ { id: nil, name: "New edition" } ]
427+
end
428+
end
429+
412430
def test_upsert_all_only_applies_last_value_when_given_duplicate_identifiers
413431
skip unless supports_insert_on_duplicate_update? && !current_adapter?(:PostgreSQLAdapter)
414432

0 commit comments

Comments
 (0)