diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index f9f977ac8b..5531178e33 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,15 @@ +* Allow using aliased attributes with `insert_all`/`upsert_all`. + + ```ruby + class Book < ApplicationRecord + alias_attribute :title, :name + end + + Book.insert_all [{ title: "Remote", author_id: 1 }], returning: :title + ``` + + *fatkodima* + * Support encrypted attributes on columns with default db values. This adds support for encrypted attributes defined on columns with default values. diff --git a/activerecord/lib/active_record/insert_all.rb b/activerecord/lib/active_record/insert_all.rb index a82cb8a547..8bc1ca1b2e 100644 --- a/activerecord/lib/active_record/insert_all.rb +++ b/activerecord/lib/active_record/insert_all.rb @@ -10,15 +10,18 @@ module ActiveRecord def initialize(model, inserts, on_duplicate:, update_only: nil, returning: nil, unique_by: nil, record_timestamps: nil) raise ArgumentError, "Empty list of attributes passed" if inserts.blank? - @model, @connection, @inserts, @keys = model, model.connection, inserts, inserts.first.keys.map(&:to_s) + @model, @connection, @inserts = model, model.connection, inserts @on_duplicate, @update_only, @returning, @unique_by = on_duplicate, update_only, returning, unique_by @record_timestamps = record_timestamps.nil? ? model.record_timestamps : record_timestamps disallow_raw_sql!(on_duplicate) disallow_raw_sql!(returning) + resolve_attribute_aliases configure_on_duplicate_update_logic + @keys = @inserts.first.keys.map(&:to_s) + if model.scope_attributes? @scope_attributes = model.scope_attributes @keys |= @scope_attributes.keys @@ -28,7 +31,7 @@ module ActiveRecord @returning = (connection.supports_insert_returning? ? primary_keys : false) if @returning.nil? @returning = false if @returning == [] - @unique_by = find_unique_index_for(unique_by) + @unique_by = find_unique_index_for(@unique_by) @on_duplicate = :skip if @on_duplicate == :update && updatable_columns.empty? ensure_valid_options_for_connection! @@ -88,6 +91,25 @@ module ActiveRecord private attr_reader :scope_attributes + def has_attribute_aliases?(attributes) + attributes.keys.any? { |attribute| model.attribute_alias?(attribute) } + end + + def resolve_attribute_aliases + return unless has_attribute_aliases?(@inserts.first) + + @inserts = @inserts.map do |insert| + insert.transform_keys { |attribute| resolve_attribute_alias(attribute) } + end + + @update_only = Array(@update_only).map { |attribute| resolve_attribute_alias(attribute) } if @update_only + @unique_by = Array(@unique_by).map { |attribute| resolve_attribute_alias(attribute) } if @unique_by + end + + def resolve_attribute_alias(attribute) + model.attribute_alias(attribute) || attribute + end + def configure_on_duplicate_update_logic if custom_update_sql_provided? && update_only.present? raise ArgumentError, "You can't set :update_only and provide custom update SQL via :on_duplicate at the same time" @@ -212,7 +234,13 @@ module ActiveRecord if insert_all.returning.is_a?(String) insert_all.returning else - format_columns(insert_all.returning) + Array(insert_all.returning).map do |attribute| + if model.attribute_alias?(attribute) + "#{quote_column(model.attribute_alias(attribute))} AS #{quote_column(attribute)}" + else + quote_column(attribute) + end + end.join(",") end end @@ -271,7 +299,11 @@ module ActiveRecord end def quote_columns(columns) - columns.map(&connection.method(:quote_column_name)) + columns.map { |column| quote_column(column) } + end + + def quote_column(column) + connection.quote_column_name(column) end end end diff --git a/activerecord/test/cases/insert_all_test.rb b/activerecord/test/cases/insert_all_test.rb index c9114bbd6a..4ec9520dba 100644 --- a/activerecord/test/cases/insert_all_test.rb +++ b/activerecord/test/cases/insert_all_test.rb @@ -263,6 +263,25 @@ class InsertAllTest < ActiveRecord::TestCase end end + def test_insert_all_and_upsert_all_with_aliased_attributes + if supports_insert_returning? + assert_difference "Book.count" do + result = Book.insert_all [{ title: "Remote", author_id: 1 }], returning: :title + assert_includes result.columns, "title" + end + end + + if supports_insert_on_duplicate_update? + Book.upsert_all [{ id: 101, title: "Perelandra", author_id: 7, isbn: "1974522598" }] + Book.upsert_all [{ id: 101, title: "Perelandra 2", author_id: 6, isbn: "111111" }], update_only: %i[ title isbn ] + + book = Book.find(101) + assert_equal "Perelandra 2", book.title, "Should have updated the title" + assert_equal "111111", book.isbn, "Should have updated the isbn" + assert_equal 7, book.author_id, "Should not have updated the author_id" + end + end + def test_upsert_logs_message_including_model_name skip unless supports_insert_on_duplicate_update? diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb index 9fdc575789..e376c0e7d9 100644 --- a/activerecord/test/models/book.rb +++ b/activerecord/test/models/book.rb @@ -11,6 +11,8 @@ class Book < ActiveRecord::Base has_one :essay + alias_attribute :title, :name + enum status: [:proposed, :written, :published] enum last_read: { unread: 0, reading: 2, read: 3, forgotten: nil } enum nullable_status: [:single, :married]