Skip to content

Commit

Permalink
Move ActiveRecord::Base.insert in Relation
Browse files Browse the repository at this point in the history
Ref: rails#50396

As well as related methods.
  • Loading branch information
byroot authored and fractaledmind committed May 13, 2024
1 parent ff022d0 commit de3f3bf
Show file tree
Hide file tree
Showing 6 changed files with 290 additions and 290 deletions.
2 changes: 1 addition & 1 deletion activerecord/lib/active_record/association_relation.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ def #{method}(attributes, **kwargs)
raise ArgumentError, "Bulk insert or upsert is currently not supported for has_many through association"
end
scoping { klass.#{method}(attributes, **kwargs) }
super
end
RUBY
end
Expand Down
21 changes: 9 additions & 12 deletions activerecord/lib/active_record/insert_all.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,16 @@ class InsertAll # :nodoc:
attr_reader :on_duplicate, :update_only, :returning, :unique_by, :update_sql

class << self
def execute(model, ...)
model.with_connection do |c|
new(model, c, ...).execute
def execute(relation, ...)
relation.model.with_connection do |c|
new(relation, c, ...).execute
end
end
end

def initialize(model, connection, inserts, on_duplicate:, update_only: nil, returning: nil, unique_by: nil, record_timestamps: nil)
@model, @connection, @inserts = model, connection, inserts.map(&:stringify_keys)
def initialize(relation, connection, inserts, on_duplicate:, update_only: nil, returning: nil, unique_by: nil, record_timestamps: nil)
@relation = relation
@model, @connection, @inserts = relation.model, connection, inserts.map(&:stringify_keys)
@on_duplicate, @update_only, @returning, @unique_by = on_duplicate, update_only, returning, unique_by
@record_timestamps = record_timestamps.nil? ? model.record_timestamps : record_timestamps

Expand All @@ -31,10 +32,8 @@ def initialize(model, connection, inserts, on_duplicate:, update_only: nil, retu
@keys = @inserts.first.keys
end

if model.scope_attributes?
@scope_attributes = model.scope_attributes
@keys |= @scope_attributes.keys
end
@scope_attributes = relation.scope_for_create.except(@model.inheritance_column)
@keys |= @scope_attributes.keys
@keys = @keys.to_set

@returning = (connection.supports_insert_returning? ? primary_keys : false) if @returning.nil?
Expand Down Expand Up @@ -74,7 +73,7 @@ def update_duplicates?
def map_key_with_value
inserts.map do |attributes|
attributes = attributes.stringify_keys
attributes.merge!(scope_attributes) if scope_attributes
attributes.merge!(@scope_attributes)
attributes.reverse_merge!(timestamps_for_create) if record_timestamps?

verify_attributes(attributes)
Expand All @@ -99,8 +98,6 @@ def keys_including_timestamps
end

private
attr_reader :scope_attributes

def has_attribute_aliases?(attributes)
attributes.keys.any? { |attribute| model.attribute_alias?(attribute) }
end
Expand Down
276 changes: 0 additions & 276 deletions activerecord/lib/active_record/persistence.rb
Original file line number Diff line number Diff line change
Expand Up @@ -87,282 +87,6 @@ def build(attributes = nil, &block)
end
end

# Inserts a single record into the database in a single SQL INSERT
# statement. It does not instantiate any models nor does it trigger
# Active Record callbacks or validations. Though passed values
# go through Active Record's type casting and serialization.
#
# See #insert_all for documentation.
def insert(attributes, returning: nil, unique_by: nil, record_timestamps: nil)
insert_all([ attributes ], returning: returning, unique_by: unique_by, record_timestamps: record_timestamps)
end

# Inserts multiple records into the database in a single SQL INSERT
# statement. It does not instantiate any models nor does it trigger
# Active Record callbacks or validations. Though passed values
# go through Active Record's type casting and serialization.
#
# The +attributes+ parameter is an Array of Hashes. Every Hash determines
# the attributes for a single row and must have the same keys.
#
# Rows are considered to be unique by every unique index on the table. Any
# duplicate rows are skipped.
# Override with <tt>:unique_by</tt> (see below).
#
# Returns an ActiveRecord::Result with its contents based on
# <tt>:returning</tt> (see below).
#
# ==== Options
#
# [:returning]
# (PostgreSQL, SQLite3, and MariaDB only) An array of attributes to return for all successfully
# inserted records, which by default is the primary key.
# Pass <tt>returning: %w[ id name ]</tt> for both id and name
# or <tt>returning: false</tt> to omit the underlying <tt>RETURNING</tt> SQL
# clause entirely.
#
# You can also pass an SQL string if you need more control on the return values
# (for example, <tt>returning: Arel.sql("id, name as new_name")</tt>).
#
# [:unique_by]
# (PostgreSQL and SQLite only) By default rows are considered to be unique
# by every unique index on the table. Any duplicate rows are skipped.
#
# To skip rows according to just one unique index pass <tt>:unique_by</tt>.
#
# Consider a Book model where no duplicate ISBNs make sense, but if any
# row has an existing id, or is not unique by another unique index,
# ActiveRecord::RecordNotUnique is raised.
#
# Unique indexes can be identified by columns or name:
#
# unique_by: :isbn
# unique_by: %i[ author_id name ]
# unique_by: :index_books_on_isbn
#
# [:record_timestamps]
# By default, automatic setting of timestamp columns is controlled by
# the model's <tt>record_timestamps</tt> config, matching typical
# behavior.
#
# To override this and force automatic setting of timestamp columns one
# way or the other, pass <tt>:record_timestamps</tt>:
#
# record_timestamps: true # Always set timestamps automatically
# record_timestamps: false # Never set timestamps automatically
#
# Because it relies on the index information from the database
# <tt>:unique_by</tt> is recommended to be paired with
# Active Record's schema_cache.
#
# ==== Example
#
# # Insert records and skip inserting any duplicates.
# # Here "Eloquent Ruby" is skipped because its id is not unique.
#
# Book.insert_all([
# { id: 1, title: "Rework", author: "David" },
# { id: 1, title: "Eloquent Ruby", author: "Russ" }
# ])
#
# # insert_all works on chained scopes, and you can use create_with
# # to set default attributes for all inserted records.
#
# author.books.create_with(created_at: Time.now).insert_all([
# { id: 1, title: "Rework" },
# { id: 2, title: "Eloquent Ruby" }
# ])
def insert_all(attributes, returning: nil, unique_by: nil, record_timestamps: nil)
InsertAll.execute(self, attributes, on_duplicate: :skip, returning: returning, unique_by: unique_by, record_timestamps: record_timestamps)
end

# Inserts a single record into the database in a single SQL INSERT
# statement. It does not instantiate any models nor does it trigger
# Active Record callbacks or validations. Though passed values
# go through Active Record's type casting and serialization.
#
# See #insert_all! for more.
def insert!(attributes, returning: nil, record_timestamps: nil)
insert_all!([ attributes ], returning: returning, record_timestamps: record_timestamps)
end

# Inserts multiple records into the database in a single SQL INSERT
# statement. It does not instantiate any models nor does it trigger
# Active Record callbacks or validations. Though passed values
# go through Active Record's type casting and serialization.
#
# The +attributes+ parameter is an Array of Hashes. Every Hash determines
# the attributes for a single row and must have the same keys.
#
# Raises ActiveRecord::RecordNotUnique if any rows violate a
# unique index on the table. In that case, no rows are inserted.
#
# To skip duplicate rows, see #insert_all. To replace them, see #upsert_all.
#
# Returns an ActiveRecord::Result with its contents based on
# <tt>:returning</tt> (see below).
#
# ==== Options
#
# [:returning]
# (PostgreSQL, SQLite3, and MariaDB only) An array of attributes to return for all successfully
# inserted records, which by default is the primary key.
# Pass <tt>returning: %w[ id name ]</tt> for both id and name
# or <tt>returning: false</tt> to omit the underlying <tt>RETURNING</tt> SQL
# clause entirely.
#
# You can also pass an SQL string if you need more control on the return values
# (for example, <tt>returning: Arel.sql("id, name as new_name")</tt>).
#
# [:record_timestamps]
# By default, automatic setting of timestamp columns is controlled by
# the model's <tt>record_timestamps</tt> config, matching typical
# behavior.
#
# To override this and force automatic setting of timestamp columns one
# way or the other, pass <tt>:record_timestamps</tt>:
#
# record_timestamps: true # Always set timestamps automatically
# record_timestamps: false # Never set timestamps automatically
#
# ==== Examples
#
# # Insert multiple records
# Book.insert_all!([
# { title: "Rework", author: "David" },
# { title: "Eloquent Ruby", author: "Russ" }
# ])
#
# # Raises ActiveRecord::RecordNotUnique because "Eloquent Ruby"
# # does not have a unique id.
# Book.insert_all!([
# { id: 1, title: "Rework", author: "David" },
# { id: 1, title: "Eloquent Ruby", author: "Russ" }
# ])
def insert_all!(attributes, returning: nil, record_timestamps: nil)
InsertAll.execute(self, attributes, on_duplicate: :raise, returning: returning, record_timestamps: record_timestamps)
end

# Updates or inserts (upserts) a single record into the database in a
# single SQL INSERT statement. It does not instantiate any models nor does
# it trigger Active Record callbacks or validations. Though passed values
# go through Active Record's type casting and serialization.
#
# See #upsert_all for documentation.
def upsert(attributes, **kwargs)
upsert_all([ attributes ], **kwargs)
end

# Updates or inserts (upserts) multiple records into the database in a
# single SQL INSERT statement. It does not instantiate any models nor does
# it trigger Active Record callbacks or validations. Though passed values
# go through Active Record's type casting and serialization.
#
# The +attributes+ parameter is an Array of Hashes. Every Hash determines
# the attributes for a single row and must have the same keys.
#
# Returns an ActiveRecord::Result with its contents based on
# <tt>:returning</tt> (see below).
#
# By default, +upsert_all+ will update all the columns that can be updated when
# there is a conflict. These are all the columns except primary keys, read-only
# columns, and columns covered by the optional +unique_by+.
#
# ==== Options
#
# [:returning]
# (PostgreSQL, SQLite3, and MariaDB only) An array of attributes to return for all successfully
# inserted records, which by default is the primary key.
# Pass <tt>returning: %w[ id name ]</tt> for both id and name
# or <tt>returning: false</tt> to omit the underlying <tt>RETURNING</tt> SQL
# clause entirely.
#
# You can also pass an SQL string if you need more control on the return values
# (for example, <tt>returning: Arel.sql("id, name as new_name")</tt>).
#
# [:unique_by]
# (PostgreSQL and SQLite only) By default rows are considered to be unique
# by every unique index on the table. Any duplicate rows are skipped.
#
# To skip rows according to just one unique index pass <tt>:unique_by</tt>.
#
# Consider a Book model where no duplicate ISBNs make sense, but if any
# row has an existing id, or is not unique by another unique index,
# ActiveRecord::RecordNotUnique is raised.
#
# Unique indexes can be identified by columns or name:
#
# unique_by: :isbn
# unique_by: %i[ author_id name ]
# unique_by: :index_books_on_isbn
#
# Because it relies on the index information from the database
# <tt>:unique_by</tt> is recommended to be paired with
# Active Record's schema_cache.
#
# [:on_duplicate]
# Configure the SQL update sentence that will be used in case of conflict.
#
# NOTE: If you use this option you must provide all the columns you want to update
# by yourself.
#
# Example:
#
# Commodity.upsert_all(
# [
# { id: 2, name: "Copper", price: 4.84 },
# { id: 4, name: "Gold", price: 1380.87 },
# { id: 6, name: "Aluminium", price: 0.35 }
# ],
# on_duplicate: Arel.sql("price = GREATEST(commodities.price, EXCLUDED.price)")
# )
#
# See the related +:update_only+ option. Both options can't be used at the same time.
#
# [:update_only]
# Provide a list of column names that will be updated in case of conflict. If not provided,
# +upsert_all+ will update all the columns that can be updated. These are all the columns
# except primary keys, read-only columns, and columns covered by the optional +unique_by+
#
# Example:
#
# Commodity.upsert_all(
# [
# { id: 2, name: "Copper", price: 4.84 },
# { id: 4, name: "Gold", price: 1380.87 },
# { id: 6, name: "Aluminium", price: 0.35 }
# ],
# update_only: [:price] # Only prices will be updated
# )
#
# See the related +:on_duplicate+ option. Both options can't be used at the same time.
#
# [:record_timestamps]
# By default, automatic setting of timestamp columns is controlled by
# the model's <tt>record_timestamps</tt> config, matching typical
# behavior.
#
# To override this and force automatic setting of timestamp columns one
# way or the other, pass <tt>:record_timestamps</tt>:
#
# record_timestamps: true # Always set timestamps automatically
# record_timestamps: false # Never set timestamps automatically
#
# ==== Examples
#
# # Inserts multiple records, performing an upsert when records have duplicate ISBNs.
# # Here "Eloquent Ruby" overwrites "Rework" because its ISBN is duplicate.
#
# Book.upsert_all([
# { title: "Rework", author: "David", isbn: "1" },
# { title: "Eloquent Ruby", author: "Russ", isbn: "1" }
# ], unique_by: :isbn)
#
# Book.find_by(isbn: "1").title # => "Eloquent Ruby"
def upsert_all(attributes, on_duplicate: :update, update_only: nil, returning: nil, unique_by: nil, record_timestamps: nil)
InsertAll.execute(self, attributes, on_duplicate: on_duplicate, update_only: update_only, returning: returning, unique_by: unique_by, record_timestamps: record_timestamps)
end

# Given an attributes hash, +instantiate+ returns a new instance of
# the appropriate class. Accepts only keys as strings.
#
Expand Down
1 change: 1 addition & 0 deletions activerecord/lib/active_record/querying.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ module Querying
:count, :average, :minimum, :maximum, :sum, :calculate,
:pluck, :pick, :ids, :async_ids, :strict_loading, :excluding, :without, :with, :with_recursive,
:async_count, :async_average, :async_minimum, :async_maximum, :async_sum, :async_pluck, :async_pick,
:insert, :insert_all, :insert!, :insert_all!, :upsert, :upsert_all
].freeze # :nodoc:
delegate(*QUERYING_METHODS, to: :all)

Expand Down

0 comments on commit de3f3bf

Please sign in to comment.