From c497da97256a4cad1590275fcb27f1daebab6e73 Mon Sep 17 00:00:00 2001 From: Aidan Haran Date: Wed, 7 May 2025 16:02:31 +0100 Subject: [PATCH 1/3] Fix multiple nil identity columns for merge insert --- .../sqlserver/database_statements.rb | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/lib/active_record/connection_adapters/sqlserver/database_statements.rb b/lib/active_record/connection_adapters/sqlserver/database_statements.rb index 30296d1a9..10755bb14 100644 --- a/lib/active_record/connection_adapters/sqlserver/database_statements.rb +++ b/lib/active_record/connection_adapters/sqlserver/database_statements.rb @@ -177,7 +177,7 @@ def build_sql_for_merge_insert(insert:, insert_all:, columns_with_uniqueness_con SELECT * FROM ( SELECT #{insert.send(:columns_list)}, #{partition_by_columns_with_uniqueness_constraints(columns_with_uniqueness_constraints:)} - FROM (#{insert.values_list}) + FROM (#{merge_insert_values_list(insert:, insert_all:)}) AS t1 (#{insert.send(:columns_list)}) ) AS ranked_source WHERE #{is_first_record_across_all_uniqueness_constraints(columns_with_uniqueness_constraints:)} @@ -206,6 +206,35 @@ def build_sql_for_merge_insert(insert:, insert_all:, columns_with_uniqueness_con sql end + # For `nil` identity columns we need to ensure that the values do not match so that they are all inserted. + # Method is a combination of `ActiveRecord::InsertAll#values_list` and `ActiveRecord::ConnectionAdapters::SQLServer::DatabaseStatements#default_insert_value`. + def merge_insert_values_list(insert:, insert_all:) + connection = insert.send(:connection) + identity_index = 0 + + types = insert.send(:extract_types_from_columns_on, insert.model.table_name, keys: insert.keys_including_timestamps) + + values_list = insert_all.map_key_with_value do |key, value| + if Arel::Nodes::SqlLiteral === value + value + elsif insert.primary_keys.include?(key) && value.nil? + column = insert.send(:column_from_key, key) + + if column.is_identity? + identity_index += 1 + table_name = quote(quote_table_name(column.table_name)) + Arel.sql("IDENT_CURRENT(#{table_name}) + IDENT_INCR(#{table_name}) * #{identity_index}") + else + connection.default_insert_value(column) + end + else + ActiveModel::Type::SerializeCastValue.serialize(type = types[key], type.cast(value)) + end + end + + connection.visitor.compile(Arel::Nodes::ValuesList.new(values_list)) + end + # === SQLServer Specific ======================================== # def execute_procedure(proc_name, *variables) From 328f5de5a1913fd53cd3accacf39a4474542038b Mon Sep 17 00:00:00 2001 From: Aidan Haran Date: Thu, 8 May 2025 13:39:41 +0100 Subject: [PATCH 2/3] Wrap in brackets to make precedence clearer --- .../connection_adapters/sqlserver/database_statements.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/active_record/connection_adapters/sqlserver/database_statements.rb b/lib/active_record/connection_adapters/sqlserver/database_statements.rb index 10755bb14..bac9e5441 100644 --- a/lib/active_record/connection_adapters/sqlserver/database_statements.rb +++ b/lib/active_record/connection_adapters/sqlserver/database_statements.rb @@ -223,7 +223,7 @@ def merge_insert_values_list(insert:, insert_all:) if column.is_identity? identity_index += 1 table_name = quote(quote_table_name(column.table_name)) - Arel.sql("IDENT_CURRENT(#{table_name}) + IDENT_INCR(#{table_name}) * #{identity_index}") + Arel.sql("IDENT_CURRENT(#{table_name}) + (IDENT_INCR(#{table_name}) * #{identity_index})") else connection.default_insert_value(column) end From 457b51da347f1909f3e94d2ce071a0abf9b4b49c Mon Sep 17 00:00:00 2001 From: Aidan Haran Date: Thu, 8 May 2025 13:41:27 +0100 Subject: [PATCH 3/3] Update CHANGELOG.md --- CHANGELOG.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bcddc4efb..3b0050259 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,9 @@ #### Added -- [#1301](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1301) Add support for INDEX INCLUDE. -- [#1312](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1312) Add support for `insert_all` and `upsert_all` -- [#1317](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1317) Reverse order of values when upserting +- [#1301](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1301) Add support for `INDEX INCLUDE`. +- [#1312](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1312) Add support for `insert_all` and `upsert_all`. +- [#1317](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1317) Reverse order of values when upserting. #### Changed @@ -13,6 +13,7 @@ #### Fixed - [#1313](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1313) Correctly retrieve the SQL Server database version. -- [#1320](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1320) Fix SQL statement to calculate `updated_at` when upserting +- [#1320](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1320) Fix SQL statement to calculate `updated_at` when upserting. +- [#1327](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1327) Fix multiple `nil` identity columns for merge insert. Please check [8-0-stable](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/blob/8-0-stable/CHANGELOG.md) for previous changes.