activerecord-sqlserver-adapter 3.1.1 → 3.2.18

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.
@@ -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
+