activerecord-sqlserver-adapter 3.1.1 → 3.2.18

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,26 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Sqlserver
4
+ module CoreExt
5
+ class ExplainSubscriber
6
+ def call(*args)
7
+ if queries = Thread.current[:available_queries_for_explain]
8
+ payload = args.last
9
+ queries << payload.values_at(:sql, :binds) unless ignore_sqlserver_payload?(payload)
10
+ end
11
+ end
12
+
13
+ IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN CACHE)
14
+ SQLSERVER_EXPLAINED_SQLS = /(select|update|delete|insert)/i
15
+
16
+ # Need to modify the regex for the TSQL generated by this adapter so we can explain the proper sql statements
17
+ def ignore_sqlserver_payload?(payload)
18
+ payload[:exception] || IGNORED_PAYLOADS.include?(payload[:name]) || payload[:sql] !~ SQLSERVER_EXPLAINED_SQLS
19
+ end
20
+
21
+ ActiveSupport::Notifications.subscribe("sql.active_record", new)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,19 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Sqlserver
4
+ module CoreExt
5
+ module Relation
6
+
7
+ private
8
+
9
+ def tables_in_string(string)
10
+ super - ['__rnt']
11
+ end
12
+
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ ActiveRecord::Relation.send :include, ActiveRecord::ConnectionAdapters::Sqlserver::CoreExt::Relation
@@ -2,7 +2,9 @@ module ActiveRecord
2
2
  module ConnectionAdapters
3
3
  module Sqlserver
4
4
  module DatabaseStatements
5
-
5
+
6
+ include CoreExt::DatabaseStatements
7
+
6
8
  def select_rows(sql, name = nil)
7
9
  raw_select sql, name, [], :fetch => :rows
8
10
  end
@@ -14,7 +16,7 @@ module ActiveRecord
14
16
  do_execute(sql,name)
15
17
  end
16
18
  end
17
-
19
+
18
20
  def exec_query(sql, name = 'SQL', binds = [], sqlserver_options = {})
19
21
  if id_insert_table_name = sqlserver_options[:insert] ? query_requires_identity_insert?(sql) : nil
20
22
  with_identity_insert_enabled(id_insert_table_name) { do_exec_query(sql, name, binds) }
@@ -22,11 +24,11 @@ module ActiveRecord
22
24
  do_exec_query(sql, name, binds)
23
25
  end
24
26
  end
25
-
27
+
26
28
  def exec_insert(sql, name, binds)
27
29
  exec_query sql, name, binds, :insert => true
28
30
  end
29
-
31
+
30
32
  def exec_delete(sql, name, binds)
31
33
  sql << "; SELECT @@ROWCOUNT AS AffectedRows"
32
34
  super.rows.first.first
@@ -38,34 +40,42 @@ module ActiveRecord
38
40
  end
39
41
 
40
42
  def outside_transaction?
41
- info_schema_query { select_value("SELECT @@TRANCOUNT") == 0 }
43
+ uncached { select_value('SELECT @@TRANCOUNT', 'SCHEMA') == 0 }
42
44
  end
43
-
45
+
44
46
  def supports_statement_cache?
45
47
  true
46
48
  end
47
49
 
50
+ def transaction(options = {})
51
+ if retry_deadlock_victim?
52
+ block_given? ? transaction_with_retry_deadlock_victim(options) { yield } : transaction_with_retry_deadlock_victim(options)
53
+ else
54
+ block_given? ? super(options) { yield } : super(options)
55
+ end
56
+ end
57
+
48
58
  def begin_db_transaction
49
59
  do_execute "BEGIN TRANSACTION"
50
60
  end
51
61
 
52
62
  def commit_db_transaction
53
- do_execute "COMMIT TRANSACTION"
63
+ disable_auto_reconnect { do_execute "COMMIT TRANSACTION" }
54
64
  end
55
65
 
56
66
  def rollback_db_transaction
57
- do_execute "ROLLBACK TRANSACTION" rescue nil
67
+ do_execute "IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION"
58
68
  end
59
69
 
60
70
  def create_savepoint
61
- do_execute "SAVE TRANSACTION #{current_savepoint_name}"
71
+ disable_auto_reconnect { do_execute "SAVE TRANSACTION #{current_savepoint_name}" }
62
72
  end
63
73
 
64
74
  def release_savepoint
65
75
  end
66
76
 
67
77
  def rollback_to_savepoint
68
- do_execute "ROLLBACK TRANSACTION #{current_savepoint_name}"
78
+ disable_auto_reconnect { do_execute "ROLLBACK TRANSACTION #{current_savepoint_name}" }
69
79
  end
70
80
 
71
81
  def add_limit_offset!(sql, options)
@@ -79,11 +89,15 @@ module ActiveRecord
79
89
  def case_sensitive_modifier(node)
80
90
  node.acts_like?(:string) ? Arel::Nodes::Bin.new(node) : node
81
91
  end
82
-
92
+
83
93
  # === SQLServer Specific ======================================== #
84
-
94
+
85
95
  def execute_procedure(proc_name, *variables)
86
- vars = variables.map{ |v| quote(v) }.join(', ')
96
+ vars = if variables.any? && variables.first.is_a?(Hash)
97
+ variables.first.map { |k,v| "@#{k} = #{quote(v)}" }
98
+ else
99
+ variables.map { |v| quote(v) }
100
+ end.join(', ')
87
101
  sql = "EXEC #{proc_name} #{vars}".strip
88
102
  name = 'Execute Procedure'
89
103
  log(sql, name) do
@@ -112,45 +126,130 @@ module ActiveRecord
112
126
  end
113
127
  end
114
128
  end
115
-
129
+
116
130
  def use_database(database=nil)
117
131
  return if sqlserver_azure?
118
132
  database ||= @connection_options[:database]
119
133
  do_execute "USE #{quote_table_name(database)}" unless database.blank?
120
134
  end
121
-
135
+
122
136
  def user_options
123
- info_schema_query do
124
- select_rows("dbcc useroptions").inject(HashWithIndifferentAccess.new) do |values,row|
125
- set_option = row[0].gsub(/\s+/,'_')
137
+ return {} if sqlserver_azure?
138
+ # fixes #535
139
+ rows = select_rows('DBCC USEROPTIONS WITH NO_INFOMSGS', 'SCHEMA')
140
+ rows = rows.first if rows.size == 2 && rows.last.empty?
141
+ rows.reduce(HashWithIndifferentAccess.new) do |values, row|
142
+ if row.instance_of? Hash
143
+ set_option = row.values[0].gsub(/\s+/, '_')
144
+ user_value = row.values[1]
145
+ elsif row.instance_of? Array
146
+ set_option = row[0].gsub(/\s+/, '_')
126
147
  user_value = row[1]
127
- values[set_option] = user_value
128
- values
129
148
  end
149
+ values[set_option] = user_value
150
+ values
151
+ end
152
+ end
153
+
154
+ def user_options_dateformat
155
+ if sqlserver_azure?
156
+ select_value 'SELECT [dateformat] FROM [sys].[syslanguages] WHERE [langid] = @@LANGID', 'SCHEMA'
157
+ else
158
+ user_options['dateformat']
159
+ end
160
+ end
161
+
162
+ def user_options_isolation_level
163
+ if sqlserver_azure?
164
+ sql = %|SELECT CASE [transaction_isolation_level]
165
+ WHEN 0 THEN NULL
166
+ WHEN 1 THEN 'READ UNCOMITTED'
167
+ WHEN 2 THEN 'READ COMITTED'
168
+ WHEN 3 THEN 'REPEATABLE READ'
169
+ WHEN 4 THEN 'SERIALIZABLE'
170
+ WHEN 5 THEN 'SNAPSHOT' END AS [isolation_level]
171
+ FROM [sys].[dm_exec_sessions]
172
+ WHERE [session_id] = @@SPID|.squish
173
+ select_value sql, 'SCHEMA'
174
+ else
175
+ user_options['isolation_level']
176
+ end
177
+ end
178
+
179
+ def user_options_language
180
+ if sqlserver_azure?
181
+ select_value 'SELECT @@LANGUAGE AS [language]', 'SCHEMA'
182
+ else
183
+ user_options['language']
130
184
  end
131
185
  end
132
186
 
133
187
  def run_with_isolation_level(isolation_level)
134
188
  raise ArgumentError, "Invalid isolation level, #{isolation_level}. Supported levels include #{valid_isolation_levels.to_sentence}." if !valid_isolation_levels.include?(isolation_level.upcase)
135
- initial_isolation_level = user_options[:isolation_level] || "READ COMMITTED"
189
+ initial_isolation_level = user_options_isolation_level || "READ COMMITTED"
136
190
  do_execute "SET TRANSACTION ISOLATION LEVEL #{isolation_level}"
137
191
  begin
138
- yield
192
+ yield
139
193
  ensure
140
194
  do_execute "SET TRANSACTION ISOLATION LEVEL #{initial_isolation_level}"
141
195
  end if block_given?
142
196
  end
143
-
197
+
144
198
  def newid_function
145
199
  select_value "SELECT NEWID()"
146
200
  end
147
-
201
+
148
202
  def newsequentialid_function
149
203
  select_value "SELECT NEWSEQUENTIALID()"
150
204
  end
151
-
205
+
206
+ def activity_stats
207
+ select_all %|
208
+ SELECT
209
+ [session_id] = s.session_id,
210
+ [user_process] = CONVERT(CHAR(1), s.is_user_process),
211
+ [login] = s.login_name,
212
+ [database] = ISNULL(db_name(r.database_id), N''),
213
+ [task_state] = ISNULL(t.task_state, N''),
214
+ [command] = ISNULL(r.command, N''),
215
+ [application] = ISNULL(s.program_name, N''),
216
+ [wait_time_ms] = ISNULL(w.wait_duration_ms, 0),
217
+ [wait_type] = ISNULL(w.wait_type, N''),
218
+ [wait_resource] = ISNULL(w.resource_description, N''),
219
+ [blocked_by] = ISNULL(CONVERT (varchar, w.blocking_session_id), ''),
220
+ [head_blocker] =
221
+ CASE
222
+ -- session has an active request, is blocked, but is blocking others
223
+ WHEN r2.session_id IS NOT NULL AND r.blocking_session_id = 0 THEN '1'
224
+ -- session is idle but has an open tran and is blocking others
225
+ WHEN r.session_id IS NULL THEN '1'
226
+ ELSE ''
227
+ END,
228
+ [total_cpu_ms] = s.cpu_time,
229
+ [total_physical_io_mb] = (s.reads + s.writes) * 8 / 1024,
230
+ [memory_use_kb] = s.memory_usage * 8192 / 1024,
231
+ [open_transactions] = ISNULL(r.open_transaction_count,0),
232
+ [login_time] = s.login_time,
233
+ [last_request_start_time] = s.last_request_start_time,
234
+ [host_name] = ISNULL(s.host_name, N''),
235
+ [net_address] = ISNULL(c.client_net_address, N''),
236
+ [execution_context_id] = ISNULL(t.exec_context_id, 0),
237
+ [request_id] = ISNULL(r.request_id, 0),
238
+ [workload_group] = N''
239
+ FROM sys.dm_exec_sessions s LEFT OUTER JOIN sys.dm_exec_connections c ON (s.session_id = c.session_id)
240
+ LEFT OUTER JOIN sys.dm_exec_requests r ON (s.session_id = r.session_id)
241
+ LEFT OUTER JOIN sys.dm_os_tasks t ON (r.session_id = t.session_id AND r.request_id = t.request_id)
242
+ LEFT OUTER JOIN
243
+ (SELECT *, ROW_NUMBER() OVER (PARTITION BY waiting_task_address ORDER BY wait_duration_ms DESC) AS row_num
244
+ FROM sys.dm_os_waiting_tasks
245
+ ) w ON (t.task_address = w.waiting_task_address) AND w.row_num = 1
246
+ LEFT OUTER JOIN sys.dm_exec_requests r2 ON (r.session_id = r2.blocking_session_id)
247
+ WHERE db_name(r.database_id) = '#{current_database}'
248
+ ORDER BY s.session_id|
249
+ end
250
+
152
251
  # === SQLServer Specific (Rake/Test Helpers) ==================== #
153
-
252
+
154
253
  def recreate_database
155
254
  remove_database_connections_and_rollback do
156
255
  do_execute "EXEC sp_MSforeachtable 'DROP TABLE ?'"
@@ -194,18 +293,18 @@ module ActiveRecord
194
293
  def current_database
195
294
  select_value 'SELECT DB_NAME()'
196
295
  end
197
-
296
+
198
297
  def charset
199
298
  select_value "SELECT SERVERPROPERTY('SqlCharSetName')"
200
299
  end
201
-
202
-
300
+
301
+
203
302
  protected
204
-
303
+
205
304
  def select(sql, name = nil, binds = [])
206
305
  exec_query(sql, name, binds).to_a
207
306
  end
208
-
307
+
209
308
  def sql_for_insert(sql, pk, id_value, sequence_name, binds)
210
309
  sql = "#{sql}; SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident"# unless binds.empty?
211
310
  super
@@ -214,24 +313,23 @@ module ActiveRecord
214
313
  def last_inserted_id(result)
215
314
  super || select_value("SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident")
216
315
  end
217
-
316
+
218
317
  # === SQLServer Specific ======================================== #
219
-
318
+
220
319
  def valid_isolation_levels
221
320
  ["READ COMMITTED", "READ UNCOMMITTED", "REPEATABLE READ", "SERIALIZABLE", "SNAPSHOT"]
222
321
  end
223
-
322
+
224
323
  # === SQLServer Specific (Executing) ============================ #
225
324
 
226
- def do_execute(sql, name = nil)
227
- name ||= 'EXECUTE'
325
+ def do_execute(sql, name = 'SQL')
228
326
  log(sql, name) do
229
- with_auto_reconnect { raw_connection_do(sql) }
327
+ with_sqlserver_error_handling { raw_connection_do(sql) }
230
328
  end
231
329
  end
232
-
330
+
233
331
  def do_exec_query(sql, name, binds)
234
- statement = quote(sql)
332
+ explaining = name == 'EXPLAIN'
235
333
  names_and_types = []
236
334
  params = []
237
335
  binds.each_with_index do |(column,value),index|
@@ -250,13 +348,20 @@ module ActiveRecord
250
348
  raise "Unknown bind columns. We can account for this."
251
349
  end
252
350
  quoted_value = ar_column ? quote(v,column) : quote(v,nil)
253
- params << "@#{index} = #{quoted_value}"
351
+ params << (explaining ? quoted_value : "@#{index} = #{quoted_value}")
352
+ end
353
+ if explaining
354
+ params.each_with_index do |param, index|
355
+ substitute_at_finder = /(@#{index})(?=(?:[^']|'[^']*')*$)/ # Finds unquoted @n values.
356
+ sql.sub! substitute_at_finder, param
357
+ end
358
+ else
359
+ sql = "EXEC sp_executesql #{quote(sql)}"
360
+ sql << ", #{quote(names_and_types.join(', '))}, #{params.join(', ')}" unless binds.empty?
254
361
  end
255
- sql = "EXEC sp_executesql #{statement}"
256
- sql << ", #{quote(names_and_types.join(', '))}, #{params.join(', ')}" unless binds.empty?
257
362
  raw_select sql, name, binds, :ar_result => true
258
363
  end
259
-
364
+
260
365
  def raw_connection_do(sql)
261
366
  case @connection_options[:mode]
262
367
  when :dblib
@@ -267,22 +372,24 @@ module ActiveRecord
267
372
  ensure
268
373
  @update_sql = false
269
374
  end
270
-
375
+
271
376
  # === SQLServer Specific (Selecting) ============================ #
272
377
 
273
- def raw_select(sql, name=nil, binds=[], options={})
274
- log(sql,name,binds) do
275
- begin
276
- handle = raw_connection_run(sql)
277
- handle_to_names_and_values(handle, options)
278
- ensure
279
- finish_statement_handle(handle)
280
- end
378
+ def raw_select(sql, name='SQL', binds=[], options={})
379
+ log(sql,name,binds) { _raw_select(sql, options) }
380
+ end
381
+
382
+ def _raw_select(sql, options={})
383
+ begin
384
+ handle = raw_connection_run(sql)
385
+ handle_to_names_and_values(handle, options)
386
+ ensure
387
+ finish_statement_handle(handle)
281
388
  end
282
389
  end
283
-
390
+
284
391
  def raw_connection_run(sql)
285
- with_auto_reconnect do
392
+ with_sqlserver_error_handling do
286
393
  case @connection_options[:mode]
287
394
  when :dblib
288
395
  @connection.execute(sql)
@@ -291,7 +398,7 @@ module ActiveRecord
291
398
  end
292
399
  end
293
400
  end
294
-
401
+
295
402
  def handle_more_results?(handle)
296
403
  case @connection_options[:mode]
297
404
  when :dblib
@@ -299,7 +406,7 @@ module ActiveRecord
299
406
  handle.more_results
300
407
  end
301
408
  end
302
-
409
+
303
410
  def handle_to_names_and_values(handle, options={})
304
411
  case @connection_options[:mode]
305
412
  when :dblib
@@ -308,7 +415,7 @@ module ActiveRecord
308
415
  handle_to_names_and_values_odbc(handle, options)
309
416
  end
310
417
  end
311
-
418
+
312
419
  def handle_to_names_and_values_dblib(handle, options={})
313
420
  query_options = {}.tap do |qo|
314
421
  qo[:timezone] = ActiveRecord::Base.default_timezone || :utc
@@ -318,7 +425,7 @@ module ActiveRecord
318
425
  columns = lowercase_schema_reflection ? handle.fields.map { |c| c.downcase } : handle.fields
319
426
  options[:ar_result] ? ActiveRecord::Result.new(columns, results) : results
320
427
  end
321
-
428
+
322
429
  def handle_to_names_and_values_odbc(handle, options={})
323
430
  @connection.use_utc = ActiveRecord::Base.default_timezone == :utc
324
431
  if options[:ar_result]
@@ -334,16 +441,17 @@ module ActiveRecord
334
441
  end
335
442
  end
336
443
  end
337
-
444
+
338
445
  def finish_statement_handle(handle)
339
446
  case @connection_options[:mode]
340
- when :dblib
447
+ when :dblib
448
+ handle.cancel if handle
341
449
  when :odbc
342
450
  handle.drop if handle && handle.respond_to?(:drop) && !handle.finished?
343
451
  end
344
452
  handle
345
453
  end
346
-
454
+
347
455
  end
348
456
  end
349
457
  end
@@ -3,6 +3,9 @@ module ActiveRecord
3
3
  class LostConnection < WrappedDatabaseException
4
4
  end
5
5
 
6
+ class DeadlockVictim < WrappedDatabaseException
7
+ end
8
+
6
9
  module ConnectionAdapters
7
10
  module Sqlserver
8
11
  module Errors
@@ -10,7 +10,7 @@ module ActiveRecord
10
10
  case value
11
11
  when String, ActiveSupport::Multibyte::Chars
12
12
  if column && column.type == :integer && value.blank?
13
- nil
13
+ value.to_i.to_s
14
14
  elsif column && column.type == :binary
15
15
  column.class.string_to_binary(value)
16
16
  elsif value.is_utf8? || (column && column.type == :string)
@@ -42,8 +42,7 @@ module ActiveRecord
42
42
  end
43
43
 
44
44
  def quote_column_name(name)
45
- @sqlserver_quoted_column_and_table_names[name] ||=
46
- name.to_s.split('.').map{ |n| n =~ /^\[.*\]$/ ? n : "[#{n.to_s.gsub(']', ']]')}]" }.join('.')
45
+ schema_cache.quote_name(name)
47
46
  end
48
47
 
49
48
  def quote_table_name(name)
@@ -68,7 +67,16 @@ module ActiveRecord
68
67
 
69
68
  def quoted_datetime(value)
70
69
  if value.acts_like?(:time)
71
- value.is_a?(Date) ? quoted_value_acts_like_time_filter(value).to_time.xmlschema.to(18) : quoted_value_acts_like_time_filter(value).iso8601(3).to(22)
70
+ time_zone_qualified_value = quoted_value_acts_like_time_filter(value)
71
+ if value.is_a?(Date)
72
+ time_zone_qualified_value.to_time.xmlschema.to(18)
73
+ else
74
+ # CHANGED [Ruby 1.8] Not needed when 1.8 is dropped.
75
+ if value.is_a?(ActiveSupport::TimeWithZone) && RUBY_VERSION < '1.9'
76
+ time_zone_qualified_value = time_zone_qualified_value.to_time
77
+ end
78
+ time_zone_qualified_value.iso8601(3).to(22)
79
+ end
72
80
  else
73
81
  quoted_date(value)
74
82
  end
@@ -0,0 +1,85 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Sqlserver
4
+ class SchemaCache < ActiveRecord::ConnectionAdapters::SchemaCache
5
+
6
+ attr_reader :view_information
7
+
8
+ def initialize(conn)
9
+ super
10
+ @table_names = nil
11
+ @view_names = nil
12
+ @view_information = {}
13
+ @quoted_names = {}
14
+ end
15
+
16
+ # Superclass Overrides
17
+
18
+ def table_exists?(table_name)
19
+ return false if table_name.blank?
20
+ key = table_name_key(table_name)
21
+ return @tables[key] if @tables.key? key
22
+ @tables[key] = connection.table_exists?(table_name)
23
+ end
24
+
25
+ def clear!
26
+ super
27
+ @table_names = nil
28
+ @view_names = nil
29
+ @view_information.clear
30
+ @quoted_names.clear
31
+ end
32
+
33
+ def clear_table_cache!(table_name)
34
+ key = table_name_key(table_name)
35
+ super(key)
36
+ super(table_name)
37
+ # SQL Server Specific
38
+ if @table_names
39
+ @table_names.delete key
40
+ @table_names.delete table_name
41
+ end
42
+ if @view_names
43
+ @view_names.delete key
44
+ @view_names.delete table_name
45
+ end
46
+ @view_information.delete key
47
+ end
48
+
49
+ # SQL Server Specific
50
+
51
+ def table_names
52
+ @table_names ||= connection.tables
53
+ end
54
+
55
+ def view_names
56
+ @view_names ||= connection.views
57
+ end
58
+
59
+ def view_exists?(table_name)
60
+ table_exists?(table_name)
61
+ end
62
+
63
+ def view_information(table_name)
64
+ key = table_name_key(table_name)
65
+ return @view_information[key] if @view_information.key? key
66
+ @view_information[key] = connection.send(:view_information, table_name)
67
+ end
68
+
69
+ def quote_name(name)
70
+ return @quoted_names[name] if @quoted_names.key? name
71
+ @quoted_names[name] = name.to_s.split('.').map{ |n| n =~ /^\[.*\]$/ ? n : "[#{n.to_s.gsub(']', ']]')}]" }.join('.')
72
+ end
73
+
74
+
75
+ private
76
+
77
+ def table_name_key(table_name)
78
+ Utils.unqualify_table_name(table_name)
79
+ end
80
+
81
+ end
82
+ end
83
+ end
84
+ end
85
+