class ActiveRecord::InsertAll
:nodoc:
def configure_on_duplicate_update_logic
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" end if update_only.present? @updatable_columns = Array(update_only) @on_duplicate = :update elsif custom_update_sql_provided? @update_sql = on_duplicate @on_duplicate = :update end end
def custom_update_sql_provided?
def custom_update_sql_provided? @custom_update_sql_provided ||= Arel.arel_node?(on_duplicate) end
def disallow_raw_sql!(value)
def disallow_raw_sql!(value) return if !value.is_a?(String) || Arel.arel_node?(value) raise ArgumentError, "Dangerous query method (method whose arguments are used as raw " \ "SQL) called: #{value}. " \ "Known-safe values can be passed " \ "by wrapping them in Arel.sql()." end
def ensure_valid_options_for_connection!
def ensure_valid_options_for_connection! if returning && !connection.supports_insert_returning? raise ArgumentError, "#{connection.class} does not support :returning" end if skip_duplicates? && !connection.supports_insert_on_duplicate_skip? raise ArgumentError, "#{connection.class} does not support skipping duplicates" end if update_duplicates? && !connection.supports_insert_on_duplicate_update? raise ArgumentError, "#{connection.class} does not support upsert" end if unique_by && !connection.supports_insert_conflict_target? raise ArgumentError, "#{connection.class} does not support :unique_by" end end
def execute
def execute message = +"#{model} " message << "Bulk " if inserts.many? message << (on_duplicate == :update ? "Upsert" : "Insert") connection.exec_insert_all to_sql, message end
def find_unique_index_for(unique_by)
def find_unique_index_for(unique_by) if !connection.supports_insert_conflict_target? return if unique_by.nil? raise ArgumentError, "#{connection.class} does not support :unique_by" end name_or_columns = unique_by || model.primary_key match = Array(name_or_columns).map(&:to_s) if index = unique_indexes.find { |i| match.include?(i.name) || i.columns == match } index elsif match == primary_keys unique_by.nil? ? nil : ActiveRecord::ConnectionAdapters::IndexDefinition.new(model.table_name, "#{model.table_name}_primary_key", true, match) else raise ArgumentError, "No unique index found for #{name_or_columns}" end end
def initialize(model, inserts, on_duplicate:, update_only: nil, returning: nil, unique_by: nil, record_timestamps: nil)
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) @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) configure_on_duplicate_update_logic if model.scope_attributes? @scope_attributes = model.scope_attributes @keys |= @scope_attributes.keys end @keys = @keys.to_set @returning = (connection.supports_insert_returning? ? primary_keys : false) if @returning.nil? @returning = false if @returning == [] @unique_by = find_unique_index_for(unique_by) @on_duplicate = :skip if @on_duplicate == :update && updatable_columns.empty? ensure_valid_options_for_connection! end
def keys_including_timestamps
def keys_including_timestamps @keys_including_timestamps ||= if record_timestamps? keys + model.all_timestamp_attributes_in_model else keys end end
def map_key_with_value
def map_key_with_value inserts.map do |attributes| attributes = attributes.stringify_keys attributes.merge!(scope_attributes) if scope_attributes attributes.reverse_merge!(timestamps_for_create) if record_timestamps? verify_attributes(attributes) keys_including_timestamps.map do |key| yield key, attributes[key] end end end
def primary_keys
def primary_keys Array(connection.schema_cache.primary_keys(model.table_name)) end
def readonly_columns
def readonly_columns primary_keys + model.readonly_attributes.to_a end
def record_timestamps?
def record_timestamps? @record_timestamps end
def skip_duplicates?
def skip_duplicates? on_duplicate == :skip end
def timestamps_for_create
def timestamps_for_create model.all_timestamp_attributes_in_model.index_with(connection.high_precision_current_timestamp) end
def to_sql
def to_sql connection.build_insert_sql(ActiveRecord::InsertAll::Builder.new(self)) end
def unique_by_columns
def unique_by_columns Array(unique_by&.columns) end
def unique_indexes
def unique_indexes connection.schema_cache.indexes(model.table_name).select(&:unique) end
def updatable_columns
def updatable_columns @updatable_columns ||= keys - readonly_columns - unique_by_columns end
def update_duplicates?
def update_duplicates? on_duplicate == :update end
def verify_attributes(attributes)
def verify_attributes(attributes) if keys_including_timestamps != attributes.keys.to_set raise ArgumentError, "All objects being inserted must have the same keys" end end