diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b47cf3a2..933601d01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## Unreleased + +#### Changed + +- [#1405](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1405) Fix `insert_all`/`upsert_all` for single character, temporary, non-ASCII and three part table names, and stop an aliased target being included in the `MERGE` table name. + ## v8.1.3 #### Added diff --git a/lib/active_record/connection_adapters/sqlserver/schema_statements.rb b/lib/active_record/connection_adapters/sqlserver/schema_statements.rb index d5a28f438..4e8726cf8 100644 --- a/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +++ b/lib/active_record/connection_adapters/sqlserver/schema_statements.rb @@ -4,6 +4,9 @@ module ActiveRecord module ConnectionAdapters module SQLServer module SchemaStatements + MERGE_TARGET_IDENTIFIER = /(?:\[[^\]]+\]|[a-z0-9_-]+)/i # :nodoc: + MERGE_TARGET_TABLE_NAME = /\A\s*MERGE\s+INTO\s+(#{MERGE_TARGET_IDENTIFIER}(?:\s*\.#{MERGE_TARGET_IDENTIFIER}){0,2})\s+(?:AS|WITH|USING)/i # :nodoc: + def create_table(table_name, **options) res = super clear_cache! @@ -726,7 +729,7 @@ def get_raw_table_name(sql) elsif s.match?(/^\s*UPDATE\s+.*/i) s.match(/UPDATE\s+([^(\s]+)\s*/i)[1] elsif s.match?(/^\s*MERGE INTO.*/i) - s.match(/^\s*MERGE\s+INTO\s+(\[?[a-z0-9_ -]+\]?\.?\[?[a-z0-9_ -]+\]?)\s+(AS|WITH|USING)/i)[1] + s.match(MERGE_TARGET_TABLE_NAME)[1] else s.match(/FROM[\s|(]+((\[[^(\]]+\])|[^(\s]+)\s*/i)[1] end.strip diff --git a/test/cases/adapter_test_sqlserver.rb b/test/cases/adapter_test_sqlserver.rb index 6d5c87d61..03b5d9cf2 100644 --- a/test/cases/adapter_test_sqlserver.rb +++ b/test/cases/adapter_test_sqlserver.rb @@ -230,6 +230,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase @identity_merge_sql = "MERGE INTO [ships] WITH (UPDLOCK, HOLDLOCK) AS target USING ( SELECT * FROM ( SELECT [id], [name], ROW_NUMBER() OVER ( PARTITION BY [id] ORDER BY [id] DESC ) AS rn_0 FROM ( VALUES (101, N'RSS Sir David Attenborough') ) AS t1 ([id], [name]) ) AS ranked_source WHERE rn_0 = 1 ) AS source ON (target.[id] = source.[id]) WHEN MATCHED THEN UPDATE SET target.[name] = source.[name] WHEN NOT MATCHED BY TARGET THEN INSERT ([id], [name]) VALUES (source.[id], source.[name]) OUTPUT INSERTED.[id]" @identity_merge_sql_unquoted = "MERGE INTO ships WITH (UPDLOCK, HOLDLOCK) AS target USING ( SELECT * FROM ( SELECT id, name, ROW_NUMBER() OVER ( PARTITION BY id ORDER BY id DESC ) AS rn_0 FROM ( VALUES (101, N'RSS Sir David Attenborough') ) AS t1 (id, name) ) AS ranked_source WHERE rn_0 = 1 ) AS source ON (target.id = source.id) WHEN MATCHED THEN UPDATE SET target.name = source.name WHEN NOT MATCHED BY TARGET THEN INSERT (id, name) VALUES (source.id, source.name) OUTPUT INSERTED.id" @identity_merge_sql_unordered = "MERGE INTO [ships] WITH (UPDLOCK, HOLDLOCK) AS target USING ( SELECT * FROM ( SELECT [name], [id], ROW_NUMBER() OVER ( PARTITION BY [id] ORDER BY [id] DESC ) AS rn_0 FROM ( VALUES (101, N'RSS Sir David Attenborough') ) AS t1 ([name], [id]) ) AS ranked_source WHERE rn_0 = 1 ) AS source ON (target.[id] = source.[id]) WHEN MATCHED THEN UPDATE SET target.[name] = source.[name] WHEN NOT MATCHED BY TARGET THEN INSERT ([name], [id]) VALUES (source.[name], source.[id]) OUTPUT INSERTED.[id]" + @identity_merge_sql_aliased_target = @identity_merge_sql.sub(" WITH (UPDLOCK, HOLDLOCK)", "") @identity_insert_sql_non_dbo = "INSERT INTO [test].[aliens] ([id],[name]) VALUES(420,'Mork')" @identity_insert_sql_non_dbo_unquoted = "INSERT INTO test.aliens ([id],[name]) VALUES(420,'Mork')" @@ -253,6 +254,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase assert_equal "[ships]", connection.send(:query_requires_identity_insert?, @identity_merge_sql) assert_equal "[ships]", connection.send(:query_requires_identity_insert?, @identity_merge_sql_unquoted) assert_equal "[ships]", connection.send(:query_requires_identity_insert?, @identity_merge_sql_unordered) + assert_equal "[ships]", connection.send(:query_requires_identity_insert?, @identity_merge_sql_aliased_target) assert_equal "[test].[aliens]", connection.send(:query_requires_identity_insert?, @identity_insert_sql_non_dbo) assert_equal "[test].[aliens]", connection.send(:query_requires_identity_insert?, @identity_insert_sql_non_dbo_unquoted) diff --git a/test/cases/schema_test_sqlserver.rb b/test/cases/schema_test_sqlserver.rb index fc645cf7f..7335c0295 100644 --- a/test/cases/schema_test_sqlserver.rb +++ b/test/cases/schema_test_sqlserver.rb @@ -122,6 +122,38 @@ class SchemaTestSQLServer < ActiveRecord::TestCase it do assert_equal "[with_numbers_1234]", connection.send(:get_raw_table_name, "MERGE INTO [with_numbers_1234] AS target") end + + it do + assert_equal "[dashboards]", connection.send(:get_raw_table_name, "MERGE INTO [dashboards] AS target USING (VALUES (1)) AS source ([id]) ON (target.[id] = source.[id]) WHEN NOT MATCHED THEN INSERT ([id]) VALUES (source.[id]);") + end + + it do + assert_equal "lock_without_defaults", connection.send(:get_raw_table_name, "MERGE INTO lock_without_defaults AS target USING (VALUES (1)) AS source ([id]) ON (target.[id] = source.[id]) WHEN NOT MATCHED THEN INSERT ([id]) VALUES (source.[id]);") + end + + it do + assert_equal "[WITH - SPACES]", connection.send(:get_raw_table_name, "MERGE INTO [WITH - SPACES] AS target USING (VALUES (1)) AS source ([id]) ON (target.[id] = source.[id]) WHEN NOT MATCHED THEN INSERT ([id]) VALUES (source.[id]);") + end + + it do + assert_equal "[with].[select notation]", connection.send(:get_raw_table_name, "MERGE INTO [with].[select notation] AS target USING (VALUES (1)) AS source ([id]) ON (target.[id] = source.[id]) WHEN NOT MATCHED THEN INSERT ([id]) VALUES (source.[id]);") + end + + it do + assert_equal "[x]", connection.send(:get_raw_table_name, "MERGE INTO [x] WITH (UPDLOCK, HOLDLOCK) AS target USING (VALUES (1)) AS source ([id]) ON (target.[id] = source.[id]) WHEN NOT MATCHED THEN INSERT ([id]) VALUES (source.[id]);") + end + + it do + assert_equal "[with].[select notation]", connection.send(:get_raw_table_name, "MERGE INTO [with].[select notation] WITH (UPDLOCK, HOLDLOCK) AS target USING (VALUES (1)) AS source ([id]) ON (target.[id] = source.[id]) WHEN NOT MATCHED THEN INSERT ([id]) VALUES (source.[id]);") + end + + it do + assert_equal "[a].[dbo].[dashboards]", connection.send(:get_raw_table_name, "MERGE INTO [a].[dbo].[dashboards] WITH (UPDLOCK, HOLDLOCK) AS target USING (VALUES (1)) AS source ([id]) ON (target.[id] = source.[id]) WHEN NOT MATCHED THEN INSERT ([id]) VALUES (source.[id]);") + end + + it do + assert_equal "with .dashboards", connection.send(:get_raw_table_name, "MERGE INTO with .dashboards WITH (UPDLOCK, HOLDLOCK) AS target USING (VALUES (1)) AS source ([id]) ON (target.[id] = source.[id]) WHEN NOT MATCHED THEN INSERT ([id]) VALUES (source.[id]);") + end end describe "CREATE VIEW statements" do