Skip to content

Commit

Permalink
Merge pull request #49840 from fatkodima/insert-returning-mariadb
Browse files Browse the repository at this point in the history
Support `RETURNING` clause for MariaDB
  • Loading branch information
byroot committed Oct 31, 2023
2 parents f2088be + 81eeecd commit cb185fc
Show file tree
Hide file tree
Showing 12 changed files with 86 additions and 41 deletions.
4 changes: 4 additions & 0 deletions activerecord/CHANGELOG.md
@@ -1,3 +1,7 @@
* Support `RETURNING` clause for MariaDB

*fatkodima*, *Nikolay Kondratyev*

* The SQLite3 adapter now implements the `supports_deferrable_constraints?` contract

Allows foreign keys to be deferred by adding the `:deferrable` key to the `foreign_key` options.
Expand Down
Expand Up @@ -580,7 +580,7 @@ def supports_nulls_not_distinct?
end

def return_value_after_insert?(column) # :nodoc:
column.auto_incremented_by_db?
column.auto_populated?
end

def async_enabled? # :nodoc:
Expand Down
Expand Up @@ -170,6 +170,10 @@ def supports_insert_on_duplicate_update?
true
end

def supports_insert_returning?
mariadb? && database_version >= "10.5.0"
end

def get_advisory_lock(lock_name, timeout = 0) # :nodoc:
query_value("SELECT GET_LOCK(#{quote(lock_name.to_s)}, #{timeout})") == 1
end
Expand Down Expand Up @@ -635,12 +639,11 @@ def default_index_type?(index) # :nodoc:
end

def build_insert_sql(insert) # :nodoc:
sql = +"INSERT #{insert.into} #{insert.values_list}"
sql = +"INSERT"
sql << " IGNORE" if insert.skip_duplicates?
sql << " #{insert.into} #{insert.values_list}"

if insert.skip_duplicates?
no_op_column = quote_column_name(insert.keys.first)
sql << " ON DUPLICATE KEY UPDATE #{no_op_column}=#{no_op_column}"
elsif insert.update_duplicates?
if insert.update_duplicates?
sql << " ON DUPLICATE KEY UPDATE "
if insert.raw_update_sql?
sql << insert.raw_update_sql
Expand All @@ -650,6 +653,7 @@ def build_insert_sql(insert) # :nodoc:
end
end

sql << " RETURNING #{insert.returning}" if insert.returning
sql
end

Expand Down
Expand Up @@ -55,6 +55,14 @@ def default_insert_value(column)
super unless column.auto_increment?
end

def returning_column_values(result)
if supports_insert_returning?
result.rows.first
else
super
end
end

def combine_multi_statements(total_sql)
total_sql.each_with_object([]) do |sql, total_sql_chunks|
previous_packet = total_sql_chunks.last
Expand Down
Expand Up @@ -66,7 +66,11 @@ def execute_batch(statements, name = nil)
end

def last_inserted_id(result)
@raw_connection&.last_id
if supports_insert_returning?
super
else
@raw_connection&.last_id
end
end

def multi_statements_enabled?
Expand Down
Expand Up @@ -290,10 +290,6 @@ def index_algorithms
{ concurrently: "CONCURRENTLY" }
end

def return_value_after_insert?(column) # :nodoc:
column.auto_populated?
end

class StatementPool < ConnectionAdapters::StatementPool # :nodoc:
def initialize(connection, max)
super(max)
Expand Down
Expand Up @@ -202,10 +202,6 @@ def connected?

alias_method :active?, :connected?

def return_value_after_insert?(column) # :nodoc:
column.auto_populated?
end

alias :reset! :reconnect!

# Disconnects from the database if already connected. Otherwise, this
Expand Down
Expand Up @@ -26,7 +26,8 @@ def exec_insert(sql, name, binds, pk = nil, sequence_name = nil, returning: nil)
check_if_write_query(sql)
mark_transaction_written_if_write(sql)

raw_execute(to_sql(sql, binds), name)
sql, _binds = sql_for_insert(sql, pk, binds, returning)
raw_execute(sql, name)
end

def exec_delete(sql, name = nil, binds = []) # :nodoc:
Expand All @@ -53,7 +54,11 @@ def raw_execute(sql, name, async: false, allow_retry: false, materialize_transac
end

def last_inserted_id(result)
result.last_insert_id
if supports_insert_returning?
super
else
result.last_insert_id
end
end

def sync_timezone_changes(conn)
Expand Down
6 changes: 3 additions & 3 deletions activerecord/lib/active_record/persistence.rb
Expand Up @@ -115,7 +115,7 @@ def insert(attributes, returning: nil, unique_by: nil, record_timestamps: nil)
# ==== Options
#
# [:returning]
# (PostgreSQL only) An array of attributes to return for all successfully
# (PostgreSQL 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
Expand Down Expand Up @@ -205,7 +205,7 @@ def insert!(attributes, returning: nil, record_timestamps: nil)
# ==== Options
#
# [:returning]
# (PostgreSQL only) An array of attributes to return for all successfully
# (PostgreSQL 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
Expand Down Expand Up @@ -271,7 +271,7 @@ def upsert(attributes, **kwargs)
# ==== Options
#
# [:returning]
# (PostgreSQL only) An array of attributes to return for all successfully
# (PostgreSQL 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
Expand Down
2 changes: 1 addition & 1 deletion activerecord/test/cases/defaults_test.rb
Expand Up @@ -173,7 +173,7 @@ class MysqlDefaultExpressionTest < ActiveRecord::TestCase
if supports_default_expression?
test "schema dump includes default expression" do
output = dump_table_schema("defaults")
assert_match %r/t\.binary\s+"uuid",\s+limit: 36,\s+default: -> { "\(uuid\(\)\)" }/i, output
assert_match %r/t\.binary\s+"uuid",\s+limit: 36,\s+default: -> { "\(?uuid\(\)\)?" }/i, output
end
end

Expand Down
63 changes: 44 additions & 19 deletions activerecord/test/cases/persistence_test.rb
Expand Up @@ -43,31 +43,56 @@ def test_populates_non_primary_key_autoincremented_column_for_a_cpk_model
assert_not_nil order_id
end

def test_fills_auto_populated_columns_on_creation
record_with_defaults = Default.create
assert_not_nil record_with_defaults.id
assert_equal "Ruby on Rails", record_with_defaults.ruby_on_rails
if current_adapter?(:PostgreSQLAdapter)
def test_fills_auto_populated_columns_on_creation
record = Default.create
assert_not_nil record.id
assert_equal "Ruby on Rails", record.ruby_on_rails

if current_adapter?(:PostgreSQLAdapter) && ActiveRecord::Base.connection.supports_virtual_columns?
assert_not_nil record_with_defaults.virtual_stored_number
end
if supports_virtual_columns?
assert_not_nil record.virtual_stored_number
end

assert_not_nil record_with_defaults.random_number
assert_not_nil record_with_defaults.modified_date
assert_not_nil record_with_defaults.modified_date_function
assert_not_nil record_with_defaults.modified_time
assert_not_nil record_with_defaults.modified_time_without_precision
assert_not_nil record_with_defaults.modified_time_function
assert_not_nil record.random_number
assert_not_nil record.modified_date
assert_not_nil record.modified_date_function
assert_not_nil record.modified_time
assert_not_nil record.modified_time_without_precision
assert_not_nil record.modified_time_function

if current_adapter?(:PostgreSQLAdapter) && ActiveRecord::Base.connection.supports_identity_columns?
klass = Class.new(ActiveRecord::Base) do
self.table_name = "postgresql_identity_table"
end
if supports_identity_columns?
klass = Class.new(ActiveRecord::Base) do
self.table_name = "postgresql_identity_table"
end

record = klass.create!
record = klass.create!
assert_not_nil record.id
end
end
elsif current_adapter?(:SQLite3Adapter)
def test_fills_auto_populated_columns_on_creation
record = Default.create
assert_not_nil record.id
assert_equal "Ruby on Rails", record.ruby_on_rails

assert_not_nil record.random_number
assert_not_nil record.modified_date
assert_not_nil record.modified_date_function
assert_not_nil record.modified_time
assert_not_nil record.modified_time_without_precision
assert_not_nil record.modified_time_function
end
end if current_adapter?(:PostgreSQLAdapter) || current_adapter?(:SQLite3Adapter)
elsif current_adapter?(:Mysql2Adapter, :TrilogyAdapter)
def test_fills_auto_populated_columns_on_creation
record = Default.create
assert_not_nil record.id
assert_not_nil record.char1

if supports_default_expression? && supports_insert_returning?
assert_not_nil record.uuid
end
end
end

def test_update_many
topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" } }
Expand Down
5 changes: 4 additions & 1 deletion activerecord/test/support/adapter_helper.rb
Expand Up @@ -22,7 +22,8 @@ def supports_default_expression?
true
elsif current_adapter?(:Mysql2Adapter, :TrilogyAdapter)
conn = ActiveRecord::Base.connection
!conn.mariadb? && conn.database_version >= "8.0.13"
(conn.mariadb? && conn.database_version >= "10.2.1") ||
(!conn.mariadb? && conn.database_version >= "8.0.13")
end
end

Expand Down Expand Up @@ -57,6 +58,8 @@ def supports_text_column_with_default?
supports_optimizer_hints?
supports_datetime_with_precision?
supports_nulls_not_distinct?
supports_identity_columns?
supports_virtual_columns?
].each do |method_name|
define_method method_name do
ActiveRecord::Base.connection.public_send(method_name)
Expand Down

0 comments on commit cb185fc

Please sign in to comment.