Skip to content

Support cross database inserts #1357

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Aug 15, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ jobs:
test:
name: Run test suite
runs-on: ubuntu-latest
timeout-minutes: 10

env:
COMPOSE_FILE: docker-compose.ci.yml
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

- [#1341](https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1341) Support more Azure services by changing language source.

#### Fixed

- [#1357(https://github.com/rails-sqlserver/activerecord-sqlserver-adapter/pull/1357) Support cross database inserts.

## v7.2.6

#### Fixed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,7 @@ def column_definitions(table_name)
end

def column_definitions_sql(database, identifier)
database = "TEMPDB" if identifier.temporary_table?
schema_name = "schema_name()"

if prepared_statements
Expand All @@ -581,12 +582,8 @@ def column_definitions_sql(database, identifier)
schema_name = quote(identifier.schema) if identifier.schema.present?
end

object_id_arg = identifier.schema.present? ? "CONCAT(#{schema_name},'.',#{object_name})" : object_name

if identifier.temporary_table?
database = "TEMPDB"
object_id_arg = "CONCAT('#{database}','..',#{object_name})"
end
object_id_arg = identifier.schema.present? ? "CONCAT('.',#{schema_name},'.',#{object_name})" : "CONCAT('..',#{object_name})"
object_id_arg = "CONCAT('#{database}',#{object_id_arg})"

%{
SELECT
Expand Down
35 changes: 25 additions & 10 deletions test/cases/adapter_test_sqlserver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,17 @@
require "models/subscriber"
require "models/minimalistic"
require "models/college"
require "models/dog"
require "models/other_dog"

class AdapterTestSQLServer < ActiveRecord::TestCase
fixtures :tasks

let(:arunit_connection) { Topic.lease_connection }
let(:arunit2_connection) { College.lease_connection }
let(:arunit_database) { arunit_connection.pool.db_config.database }
let(:arunit2_database) { arunit2_connection.pool.db_config.database }

let(:basic_insert_sql) { "INSERT INTO [funny_jokes] ([name]) VALUES('Knock knock')" }
let(:basic_update_sql) { "UPDATE [customers] SET [address_street] = NULL WHERE [id] = 2" }
let(:basic_select_sql) { "SELECT * FROM [customers] WHERE ([customers].[id] = 1)" }
Expand Down Expand Up @@ -50,21 +57,14 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
assert Topic.table_exists?, "Topics table name of 'dbo.topics' should return true for exists."

# Test when database and owner included in table name.
db_config = ActiveRecord::Base.configurations.configs_for(env_name: "arunit", name: "primary")
Topic.table_name = "#{db_config.database}.dbo.topics"
Topic.table_name = "#{arunit_database}.dbo.topics"
assert Topic.table_exists?, "Topics table name of '[DATABASE].dbo.topics' should return true for exists."
ensure
Topic.table_name = "topics"
end
end

it "test table existence across database schemas" do
arunit_connection = Topic.lease_connection
arunit2_connection = College.lease_connection

arunit_database = arunit_connection.pool.db_config.database
arunit2_database = arunit2_connection.pool.db_config.database

# Assert that connections use different default databases schemas.
assert_not_equal arunit_database, arunit2_database

Expand Down Expand Up @@ -200,6 +200,9 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
@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'"
@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'"
@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"

@non_identity_insert_sql_cross_database = "INSERT INTO #{arunit2_database}.dbo.dogs SELECT * FROM #{arunit_database}.dbo.dogs"
@identity_insert_sql_cross_database = "INSERT INTO #{arunit2_database}.dbo.dogs(id) SELECT id FROM #{arunit_database}.dbo.dogs"
end

it "return quoted table_name to #query_requires_identity_insert? when INSERT sql contains id column" do
Expand All @@ -216,20 +219,32 @@ class AdapterTestSQLServer < ActiveRecord::TestCase
assert_equal "[test].[aliens]", connection.send(:query_requires_identity_insert?, @identity_insert_sql_non_dbo_sp)
assert_equal "[test].[aliens]", connection.send(:query_requires_identity_insert?, @identity_insert_sql_non_dbo_unquoted_sp)
assert_equal "[test].[aliens]", connection.send(:query_requires_identity_insert?, @identity_insert_sql_non_dbo_unordered_sp)

assert_equal "[#{arunit2_database}].[dbo].[dogs]", connection.send(:query_requires_identity_insert?, @identity_insert_sql_cross_database)
end

it "return false to #query_requires_identity_insert? for normal SQL" do
[basic_insert_sql, basic_update_sql, basic_select_sql].each do |sql|
[basic_insert_sql, basic_update_sql, basic_select_sql, @non_identity_insert_sql_cross_database].each do |sql|
assert !connection.send(:query_requires_identity_insert?, sql), "SQL was #{sql}"
end
end

it "find identity column using #identity_columns" do
it "find identity column" do
task_id_column = Task.columns_hash["id"]
assert_equal task_id_column.name, connection.send(:identity_columns, Task.table_name).first.name
assert_equal task_id_column.sql_type, connection.send(:identity_columns, Task.table_name).first.sql_type
end

it "find identity column cross database" do
id_column = Dog.columns_hash["id"]
assert_equal id_column.name, arunit2_connection.send(:identity_columns, Dog.table_name).first.name
assert_equal id_column.sql_type, arunit2_connection.send(:identity_columns, Dog.table_name).first.sql_type

id_column = OtherDog.columns_hash["id"]
assert_equal id_column.name, arunit_connection.send(:identity_columns, OtherDog.table_name).first.name
assert_equal id_column.sql_type, arunit_connection.send(:identity_columns, OtherDog.table_name).first.sql_type
end

it "return an empty array when calling #identity_columns for a table_name with no identity" do
_(connection.send(:identity_columns, Subscriber.table_name)).must_equal []
end
Expand Down
9 changes: 9 additions & 0 deletions test/cases/temp_test_sqlserver.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# frozen_string_literal: true

require "cases/helper_sqlserver"

class TempTestSQLServer < ActiveRecord::TestCase
# it "assert true" do
# assert true
# end
end