Skip to content

Commit 1f94251

Browse files
committed
Change identity columns to use direct SQL query
1 parent bea0578 commit 1f94251

File tree

3 files changed

+79
-7
lines changed

3 files changed

+79
-7
lines changed

lib/active_record/connection_adapters/sqlserver/database_statements.rb

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -407,17 +407,56 @@ def query_requires_identity_insert?(sql)
407407
return false unless insert_sql?(sql)
408408

409409
raw_table_name = get_raw_table_name(sql)
410-
id_column = identity_columns(raw_table_name).first
410+
id_column_name = identity_columns(raw_table_name).first
411411

412-
id_column && sql =~ /^\s*(INSERT|EXEC sp_executesql N'INSERT)[^(]+\([^)]*\b(#{id_column.name})\b,?[^)]*\)/i ? SQLServer::Utils.extract_identifiers(raw_table_name).quoted : false
412+
id_column_name && sql =~ /^\s*(INSERT|EXEC sp_executesql N'INSERT)[^(]+\([^)]*\b(#{id_column_name})\b,?[^)]*\)/i ? SQLServer::Utils.extract_identifiers(raw_table_name).quoted : false
413413
end
414414

415415
def insert_sql?(sql)
416416
!(sql =~ /\A\s*(INSERT|EXEC sp_executesql N'INSERT)/i).nil?
417417
end
418418

419419
def identity_columns(table_name)
420-
schema_cache.columns(table_name).select(&:is_identity?)
420+
identifier = database_prefix_identifier(table_name)
421+
database = identifier.fully_qualified_database_quoted
422+
423+
# if database is specified in the query we may be doing
424+
# a cross database query and cannot rely on schema_cache.
425+
# schema_cache would only be populated if the database
426+
# exists in rails database.yml
427+
# We bother to check and use schema_cache if possible because
428+
# AR core has some tests that audit the number of queries performed
429+
if database.blank?
430+
schema_cache.columns(table_name).select(&:is_identity?).map(&:name)
431+
else
432+
identity_columns_select(table_name)
433+
end
434+
end
435+
436+
def identity_columns_select(table_name)
437+
identifier = database_prefix_identifier(table_name)
438+
database = identifier.fully_qualified_database_quoted
439+
database += "." unless database.blank?
440+
441+
schema_name = "schema_name()"
442+
object_name = quote(identifier.object)
443+
444+
if identifier.schema.present?
445+
schema_name = quote(identifier.schema)
446+
end
447+
448+
sql = <<~SQL
449+
SELECT
450+
#{lowercase_schema_reflection_sql('c.name')} AS [name]
451+
FROM #{database}sys.columns c
452+
INNER JOIN #{database}sys.objects o ON c.object_id = o.object_id --poop
453+
INNER JOIN #{database}sys.schemas s ON o.schema_id = s.schema_id
454+
WHERE o.name = #{object_name} AND s.name = #{schema_name} AND c.is_identity = 1
455+
ORDER BY c.column_id
456+
SQL
457+
458+
results = internal_exec_query(sql, "SCHEMA")
459+
results.map { |row| row["name"] }
421460
end
422461

423462
# === SQLServer Specific (Selecting) ============================ #

lib/active_record/connection_adapters/sqlserver/schema_statements.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def new_column(name, default, sql_type_metadata, null, default_function = nil, c
9898

9999
def primary_keys(table_name)
100100
primaries = primary_keys_select(table_name)
101-
primaries.present? ? primaries : identity_columns(table_name).map(&:name)
101+
primaries.present? ? primaries : identity_columns(table_name)
102102
end
103103

104104
def primary_keys_select(table_name)

test/cases/adapter_test_sqlserver.rb

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,21 @@
88
require "models/minimalistic"
99
require "models/college"
1010

11+
require "models/dog" # A model that exists in both AR databases
12+
1113
class AdapterTestSQLServer < ActiveRecord::TestCase
1214
fixtures :tasks
1315

1416
let(:basic_insert_sql) { "INSERT INTO [funny_jokes] ([name]) VALUES('Knock knock')" }
1517
let(:basic_update_sql) { "UPDATE [customers] SET [address_street] = NULL WHERE [id] = 2" }
1618
let(:basic_select_sql) { "SELECT * FROM [customers] WHERE ([customers].[id] = 1)" }
19+
let(:cross_database_insert_sql) do
20+
arunit_connection = Topic.lease_connection
21+
arunit2_connection = College.lease_connection
22+
arunit_database = arunit_connection.pool.db_config.database
23+
arunit2_database = arunit2_connection.pool.db_config.database
24+
"INSERT INTO #{arunit2_database}.dbo.dogs SELECT * FROM #{arunit_database}.dbo.dogs"
25+
end
1726

1827
it "has basic and non-sensitive information in the adapters inspect method" do
1928
string = connection.inspect
@@ -83,6 +92,21 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
8392
assert arunit2_connection.table_exists?("#{arunit_database}.dbo.topics"), 'Topics table exists using Colleges connection'
8493
end
8594

95+
# it "test sql insert across databases" do
96+
# arunit_connection = Topic.lease_connection
97+
# arunit2_connection = College.lease_connection
98+
#
99+
# arunit_database = arunit_connection.pool.db_config.database
100+
# arunit2_database = arunit2_connection.pool.db_config.database
101+
#
102+
# sql = <<~SQL
103+
# INSERT INTO #{arunit2_database}.dbo.dogs SELECT * FROM #{arunit_database}.dbo.dogs
104+
# SQL
105+
#
106+
# arunit_connection.exec_insert sql
107+
# assert arunit_connection.send(:query_requires_identity_insert?, sql)
108+
#
109+
# end
86110
it "return true to insert sql query for inserts only" do
87111
assert connection.send(:insert_sql?, "INSERT...")
88112
assert connection.send(:insert_sql?, "EXEC sp_executesql N'INSERT INTO [fk_test_has_fks] ([fk_id]) VALUES (@0); SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident', N'@0 int', @0 = 0")
@@ -200,6 +224,7 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
200224
@identity_insert_sql_non_dbo_sp = "EXEC sp_executesql N'INSERT INTO [test].[aliens] ([id],[name]) VALUES (@0, @1)', N'@0 int, @1 nvarchar(255)', @0 = 420, @1 = N'Mork'"
201225
@identity_insert_sql_non_dbo_unquoted_sp = "EXEC sp_executesql N'INSERT INTO test.aliens (id, name) VALUES (@0, @1)', N'@0 int, @1 nvarchar(255)', @0 = 420, @1 = N'Mork'"
202226
@identity_insert_sql_non_dbo_unordered_sp = "EXEC sp_executesql N'INSERT INTO [test].[aliens] ([name],[id]) VALUES (@0, @1)', N'@0 nvarchar(255), @1 int', @0 = N'Mork', @1 = 420"
227+
203228
end
204229

205230
it "return quoted table_name to #query_requires_identity_insert? when INSERT sql contains id column" do
@@ -216,18 +241,26 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
216241
assert_equal "[test].[aliens]", connection.send(:query_requires_identity_insert?, @identity_insert_sql_non_dbo_sp)
217242
assert_equal "[test].[aliens]", connection.send(:query_requires_identity_insert?, @identity_insert_sql_non_dbo_unquoted_sp)
218243
assert_equal "[test].[aliens]", connection.send(:query_requires_identity_insert?, @identity_insert_sql_non_dbo_unordered_sp)
244+
219245
end
220246

221247
it "return false to #query_requires_identity_insert? for normal SQL" do
222-
[basic_insert_sql, basic_update_sql, basic_select_sql].each do |sql|
248+
[basic_insert_sql, basic_update_sql, basic_select_sql, cross_database_insert_sql].each do |sql|
223249
assert !connection.send(:query_requires_identity_insert?, sql), "SQL was #{sql}"
224250
end
225251
end
226252

227253
it "find identity column using #identity_columns" do
228254
task_id_column = Task.columns_hash["id"]
229-
assert_equal task_id_column.name, connection.send(:identity_columns, Task.table_name).first.name
230-
assert_equal task_id_column.sql_type, connection.send(:identity_columns, Task.table_name).first.sql_type
255+
assert_equal task_id_column.name, connection.send(:identity_columns, Task.table_name).first
256+
257+
258+
end
259+
260+
it "find identity column in other database" do
261+
arunit2_connection = College.lease_connection
262+
arunit2_database = arunit2_connection.pool.db_config.database
263+
assert_equal Dog.columns_hash["id"].name, connection.send(:identity_columns, "#{arunit2_database}.dbo.dogs").first
231264
end
232265

233266
it "return an empty array when calling #identity_columns for a table_name with no identity" do

0 commit comments

Comments
 (0)