diff --git a/lib/active_record/connection_adapters/sqlserver/database_statements.rb b/lib/active_record/connection_adapters/sqlserver/database_statements.rb index a3a8e6481..d76a59f20 100644 --- a/lib/active_record/connection_adapters/sqlserver/database_statements.rb +++ b/lib/active_record/connection_adapters/sqlserver/database_statements.rb @@ -286,7 +286,11 @@ def current_database end def charset - select_value "SELECT SERVERPROPERTY('SqlCharSetName')" + select_value "SELECT DATABASEPROPERTYEX('#{current_database}', 'SqlCharSetName')" + end + + def collation + select_value "SELECT DATABASEPROPERTYEX('#{current_database}', 'Collation')" end protected diff --git a/lib/active_record/tasks/sqlserver_database_tasks.rb b/lib/active_record/tasks/sqlserver_database_tasks.rb new file mode 100644 index 000000000..d4a79c2e4 --- /dev/null +++ b/lib/active_record/tasks/sqlserver_database_tasks.rb @@ -0,0 +1,94 @@ +require 'shellwords' + +module ActiveRecord + module Tasks # :nodoc: + class SQLServerDatabaseTasks # :nodoc: + DEFAULT_COLLATION = 'SQL_Latin1_General_CP1_CI_AS' + + delegate :connection, :establish_connection, :clear_active_connections!, + to: ActiveRecord::Base + + def initialize(configuration) + @configuration = configuration + end + + def create(master_established = false) + establish_master_connection unless master_established + connection.create_database configuration['database'], default_collation + establish_connection configuration + rescue ActiveRecord::StatementInvalid => error + if /[Dd]atabase .* already exists/ === error.message + raise DatabaseAlreadyExists + else + raise + end + end + + def drop + establish_master_connection + connection.drop_database configuration['database'] + end + + def charset + connection.charset + end + + def collation + connection.collation + end + + def purge + clear_active_connections! + drop + create true + end + + def structure_dump(filename) + command = ([ + "defncopy", + "-S #{Shellwords.escape(configuration['host'])}", + "-D #{Shellwords.escape(configuration['database'])}", + "-U #{Shellwords.escape(configuration['username'])}", + "-P #{Shellwords.escape(configuration['password'])}", + "-o #{Shellwords.escape(filename)}", + ] + .concat(connection.tables.map{|t| Shellwords.escape(t)}) + .concat(connection.views.map{|v| Shellwords.escape(v)}) + ).join(' ') + raise 'Error dumping database' unless Kernel.system(command) + dump = File.read(filename).gsub(/^USE .*$\nGO\n/, '') # Strip db USE statements + dump.gsub!(/nvarchar\(-1\)/, 'nvarchar(max)') # Fix nvarchar(-1) column defs + dump.gsub!(/text\(\d+\)/, 'text') # Fix text(16) column defs + File.open(filename, "w") { |file| file.puts dump } + warn "NOTE: FreeTDS defncopy is used for dumping, which does yet not properly dump foreign key constraints." + end + + def structure_load(filename) + command = ([ + "tsql", + "-S #{Shellwords.escape(configuration['host'])}", + "-D #{Shellwords.escape(configuration['database'])}", + "-U #{Shellwords.escape(configuration['username'])}", + "-P #{Shellwords.escape(configuration['password'])}", + "< #{Shellwords.escape(filename)}", + ]).join(' ') + raise 'Error loading database' unless Kernel.system(command) + end + + private + + def default_collation + configuration['collation'] || DEFAULT_COLLATION + end + + def configuration + @configuration + end + + def establish_master_connection + establish_connection configuration.merge('database' => 'master') + end + end + DatabaseTasks.register_task(/sqlserver/, ActiveRecord::Tasks::SQLServerDatabaseTasks) + end +end diff --git a/lib/activerecord-sqlserver-adapter.rb b/lib/activerecord-sqlserver-adapter.rb index b420ca178..865fdbb84 100644 --- a/lib/activerecord-sqlserver-adapter.rb +++ b/lib/activerecord-sqlserver-adapter.rb @@ -1 +1,2 @@ require 'active_record/connection_adapters/sqlserver_adapter' +require 'active_record/tasks/sqlserver_database_tasks' diff --git a/test/cases/rake_test_sqlserver.rb b/test/cases/rake_test_sqlserver.rb new file mode 100644 index 000000000..bf856ca07 --- /dev/null +++ b/test/cases/rake_test_sqlserver.rb @@ -0,0 +1,232 @@ +require 'cases/sqlserver_helper' +require 'active_record/tasks/sqlserver_database_tasks' + +class SQLServerDBCreateTest < ActiveRecord::TestCase + def setup + @connection = stub(create_database: true) + @configuration = { + 'adapter' => 'sqlserver', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_establishes_connection_to_master_database + ActiveRecord::Base.expects(:establish_connection).with( + 'adapter' => 'sqlserver', + 'database' => 'master', + ) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_creates_database_with_default_collation + @connection.expects(:create_database). + with('my-app-db', 'SQL_Latin1_General_CP1_CI_AS') + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_creates_database_with_given_collation + @connection.expects(:create_database). + with('my-app-db', 'Greek_BIN') + + ActiveRecord::Tasks::DatabaseTasks.create @configuration. + merge('collation' => 'Greek_BIN') + end + + def test_establishes_connection_to_new_database + ActiveRecord::Base.expects(:establish_connection).with(@configuration) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_db_create_with_error_prints_message + ActiveRecord::Base.stubs(:establish_connection).raises(Exception) + + $stderr.stubs(:puts).returns(true) + $stderr.expects(:puts). + with("Couldn't create database for #{@configuration.inspect}") + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_create_when_database_exists_outputs_info_to_stderr + $stderr.expects(:puts).with("my-app-db already exists").once + + ActiveRecord::Base.connection.stubs(:create_database).raises( + ActiveRecord::StatementInvalid.new('database "my-app-db" already exists') + ) + + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end +end + +class SQLServerDBDropTest < ActiveRecord::TestCase + def setup + @connection = stub(drop_database: true) + @configuration = { + 'adapter' => 'sqlserver', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_establishes_connection_to_master_database + ActiveRecord::Base.expects(:establish_connection).with( + 'adapter' => 'sqlserver', + 'database' => 'master', + ) + + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + end + + def test_drops_database + @connection.expects(:drop_database).with('my-app-db') + + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + end +end + +class SQLServerPurgeTest < ActiveRecord::TestCase + def setup + @connection = stub(create_database: true, drop_database: true) + @configuration = { + 'adapter' => 'sqlserver', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:clear_active_connections!).returns(true) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_clears_active_connections + ActiveRecord::Base.expects(:clear_active_connections!) + + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + + def test_establishes_connection_to_master_database + ActiveRecord::Base.expects(:establish_connection).with( + 'adapter' => 'sqlserver', + 'database' => 'master', + ) + + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + + def test_drops_database + @connection.expects(:drop_database).with('my-app-db') + + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + + def test_creates_database + @connection.expects(:create_database). + with('my-app-db', 'SQL_Latin1_General_CP1_CI_AS') + + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end + + def test_establishes_connection + ActiveRecord::Base.expects(:establish_connection).with(@configuration) + + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end +end + +class SQLServerDBCharsetTest < ActiveRecord::TestCase + def setup + @connection = stub(create_database: true) + @configuration = { + 'adapter' => 'sqlserver', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_db_retrieves_charset + @connection.expects(:charset) + ActiveRecord::Tasks::DatabaseTasks.charset @configuration + end +end + +class SQLServerDBCollationTest < ActiveRecord::TestCase + def setup + @connection = stub(create_database: true) + @configuration = { + 'adapter' => 'sqlserver', + 'database' => 'my-app-db' + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_db_retrieves_collation + @connection.expects(:collation) + ActiveRecord::Tasks::DatabaseTasks.collation @configuration + end +end + +class SQLServerStructureDumpTest < ActiveRecord::TestCase + def setup + @connection = stub(tables: ['a_table'], views: ['a_view']) + @configuration = { + 'adapter' => 'sqlserver', + 'database' => 'my-app-db', + 'host' => 'a.host', + 'username' => 'user', + 'password' => 'pass', + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_structure_dump + filename = "awesome-file.sql" + Kernel.expects(:system).with("defncopy -S a.host -D my-app-db -U user -P pass -o #{filename} a_table a_view").returns(true) + File.expects(:read).with(filename).returns('') + File.expects(:open).with(filename, "w") + + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) + end +end + +class SQLServerStructureLoadTest < ActiveRecord::TestCase + def setup + @connection = stub + @configuration = { + 'adapter' => 'sqlserver', + 'database' => 'my-app-db', + 'host' => 'a.host', + 'username' => 'user', + 'password' => 'pass', + } + + ActiveRecord::Base.stubs(:connection).returns(@connection) + end + + def test_structure_load + filename = "awesome-file.sql" + Kernel.expects(:system).with("tsql -S a.host -D my-app-db -U user -P pass < awesome-file.sql").returns(true) + + ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) + end + + def test_structure_load_accepts_path_with_spaces + filename = "awesome file.sql" + Kernel.expects(:system).with("tsql -S a.host -D my-app-db -U user -P pass < awesome\\ file.sql").returns(true) + + ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) + end +end +