aq1018-sqlserver-2000-2008-adpater 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +55 -0
- data/MIT-LICENSE +20 -0
- data/README +0 -0
- data/RUNNING_UNIT_TESTS +60 -0
- data/Rakefile +44 -0
- data/lib/active_record/connection_adapters/sqlserver_adapter.rb +984 -0
- data/lib/core_ext/active_record.rb +93 -0
- data/lib/core_ext/dbi.rb +87 -0
- data/test/cases/aaaa_create_tables_test_sqlserver.rb +19 -0
- data/test/cases/adapter_test_sqlserver.rb +560 -0
- data/test/cases/basics_test_sqlserver.rb +21 -0
- data/test/cases/calculations_test_sqlserver.rb +20 -0
- data/test/cases/column_test_sqlserver.rb +246 -0
- data/test/cases/connection_test_sqlserver.rb +103 -0
- data/test/cases/eager_association_test_sqlserver.rb +22 -0
- data/test/cases/inheritance_test_sqlserver.rb +28 -0
- data/test/cases/migration_test_sqlserver.rb +57 -0
- data/test/cases/offset_and_limit_test_sqlserver.rb +89 -0
- data/test/cases/pessimistic_locking_test_sqlserver.rb +100 -0
- data/test/cases/query_cache_test_sqlserver.rb +24 -0
- data/test/cases/schema_dumper_test_sqlserver.rb +61 -0
- data/test/cases/specific_schema_test_sqlserver.rb +25 -0
- data/test/cases/sqlserver_helper.rb +100 -0
- data/test/cases/table_name_test_sqlserver.rb +22 -0
- data/test/cases/unicode_test_sqlserver.rb +44 -0
- data/test/connections/native_sqlserver/connection.rb +23 -0
- data/test/connections/native_sqlserver_odbc/connection.rb +27 -0
- data/test/migrations/transaction_table/1_table_will_never_be_created.rb +11 -0
- data/test/schema/sqlserver_specific_schema.rb +77 -0
- metadata +85 -0
data/CHANGELOG
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
|
2
|
+
MASTER
|
3
|
+
|
4
|
+
*
|
5
|
+
|
6
|
+
|
7
|
+
* 2.2.5 (January 4th, 2009)
|
8
|
+
|
9
|
+
* Added a log_info_schema_queries class attribute and make all queries to INFORMATION_SCHEMA silent by
|
10
|
+
default. [Ken Collins]
|
11
|
+
|
12
|
+
* Fix millisecond support in datetime columns. ODBC::Timestamp incorrectly takes SQL Server milliseconds
|
13
|
+
and applies them as nanoseconds. We cope with this at the DBI layer by using SQLServerDBI::Type::SqlserverTimestamp
|
14
|
+
class to parse the before type cast value appropriately. Also update the adapters #quoted_date method
|
15
|
+
to work more simply by converting ruby's #usec milliseconds to SQL Server microseconds. [Ken Collins]
|
16
|
+
|
17
|
+
* Core extensions for ActiveRecord now reflect on the connection before doing SQL Server things. Now
|
18
|
+
this adapter is compatible for using with other adapters. [Ken Collins]
|
19
|
+
|
20
|
+
|
21
|
+
* 2.2.4 (December 5th, 2008)
|
22
|
+
|
23
|
+
* Fix a type left in #views_real_column_name. Also cache #view_information lookups. [Ken Collins]
|
24
|
+
|
25
|
+
|
26
|
+
* 2.2.3 (December 5th, 2008)
|
27
|
+
|
28
|
+
* Changing back to using real table name in column_definitions. Makes sure views get back only the columns
|
29
|
+
that are defined for them with correct names, etc. Now supporting views by looking for NULL default and
|
30
|
+
then if table name is a view, perform a targeted with sub select to the real table name and column name
|
31
|
+
to find true default. [Ken Collins]
|
32
|
+
|
33
|
+
* Ensure that add_limit_offset! does not alter sub queries. [Erik Bryn]
|
34
|
+
|
35
|
+
|
36
|
+
2.2.2 (December 2nd, 2008)
|
37
|
+
|
38
|
+
* Add support for view defaults by making column_definitions use real table name for schema info. [Ken Collins]
|
39
|
+
|
40
|
+
* Include version in connection method and inspection. [Ken Collins]
|
41
|
+
|
42
|
+
|
43
|
+
2.2.1 (November 25th, 2008)
|
44
|
+
|
45
|
+
* Add identity insert support for views. Cache #views so that identity #table_name_or_views_table_name
|
46
|
+
will run quickly. [Ken Collins]
|
47
|
+
|
48
|
+
* Add views support. ActiveRecord classes can use views. The connection now has a #views method and
|
49
|
+
#table_exists? will not fall back to checking views too. [Ken Collins]
|
50
|
+
|
51
|
+
|
52
|
+
2.2.0 (November 21st, 2008)
|
53
|
+
|
54
|
+
* Release for rails 2.2.2. Many many changes. [Ken Collins], [Murray Steele], [Shawn Balestracci], [Joe Rafaniello]
|
55
|
+
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
File without changes
|
data/RUNNING_UNIT_TESTS
ADDED
@@ -0,0 +1,60 @@
|
|
1
|
+
== Creating the test database
|
2
|
+
|
3
|
+
The default names for the test databases are "activerecord_unittest" and
|
4
|
+
"activerecord_unittest2". If you want to use another database name then be sure
|
5
|
+
to update the connection adapter setups you want to test with in
|
6
|
+
test/connections/<your database>/connection.rb.
|
7
|
+
|
8
|
+
|
9
|
+
== Requirements
|
10
|
+
|
11
|
+
The following gems need to be installed. Make sure you have gems.github.com as a
|
12
|
+
source. http://github.com/blog/97-github-loves-rubygems-1-2
|
13
|
+
|
14
|
+
* gem install thoughtbot-shoulda -s http://gems.github.com
|
15
|
+
* gem install mocha
|
16
|
+
|
17
|
+
The tests of this adapter depend on the existence of rails checkout. All the tests
|
18
|
+
defined by rails are re-used. For this to work the following directory structure
|
19
|
+
is assumed to exist:
|
20
|
+
|
21
|
+
#{RAILS_ROOT}/vendor/plugins/adapters/sqlserver
|
22
|
+
#{RAILS_ROOT}/vendor/rails/activerecord/test
|
23
|
+
|
24
|
+
Define a user named 'rails' in SQL Server with all privileges granted. Use an empty
|
25
|
+
password for user 'rails', or alternatively use the OSQLPASSWORD environment variable
|
26
|
+
which allows you to set a default password for the current session.
|
27
|
+
|
28
|
+
Then run "rake create_databases".
|
29
|
+
|
30
|
+
|
31
|
+
== Running with Rake
|
32
|
+
|
33
|
+
The easiest way to run the unit tests is through Rake. Either run "rake test_sqlserver"
|
34
|
+
or "rake test_sqlserver_odbc". For more information, checkout the full array
|
35
|
+
of rake tasks with "rake -T"
|
36
|
+
|
37
|
+
Rake can be found at http://rake.rubyforge.org
|
38
|
+
|
39
|
+
|
40
|
+
== Running with Autotest
|
41
|
+
|
42
|
+
Using autotest is easy, just run "autotest" and the tests will run continually in the
|
43
|
+
same order as the rake test command. By default autotest will use ODBC connection. If
|
44
|
+
you want to change this you can edit the autotest/sqlserver.rb file and set odbc_mode
|
45
|
+
to false.
|
46
|
+
|
47
|
+
Lastly, you can run autotest on just the adapter specific tests with "autotest sqlserver".
|
48
|
+
This will continuously run ONLY the SQL Sever specific behavior tests which are much
|
49
|
+
quicker to run than the entire active record test suite.
|
50
|
+
|
51
|
+
|
52
|
+
== Running by hand
|
53
|
+
|
54
|
+
Unit tests are located in test directory. If you only want to run a single test suite,
|
55
|
+
you can do so with:
|
56
|
+
|
57
|
+
rake test_sqlserver TEST=base_test.rb
|
58
|
+
|
59
|
+
That'll run the base suite using the SQLServer-Ruby adapter.
|
60
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/testtask'
|
4
|
+
|
5
|
+
|
6
|
+
desc 'Create the SQL Server test databases'
|
7
|
+
task :create_databases do
|
8
|
+
# Define a user named 'rails' in SQL Server with all privileges granted
|
9
|
+
# Use an empty password for user 'rails', or alternatively use the OSQLPASSWORD environment variable
|
10
|
+
# which allows you to set a default password for the current session.
|
11
|
+
%x( osql -S 192.168.0.99 -U rails -Q "create database activerecord_unittest" -P )
|
12
|
+
%x( osql -S 192.168.0.99 -U rails -Q "create database activerecord_unittest2" -P )
|
13
|
+
%x( osql -S 192.168.0.99 -U rails -d activerecord_unittest -Q "exec sp_grantdbaccess 'rails'" -P )
|
14
|
+
%x( osql -S 192.168.0.99 -U rails -d activerecord_unittest2 -Q "exec sp_grantdbaccess 'rails'" -P )
|
15
|
+
%x( osql -S 192.168.0.99 -U rails -d activerecord_unittest -Q "grant BACKUP DATABASE, BACKUP LOG, CREATE DEFAULT, CREATE FUNCTION, CREATE PROCEDURE, CREATE RULE, CREATE TABLE, CREATE VIEW to 'rails';" -P )
|
16
|
+
%x( osql -S 192.168.0.99 -U rails -d activerecord_unittest2 -Q "grant BACKUP DATABASE, BACKUP LOG, CREATE DEFAULT, CREATE FUNCTION, CREATE PROCEDURE, CREATE RULE, CREATE TABLE, CREATE VIEW to 'rails';" -P )
|
17
|
+
end
|
18
|
+
|
19
|
+
desc 'Drop the SQL Server test databases'
|
20
|
+
task :drop_databases do
|
21
|
+
%x( osql -S 192.168.0.99 -U rails -Q "drop database activerecord_unittest" -P )
|
22
|
+
%x( osql -S 192.168.0.99 -U rails -Q "drop database activerecord_unittest2" -P )
|
23
|
+
end
|
24
|
+
|
25
|
+
desc 'Recreate the SQL Server test databases'
|
26
|
+
task :recreate_databases => [:drop_databases, :create_databases]
|
27
|
+
|
28
|
+
|
29
|
+
for adapter in %w( sqlserver sqlserver_odbc )
|
30
|
+
Rake::TestTask.new("test_#{adapter}") { |t|
|
31
|
+
t.libs << "test"
|
32
|
+
t.libs << "test/connections/native_#{adapter}"
|
33
|
+
t.libs << "../../rails/activerecord/test/"
|
34
|
+
t.test_files = (
|
35
|
+
Dir.glob("test/cases/**/*_test_sqlserver.rb").sort +
|
36
|
+
Dir.glob("../../rails/activerecord/test/**/*_test.rb").sort )
|
37
|
+
t.verbose = true
|
38
|
+
}
|
39
|
+
|
40
|
+
namespace adapter do
|
41
|
+
task :test => "test_#{adapter}"
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
@@ -0,0 +1,984 @@
|
|
1
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
2
|
+
require_library_or_gem 'dbi' unless defined?(DBI)
|
3
|
+
require 'core_ext/dbi'
|
4
|
+
require 'core_ext/active_record'
|
5
|
+
require 'base64'
|
6
|
+
|
7
|
+
module ActiveRecord
|
8
|
+
|
9
|
+
class Base
|
10
|
+
|
11
|
+
def self.sqlserver_connection(config) #:nodoc:
|
12
|
+
config.symbolize_keys!
|
13
|
+
mode = config[:mode] ? config[:mode].to_s.upcase : 'ADO'
|
14
|
+
username = config[:username] ? config[:username].to_s : 'sa'
|
15
|
+
password = config[:password] ? config[:password].to_s : ''
|
16
|
+
if mode == "ODBC"
|
17
|
+
raise ArgumentError, "Missing DSN. Argument ':dsn' must be set in order for this adapter to work." unless config.has_key?(:dsn)
|
18
|
+
dsn = config[:dsn]
|
19
|
+
driver_url = "DBI:ODBC:#{dsn}"
|
20
|
+
else
|
21
|
+
raise ArgumentError, "Missing Database. Argument ':database' must be set in order for this adapter to work." unless config.has_key?(:database)
|
22
|
+
database = config[:database]
|
23
|
+
host = config[:host] ? config[:host].to_s : 'localhost'
|
24
|
+
driver_url = "DBI:ADO:Provider=SQLOLEDB;Data Source=#{host};Initial Catalog=#{database};User ID=#{username};Password=#{password};"
|
25
|
+
end
|
26
|
+
conn = DBI.connect(driver_url, username, password)
|
27
|
+
conn["AutoCommit"] = true
|
28
|
+
ConnectionAdapters::SQLServerAdapter.new(conn, logger, [driver_url, username, password])
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
module ConnectionAdapters
|
34
|
+
|
35
|
+
class SQLServerColumn < Column
|
36
|
+
|
37
|
+
def initialize(name, default, sql_type = nil, null = true, sqlserver_options = {})
|
38
|
+
@sqlserver_options = sqlserver_options
|
39
|
+
super(name, default, sql_type, null)
|
40
|
+
end
|
41
|
+
|
42
|
+
class << self
|
43
|
+
|
44
|
+
def string_to_binary(value)
|
45
|
+
"0x#{value.unpack("H*")[0]}"
|
46
|
+
end
|
47
|
+
|
48
|
+
def binary_to_string(value)
|
49
|
+
value =~ /[^[:xdigit:]]/ ? value : [value].pack('H*')
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
def is_identity?
|
55
|
+
@sqlserver_options[:is_identity]
|
56
|
+
end
|
57
|
+
|
58
|
+
def is_special?
|
59
|
+
# TODO: Not sure if these should be added: varbinary(max), nchar, nvarchar(max)
|
60
|
+
sql_type =~ /^text|ntext|image$/
|
61
|
+
end
|
62
|
+
|
63
|
+
def is_utf8?
|
64
|
+
sql_type =~ /nvarchar|ntext|nchar/i
|
65
|
+
end
|
66
|
+
|
67
|
+
def table_name
|
68
|
+
@sqlserver_options[:table_name]
|
69
|
+
end
|
70
|
+
|
71
|
+
def table_klass
|
72
|
+
@table_klass ||= table_name.classify.constantize rescue nil
|
73
|
+
(@table_klass && @table_klass < ActiveRecord::Base) ? @table_klass : nil
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def extract_limit(sql_type)
|
79
|
+
case sql_type
|
80
|
+
when /^smallint/i
|
81
|
+
2
|
82
|
+
when /^int/i
|
83
|
+
4
|
84
|
+
when /^bigint/i
|
85
|
+
8
|
86
|
+
when /\(max\)/, /decimal/, /numeric/
|
87
|
+
nil
|
88
|
+
else
|
89
|
+
super
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def simplified_type(field_type)
|
94
|
+
case field_type
|
95
|
+
when /real/i then :float
|
96
|
+
when /money/i then :decimal
|
97
|
+
when /image/i then :binary
|
98
|
+
when /bit/i then :boolean
|
99
|
+
when /uniqueidentifier/i then :string
|
100
|
+
when /datetime/i then simplified_datetime
|
101
|
+
else super
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def simplified_datetime
|
106
|
+
if table_klass && table_klass.coerced_sqlserver_date_columns.include?(name)
|
107
|
+
:date
|
108
|
+
elsif table_klass && table_klass.coerced_sqlserver_time_columns.include?(name)
|
109
|
+
:time
|
110
|
+
else
|
111
|
+
:datetime
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
end #SQLServerColumn
|
116
|
+
|
117
|
+
# In ADO mode, this adapter will ONLY work on Windows systems, since it relies on
|
118
|
+
# Win32OLE, which, to my knowledge, is only available on Windows.
|
119
|
+
#
|
120
|
+
# This mode also relies on the ADO support in the DBI module. If you are using the
|
121
|
+
# one-click installer of Ruby, then you already have DBI installed, but the ADO module
|
122
|
+
# is *NOT* installed. You will need to get the latest source distribution of Ruby-DBI
|
123
|
+
# from http://ruby-dbi.sourceforge.net/ unzip it, and copy the file from
|
124
|
+
# <tt>src/lib/dbd_ado/ADO.rb</tt> to <tt>X:/Ruby/lib/ruby/site_ruby/1.8/DBD/ADO/ADO.rb</tt>
|
125
|
+
#
|
126
|
+
# You will more than likely need to create the ADO directory. Once you've installed
|
127
|
+
# that file, you are ready to go.
|
128
|
+
#
|
129
|
+
# In ODBC mode, the adapter requires the ODBC support in the DBI module which requires
|
130
|
+
# the Ruby ODBC module. Ruby ODBC 0.996 was used in development and testing,
|
131
|
+
# and it is available at http://www.ch-werner.de/rubyodbc/
|
132
|
+
#
|
133
|
+
# Options:
|
134
|
+
#
|
135
|
+
# * <tt>:mode</tt> -- ADO or ODBC. Defaults to ADO.
|
136
|
+
# * <tt>:username</tt> -- Defaults to sa.
|
137
|
+
# * <tt>:password</tt> -- Defaults to empty string.
|
138
|
+
# * <tt>:windows_auth</tt> -- Defaults to "User ID=#{username};Password=#{password}"
|
139
|
+
#
|
140
|
+
# ADO specific options:
|
141
|
+
#
|
142
|
+
# * <tt>:host</tt> -- Defaults to localhost.
|
143
|
+
# * <tt>:database</tt> -- The name of the database. No default, must be provided.
|
144
|
+
# * <tt>:windows_auth</tt> -- Use windows authentication instead of username/password.
|
145
|
+
#
|
146
|
+
# ODBC specific options:
|
147
|
+
#
|
148
|
+
# * <tt>:dsn</tt> -- Defaults to nothing.
|
149
|
+
#
|
150
|
+
class SQLServerAdapter < AbstractAdapter
|
151
|
+
|
152
|
+
ADAPTER_NAME = 'SQLServer'.freeze
|
153
|
+
VERSION = '2.2.5'.freeze
|
154
|
+
DATABASE_VERSION_REGEXP = /Microsoft SQL Server\s+(\d{4})/
|
155
|
+
SUPPORTED_VERSIONS = [2000,2005,2008].freeze
|
156
|
+
LIMITABLE_TYPES = ['string','integer','float','char','nchar','varchar','nvarchar'].freeze
|
157
|
+
|
158
|
+
cattr_accessor :native_text_database_type, :native_binary_database_type, :log_info_schema_queries
|
159
|
+
|
160
|
+
class << self
|
161
|
+
|
162
|
+
def type_limitable?(type)
|
163
|
+
LIMITABLE_TYPES.include?(type.to_s)
|
164
|
+
end
|
165
|
+
|
166
|
+
end
|
167
|
+
|
168
|
+
def initialize(connection, logger, connection_options=nil)
|
169
|
+
super(connection, logger)
|
170
|
+
@connection_options = connection_options
|
171
|
+
initialize_sqlserver_caches
|
172
|
+
unless SUPPORTED_VERSIONS.include?(database_year)
|
173
|
+
raise NotImplementedError, "Currently, only #{SUPPORTED_VERSIONS.to_sentence} are supported."
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# ABSTRACT ADAPTER =========================================#
|
178
|
+
|
179
|
+
def adapter_name
|
180
|
+
ADAPTER_NAME
|
181
|
+
end
|
182
|
+
|
183
|
+
def supports_migrations?
|
184
|
+
true
|
185
|
+
end
|
186
|
+
|
187
|
+
def supports_ddl_transactions?
|
188
|
+
true
|
189
|
+
end
|
190
|
+
|
191
|
+
def database_version
|
192
|
+
@database_version ||= info_schema_query { select_value('SELECT @@version') }
|
193
|
+
end
|
194
|
+
|
195
|
+
def database_year
|
196
|
+
DATABASE_VERSION_REGEXP.match(database_version)[1].to_i
|
197
|
+
end
|
198
|
+
|
199
|
+
def sqlserver?
|
200
|
+
true
|
201
|
+
end
|
202
|
+
|
203
|
+
def sqlserver_2000?
|
204
|
+
database_year == 2000
|
205
|
+
end
|
206
|
+
|
207
|
+
def sqlserver_2005?
|
208
|
+
database_year == 2005
|
209
|
+
end
|
210
|
+
|
211
|
+
def sqlserver_2008?
|
212
|
+
database_year == 2008
|
213
|
+
end
|
214
|
+
|
215
|
+
def version
|
216
|
+
self.class::VERSION
|
217
|
+
end
|
218
|
+
|
219
|
+
def inspect
|
220
|
+
"#<#{self.class} version: #{version}, year: #{database_year}, connection_options: #{@connection_options.inspect}>"
|
221
|
+
end
|
222
|
+
|
223
|
+
def native_text_database_type
|
224
|
+
self.class.native_text_database_type || ((sqlserver_2005? || sqlserver_2008?) ? 'varchar(max)' : 'text')
|
225
|
+
end
|
226
|
+
|
227
|
+
def native_binary_database_type
|
228
|
+
self.class.native_binary_database_type || ((sqlserver_2005? || sqlserver_2008?) ? 'varbinary(max)' : 'image')
|
229
|
+
end
|
230
|
+
|
231
|
+
# QUOTING ==================================================#
|
232
|
+
|
233
|
+
def quote(value, column = nil)
|
234
|
+
case value
|
235
|
+
when String, ActiveSupport::Multibyte::Chars
|
236
|
+
if column && column.type == :binary
|
237
|
+
column.class.string_to_binary(value)
|
238
|
+
elsif column && column.respond_to?(:is_utf8?) && column.is_utf8?
|
239
|
+
quoted_utf8_value(value)
|
240
|
+
else
|
241
|
+
super
|
242
|
+
end
|
243
|
+
else
|
244
|
+
super
|
245
|
+
end
|
246
|
+
end
|
247
|
+
|
248
|
+
def quote_string(string)
|
249
|
+
string.to_s.gsub(/\'/, "''")
|
250
|
+
end
|
251
|
+
|
252
|
+
def quote_column_name(column_name)
|
253
|
+
column_name.to_s.split('.').map{ |name| "[#{name}]" }.join('.')
|
254
|
+
end
|
255
|
+
|
256
|
+
def quote_table_name(table_name)
|
257
|
+
return table_name if table_name =~ /^\[.*\]$/
|
258
|
+
quote_column_name(table_name)
|
259
|
+
end
|
260
|
+
|
261
|
+
def quoted_true
|
262
|
+
'1'
|
263
|
+
end
|
264
|
+
|
265
|
+
def quoted_false
|
266
|
+
'0'
|
267
|
+
end
|
268
|
+
|
269
|
+
def quoted_date(value)
|
270
|
+
if value.acts_like?(:time) && value.respond_to?(:usec)
|
271
|
+
"#{super}.#{sprintf("%03d",value.usec/1000)}"
|
272
|
+
else
|
273
|
+
super
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def quoted_utf8_value(value)
|
278
|
+
"N'#{quote_string(value)}'"
|
279
|
+
end
|
280
|
+
|
281
|
+
# REFERENTIAL INTEGRITY ====================================#
|
282
|
+
|
283
|
+
def disable_referential_integrity(&block)
|
284
|
+
do_execute "EXEC sp_MSForEachTable 'ALTER TABLE ? NOCHECK CONSTRAINT ALL'"
|
285
|
+
yield
|
286
|
+
ensure
|
287
|
+
do_execute "EXEC sp_MSForEachTable 'ALTER TABLE ? CHECK CONSTRAINT ALL'"
|
288
|
+
end
|
289
|
+
|
290
|
+
# CONNECTION MANAGEMENT ====================================#
|
291
|
+
|
292
|
+
def active?
|
293
|
+
raw_connection.execute("SELECT 1").finish
|
294
|
+
true
|
295
|
+
rescue DBI::DatabaseError, DBI::InterfaceError
|
296
|
+
false
|
297
|
+
end
|
298
|
+
|
299
|
+
def reconnect!
|
300
|
+
disconnect!
|
301
|
+
@connection = DBI.connect(*@connection_options)
|
302
|
+
rescue DBI::DatabaseError => e
|
303
|
+
@logger.warn "#{adapter_name} reconnection failed: #{e.message}" if @logger
|
304
|
+
false
|
305
|
+
end
|
306
|
+
|
307
|
+
def disconnect!
|
308
|
+
raw_connection.disconnect rescue nil
|
309
|
+
end
|
310
|
+
|
311
|
+
def finish_statement_handle(handle)
|
312
|
+
handle.finish if handle && handle.respond_to?(:finish) && !handle.finished?
|
313
|
+
handle
|
314
|
+
end
|
315
|
+
|
316
|
+
# DATABASE STATEMENTS ======================================#
|
317
|
+
|
318
|
+
def select_rows(sql, name = nil)
|
319
|
+
raw_select(sql,name).last
|
320
|
+
end
|
321
|
+
|
322
|
+
def execute(sql, name = nil, &block)
|
323
|
+
if table_name = query_requires_identity_insert?(sql)
|
324
|
+
handle = with_identity_insert_enabled(table_name) { raw_execute(sql,name,&block) }
|
325
|
+
else
|
326
|
+
handle = raw_execute(sql,name,&block)
|
327
|
+
end
|
328
|
+
finish_statement_handle(handle)
|
329
|
+
end
|
330
|
+
|
331
|
+
def begin_db_transaction
|
332
|
+
do_execute "BEGIN TRANSACTION"
|
333
|
+
end
|
334
|
+
|
335
|
+
def commit_db_transaction
|
336
|
+
do_execute "COMMIT TRANSACTION"
|
337
|
+
end
|
338
|
+
|
339
|
+
def rollback_db_transaction
|
340
|
+
do_execute "ROLLBACK TRANSACTION" rescue nil
|
341
|
+
end
|
342
|
+
|
343
|
+
def add_limit_offset!(sql, options)
|
344
|
+
# Validate and/or convert integers for :limit and :offets options.
|
345
|
+
if options[:offset]
|
346
|
+
raise ArgumentError, "offset should have a limit" unless options[:limit]
|
347
|
+
unless options[:offset].kind_of?(Integer)
|
348
|
+
if options[:offset] =~ /^\d+$/
|
349
|
+
options[:offset] = options[:offset].to_i
|
350
|
+
else
|
351
|
+
raise ArgumentError, "offset should be an integer"
|
352
|
+
end
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
|
357
|
+
if options[:limit] && !(options[:limit].kind_of?(Integer))
|
358
|
+
if options[:limit] =~ /^\d+$/
|
359
|
+
options[:limit] = options[:limit].to_i
|
360
|
+
else
|
361
|
+
raise ArgumentError, "limit should be an integer"
|
362
|
+
end
|
363
|
+
end
|
364
|
+
|
365
|
+
|
366
|
+
# The business of adding limit/offset
|
367
|
+
if options[:limit] and options[:offset]
|
368
|
+
|
369
|
+
# adjust :limit to accommodate edge cases where there is not enough rows to reach the limit.
|
370
|
+
# Do we even need this??
|
371
|
+
|
372
|
+
total_rows = select_value("SELECT count(*) as TotalRows from (#{sql.sub(/\bSELECT(\s+DISTINCT)?\b/i, "SELECT#{$1} TOP 1000000000")}) tally").to_i
|
373
|
+
if (options[:limit] + options[:offset]) >= total_rows
|
374
|
+
options[:limit] = (total_rows - options[:offset] >= 0) ? (total_rows - options[:offset]) : 0
|
375
|
+
end
|
376
|
+
|
377
|
+
# Make sure we do not need a special limit/offset for association limiting. http://gist.github.com/25118
|
378
|
+
add_limit_offset_for_association_limiting!(sql,options) and return if sql_for_association_limiting?(sql)
|
379
|
+
|
380
|
+
# Wrap the SQL query in a bunch of outer SQL queries that emulate proper LIMIT,OFFSET support.
|
381
|
+
#
|
382
|
+
sql.sub!(/^\s*SELECT(\s+DISTINCT)?/i, "SELECT * FROM (SELECT TOP #{options[:limit]} * FROM (SELECT#{$1} TOP #{options[:limit] + options[:offset]}")
|
383
|
+
|
384
|
+
|
385
|
+
sql << ") AS tmp1"
|
386
|
+
|
387
|
+
if options[:order]
|
388
|
+
order = options[:order].split(',').map do |field|
|
389
|
+
order_by_column, order_direction = field.split(" ")
|
390
|
+
order_by_column = quote_column_name(order_by_column)
|
391
|
+
# Investigate the SQL query to figure out if the order_by_column has been renamed.
|
392
|
+
if sql =~ /#{Regexp.escape(order_by_column)} AS (t\d_r\d\d?)/
|
393
|
+
# Fx "[foo].[bar] AS t4_r2" was found in the SQL. Use the column alias (ie 't4_r2') for the subsequent orderings
|
394
|
+
order_by_column = $1
|
395
|
+
elsif order_by_column =~ /\w+\.\[?(\w+)\]?/
|
396
|
+
order_by_column = $1
|
397
|
+
else
|
398
|
+
# It doesn't appear that the column name has been renamed as part of the query. Use just the column
|
399
|
+
# name rather than the full identifier for the outer queries.
|
400
|
+
order_by_column = order_by_column.split('.').last
|
401
|
+
end
|
402
|
+
# Put the column name and eventual direction back together
|
403
|
+
[order_by_column, order_direction].join(' ').strip
|
404
|
+
end.join(', ')
|
405
|
+
sql << " ORDER BY #{change_order_direction(order)}) AS tmp2 ORDER BY #{order}"
|
406
|
+
else
|
407
|
+
sql << ") AS tmp2"
|
408
|
+
end
|
409
|
+
elsif options[:limit] && sql !~ /^\s*SELECT (@@|COUNT\()/i
|
410
|
+
if md = sql.match(/^(\s*SELECT)(\s+DISTINCT)?(.*)/im)
|
411
|
+
sql.replace "#{md[1]}#{md[2]} TOP #{options[:limit]}#{md[3]}"
|
412
|
+
else
|
413
|
+
# Account for building SQL fragments without SELECT yet. See #update_all and #limited_update_conditions.
|
414
|
+
sql.replace "TOP #{options[:limit]} #{sql}"
|
415
|
+
end
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
def add_lock!(sql, options)
|
420
|
+
# http://blog.sqlauthority.com/2007/04/27/sql-server-2005-locking-hints-and-examples/
|
421
|
+
return unless options[:lock]
|
422
|
+
lock_type = options[:lock] == true ? 'WITH(HOLDLOCK, ROWLOCK)' : options[:lock]
|
423
|
+
from_table = sql.match(/FROM(.*)WHERE/im)[1]
|
424
|
+
sql.sub! from_table, "#{from_table}#{lock_type} "
|
425
|
+
end
|
426
|
+
|
427
|
+
def empty_insert_statement(table_name)
|
428
|
+
"INSERT INTO #{quote_table_name(table_name)} DEFAULT VALUES"
|
429
|
+
end
|
430
|
+
|
431
|
+
def case_sensitive_equality_operator
|
432
|
+
"COLLATE Latin1_General_CS_AS ="
|
433
|
+
end
|
434
|
+
|
435
|
+
def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
|
436
|
+
match_data = where_sql.match(/(.*)WHERE/)
|
437
|
+
limit = match_data[1]
|
438
|
+
where_sql.sub!(limit,'')
|
439
|
+
"WHERE #{quoted_primary_key} IN (SELECT #{limit} #{quoted_primary_key} FROM #{quoted_table_name} #{where_sql})"
|
440
|
+
end
|
441
|
+
|
442
|
+
# SCHEMA STATEMENTS ========================================#
|
443
|
+
|
444
|
+
def native_database_types
|
445
|
+
{
|
446
|
+
:primary_key => "int NOT NULL IDENTITY(1, 1) PRIMARY KEY",
|
447
|
+
:string => { :name => "varchar", :limit => 255 },
|
448
|
+
:text => { :name => native_text_database_type },
|
449
|
+
:integer => { :name => "int", :limit => 4 },
|
450
|
+
:float => { :name => "float", :limit => 8 },
|
451
|
+
:decimal => { :name => "decimal" },
|
452
|
+
:datetime => { :name => "datetime" },
|
453
|
+
:timestamp => { :name => "datetime" },
|
454
|
+
:time => { :name => "datetime" },
|
455
|
+
:date => { :name => "datetime" },
|
456
|
+
:binary => { :name => native_binary_database_type },
|
457
|
+
:boolean => { :name => "bit"},
|
458
|
+
# These are custom types that may move somewhere else for good schema_dumper.rb hacking to output them.
|
459
|
+
:char => { :name => 'char' },
|
460
|
+
:varchar_max => { :name => 'varchar(max)' },
|
461
|
+
:nchar => { :name => "nchar" },
|
462
|
+
:nvarchar => { :name => "nvarchar", :limit => 255 },
|
463
|
+
:nvarchar_max => { :name => "nvarchar(max)" },
|
464
|
+
:ntext => { :name => "ntext" }
|
465
|
+
}
|
466
|
+
end
|
467
|
+
|
468
|
+
def table_alias_length
|
469
|
+
128
|
470
|
+
end
|
471
|
+
|
472
|
+
def tables(name = nil)
|
473
|
+
info_schema_query do
|
474
|
+
select_values "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME <> 'dtproperties'"
|
475
|
+
end
|
476
|
+
end
|
477
|
+
|
478
|
+
def views(name = nil)
|
479
|
+
@sqlserver_views_cache ||=
|
480
|
+
info_schema_query { select_values("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_NAME NOT IN ('sysconstraints','syssegments')") }
|
481
|
+
end
|
482
|
+
|
483
|
+
def view_information(table_name)
|
484
|
+
table_name = unqualify_table_name(table_name)
|
485
|
+
@sqlserver_view_information_cache[table_name] ||=
|
486
|
+
info_schema_query { select_one("SELECT * FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_NAME = '#{table_name}'") }
|
487
|
+
end
|
488
|
+
|
489
|
+
def view_table_name(table_name)
|
490
|
+
view_info = view_information(table_name)
|
491
|
+
view_info ? get_table_name(view_info['VIEW_DEFINITION']) : table_name
|
492
|
+
end
|
493
|
+
|
494
|
+
def table_exists?(table_name)
|
495
|
+
super || views.include?(table_name.to_s)
|
496
|
+
end
|
497
|
+
|
498
|
+
def indexes(table_name, name = nil)
|
499
|
+
select("EXEC sp_helpindex #{quote_table_name(table_name)}",name).inject([]) do |indexes,index|
|
500
|
+
if index['index_description'] =~ /primary key/
|
501
|
+
indexes
|
502
|
+
else
|
503
|
+
name = index['index_name']
|
504
|
+
unique = index['index_description'] =~ /unique/
|
505
|
+
columns = index['index_keys'].split(',').map do |column|
|
506
|
+
column.strip!
|
507
|
+
column.gsub! '(-)', '' if column.ends_with?('(-)')
|
508
|
+
column
|
509
|
+
end
|
510
|
+
indexes << IndexDefinition.new(table_name, name, unique, columns)
|
511
|
+
end
|
512
|
+
end
|
513
|
+
end
|
514
|
+
|
515
|
+
def columns(table_name, name = nil)
|
516
|
+
return [] if table_name.blank?
|
517
|
+
cache_key = unqualify_table_name(table_name)
|
518
|
+
@sqlserver_columns_cache[cache_key] ||= column_definitions(table_name).collect do |ci|
|
519
|
+
sqlserver_options = ci.except(:name,:default_value,:type,:null)
|
520
|
+
SQLServerColumn.new ci[:name], ci[:default_value], ci[:type], ci[:null], sqlserver_options
|
521
|
+
end
|
522
|
+
end
|
523
|
+
|
524
|
+
def create_table(table_name, options = {})
|
525
|
+
super
|
526
|
+
remove_sqlserver_columns_cache_for(table_name)
|
527
|
+
end
|
528
|
+
|
529
|
+
def rename_table(table_name, new_name)
|
530
|
+
do_execute "EXEC sp_rename '#{table_name}', '#{new_name}'"
|
531
|
+
end
|
532
|
+
|
533
|
+
def drop_table(table_name, options = {})
|
534
|
+
super
|
535
|
+
remove_sqlserver_columns_cache_for(table_name)
|
536
|
+
end
|
537
|
+
|
538
|
+
def add_column(table_name, column_name, type, options = {})
|
539
|
+
super
|
540
|
+
remove_sqlserver_columns_cache_for(table_name)
|
541
|
+
end
|
542
|
+
|
543
|
+
def remove_column(table_name, *column_names)
|
544
|
+
column_names.flatten.each do |column_name|
|
545
|
+
remove_check_constraints(table_name, column_name)
|
546
|
+
remove_default_constraint(table_name, column_name)
|
547
|
+
remove_indexes(table_name, column_name)
|
548
|
+
do_execute "ALTER TABLE #{quote_table_name(table_name)} DROP COLUMN #{quote_column_name(column_name)}"
|
549
|
+
end
|
550
|
+
remove_sqlserver_columns_cache_for(table_name)
|
551
|
+
end
|
552
|
+
|
553
|
+
def change_column(table_name, column_name, type, options = {})
|
554
|
+
sql_commands = []
|
555
|
+
change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
|
556
|
+
change_column_sql << " NOT NULL" if options[:null] == false
|
557
|
+
sql_commands << change_column_sql
|
558
|
+
if options_include_default?(options)
|
559
|
+
remove_default_constraint(table_name, column_name)
|
560
|
+
sql_commands << "ALTER TABLE #{quote_table_name(table_name)} ADD CONSTRAINT #{default_name(table_name,column_name)} DEFAULT #{quote(options[:default])} FOR #{quote_column_name(column_name)}"
|
561
|
+
end
|
562
|
+
sql_commands.each { |c| do_execute(c) }
|
563
|
+
remove_sqlserver_columns_cache_for(table_name)
|
564
|
+
end
|
565
|
+
|
566
|
+
def change_column_default(table_name, column_name, default)
|
567
|
+
remove_default_constraint(table_name, column_name)
|
568
|
+
do_execute "ALTER TABLE #{quote_table_name(table_name)} ADD CONSTRAINT #{default_name(table_name, column_name)} DEFAULT #{quote(default)} FOR #{quote_column_name(column_name)}"
|
569
|
+
remove_sqlserver_columns_cache_for(table_name)
|
570
|
+
end
|
571
|
+
|
572
|
+
def rename_column(table_name, column_name, new_column_name)
|
573
|
+
column_for(table_name,column_name)
|
574
|
+
do_execute "EXEC sp_rename '#{table_name}.#{column_name}', '#{new_column_name}', 'COLUMN'"
|
575
|
+
remove_sqlserver_columns_cache_for(table_name)
|
576
|
+
end
|
577
|
+
|
578
|
+
def remove_index(table_name, options = {})
|
579
|
+
do_execute "DROP INDEX #{table_name}.#{quote_column_name(index_name(table_name, options))}"
|
580
|
+
end
|
581
|
+
|
582
|
+
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
|
583
|
+
limit = nil unless self.class.type_limitable?(type)
|
584
|
+
case type.to_s
|
585
|
+
when 'integer'
|
586
|
+
case limit
|
587
|
+
when 1..2 then 'smallint'
|
588
|
+
when 3..4, nil then 'integer'
|
589
|
+
when 5..8 then 'bigint'
|
590
|
+
else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
|
591
|
+
end
|
592
|
+
else
|
593
|
+
super
|
594
|
+
end
|
595
|
+
end
|
596
|
+
|
597
|
+
def add_order_by_for_association_limiting!(sql, options)
|
598
|
+
# Disertation http://gist.github.com/24073
|
599
|
+
# Information http://weblogs.sqlteam.com/jeffs/archive/2007/12/13/select-distinct-order-by-error.aspx
|
600
|
+
return sql if options[:order].blank?
|
601
|
+
columns = sql.match(/SELECT\s+DISTINCT(.*)FROM/)[1].strip
|
602
|
+
sql.sub!(/SELECT\s+DISTINCT/,'SELECT')
|
603
|
+
sql << "GROUP BY #{columns} ORDER BY #{order_to_min_set(options[:order])}"
|
604
|
+
end
|
605
|
+
|
606
|
+
def change_column_null(table_name, column_name, null, default = nil)
|
607
|
+
column = column_for(table_name,column_name)
|
608
|
+
unless null || default.nil?
|
609
|
+
do_execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
|
610
|
+
end
|
611
|
+
sql = "ALTER TABLE #{table_name} ALTER COLUMN #{quote_column_name(column_name)} #{type_to_sql column.type, column.limit, column.precision, column.scale}"
|
612
|
+
sql << " NOT NULL" unless null
|
613
|
+
do_execute sql
|
614
|
+
end
|
615
|
+
|
616
|
+
def pk_and_sequence_for(table_name)
|
617
|
+
idcol = identity_column(table_name)
|
618
|
+
idcol ? [idcol.name,nil] : nil
|
619
|
+
end
|
620
|
+
|
621
|
+
# RAKE UTILITY METHODS =====================================#
|
622
|
+
|
623
|
+
def recreate_database(name)
|
624
|
+
existing_database = current_database.to_s
|
625
|
+
if name.to_s == existing_database
|
626
|
+
do_execute 'USE master'
|
627
|
+
end
|
628
|
+
drop_database(name)
|
629
|
+
create_database(name)
|
630
|
+
ensure
|
631
|
+
do_execute "USE #{existing_database}" if name.to_s == existing_database
|
632
|
+
end
|
633
|
+
|
634
|
+
def drop_database(name)
|
635
|
+
retry_count = 0
|
636
|
+
max_retries = 1
|
637
|
+
begin
|
638
|
+
do_execute "DROP DATABASE #{name}"
|
639
|
+
rescue ActiveRecord::StatementInvalid => err
|
640
|
+
# Remove existing connections and rollback any transactions if we received the message
|
641
|
+
# 'Cannot drop the database 'test' because it is currently in use'
|
642
|
+
if err.message =~ /because it is currently in use/
|
643
|
+
raise if retry_count >= max_retries
|
644
|
+
retry_count += 1
|
645
|
+
remove_database_connections_and_rollback(name)
|
646
|
+
retry
|
647
|
+
else
|
648
|
+
raise
|
649
|
+
end
|
650
|
+
end
|
651
|
+
end
|
652
|
+
|
653
|
+
def create_database(name)
|
654
|
+
do_execute "CREATE DATABASE #{name}"
|
655
|
+
end
|
656
|
+
|
657
|
+
def current_database
|
658
|
+
select_value 'SELECT DB_NAME()'
|
659
|
+
end
|
660
|
+
|
661
|
+
def remove_database_connections_and_rollback(name)
|
662
|
+
# This should disconnect all other users and rollback any transactions for SQL 2000 and 2005
|
663
|
+
# http://sqlserver2000.databases.aspfaq.com/how-do-i-drop-a-sql-server-database.html
|
664
|
+
do_execute "ALTER DATABASE #{name} SET SINGLE_USER WITH ROLLBACK IMMEDIATE"
|
665
|
+
end
|
666
|
+
|
667
|
+
|
668
|
+
|
669
|
+
protected
|
670
|
+
|
671
|
+
# DATABASE STATEMENTS ======================================
|
672
|
+
|
673
|
+
def select(sql, name = nil, ignore_special_columns = false)
|
674
|
+
repair_special_columns(sql) unless ignore_special_columns
|
675
|
+
fields, rows = raw_select(sql,name)
|
676
|
+
rows.inject([]) do |results,row|
|
677
|
+
row_hash = {}
|
678
|
+
fields.each_with_index do |f, i|
|
679
|
+
row_hash[f] = row[i]
|
680
|
+
end
|
681
|
+
results << row_hash
|
682
|
+
end
|
683
|
+
end
|
684
|
+
|
685
|
+
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
|
686
|
+
super || select_value("SELECT SCOPE_IDENTITY() AS Ident")
|
687
|
+
end
|
688
|
+
|
689
|
+
def update_sql(sql, name = nil)
|
690
|
+
execute(sql, name)
|
691
|
+
select_value('SELECT @@ROWCOUNT AS AffectedRows')
|
692
|
+
end
|
693
|
+
|
694
|
+
def info_schema_query
|
695
|
+
log_info_schema_queries ? yield : ActiveRecord::Base.silence{ yield }
|
696
|
+
end
|
697
|
+
|
698
|
+
def raw_execute(sql, name = nil, &block)
|
699
|
+
log(sql, name) do
|
700
|
+
if block_given?
|
701
|
+
raw_connection.execute(sql) { |handle| yield(handle) }
|
702
|
+
else
|
703
|
+
raw_connection.execute(sql)
|
704
|
+
end
|
705
|
+
end
|
706
|
+
end
|
707
|
+
|
708
|
+
def without_type_conversion
|
709
|
+
raw_connection.convert_types = false if raw_connection.respond_to?(:convert_types=)
|
710
|
+
yield
|
711
|
+
ensure
|
712
|
+
raw_connection.convert_types = true if raw_connection.respond_to?(:convert_types=)
|
713
|
+
end
|
714
|
+
|
715
|
+
def do_execute(sql,name=nil)
|
716
|
+
log(sql, name || 'EXECUTE') do
|
717
|
+
raw_connection.do(sql)
|
718
|
+
end
|
719
|
+
end
|
720
|
+
|
721
|
+
def raw_select(sql, name = nil)
|
722
|
+
handle = raw_execute(sql,name)
|
723
|
+
fields = handle.column_names
|
724
|
+
results = handle_as_array(handle)
|
725
|
+
rows = results.inject([]) do |rows,row|
|
726
|
+
row.each_with_index do |value, i|
|
727
|
+
# DEPRECATED in DBI 0.4.0 and above. Remove when 0.2.2 and lower is no longer supported.
|
728
|
+
if value.is_a? DBI::Timestamp
|
729
|
+
row[i] = value.to_sqlserver_string
|
730
|
+
end
|
731
|
+
end
|
732
|
+
rows << row
|
733
|
+
end
|
734
|
+
return fields, rows
|
735
|
+
end
|
736
|
+
|
737
|
+
def handle_as_array(handle)
|
738
|
+
array = handle.inject([]) do |rows,row|
|
739
|
+
rows << row.inject([]){ |values,value| values << value }
|
740
|
+
end
|
741
|
+
finish_statement_handle(handle)
|
742
|
+
array
|
743
|
+
end
|
744
|
+
|
745
|
+
def add_limit_offset_for_association_limiting!(sql, options)
|
746
|
+
sql.replace %|
|
747
|
+
SET NOCOUNT ON
|
748
|
+
DECLARE @row_number TABLE (row int identity(1,1), id int)
|
749
|
+
INSERT INTO @row_number (id)
|
750
|
+
#{sql}
|
751
|
+
SET NOCOUNT OFF
|
752
|
+
SELECT id FROM (
|
753
|
+
SELECT TOP #{options[:limit]} * FROM (
|
754
|
+
SELECT TOP #{options[:limit] + options[:offset]} * FROM @row_number ORDER BY row
|
755
|
+
) AS tmp1 ORDER BY row DESC
|
756
|
+
) AS tmp2 ORDER BY row
|
757
|
+
|.gsub(/[ \t\r\n]+/,' ')
|
758
|
+
end
|
759
|
+
|
760
|
+
# SCHEMA STATEMENTS ========================================#
|
761
|
+
|
762
|
+
def remove_check_constraints(table_name, column_name)
|
763
|
+
constraints = info_schema_query { select_values("SELECT CONSTRAINT_NAME FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE where TABLE_NAME = '#{quote_string(table_name)}' and COLUMN_NAME = '#{quote_string(column_name)}'") }
|
764
|
+
constraints.each do |constraint|
|
765
|
+
do_execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{quote_column_name(constraint)}"
|
766
|
+
end
|
767
|
+
end
|
768
|
+
|
769
|
+
def remove_default_constraint(table_name, column_name)
|
770
|
+
constraints = select_values("SELECT def.name FROM sysobjects def, syscolumns col, sysobjects tab WHERE col.cdefault = def.id AND col.name = '#{quote_string(column_name)}' AND tab.name = '#{quote_string(table_name)}' AND col.id = tab.id")
|
771
|
+
constraints.each do |constraint|
|
772
|
+
do_execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{quote_column_name(constraint)}"
|
773
|
+
end
|
774
|
+
end
|
775
|
+
|
776
|
+
def remove_indexes(table_name, column_name)
|
777
|
+
indexes(table_name).select{ |index| index.columns.include?(column_name.to_s) }.each do |index|
|
778
|
+
remove_index(table_name, {:name => index.name})
|
779
|
+
end
|
780
|
+
end
|
781
|
+
|
782
|
+
def default_name(table_name, column_name)
|
783
|
+
"DF_#{table_name}_#{column_name}"
|
784
|
+
end
|
785
|
+
|
786
|
+
# IDENTITY INSERTS =========================================#
|
787
|
+
|
788
|
+
def with_identity_insert_enabled(table_name, &block)
|
789
|
+
table_name = quote_table_name(table_name_or_views_table_name(table_name))
|
790
|
+
set_identity_insert(table_name, true)
|
791
|
+
yield
|
792
|
+
ensure
|
793
|
+
set_identity_insert(table_name, false)
|
794
|
+
end
|
795
|
+
|
796
|
+
def set_identity_insert(table_name, enable = true)
|
797
|
+
sql = "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}"
|
798
|
+
do_execute(sql,'IDENTITY_INSERT')
|
799
|
+
rescue Exception => e
|
800
|
+
raise ActiveRecordError, "IDENTITY_INSERT could not be turned #{enable ? 'ON' : 'OFF'} for table #{table_name}"
|
801
|
+
end
|
802
|
+
|
803
|
+
def query_requires_identity_insert?(sql)
|
804
|
+
if insert_sql?(sql)
|
805
|
+
table_name = get_table_name(sql)
|
806
|
+
id_column = identity_column(table_name)
|
807
|
+
id_column && sql =~ /INSERT[^(]+\([^)]*\[#{id_column.name}\][^)]*\)/i ? table_name : false
|
808
|
+
else
|
809
|
+
false
|
810
|
+
end
|
811
|
+
end
|
812
|
+
|
813
|
+
def identity_column(table_name)
|
814
|
+
columns(table_name).detect(&:is_identity?)
|
815
|
+
end
|
816
|
+
|
817
|
+
def table_name_or_views_table_name(table_name)
|
818
|
+
unquoted_table_name = unqualify_table_name(table_name)
|
819
|
+
views.include?(unquoted_table_name) ? view_table_name(unquoted_table_name) : unquoted_table_name
|
820
|
+
end
|
821
|
+
|
822
|
+
# HELPER METHODS ===========================================#
|
823
|
+
|
824
|
+
def insert_sql?(sql)
|
825
|
+
!(sql =~ /^\s*INSERT/i).nil?
|
826
|
+
end
|
827
|
+
|
828
|
+
def unqualify_table_name(table_name)
|
829
|
+
table_name.to_s.split('.').last.gsub(/[\[\]]/,'')
|
830
|
+
end
|
831
|
+
|
832
|
+
def unqualify_db_name(table_name)
|
833
|
+
table_names = table_name.to_s.split('.')
|
834
|
+
table_names.length == 3 ? table_names.first.tr('[]','') : nil
|
835
|
+
end
|
836
|
+
|
837
|
+
def get_table_name(sql)
|
838
|
+
if sql =~ /^\s*insert\s+into\s+([^\(\s]+)\s*|^\s*update\s+([^\(\s]+)\s*/i
|
839
|
+
$1 || $2
|
840
|
+
elsif sql =~ /from\s+([^\(\s]+)\s*/i
|
841
|
+
$1
|
842
|
+
else
|
843
|
+
nil
|
844
|
+
end
|
845
|
+
end
|
846
|
+
|
847
|
+
def orders_and_dirs_set(order)
|
848
|
+
orders = order.sub('ORDER BY','').split(',').map(&:strip).reject(&:blank?)
|
849
|
+
orders_dirs = orders.map do |ord|
|
850
|
+
dir = nil
|
851
|
+
if match_data = ord.match(/\b(asc|desc)$/i)
|
852
|
+
dir = match_data[1]
|
853
|
+
ord.sub!(dir,'').strip!
|
854
|
+
dir.upcase!
|
855
|
+
end
|
856
|
+
[ord,dir]
|
857
|
+
end
|
858
|
+
end
|
859
|
+
|
860
|
+
def views_real_column_name(table_name,column_name)
|
861
|
+
view_definition = view_information(table_name)['VIEW_DEFINITION']
|
862
|
+
match_data = view_definition.match(/([\w-]*)\s+as\s+#{column_name}/im)
|
863
|
+
match_data ? match_data[1] : column_name
|
864
|
+
end
|
865
|
+
|
866
|
+
def order_to_min_set(order)
|
867
|
+
orders_dirs = orders_and_dirs_set(order)
|
868
|
+
orders_dirs.map do |o,d|
|
869
|
+
"MIN(#{o}) #{d}".strip
|
870
|
+
end.join(', ')
|
871
|
+
end
|
872
|
+
|
873
|
+
def sql_for_association_limiting?(sql)
|
874
|
+
if md = sql.match(/^\s*SELECT(.*)FROM.*GROUP BY.*ORDER BY.*/im)
|
875
|
+
select_froms = md[1].split(',')
|
876
|
+
select_froms.size == 1 && !select_froms.first.include?('*')
|
877
|
+
end
|
878
|
+
end
|
879
|
+
|
880
|
+
def remove_sqlserver_columns_cache_for(table_name)
|
881
|
+
cache_key = unqualify_table_name(table_name)
|
882
|
+
@sqlserver_columns_cache[cache_key] = nil
|
883
|
+
initialize_sqlserver_caches(false)
|
884
|
+
end
|
885
|
+
|
886
|
+
def initialize_sqlserver_caches(reset_columns=true)
|
887
|
+
@sqlserver_columns_cache = {} if reset_columns
|
888
|
+
@sqlserver_views_cache = nil
|
889
|
+
@sqlserver_view_information_cache = {}
|
890
|
+
end
|
891
|
+
|
892
|
+
def column_definitions(table_name)
|
893
|
+
db_name = unqualify_db_name(table_name)
|
894
|
+
table_name = unqualify_table_name(table_name)
|
895
|
+
sql = %{
|
896
|
+
SELECT
|
897
|
+
columns.TABLE_NAME as table_name,
|
898
|
+
columns.COLUMN_NAME as name,
|
899
|
+
columns.DATA_TYPE as type,
|
900
|
+
columns.COLUMN_DEFAULT as default_value,
|
901
|
+
columns.NUMERIC_SCALE as numeric_scale,
|
902
|
+
columns.NUMERIC_PRECISION as numeric_precision,
|
903
|
+
CASE
|
904
|
+
WHEN columns.DATA_TYPE IN ('nchar','nvarchar') THEN columns.CHARACTER_MAXIMUM_LENGTH
|
905
|
+
ELSE COL_LENGTH(columns.TABLE_NAME, columns.COLUMN_NAME)
|
906
|
+
END as length,
|
907
|
+
CASE
|
908
|
+
WHEN columns.IS_NULLABLE = 'YES' THEN 1
|
909
|
+
ELSE NULL
|
910
|
+
end as is_nullable,
|
911
|
+
CASE
|
912
|
+
WHEN COLUMNPROPERTY(OBJECT_ID(columns.TABLE_NAME), columns.COLUMN_NAME, 'IsIdentity') = 0 THEN NULL
|
913
|
+
ELSE 1
|
914
|
+
END as is_identity
|
915
|
+
FROM #{db_name}INFORMATION_SCHEMA.COLUMNS columns
|
916
|
+
WHERE columns.TABLE_NAME = '#{table_name}'
|
917
|
+
ORDER BY columns.ordinal_position
|
918
|
+
}.gsub(/[ \t\r\n]+/,' ')
|
919
|
+
results = info_schema_query { without_type_conversion{ select(sql,nil,true) } }
|
920
|
+
results.collect do |ci|
|
921
|
+
ci.symbolize_keys!
|
922
|
+
ci[:type] = case ci[:type]
|
923
|
+
when /^bit|image|text|ntext|datetime$/
|
924
|
+
ci[:type]
|
925
|
+
when /^numeric|decimal$/i
|
926
|
+
"#{ci[:type]}(#{ci[:numeric_precision]},#{ci[:numeric_scale]})"
|
927
|
+
when /^char|nchar|varchar|nvarchar|varbinary|bigint|int|smallint$/
|
928
|
+
ci[:length].to_i == -1 ? "#{ci[:type]}(max)" : "#{ci[:type]}(#{ci[:length]})"
|
929
|
+
else
|
930
|
+
ci[:type]
|
931
|
+
end
|
932
|
+
if ci[:default_value].nil? && views.include?(table_name)
|
933
|
+
real_table_name = table_name_or_views_table_name(table_name)
|
934
|
+
real_column_name = views_real_column_name(table_name,ci[:name])
|
935
|
+
col_default_sql = "SELECT c.COLUMN_DEFAULT FROM INFORMATION_SCHEMA.COLUMNS c WHERE c.TABLE_NAME = '#{real_table_name}' AND c.COLUMN_NAME = '#{real_column_name}'"
|
936
|
+
ci[:default_value] = info_schema_query { without_type_conversion{ select_value(col_default_sql) } }
|
937
|
+
end
|
938
|
+
ci[:default_value] = case ci[:default_value]
|
939
|
+
when nil, '(null)', '(NULL)'
|
940
|
+
nil
|
941
|
+
else
|
942
|
+
ci[:default_value].match(/\A\(+N?'?(.*?)'?\)+\Z/)[1]
|
943
|
+
end
|
944
|
+
ci[:null] = ci[:is_nullable].to_i == 1 ; ci.delete(:is_nullable)
|
945
|
+
ci
|
946
|
+
end
|
947
|
+
end
|
948
|
+
|
949
|
+
def column_for(table_name, column_name)
|
950
|
+
unless column = columns(table_name).detect { |c| c.name == column_name.to_s }
|
951
|
+
raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
|
952
|
+
end
|
953
|
+
column
|
954
|
+
end
|
955
|
+
|
956
|
+
def change_order_direction(order)
|
957
|
+
order.split(",").collect {|fragment|
|
958
|
+
case fragment
|
959
|
+
when /\bDESC\b/i then fragment.gsub(/\bDESC\b/i, "ASC")
|
960
|
+
when /\bASC\b/i then fragment.gsub(/\bASC\b/i, "DESC")
|
961
|
+
else String.new(fragment).split(',').join(' DESC,') + ' DESC'
|
962
|
+
end
|
963
|
+
}.join(",")
|
964
|
+
end
|
965
|
+
|
966
|
+
def special_columns(table_name)
|
967
|
+
columns(table_name).select(&:is_special?).map(&:name)
|
968
|
+
end
|
969
|
+
|
970
|
+
def repair_special_columns(sql)
|
971
|
+
special_cols = special_columns(get_table_name(sql))
|
972
|
+
for col in special_cols.to_a
|
973
|
+
sql.gsub!(/((\.|\s|\()\[?#{col.to_s}\]?)\s?=\s?/, '\1 LIKE ')
|
974
|
+
sql.gsub!(/ORDER BY #{col.to_s}/i, '')
|
975
|
+
end
|
976
|
+
sql
|
977
|
+
end
|
978
|
+
|
979
|
+
end #class SQLServerAdapter < AbstractAdapter
|
980
|
+
|
981
|
+
end #module ConnectionAdapters
|
982
|
+
|
983
|
+
end #module ActiveRecord
|
984
|
+
|