activerecord-sqlserver-adapter 2.3.24 → 3.0.0
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 +5 -108
- data/MIT-LICENSE +1 -1
- data/README.rdoc +33 -61
- data/lib/active_record/connection_adapters/sqlserver/core_ext/active_record.rb +57 -0
- data/lib/active_record/connection_adapters/sqlserver/core_ext/odbc.rb +57 -0
- data/lib/active_record/connection_adapters/sqlserver/database_limits.rb +49 -0
- data/lib/active_record/connection_adapters/sqlserver/database_statements.rb +336 -0
- data/lib/active_record/connection_adapters/sqlserver/errors.rb +33 -0
- data/lib/active_record/connection_adapters/sqlserver/query_cache.rb +17 -0
- data/lib/active_record/connection_adapters/sqlserver/quoting.rb +61 -0
- data/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +373 -0
- data/lib/active_record/connection_adapters/sqlserver_adapter.rb +131 -1121
- data/lib/arel/engines/sql/compilers/sqlserver_compiler.rb +267 -0
- metadata +26 -76
- data/RUNNING_UNIT_TESTS +0 -31
- data/Rakefile +0 -60
- data/lib/active_record/connection_adapters/sqlserver_adapter/core_ext/active_record.rb +0 -151
- data/lib/active_record/connection_adapters/sqlserver_adapter/core_ext/odbc.rb +0 -40
- data/test/cases/aaaa_create_tables_test_sqlserver.rb +0 -19
- data/test/cases/adapter_test_sqlserver.rb +0 -755
- data/test/cases/attribute_methods_test_sqlserver.rb +0 -33
- data/test/cases/basics_test_sqlserver.rb +0 -86
- data/test/cases/calculations_test_sqlserver.rb +0 -20
- data/test/cases/column_test_sqlserver.rb +0 -354
- data/test/cases/connection_test_sqlserver.rb +0 -148
- data/test/cases/eager_association_test_sqlserver.rb +0 -42
- data/test/cases/execute_procedure_test_sqlserver.rb +0 -35
- data/test/cases/inheritance_test_sqlserver.rb +0 -28
- data/test/cases/method_scoping_test_sqlserver.rb +0 -28
- data/test/cases/migration_test_sqlserver.rb +0 -108
- data/test/cases/named_scope_test_sqlserver.rb +0 -21
- data/test/cases/offset_and_limit_test_sqlserver.rb +0 -108
- data/test/cases/pessimistic_locking_test_sqlserver.rb +0 -125
- data/test/cases/query_cache_test_sqlserver.rb +0 -24
- data/test/cases/schema_dumper_test_sqlserver.rb +0 -72
- data/test/cases/specific_schema_test_sqlserver.rb +0 -154
- data/test/cases/sqlserver_helper.rb +0 -140
- data/test/cases/table_name_test_sqlserver.rb +0 -38
- data/test/cases/transaction_test_sqlserver.rb +0 -93
- data/test/cases/unicode_test_sqlserver.rb +0 -54
- data/test/cases/validations_test_sqlserver.rb +0 -18
- data/test/connections/native_sqlserver/connection.rb +0 -26
- data/test/connections/native_sqlserver_odbc/connection.rb +0 -28
- data/test/migrations/transaction_table/1_table_will_never_be_created.rb +0 -11
- data/test/schema/sqlserver_specific_schema.rb +0 -113
@@ -0,0 +1,49 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module Sqlserver
|
4
|
+
module DatabaseLimits
|
5
|
+
|
6
|
+
def table_alias_length
|
7
|
+
128
|
8
|
+
end
|
9
|
+
|
10
|
+
def column_name_length
|
11
|
+
128
|
12
|
+
end
|
13
|
+
|
14
|
+
def table_name_length
|
15
|
+
128
|
16
|
+
end
|
17
|
+
|
18
|
+
def index_name_length
|
19
|
+
128
|
20
|
+
end
|
21
|
+
|
22
|
+
def columns_per_table
|
23
|
+
1024
|
24
|
+
end
|
25
|
+
|
26
|
+
def indexes_per_table
|
27
|
+
999
|
28
|
+
end
|
29
|
+
|
30
|
+
def columns_per_multicolumn_index
|
31
|
+
16
|
32
|
+
end
|
33
|
+
|
34
|
+
def in_clause_length
|
35
|
+
65536
|
36
|
+
end
|
37
|
+
|
38
|
+
def sql_query_length
|
39
|
+
65536 * 4096
|
40
|
+
end
|
41
|
+
|
42
|
+
def joins_per_query
|
43
|
+
256
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,336 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module Sqlserver
|
4
|
+
module DatabaseStatements
|
5
|
+
|
6
|
+
def select_one(sql, name = nil)
|
7
|
+
result = raw_select sql, name, :fetch => :one
|
8
|
+
(result && result.first.present?) ? result.first : nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def select_rows(sql, name = nil)
|
12
|
+
raw_select sql, name, :fetch => :rows
|
13
|
+
end
|
14
|
+
|
15
|
+
def execute(sql, name = nil, skip_logging = false)
|
16
|
+
if table_name = query_requires_identity_insert?(sql)
|
17
|
+
with_identity_insert_enabled(table_name) { do_execute(sql,name) }
|
18
|
+
else
|
19
|
+
do_execute(sql,name)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def outside_transaction?
|
24
|
+
info_schema_query { select_value("SELECT @@TRANCOUNT") == 0 }
|
25
|
+
end
|
26
|
+
|
27
|
+
def begin_db_transaction
|
28
|
+
do_execute "BEGIN TRANSACTION"
|
29
|
+
end
|
30
|
+
|
31
|
+
def commit_db_transaction
|
32
|
+
do_execute "COMMIT TRANSACTION"
|
33
|
+
end
|
34
|
+
|
35
|
+
def rollback_db_transaction
|
36
|
+
do_execute "ROLLBACK TRANSACTION" rescue nil
|
37
|
+
end
|
38
|
+
|
39
|
+
def create_savepoint
|
40
|
+
do_execute "SAVE TRANSACTION #{current_savepoint_name}"
|
41
|
+
end
|
42
|
+
|
43
|
+
def release_savepoint
|
44
|
+
end
|
45
|
+
|
46
|
+
def rollback_to_savepoint
|
47
|
+
do_execute "ROLLBACK TRANSACTION #{current_savepoint_name}"
|
48
|
+
end
|
49
|
+
|
50
|
+
def add_limit_offset!(sql, options)
|
51
|
+
raise NotImplementedError, 'This has been moved to the SQLServerCompiler in Arel.'
|
52
|
+
end
|
53
|
+
|
54
|
+
def empty_insert_statement_value
|
55
|
+
"DEFAULT VALUES"
|
56
|
+
end
|
57
|
+
|
58
|
+
def case_sensitive_equality_operator
|
59
|
+
cs_equality_operator
|
60
|
+
end
|
61
|
+
|
62
|
+
def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
|
63
|
+
match_data = where_sql.match(/^(.*?[\]\) ])WHERE[\[\( ]/)
|
64
|
+
limit = match_data[1]
|
65
|
+
where_sql.sub!(limit,'')
|
66
|
+
"WHERE #{quoted_primary_key} IN (SELECT #{limit} #{quoted_primary_key} FROM #{quoted_table_name} #{where_sql})"
|
67
|
+
end
|
68
|
+
|
69
|
+
# === SQLServer Specific ======================================== #
|
70
|
+
|
71
|
+
def execute_procedure(proc_name, *variables)
|
72
|
+
vars = variables.map{ |v| quote(v) }.join(', ')
|
73
|
+
sql = "EXEC #{proc_name} #{vars}".strip
|
74
|
+
results = []
|
75
|
+
log(sql,'Execute Procedure') do
|
76
|
+
raw_connection_run(sql) do |handle|
|
77
|
+
get_rows = lambda {
|
78
|
+
rows = handle_to_names_and_values handle, :fetch => :all
|
79
|
+
rows.each_with_index { |r,i| rows[i] = r.with_indifferent_access }
|
80
|
+
results << rows
|
81
|
+
}
|
82
|
+
get_rows.call
|
83
|
+
while handle_more_results?(handle)
|
84
|
+
get_rows.call
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
results.many? ? results : results.first
|
89
|
+
end
|
90
|
+
|
91
|
+
def use_database(database=nil)
|
92
|
+
database ||= @connection_options[:database]
|
93
|
+
do_execute "USE #{quote_table_name(database)}" unless database.blank?
|
94
|
+
end
|
95
|
+
|
96
|
+
def user_options
|
97
|
+
info_schema_query do
|
98
|
+
select_rows("dbcc useroptions").inject(HashWithIndifferentAccess.new) do |values,row|
|
99
|
+
set_option = row[0].gsub(/\s+/,'_')
|
100
|
+
user_value = row[1]
|
101
|
+
values[set_option] = user_value
|
102
|
+
values
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def run_with_isolation_level(isolation_level)
|
108
|
+
raise ArgumentError, "Invalid isolation level, #{isolation_level}. Supported levels include #{valid_isolation_levels.to_sentence}." if !valid_isolation_levels.include?(isolation_level.upcase)
|
109
|
+
initial_isolation_level = user_options[:isolation_level] || "READ COMMITTED"
|
110
|
+
do_execute "SET TRANSACTION ISOLATION LEVEL #{isolation_level}"
|
111
|
+
begin
|
112
|
+
yield
|
113
|
+
ensure
|
114
|
+
do_execute "SET TRANSACTION ISOLATION LEVEL #{initial_isolation_level}"
|
115
|
+
end if block_given?
|
116
|
+
end
|
117
|
+
|
118
|
+
def newid_function
|
119
|
+
select_value "SELECT NEWID()"
|
120
|
+
end
|
121
|
+
|
122
|
+
def newsequentialid_function
|
123
|
+
select_value "SELECT NEWSEQUENTIALID()"
|
124
|
+
end
|
125
|
+
|
126
|
+
# === SQLServer Specific (Rake/Test Helpers) ==================== #
|
127
|
+
|
128
|
+
def recreate_database
|
129
|
+
remove_database_connections_and_rollback do
|
130
|
+
do_execute "EXEC sp_MSforeachtable 'DROP TABLE ?'"
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def recreate_database!(database=nil)
|
135
|
+
current_db = current_database
|
136
|
+
database ||= current_db
|
137
|
+
this_db = database.to_s == current_db
|
138
|
+
do_execute 'USE master' if this_db
|
139
|
+
drop_database(database)
|
140
|
+
create_database(database)
|
141
|
+
ensure
|
142
|
+
use_database(current_db) if this_db
|
143
|
+
end
|
144
|
+
|
145
|
+
def drop_database(database)
|
146
|
+
retry_count = 0
|
147
|
+
max_retries = 1
|
148
|
+
begin
|
149
|
+
do_execute "DROP DATABASE #{quote_table_name(database)}"
|
150
|
+
rescue ActiveRecord::StatementInvalid => err
|
151
|
+
if err.message =~ /because it is currently in use/i
|
152
|
+
raise if retry_count >= max_retries
|
153
|
+
retry_count += 1
|
154
|
+
remove_database_connections_and_rollback(database)
|
155
|
+
retry
|
156
|
+
else
|
157
|
+
raise
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def create_database(database)
|
163
|
+
do_execute "CREATE DATABASE #{quote_table_name(database)}"
|
164
|
+
end
|
165
|
+
|
166
|
+
def current_database
|
167
|
+
select_value 'SELECT DB_NAME()'
|
168
|
+
end
|
169
|
+
|
170
|
+
def charset
|
171
|
+
select_value "SELECT SERVERPROPERTY('SqlCharSetName')"
|
172
|
+
end
|
173
|
+
|
174
|
+
|
175
|
+
protected
|
176
|
+
|
177
|
+
def select(sql, name = nil)
|
178
|
+
raw_select sql, name, :fetch => :all
|
179
|
+
end
|
180
|
+
|
181
|
+
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
|
182
|
+
super || select_value("SELECT SCOPE_IDENTITY() AS Ident")
|
183
|
+
end
|
184
|
+
|
185
|
+
def update_sql(sql, name = nil)
|
186
|
+
execute(sql, name)
|
187
|
+
select_value('SELECT @@ROWCOUNT AS AffectedRows')
|
188
|
+
end
|
189
|
+
|
190
|
+
# === SQLServer Specific ======================================== #
|
191
|
+
|
192
|
+
def valid_isolation_levels
|
193
|
+
["READ COMMITTED", "READ UNCOMMITTED", "REPEATABLE READ", "SERIALIZABLE", "SNAPSHOT"]
|
194
|
+
end
|
195
|
+
|
196
|
+
# === SQLServer Specific (Executing) ============================ #
|
197
|
+
|
198
|
+
def do_execute(sql, name = nil)
|
199
|
+
name ||= 'EXECUTE'
|
200
|
+
log(sql, name) do
|
201
|
+
with_auto_reconnect { raw_connection_do(sql) }
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def raw_connection_do(sql)
|
206
|
+
case connection_mode
|
207
|
+
when :odbc
|
208
|
+
raw_connection.do(sql)
|
209
|
+
else :adonet
|
210
|
+
raw_connection.create_command.tap{ |cmd| cmd.command_text = sql }.execute_non_query
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
# === SQLServer Specific (Selecting) ============================ #
|
215
|
+
|
216
|
+
def raw_select(sql, name=nil, options={})
|
217
|
+
log(sql,name) do
|
218
|
+
begin
|
219
|
+
handle = raw_connection_run(sql)
|
220
|
+
handle_to_names_and_values(handle, options)
|
221
|
+
ensure
|
222
|
+
finish_statement_handle(handle)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
def raw_connection_run(sql)
|
228
|
+
with_auto_reconnect do
|
229
|
+
case connection_mode
|
230
|
+
when :odbc
|
231
|
+
block_given? ? raw_connection.run_block(sql) { |handle| yield(handle) } : raw_connection.run(sql)
|
232
|
+
else :adonet
|
233
|
+
raw_connection.create_command.tap{ |cmd| cmd.command_text = sql }.execute_reader
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def handle_more_results?(handle)
|
239
|
+
case connection_mode
|
240
|
+
when :odbc
|
241
|
+
handle.more_results
|
242
|
+
when :adonet
|
243
|
+
handle.next_result
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def handle_to_names_and_values(handle, options={})
|
248
|
+
case connection_mode
|
249
|
+
when :odbc
|
250
|
+
handle_to_names_and_values_odbc(handle, options)
|
251
|
+
when :adonet
|
252
|
+
handle_to_names_and_values_adonet(handle, options)
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def handle_to_names_and_values_odbc(handle, options={})
|
257
|
+
case options[:fetch]
|
258
|
+
when :all, :one
|
259
|
+
rows = if options[:fetch] == :all
|
260
|
+
handle.fetch_all || []
|
261
|
+
else
|
262
|
+
row = handle.fetch
|
263
|
+
row ? [row] : [[]]
|
264
|
+
end
|
265
|
+
names = handle.columns(true).map{ |c| c.name }
|
266
|
+
names_and_values = []
|
267
|
+
rows.each do |row|
|
268
|
+
h = {}
|
269
|
+
i = 0
|
270
|
+
while i < row.size
|
271
|
+
v = row[i]
|
272
|
+
h[names[i]] = v.respond_to?(:to_sqlserver_string) ? v.to_sqlserver_string : v
|
273
|
+
i += 1
|
274
|
+
end
|
275
|
+
names_and_values << h
|
276
|
+
end
|
277
|
+
names_and_values
|
278
|
+
when :rows
|
279
|
+
rows = handle.fetch_all || []
|
280
|
+
rows.each do |row|
|
281
|
+
i = 0
|
282
|
+
while i < row.size
|
283
|
+
v = row[i]
|
284
|
+
row[i] = v.to_sqlserver_string if v.respond_to?(:to_sqlserver_string)
|
285
|
+
i += 1
|
286
|
+
end
|
287
|
+
end
|
288
|
+
rows
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
def handle_to_names_and_values_adonet(handle, options={})
|
293
|
+
if handle.has_rows
|
294
|
+
fields = []
|
295
|
+
rows = []
|
296
|
+
fields_named = false
|
297
|
+
while handle.read
|
298
|
+
row = []
|
299
|
+
handle.visible_field_count.times do |row_index|
|
300
|
+
value = handle.get_value(row_index)
|
301
|
+
value = if value.is_a? System::String
|
302
|
+
value.to_s
|
303
|
+
elsif value.is_a? System::DBNull
|
304
|
+
nil
|
305
|
+
elsif value.is_a? System::DateTime
|
306
|
+
value.to_string("yyyy-MM-dd HH:MM:ss.fff").to_s
|
307
|
+
else
|
308
|
+
value
|
309
|
+
end
|
310
|
+
row << value
|
311
|
+
fields << handle.get_name(row_index).to_s unless fields_named
|
312
|
+
end
|
313
|
+
rows << row
|
314
|
+
fields_named = true
|
315
|
+
end
|
316
|
+
else
|
317
|
+
fields, rows = [], []
|
318
|
+
end
|
319
|
+
[fields,rows]
|
320
|
+
end
|
321
|
+
|
322
|
+
def finish_statement_handle(handle)
|
323
|
+
case connection_mode
|
324
|
+
when :odbc
|
325
|
+
handle.drop if handle && handle.respond_to?(:drop) && !handle.finished?
|
326
|
+
when :adonet
|
327
|
+
handle.close if handle && handle.respond_to?(:close) && !handle.is_closed
|
328
|
+
handle.dispose if handle && handle.respond_to?(:dispose)
|
329
|
+
end
|
330
|
+
handle
|
331
|
+
end
|
332
|
+
|
333
|
+
end
|
334
|
+
end
|
335
|
+
end
|
336
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
|
3
|
+
class LostConnection < WrappedDatabaseException
|
4
|
+
end
|
5
|
+
|
6
|
+
module ConnectionAdapters
|
7
|
+
module Sqlserver
|
8
|
+
module Errors
|
9
|
+
|
10
|
+
LOST_CONNECTION_EXCEPTIONS = {
|
11
|
+
:odbc => ['ODBC::Error','ODBC_UTF8::Error','ODBC_NONE::Error'],
|
12
|
+
:adonet => ['TypeError','System::Data::SqlClient::SqlException']
|
13
|
+
}.freeze
|
14
|
+
|
15
|
+
LOST_CONNECTION_MESSAGES = {
|
16
|
+
:odbc => [/link failure/, /server failed/, /connection was already closed/, /invalid handle/i],
|
17
|
+
:adonet => [/current state is closed/, /network-related/]
|
18
|
+
}.freeze
|
19
|
+
|
20
|
+
|
21
|
+
def lost_connection_exceptions
|
22
|
+
exceptions = LOST_CONNECTION_EXCEPTIONS[connection_mode]
|
23
|
+
@lost_connection_exceptions ||= exceptions ? exceptions.map{ |e| e.constantize rescue nil }.compact : []
|
24
|
+
end
|
25
|
+
|
26
|
+
def lost_connection_messages
|
27
|
+
LOST_CONNECTION_MESSAGES[connection_mode]
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module Sqlserver
|
4
|
+
module Quoting
|
5
|
+
|
6
|
+
def quote(value, column = nil)
|
7
|
+
case value
|
8
|
+
when String, ActiveSupport::Multibyte::Chars
|
9
|
+
if column && column.type == :binary
|
10
|
+
column.class.string_to_binary(value)
|
11
|
+
elsif quote_value_as_utf8?(value) || column && column.respond_to?(:is_utf8?) && column.is_utf8?
|
12
|
+
quoted_utf8_value(value)
|
13
|
+
else
|
14
|
+
super
|
15
|
+
end
|
16
|
+
else
|
17
|
+
super
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def quote_string(string)
|
22
|
+
string.to_s.gsub(/\'/, "''")
|
23
|
+
end
|
24
|
+
|
25
|
+
def quote_column_name(column_name)
|
26
|
+
column_name.to_s.split('.').map{ |name| name =~ /^\[.*\]$/ ? name : "[#{name}]" }.join('.')
|
27
|
+
end
|
28
|
+
|
29
|
+
def quote_table_name(table_name)
|
30
|
+
return table_name if table_name =~ /^\[.*\]$/
|
31
|
+
quote_column_name(table_name)
|
32
|
+
end
|
33
|
+
|
34
|
+
def quoted_true
|
35
|
+
'1'
|
36
|
+
end
|
37
|
+
|
38
|
+
def quoted_false
|
39
|
+
'0'
|
40
|
+
end
|
41
|
+
|
42
|
+
def quoted_date(value)
|
43
|
+
if value.acts_like?(:time) && value.respond_to?(:usec)
|
44
|
+
"#{super}.#{sprintf("%03d",value.usec/1000)}"
|
45
|
+
else
|
46
|
+
super
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def quoted_utf8_value(value)
|
51
|
+
"N'#{quote_string(value)}'"
|
52
|
+
end
|
53
|
+
|
54
|
+
def quote_value_as_utf8?(value)
|
55
|
+
value.is_utf8? || enable_default_unicode_types
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|