sic-activerecord-sqlserver-adapter 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (25) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG +603 -0
  3. data/MIT-LICENSE +20 -0
  4. data/VERSION +1 -0
  5. data/lib/active_record/connection_adapters/sqlserver/core_ext/active_record.rb +42 -0
  6. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain.rb +41 -0
  7. data/lib/active_record/connection_adapters/sqlserver/core_ext/explain_subscriber.rb +4 -0
  8. data/lib/active_record/connection_adapters/sqlserver/core_ext/odbc.rb +38 -0
  9. data/lib/active_record/connection_adapters/sqlserver/core_ext/relation.rb +19 -0
  10. data/lib/active_record/connection_adapters/sqlserver/database_limits.rb +49 -0
  11. data/lib/active_record/connection_adapters/sqlserver/database_statements.rb +458 -0
  12. data/lib/active_record/connection_adapters/sqlserver/errors.rb +36 -0
  13. data/lib/active_record/connection_adapters/sqlserver/quoting.rb +109 -0
  14. data/lib/active_record/connection_adapters/sqlserver/schema_cache.rb +85 -0
  15. data/lib/active_record/connection_adapters/sqlserver/schema_statements.rb +395 -0
  16. data/lib/active_record/connection_adapters/sqlserver/showplan.rb +67 -0
  17. data/lib/active_record/connection_adapters/sqlserver/showplan/printer_table.rb +69 -0
  18. data/lib/active_record/connection_adapters/sqlserver/showplan/printer_xml.rb +25 -0
  19. data/lib/active_record/connection_adapters/sqlserver/utils.rb +32 -0
  20. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +545 -0
  21. data/lib/active_record/sqlserver_test_case.rb +17 -0
  22. data/lib/activerecord-sqlserver-adapter.rb +1 -0
  23. data/lib/arel/select_manager_sqlserver.rb +64 -0
  24. data/lib/arel/visitors/sqlserver.rb +326 -0
  25. metadata +98 -0
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008-2011
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/VERSION ADDED
@@ -0,0 +1 @@
1
+ 4.0.0
@@ -0,0 +1,42 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Sqlserver
4
+ module CoreExt
5
+ module ActiveRecord
6
+
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ class_attribute :coerced_sqlserver_date_columns, :coerced_sqlserver_time_columns
11
+ self.coerced_sqlserver_date_columns = Set.new
12
+ self.coerced_sqlserver_time_columns = Set.new
13
+ end
14
+
15
+ module ClassMethods
16
+
17
+ def execute_procedure(proc_name, *variables)
18
+ if connection.respond_to?(:execute_procedure)
19
+ connection.execute_procedure(proc_name,*variables)
20
+ else
21
+ []
22
+ end
23
+ end
24
+
25
+ def coerce_sqlserver_date(*attributes)
26
+ self.coerced_sqlserver_date_columns += attributes.map(&:to_s)
27
+ end
28
+
29
+ def coerce_sqlserver_time(*attributes)
30
+ self.coerced_sqlserver_time_columns += attributes.map(&:to_s)
31
+ end
32
+
33
+ end
34
+
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+
42
+ ActiveRecord::Base.send :include, ActiveRecord::ConnectionAdapters::Sqlserver::CoreExt::ActiveRecord
@@ -0,0 +1,41 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Sqlserver
4
+ module CoreExt
5
+ module Explain
6
+
7
+ SQLSERVER_STATEMENT_PREFIX = "EXEC sp_executesql "
8
+ SQLSERVER_PARAM_MATCHER = /@\d+ =/
9
+
10
+ def exec_explain(queries)
11
+ unprepared_queries = queries.map { |sql, bind| [unprepare_sqlserver_statement(sql), bind] }
12
+ super(unprepared_queries)
13
+ end
14
+
15
+ private
16
+
17
+ # This is somewhat hacky, but it should reliably reformat our prepared sql statment
18
+ # which uses sp_executesql to just the first argument, then unquote it. Likewise our
19
+ # do_exec_query method should substitude the @n args withe the quoted values.
20
+ def unprepare_sqlserver_statement(sql)
21
+ if sql.starts_with?(SQLSERVER_STATEMENT_PREFIX)
22
+ executesql = sql.from(SQLSERVER_STATEMENT_PREFIX.length)
23
+ executesql_args = executesql.split(', ')
24
+ found_args = executesql_args.reject! { |arg| arg =~ SQLSERVER_PARAM_MATCHER }
25
+ executesql_args.pop if found_args && executesql_args.many?
26
+ executesql = executesql_args.join(', ').strip.match(/N'(.*)'/m)[1]
27
+ Utils.unquote_string(executesql)
28
+ else
29
+ sql
30
+ end
31
+ end
32
+
33
+
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+
40
+ ActiveRecord::Base.extend ActiveRecord::ConnectionAdapters::Sqlserver::CoreExt::Explain
41
+ ActiveRecord::Relation.send :include, ActiveRecord::ConnectionAdapters::Sqlserver::CoreExt::Explain
@@ -0,0 +1,4 @@
1
+ silence_warnings do
2
+ # Already defined in Rails
3
+ ActiveRecord::ExplainSubscriber::EXPLAINED_SQLS = /(select|update|delete|insert)\b/i
4
+ end
@@ -0,0 +1,38 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Sqlserver
4
+ module CoreExt
5
+ module ODBC
6
+
7
+ module Statement
8
+
9
+ def finished?
10
+ begin
11
+ connected?
12
+ false
13
+ rescue ::ODBC::Error
14
+ true
15
+ end
16
+ end
17
+
18
+ end
19
+
20
+ module Database
21
+
22
+ def run_block(*args)
23
+ yield sth = run(*args)
24
+ sth.drop
25
+ end
26
+
27
+ end
28
+
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+
36
+ ODBC::Statement.send :include, ActiveRecord::ConnectionAdapters::Sqlserver::CoreExt::ODBC::Statement
37
+ ODBC::Database.send :include, ActiveRecord::ConnectionAdapters::Sqlserver::CoreExt::ODBC::Database
38
+
@@ -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
@@ -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,458 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ module Sqlserver
4
+ module DatabaseStatements
5
+
6
+ def select_rows(sql, name = nil, binds = [])
7
+ raw_select sql, name, binds, :fetch => :rows
8
+ end
9
+
10
+ def execute(sql, name = nil)
11
+ if id_insert_table_name = query_requires_identity_insert?(sql)
12
+ with_identity_insert_enabled(id_insert_table_name) { do_execute(sql,name) }
13
+ else
14
+ do_execute(sql,name)
15
+ end
16
+ end
17
+
18
+ # TODO I bet there's a better way than a regex to take care of this
19
+ def exec_query(sql, name = 'SQL', binds = [], sqlserver_options = {})
20
+ # We can't update Identiy columns in sqlserver. So, strip out the id from the update.
21
+ if sql =~ /UPDATE/
22
+ # take off a comma before or after. This could probably be done better
23
+ if sql =~ /, \[id\] = @?[0-9]*/
24
+ sql.gsub! /, \[id\] = @?[0-9]*/, ''
25
+ elsif sql =~ /\s\[id\] = @?[0-9]*,/
26
+ sql.gsub! /\s\[id\] = @?[0-9]*,/, ''
27
+ end
28
+ end
29
+
30
+ if id_insert_table_name = sqlserver_options[:insert] ? query_requires_identity_insert?(sql) : nil
31
+ with_identity_insert_enabled(id_insert_table_name) { do_exec_query(sql, name, binds) }
32
+ else
33
+ do_exec_query(sql, name, binds)
34
+ end
35
+ end
36
+
37
+ #The abstract adapter ignores the last two parameters also
38
+ def exec_insert(sql, name, binds, pk = nil, sequence_name = nil)
39
+ exec_query sql, name, binds, :insert => true
40
+ end
41
+
42
+ def exec_delete(sql, name, binds)
43
+ sql << "; SELECT @@ROWCOUNT AS AffectedRows"
44
+ super.rows.first.first
45
+ end
46
+
47
+ def exec_update(sql, name, binds)
48
+ sql << "; SELECT @@ROWCOUNT AS AffectedRows"
49
+ super.rows.first.first
50
+ end
51
+
52
+ def supports_statement_cache?
53
+ true
54
+ end
55
+
56
+ def begin_db_transaction
57
+ do_execute "BEGIN TRANSACTION"
58
+ end
59
+
60
+ def commit_db_transaction
61
+ disable_auto_reconnect { do_execute "COMMIT TRANSACTION" }
62
+ end
63
+
64
+ def rollback_db_transaction
65
+ do_execute "IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION"
66
+ end
67
+
68
+ def create_savepoint
69
+ disable_auto_reconnect { do_execute "SAVE TRANSACTION #{current_savepoint_name}" }
70
+ end
71
+
72
+ def release_savepoint
73
+ end
74
+
75
+ def rollback_to_savepoint
76
+ disable_auto_reconnect { do_execute "ROLLBACK TRANSACTION #{current_savepoint_name}" }
77
+ end
78
+
79
+ def add_limit_offset!(sql, options)
80
+ raise NotImplementedError, 'This has been moved to the SQLServerCompiler in Arel.'
81
+ end
82
+
83
+ def empty_insert_statement_value
84
+ "DEFAULT VALUES"
85
+ end
86
+
87
+ def case_sensitive_modifier(node)
88
+ node.acts_like?(:string) ? Arel::Nodes::Bin.new(node) : node
89
+ end
90
+
91
+ # === SQLServer Specific ======================================== #
92
+
93
+ def execute_procedure(proc_name, *variables)
94
+ vars = if variables.any? && variables.first.is_a?(Hash)
95
+ variables.first.map { |k,v| "@#{k} = #{quote(v)}" }
96
+ else
97
+ variables.map { |v| quote(v) }
98
+ end.join(', ')
99
+ sql = "EXEC #{proc_name} #{vars}".strip
100
+ name = 'Execute Procedure'
101
+ log(sql, name) do
102
+ case @connection_options[:mode]
103
+ when :dblib
104
+ result = @connection.execute(sql)
105
+ result.each(:as => :hash, :cache_rows => true) do |row|
106
+ r = row.with_indifferent_access
107
+ yield(r) if block_given?
108
+ end
109
+ result.each.map{ |row| row.is_a?(Hash) ? row.with_indifferent_access : row }
110
+ when :odbc
111
+ results = []
112
+ raw_connection_run(sql) do |handle|
113
+ get_rows = lambda {
114
+ rows = handle_to_names_and_values handle, :fetch => :all
115
+ rows.each_with_index { |r,i| rows[i] = r.with_indifferent_access }
116
+ results << rows
117
+ }
118
+ get_rows.call
119
+ while handle_more_results?(handle)
120
+ get_rows.call
121
+ end
122
+ end
123
+ results.many? ? results : results.first
124
+ end
125
+ end
126
+ end
127
+
128
+ def use_database(database=nil)
129
+ return if sqlserver_azure?
130
+ database ||= @connection_options[:database]
131
+ do_execute "USE #{quote_table_name(database)}" unless database.blank?
132
+ end
133
+
134
+ def user_options
135
+ return {} if sqlserver_azure?
136
+ select_rows("dbcc useroptions",'SCHEMA').inject(HashWithIndifferentAccess.new) do |values,row|
137
+ if row.instance_of? Hash
138
+ set_option = row.values[0].gsub(/\s+/,'_')
139
+ user_value = row.values[1]
140
+ elsif row.instance_of? Array
141
+ set_option = row[0].gsub(/\s+/,'_')
142
+ user_value = row[1]
143
+ end
144
+ values[set_option] = user_value
145
+ values
146
+ end
147
+ end
148
+
149
+ # TODO Rails 4 now supports isolation levels
150
+ def user_options_dateformat
151
+ if sqlserver_azure?
152
+ select_value 'SELECT [dateformat] FROM [sys].[syslanguages] WHERE [langid] = @@LANGID', 'SCHEMA'
153
+ else
154
+ user_options['dateformat']
155
+ end
156
+ end
157
+
158
+ def user_options_isolation_level
159
+ if sqlserver_azure?
160
+ sql = %|SELECT CASE [transaction_isolation_level]
161
+ WHEN 0 THEN NULL
162
+ WHEN 1 THEN 'READ UNCOMITTED'
163
+ WHEN 2 THEN 'READ COMITTED'
164
+ WHEN 3 THEN 'REPEATABLE READ'
165
+ WHEN 4 THEN 'SERIALIZABLE'
166
+ WHEN 5 THEN 'SNAPSHOT' END AS [isolation_level]
167
+ FROM [sys].[dm_exec_sessions]
168
+ WHERE [session_id] = @@SPID|.squish
169
+ select_value sql, 'SCHEMA'
170
+ else
171
+ user_options['isolation_level']
172
+ end
173
+ end
174
+
175
+ def user_options_language
176
+ if sqlserver_azure?
177
+ select_value 'SELECT @@LANGUAGE AS [language]', 'SCHEMA'
178
+ else
179
+ user_options['language']
180
+ end
181
+ end
182
+
183
+ def run_with_isolation_level(isolation_level)
184
+ raise ArgumentError, "Invalid isolation level, #{isolation_level}. Supported levels include #{valid_isolation_levels.to_sentence}." if !valid_isolation_levels.include?(isolation_level.upcase)
185
+ initial_isolation_level = user_options_isolation_level || "READ COMMITTED"
186
+ do_execute "SET TRANSACTION ISOLATION LEVEL #{isolation_level}"
187
+ begin
188
+ yield
189
+ ensure
190
+ do_execute "SET TRANSACTION ISOLATION LEVEL #{initial_isolation_level}"
191
+ end if block_given?
192
+ end
193
+
194
+ def newid_function
195
+ select_value "SELECT NEWID()"
196
+ end
197
+
198
+ def newsequentialid_function
199
+ select_value "SELECT NEWSEQUENTIALID()"
200
+ end
201
+
202
+ def activity_stats
203
+ select_all %|
204
+ SELECT
205
+ [session_id] = s.session_id,
206
+ [user_process] = CONVERT(CHAR(1), s.is_user_process),
207
+ [login] = s.login_name,
208
+ [database] = ISNULL(db_name(r.database_id), N''),
209
+ [task_state] = ISNULL(t.task_state, N''),
210
+ [command] = ISNULL(r.command, N''),
211
+ [application] = ISNULL(s.program_name, N''),
212
+ [wait_time_ms] = ISNULL(w.wait_duration_ms, 0),
213
+ [wait_type] = ISNULL(w.wait_type, N''),
214
+ [wait_resource] = ISNULL(w.resource_description, N''),
215
+ [blocked_by] = ISNULL(CONVERT (varchar, w.blocking_session_id), ''),
216
+ [head_blocker] =
217
+ CASE
218
+ -- session has an active request, is blocked, but is blocking others
219
+ WHEN r2.session_id IS NOT NULL AND r.blocking_session_id = 0 THEN '1'
220
+ -- session is idle but has an open tran and is blocking others
221
+ WHEN r.session_id IS NULL THEN '1'
222
+ ELSE ''
223
+ END,
224
+ [total_cpu_ms] = s.cpu_time,
225
+ [total_physical_io_mb] = (s.reads + s.writes) * 8 / 1024,
226
+ [memory_use_kb] = s.memory_usage * 8192 / 1024,
227
+ [open_transactions] = ISNULL(r.open_transaction_count,0),
228
+ [login_time] = s.login_time,
229
+ [last_request_start_time] = s.last_request_start_time,
230
+ [host_name] = ISNULL(s.host_name, N''),
231
+ [net_address] = ISNULL(c.client_net_address, N''),
232
+ [execution_context_id] = ISNULL(t.exec_context_id, 0),
233
+ [request_id] = ISNULL(r.request_id, 0),
234
+ [workload_group] = N''
235
+ FROM sys.dm_exec_sessions s LEFT OUTER JOIN sys.dm_exec_connections c ON (s.session_id = c.session_id)
236
+ LEFT OUTER JOIN sys.dm_exec_requests r ON (s.session_id = r.session_id)
237
+ LEFT OUTER JOIN sys.dm_os_tasks t ON (r.session_id = t.session_id AND r.request_id = t.request_id)
238
+ LEFT OUTER JOIN
239
+ (SELECT *, ROW_NUMBER() OVER (PARTITION BY waiting_task_address ORDER BY wait_duration_ms DESC) AS row_num
240
+ FROM sys.dm_os_waiting_tasks
241
+ ) w ON (t.task_address = w.waiting_task_address) AND w.row_num = 1
242
+ LEFT OUTER JOIN sys.dm_exec_requests r2 ON (r.session_id = r2.blocking_session_id)
243
+ WHERE db_name(r.database_id) = '#{current_database}'
244
+ ORDER BY s.session_id|
245
+ end
246
+
247
+ # === SQLServer Specific (Rake/Test Helpers) ==================== #
248
+
249
+ def recreate_database
250
+ remove_database_connections_and_rollback do
251
+ do_execute "EXEC sp_MSforeachtable 'DROP TABLE ?'"
252
+ end
253
+ end
254
+
255
+ def recreate_database!(database=nil)
256
+ current_db = current_database
257
+ database ||= current_db
258
+ this_db = database.to_s == current_db
259
+ do_execute 'USE master' if this_db
260
+ drop_database(database)
261
+ create_database(database)
262
+ ensure
263
+ use_database(current_db) if this_db
264
+ end
265
+
266
+ def drop_database(database)
267
+ retry_count = 0
268
+ max_retries = 1
269
+ begin
270
+ do_execute "DROP DATABASE #{quote_table_name(database)}"
271
+ rescue ActiveRecord::StatementInvalid => err
272
+ if err.message =~ /because it is currently in use/i
273
+ raise if retry_count >= max_retries
274
+ retry_count += 1
275
+ remove_database_connections_and_rollback(database)
276
+ retry
277
+ elsif err.message =~ /does not exist/i
278
+ nil
279
+ else
280
+ raise
281
+ end
282
+ end
283
+ end
284
+
285
+ def create_database(database, collation=@connection_options[:collation])
286
+ if collation
287
+ do_execute "CREATE DATABASE #{quote_table_name(database)} COLLATE #{collation}"
288
+ else
289
+ do_execute "CREATE DATABASE #{quote_table_name(database)}"
290
+ end
291
+ end
292
+
293
+ def current_database
294
+ select_value 'SELECT DB_NAME()'
295
+ end
296
+
297
+ def charset
298
+ select_value "SELECT SERVERPROPERTY('SqlCharSetName')"
299
+ end
300
+
301
+
302
+ protected
303
+
304
+ def select(sql, name = nil, binds = [])
305
+ exec_query(sql, name, binds)
306
+ end
307
+
308
+ def sql_for_insert(sql, pk, id_value, sequence_name, binds)
309
+ sql = "#{sql}; SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident"# unless binds.empty?
310
+ super
311
+ end
312
+
313
+ def last_inserted_id(result)
314
+ super || select_value("SELECT CAST(SCOPE_IDENTITY() AS bigint) AS Ident")
315
+ end
316
+
317
+ # === SQLServer Specific ======================================== #
318
+
319
+ def valid_isolation_levels
320
+ ["READ COMMITTED", "READ UNCOMMITTED", "REPEATABLE READ", "SERIALIZABLE", "SNAPSHOT"]
321
+ end
322
+
323
+ # === SQLServer Specific (Executing) ============================ #
324
+
325
+ def do_execute(sql, name = 'SQL')
326
+ log(sql, name) do
327
+ with_sqlserver_error_handling { raw_connection_do(sql) }
328
+ end
329
+ end
330
+
331
+ def do_exec_query(sql, name, binds)
332
+ explaining = name == 'EXPLAIN'
333
+ names_and_types = []
334
+ params = []
335
+ binds.each_with_index do |(column,value),index|
336
+ ar_column = column.is_a?(ActiveRecord::ConnectionAdapters::Column)
337
+ next if ar_column && column.sql_type == 'timestamp'
338
+ v = value
339
+ names_and_types << if ar_column
340
+ v = value.to_i if column.is_integer? && value.present?
341
+ "@#{index} #{column.sql_type_for_statement}"
342
+ elsif column.acts_like?(:string)
343
+ "@#{index} nvarchar(max)"
344
+ elsif column.is_a?(Fixnum)
345
+ v = value.to_i
346
+ "@#{index} int"
347
+ else
348
+ raise "Unknown bind columns. We can account for this."
349
+ end
350
+ quoted_value = ar_column ? quote(v,column) : quote(v,nil)
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?
361
+ end
362
+ raw_select sql, name, binds, :ar_result => true
363
+ end
364
+
365
+ def raw_connection_do(sql)
366
+ case @connection_options[:mode]
367
+ when :dblib
368
+ @connection.execute(sql).do
369
+ when :odbc
370
+ @connection.do(sql)
371
+ end
372
+ ensure
373
+ @update_sql = false
374
+ end
375
+
376
+ # === SQLServer Specific (Selecting) ============================ #
377
+
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)
388
+ end
389
+ end
390
+
391
+ def raw_connection_run(sql)
392
+ with_sqlserver_error_handling do
393
+ case @connection_options[:mode]
394
+ when :dblib
395
+ @connection.execute(sql)
396
+ when :odbc
397
+ block_given? ? @connection.run_block(sql) { |handle| yield(handle) } : @connection.run(sql)
398
+ end
399
+ end
400
+ end
401
+
402
+ def handle_more_results?(handle)
403
+ case @connection_options[:mode]
404
+ when :dblib
405
+ when :odbc
406
+ handle.more_results
407
+ end
408
+ end
409
+
410
+ def handle_to_names_and_values(handle, options={})
411
+ case @connection_options[:mode]
412
+ when :dblib
413
+ handle_to_names_and_values_dblib(handle, options)
414
+ when :odbc
415
+ handle_to_names_and_values_odbc(handle, options)
416
+ end
417
+ end
418
+
419
+ def handle_to_names_and_values_dblib(handle, options={})
420
+ query_options = {}.tap do |qo|
421
+ qo[:timezone] = ActiveRecord::Base.default_timezone || :utc
422
+ qo[:as] = (options[:ar_result] || options[:fetch] == :rows) ? :array : :hash
423
+ end
424
+ results = handle.each(query_options)
425
+ columns = lowercase_schema_reflection ? handle.fields.map { |c| c.downcase } : handle.fields
426
+ options[:ar_result] ? ActiveRecord::Result.new(columns, results) : results
427
+ end
428
+
429
+ def handle_to_names_and_values_odbc(handle, options={})
430
+ @connection.use_utc = ActiveRecord::Base.default_timezone == :utc
431
+ if options[:ar_result]
432
+ columns = lowercase_schema_reflection ? handle.columns(true).map { |c| c.name.downcase } : handle.columns(true).map { |c| c.name }
433
+ rows = handle.fetch_all || []
434
+ ActiveRecord::Result.new(columns, rows)
435
+ else
436
+ case options[:fetch]
437
+ when :all
438
+ handle.each_hash || []
439
+ when :rows
440
+ handle.fetch_all || []
441
+ end
442
+ end
443
+ end
444
+
445
+ def finish_statement_handle(handle)
446
+ case @connection_options[:mode]
447
+ when :dblib
448
+ handle.cancel if handle
449
+ when :odbc
450
+ handle.drop if handle && handle.respond_to?(:drop) && !handle.finished?
451
+ end
452
+ handle
453
+ end
454
+
455
+ end
456
+ end
457
+ end
458
+ end