From 06b04dce04d2b8e1193816fbec64ac02a3f8594d Mon Sep 17 00:00:00 2001 From: Matias Grunberg Date: Wed, 7 Apr 2021 11:20:56 -0300 Subject: [PATCH] add skip_duplicates support. Pending refactor --- .../sqlserver/database_statements.rb | 31 +++++++++++++++++++ .../connection_adapters/sqlserver_adapter.rb | 2 +- test/cases/coerced_tests.rb | 5 +++ test/cases/insert_all_test_sqlserver.rb | 15 +++++++++ 4 files changed, 52 insertions(+), 1 deletion(-) create mode 100644 test/cases/insert_all_test_sqlserver.rb diff --git a/lib/active_record/connection_adapters/sqlserver/database_statements.rb b/lib/active_record/connection_adapters/sqlserver/database_statements.rb index a3869cdc2..db70d5eff 100644 --- a/lib/active_record/connection_adapters/sqlserver/database_statements.rb +++ b/lib/active_record/connection_adapters/sqlserver/database_statements.rb @@ -140,6 +140,37 @@ def default_insert_value(column) private :default_insert_value def build_insert_sql(insert) # :nodoc: + if insert.skip_duplicates? + # Do we have a unique_by index? Use index columns + conflict_columns = if (unique_by = insert.send(:insert_all).unique_by) + [unique_by.columns] + else + # Compare against every unique constraint (primary key included). + # Discard constraints that are not fully included on insert.keys. Prevents invalid queries. + # Example: ignore unique index for columns ["name"] if insert keys is ["description"] + insert_all = insert.send(:insert_all) + + (insert_all.send(:unique_indexes).map(&:columns) + [insert_all.primary_keys]).select do |columns| + columns.to_set.subset?(insert.keys) + end + end + includes_primary_key = (insert.send(:insert_all).primary_keys.to_set & insert.keys).present? + + sql = +"" + sql << "SET IDENTITY_INSERT #{insert.model.quoted_table_name} ON;" if includes_primary_key + sql << "MERGE INTO #{insert.model.quoted_table_name} WITH (UPDLOCK, HOLDLOCK) AS target" + sql << " USING (SELECT DISTINCT * FROM (#{insert.values_list}) AS t1 (#{insert.send(:columns_list)})) AS source" + sql << " ON (#{conflict_columns.map { |columns| columns.map { |column| "target.#{quote_column_name(column)} = source.#{quote_column_name(column)}" }.join(" AND ") }.join(") OR (")})" + sql << " WHEN NOT MATCHED BY TARGET THEN" + sql << " INSERT (#{insert.send(:columns_list)}) VALUES (#{insert.keys.map { |column| "source.#{quote_column_name(column)}" }.join(", ")})" + if returning = insert.send(:insert_all).returning + sql << " OUTPUT " << returning.map { |column| "INSERTED.#{quote_column_name(column)}" }.join(", ") + end + sql << ";" + sql << "SET IDENTITY_INSERT #{insert.model.quoted_table_name} OFF;" if includes_primary_key + return sql + end + sql = +"INSERT #{insert.into}" if returning = insert.send(:insert_all).returning diff --git a/lib/active_record/connection_adapters/sqlserver_adapter.rb b/lib/active_record/connection_adapters/sqlserver_adapter.rb index c30909aee..1f963f827 100644 --- a/lib/active_record/connection_adapters/sqlserver_adapter.rb +++ b/lib/active_record/connection_adapters/sqlserver_adapter.rb @@ -169,7 +169,7 @@ def supports_insert_returning? end def supports_insert_on_duplicate_skip? - false + true end def supports_insert_on_duplicate_update? diff --git a/test/cases/coerced_tests.rb b/test/cases/coerced_tests.rb index bc221e98d..2129507af 100644 --- a/test/cases/coerced_tests.rb +++ b/test/cases/coerced_tests.rb @@ -1555,3 +1555,8 @@ class ReloadModelsTest < ActiveRecord::TestCase # `activesupport/lib/active_support/testing/isolation.rb` exceeds what Windows can handle. coerce_tests! :test_has_one_with_reload if RbConfig::CONFIG["host_os"] =~ /mswin|mingw/ end + +class InsertAllTest < ActiveRecord::TestCase + # Skip this until upsert is supported + coerce_tests! :test_insert +end diff --git a/test/cases/insert_all_test_sqlserver.rb b/test/cases/insert_all_test_sqlserver.rb new file mode 100644 index 000000000..17d0949d0 --- /dev/null +++ b/test/cases/insert_all_test_sqlserver.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require "cases/helper_sqlserver" +require "models/book" + +class InsertAllTestSQLServer < ActiveRecord::TestCase + fixtures :books + + # Issue https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/issues/847 + it "execute insert_all with a single element" do + assert_difference "Book.count", +1 do + Book.insert_all [{ name: "Rework", author_id: 1 }] + end + end +end