Skip to content

Commit 348361d

Browse files
committed
DatabaseTasks support for all tasks! Uses FreeTDS defncopy for structure dump.
1 parent 98fed04 commit 348361d

File tree

10 files changed

+347
-142
lines changed

10 files changed

+347
-142
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
11

2+
## v4.2.2
3+
4+
#### Added
5+
6+
* DatabaseTasks support for all tasks! Uses FreeTDS `defncopy` for structure dump.
7+
8+
29
## v4.2.1
310

411
#### Fixed

README.md

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,7 @@
88

99
## Code Name Kantishna
1010

11-
The SQL Server adapter for ActiveRecord. If you need the adapter for SQL Server 2008 or 2005, you are still in the right spot. Just install the latest 3.2.x to 4.1.x version of the adapter. We follow a rational versioning policy that tracks ActiveRecord. That means that our 4.2.x version of the adapter is only for the latest 4.2 version of Rails. We also have stable branches for each major/minor release of ActiveRecord.
12-
13-
14-
#### Testing Rake Tasks Support
15-
16-
This is a long story, but if you are not working with a legacy database and you can trust your schema.rb to setup your local development or test database, then we have adapter level support for rails :db rake tasks. Please read this wiki page for full details.
17-
18-
http://wiki.github.com/rails-sqlserver/activerecord-sqlserver-adapter/rails-db-rake-tasks
11+
The SQL Server adapter for ActiveRecord v4.2 using SQL Server 2012 or higher. If you need the adapter for SQL Server 2008 or 2005, you are still in the right spot. Just install the latest 3.2.x to 4.1.x version of the adapter. We follow a rational versioning policy that tracks ActiveRecord. That means that our 4.2.x version of the adapter is only for the latest 4.2 version of Rails. We also have stable branches for each major/minor release of ActiveRecord.
1912

2013

2114
#### Executing Stored Procedures

lib/active_record/connection_adapters/sqlserver/database_statements.rb

Lines changed: 0 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -192,71 +192,6 @@ def newsequentialid_function
192192
select_value 'SELECT NEWSEQUENTIALID()'
193193
end
194194

195-
# === SQLServer Specific (Rake/Test Helpers) ==================== #
196-
197-
def recreate_database
198-
remove_database_connections_and_rollback do
199-
do_execute "EXEC sp_MSforeachtable 'DROP TABLE ?'"
200-
end
201-
end
202-
203-
def recreate_database!(database = nil)
204-
database ||= current_database
205-
drop_database(database)
206-
create_database(database)
207-
ensure
208-
use_database(database)
209-
end
210-
211-
def drop_database(database)
212-
retry_count = 0
213-
max_retries = 1
214-
name = SQLServer::Utils.extract_identifiers(database)
215-
begin
216-
do_execute "
217-
USE master
218-
IF EXISTS (
219-
SELECT * FROM [sys].[databases]
220-
WHERE name = #{quote(name.object)}
221-
) DROP DATABASE #{name}
222-
".squish
223-
rescue ActiveRecord::StatementInvalid => err
224-
if err.message =~ /because it is currently in use/i
225-
raise if retry_count >= max_retries
226-
retry_count += 1
227-
remove_database_connections_and_rollback(database)
228-
retry
229-
elsif err.message =~ /does not exist/i
230-
nil
231-
else
232-
raise
233-
end
234-
end
235-
end
236-
237-
def create_database(database, options = {})
238-
name = SQLServer::Utils.extract_identifiers(database)
239-
options = {collation: @connection_options[:collation]}.merge!(options.symbolize_keys)
240-
options = options.select { |_, v| v.present? }
241-
option_string = options.inject("") do |memo, (key, value)|
242-
memo += case key
243-
when :collation
244-
" COLLATE #{value}"
245-
else
246-
""
247-
end
248-
end
249-
do_execute "CREATE DATABASE #{name}#{option_string}"
250-
end
251-
252-
def current_database
253-
select_value 'SELECT DB_NAME()'
254-
end
255-
256-
def charset
257-
select_value "SELECT SERVERPROPERTY('SqlCharSetName')"
258-
end
259-
260195

261196
protected
262197

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
module ActiveRecord
2+
module ConnectionAdapters
3+
module SQLServer
4+
module DatabaseTasks
5+
6+
def create_database(database, options = {})
7+
name = SQLServer::Utils.extract_identifiers(database)
8+
options = {collation: @connection_options[:collation]}.merge!(options.symbolize_keys)
9+
options = options.select { |_, v| v.present? }
10+
option_string = options.inject("") do |memo, (key, value)|
11+
memo += case key
12+
when :collation
13+
" COLLATE #{value}"
14+
else
15+
""
16+
end
17+
end
18+
do_execute "CREATE DATABASE #{name}#{option_string}"
19+
end
20+
21+
def drop_database(database)
22+
name = SQLServer::Utils.extract_identifiers(database)
23+
do_execute "DROP DATABASE #{name}"
24+
end
25+
26+
def current_database
27+
select_value 'SELECT DB_NAME()'
28+
end
29+
30+
def charset
31+
select_value "SELECT DATABASEPROPERTYEX(DB_NAME(), 'SqlCharSetName')"
32+
end
33+
34+
def collation
35+
select_value "SELECT DATABASEPROPERTYEX(DB_NAME(), 'Collation')"
36+
end
37+
38+
end
39+
end
40+
end
41+
end
42+
43+
44+

lib/active_record/connection_adapters/sqlserver_adapter.rb

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
require 'active_record/connection_adapters/sqlserver/type'
1111
require 'active_record/connection_adapters/sqlserver/database_limits'
1212
require 'active_record/connection_adapters/sqlserver/database_statements'
13+
require 'active_record/connection_adapters/sqlserver/database_tasks'
1314
require 'active_record/connection_adapters/sqlserver/transaction'
1415
require 'active_record/connection_adapters/sqlserver/errors'
1516
require 'active_record/connection_adapters/sqlserver/schema_cache'
@@ -21,6 +22,7 @@
2122
require 'active_record/connection_adapters/sqlserver/utils'
2223
require 'active_record/sqlserver_base'
2324
require 'active_record/connection_adapters/sqlserver_column'
25+
require 'active_record/tasks/sqlserver_database_tasks'
2426

2527
module ActiveRecord
2628
module ConnectionAdapters
@@ -31,7 +33,8 @@ class SQLServerAdapter < AbstractAdapter
3133
SQLServer::DatabaseStatements,
3234
SQLServer::Showplan,
3335
SQLServer::SchemaStatements,
34-
SQLServer::DatabaseLimits
36+
SQLServer::DatabaseLimits,
37+
SQLServer::DatabaseTasks
3538

3639
ADAPTER_NAME = 'SQLServer'.freeze
3740

@@ -250,6 +253,8 @@ def translate_exception(e, message)
250253
InvalidForeignKey.new(message, e)
251254
when /has been chosen as the deadlock victim/i
252255
DeadlockVictim.new(message, e)
256+
when /database .* does not exist/i
257+
NoDatabaseError.new(message, e)
253258
else
254259
super
255260
end
@@ -350,16 +355,6 @@ def initialize_dateformatter
350355
::Time::DATE_FORMATS[:_sqlserver_dateformat] = dateformat
351356
end
352357

353-
def remove_database_connections_and_rollback(database = nil)
354-
name = SQLServer::Utils.extract_identifiers(database || current_database)
355-
do_execute "ALTER DATABASE #{name} SET SINGLE_USER WITH ROLLBACK IMMEDIATE"
356-
begin
357-
yield
358-
ensure
359-
do_execute "ALTER DATABASE #{name} SET MULTI_USER"
360-
end if block_given?
361-
end
362-
363358
end
364359
end
365360
end
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
require 'active_record/tasks/database_tasks'
2+
require 'shellwords'
3+
require 'ipaddr'
4+
require 'socket'
5+
6+
module ActiveRecord
7+
module Tasks
8+
9+
class SQLServerDatabaseTasks
10+
11+
DEFAULT_COLLATION = 'SQL_Latin1_General_CP1_CI_AS'
12+
13+
delegate :connection, :establish_connection, :clear_active_connections!,
14+
to: ActiveRecord::Base
15+
16+
def initialize(configuration)
17+
@configuration = configuration
18+
end
19+
20+
def create(master_established = false)
21+
establish_master_connection unless master_established
22+
connection.create_database configuration['database'], configuration.merge('collation' => default_collation)
23+
establish_connection configuration
24+
rescue ActiveRecord::StatementInvalid => error
25+
if /database .* already exists/i === error.message
26+
raise DatabaseAlreadyExists
27+
else
28+
raise
29+
end
30+
end
31+
32+
def drop
33+
establish_master_connection
34+
connection.drop_database configuration['database']
35+
end
36+
37+
def charset
38+
connection.charset
39+
end
40+
41+
def collation
42+
connection.collation
43+
end
44+
45+
def purge
46+
clear_active_connections!
47+
drop
48+
create true
49+
end
50+
51+
def structure_dump(filename)
52+
command = [
53+
"defncopy",
54+
"-S #{Shellwords.escape(configuration['host'])}",
55+
"-D #{Shellwords.escape(configuration['database'])}",
56+
"-U #{Shellwords.escape(configuration['username'])}",
57+
"-P #{Shellwords.escape(configuration['password'])}",
58+
"-o #{Shellwords.escape(filename)}",
59+
]
60+
table_args = connection.tables.map { |t| Shellwords.escape(t) }
61+
command.concat(table_args)
62+
view_args = connection.views.map { |v| Shellwords.escape(v) }
63+
command.concat(view_args)
64+
raise 'Error dumping database' unless Kernel.system(command.join(' '))
65+
dump = File.read(filename)
66+
dump.gsub!(/^USE .*$\nGO\n/, '') # Strip db USE statements
67+
dump.gsub!(/^GO\n/, '') # Strip db GO statements
68+
dump.gsub!(/nvarchar\(8000\)/, 'nvarchar(4000)') # Fix nvarchar(8000) column defs
69+
dump.gsub!(/nvarchar\(-1\)/, 'nvarchar(max)') # Fix nvarchar(-1) column defs
70+
dump.gsub!(/text\(\d+\)/, 'text') # Fix text(16) column defs
71+
File.open(filename, "w") { |file| file.puts dump }
72+
end
73+
74+
def structure_load(filename)
75+
connection.execute File.read(filename)
76+
end
77+
78+
79+
private
80+
81+
def configuration
82+
@configuration
83+
end
84+
85+
def default_collation
86+
configuration['collation'] || DEFAULT_COLLATION
87+
end
88+
89+
def establish_master_connection
90+
establish_connection configuration.merge('database' => 'master')
91+
end
92+
93+
end
94+
95+
module DatabaseTasksSQLServer
96+
97+
extend ActiveSupport::Concern
98+
99+
module ClassMethods
100+
101+
LOCAL_IPADDR = [
102+
IPAddr.new('192.168.0.0/16'),
103+
IPAddr.new('10.0.0.0/8'),
104+
IPAddr.new('172.16.0.0/12')
105+
]
106+
107+
private
108+
109+
def local_database?(configuration)
110+
super || local_ipaddr?(configuration_host_ip(configuration))
111+
end
112+
113+
def configuration_host_ip(configuration)
114+
return nil unless configuration['host']
115+
Socket::getaddrinfo(configuration['host'], 'echo', Socket::AF_INET)[0][3]
116+
end
117+
118+
def local_ipaddr?(host_ip)
119+
return false unless host_ip
120+
LOCAL_IPADDR.any? { |ip| ip.include?(host_ip) }
121+
end
122+
123+
end
124+
125+
end
126+
127+
DatabaseTasks.register_task %r{sqlserver}, SQLServerDatabaseTasks
128+
DatabaseTasks.send :include, DatabaseTasksSQLServer
129+
130+
end
131+
end

test/cases/coerced_tests.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,20 @@ module ConnectionAdapters
222222

223223

224224

225+
module ActiveRecord
226+
class DatabaseTasksCreateAllTest < ActiveRecord::TestCase
227+
# We extend `local_database?` so that common VM IPs can be used.
228+
coerce_tests! :test_ignores_remote_databases, :test_warning_for_remote_databases
229+
end
230+
class DatabaseTasksDropAllTest < ActiveRecord::TestCase
231+
# We extend `local_database?` so that common VM IPs can be used.
232+
coerce_tests! :test_ignores_remote_databases, :test_warning_for_remote_databases
233+
end
234+
end
235+
236+
237+
238+
225239
class DefaultScopingTest < ActiveRecord::TestCase
226240

227241
# We are not doing order duplicate removal anymore.

0 commit comments

Comments
 (0)