activerecord-sqlserver-adapter 2.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.
Files changed (37) hide show
  1. data/CHANGELOG +175 -0
  2. data/MIT-LICENSE +20 -0
  3. data/Manifest +36 -0
  4. data/README.rdoc +175 -0
  5. data/RUNNING_UNIT_TESTS +60 -0
  6. data/Rakefile +18 -0
  7. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +1126 -0
  8. data/lib/activerecord-sqlserver-adapter.rb +1 -0
  9. data/lib/core_ext/active_record.rb +133 -0
  10. data/lib/core_ext/dbi.rb +85 -0
  11. data/tasks/sqlserver.rake +31 -0
  12. data/test/cases/aaaa_create_tables_test_sqlserver.rb +19 -0
  13. data/test/cases/adapter_test_sqlserver.rb +707 -0
  14. data/test/cases/attribute_methods_test_sqlserver.rb +33 -0
  15. data/test/cases/basics_test_sqlserver.rb +21 -0
  16. data/test/cases/calculations_test_sqlserver.rb +20 -0
  17. data/test/cases/column_test_sqlserver.rb +264 -0
  18. data/test/cases/connection_test_sqlserver.rb +142 -0
  19. data/test/cases/eager_association_test_sqlserver.rb +42 -0
  20. data/test/cases/execute_procedure_test_sqlserver.rb +33 -0
  21. data/test/cases/inheritance_test_sqlserver.rb +28 -0
  22. data/test/cases/method_scoping_test_sqlserver.rb +28 -0
  23. data/test/cases/migration_test_sqlserver.rb +93 -0
  24. data/test/cases/offset_and_limit_test_sqlserver.rb +108 -0
  25. data/test/cases/pessimistic_locking_test_sqlserver.rb +125 -0
  26. data/test/cases/query_cache_test_sqlserver.rb +24 -0
  27. data/test/cases/schema_dumper_test_sqlserver.rb +72 -0
  28. data/test/cases/specific_schema_test_sqlserver.rb +57 -0
  29. data/test/cases/sqlserver_helper.rb +123 -0
  30. data/test/cases/table_name_test_sqlserver.rb +22 -0
  31. data/test/cases/transaction_test_sqlserver.rb +93 -0
  32. data/test/cases/unicode_test_sqlserver.rb +50 -0
  33. data/test/connections/native_sqlserver/connection.rb +23 -0
  34. data/test/connections/native_sqlserver_odbc/connection.rb +25 -0
  35. data/test/migrations/transaction_table/1_table_will_never_be_created.rb +11 -0
  36. data/test/schema/sqlserver_specific_schema.rb +91 -0
  37. metadata +120 -0
@@ -0,0 +1,18 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+ require 'echoe'
5
+
6
+ Echoe.new('activerecord-sqlserver-adapter','2.2.18') do |p|
7
+ p.summary = "SQL Server 2000, 2005 and 2008 Adapter For Rails."
8
+ p.description = "SQL Server 2000, 2005 and 2008 Adapter For Rails."
9
+ p.author = ["Ken Collins","Murray Steele","Shawn Balestracci","Joe Rafaniello","Tom Ward"]
10
+ p.email = "ken@metaskills.net"
11
+ p.url = "http://github.com/rails-sqlserver"
12
+ p.runtime_dependencies = ["dbi =0.4.1","dbd-odbc =0.2.4"]
13
+ p.include_gemspec = false
14
+ p.ignore_pattern = ["autotest/*","*.gemspec","lib/rails-sqlserver-2000-2005-adapter.rb"]
15
+ p.project = 'arsqlserver'
16
+ end
17
+
18
+ Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].sort.each { |ext| load(ext) }
@@ -0,0 +1,1126 @@
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
+ ConnectionAdapters::SQLServerAdapter.new(logger, [driver_url, username, password])
27
+ end
28
+
29
+ protected
30
+
31
+ def self.did_retry_sqlserver_connection(connection,count)
32
+ logger.info "CONNECTION RETRY: #{connection.class.name} retry ##{count}."
33
+ end
34
+
35
+ def self.did_lose_sqlserver_connection(connection)
36
+ logger.info "CONNECTION LOST: #{connection.class.name}"
37
+ end
38
+
39
+ end
40
+
41
+ module ConnectionAdapters
42
+
43
+ class SQLServerColumn < Column
44
+
45
+ def initialize(name, default, sql_type = nil, null = true, sqlserver_options = {})
46
+ @sqlserver_options = sqlserver_options
47
+ super(name, default, sql_type, null)
48
+ end
49
+
50
+ class << self
51
+
52
+ def string_to_utf8_encoding(value)
53
+ value.force_encoding('UTF-8') rescue value
54
+ end
55
+
56
+ def string_to_binary(value)
57
+ "0x#{value.unpack("H*")[0]}"
58
+ end
59
+
60
+ def binary_to_string(value)
61
+ value =~ /[^[:xdigit:]]/ ? value : [value].pack('H*')
62
+ end
63
+
64
+ end
65
+
66
+ def type_cast(value)
67
+ if value && type == :string && is_utf8?
68
+ self.class.string_to_utf8_encoding(value)
69
+ else
70
+ super
71
+ end
72
+ end
73
+
74
+ def type_cast_code(var_name)
75
+ if type == :string && is_utf8?
76
+ "#{self.class.name}.string_to_utf8_encoding(#{var_name})"
77
+ else
78
+ super
79
+ end
80
+ end
81
+
82
+ def is_identity?
83
+ @sqlserver_options[:is_identity]
84
+ end
85
+
86
+ def is_special?
87
+ # TODO: Not sure if these should be added: varbinary(max), nchar, nvarchar(max)
88
+ sql_type =~ /^text|ntext|image$/
89
+ end
90
+
91
+ def is_utf8?
92
+ sql_type =~ /nvarchar|ntext|nchar/i
93
+ end
94
+
95
+ def table_name
96
+ @sqlserver_options[:table_name]
97
+ end
98
+
99
+ def table_klass
100
+ @table_klass ||= begin
101
+ table_name.classify.constantize
102
+ rescue StandardError, NameError, LoadError
103
+ nil
104
+ end
105
+ (@table_klass && @table_klass < ActiveRecord::Base) ? @table_klass : nil
106
+ end
107
+
108
+ private
109
+
110
+ def extract_limit(sql_type)
111
+ case sql_type
112
+ when /^smallint/i
113
+ 2
114
+ when /^int/i
115
+ 4
116
+ when /^bigint/i
117
+ 8
118
+ when /\(max\)/, /decimal/, /numeric/
119
+ nil
120
+ else
121
+ super
122
+ end
123
+ end
124
+
125
+ def simplified_type(field_type)
126
+ case field_type
127
+ when /real/i then :float
128
+ when /money/i then :decimal
129
+ when /image/i then :binary
130
+ when /bit/i then :boolean
131
+ when /uniqueidentifier/i then :string
132
+ when /datetime/i then simplified_datetime
133
+ when /varchar\(max\)/ then :text
134
+ else super
135
+ end
136
+ end
137
+
138
+ def simplified_datetime
139
+ if table_klass && table_klass.coerced_sqlserver_date_columns.include?(name)
140
+ :date
141
+ elsif table_klass && table_klass.coerced_sqlserver_time_columns.include?(name)
142
+ :time
143
+ else
144
+ :datetime
145
+ end
146
+ end
147
+
148
+ end #SQLServerColumn
149
+
150
+ # In ADO mode, this adapter will ONLY work on Windows systems, since it relies on
151
+ # Win32OLE, which, to my knowledge, is only available on Windows.
152
+ #
153
+ # This mode also relies on the ADO support in the DBI module. If you are using the
154
+ # one-click installer of Ruby, then you already have DBI installed, but the ADO module
155
+ # is *NOT* installed. You will need to get the latest source distribution of Ruby-DBI
156
+ # from http://ruby-dbi.sourceforge.net/ unzip it, and copy the file from
157
+ # <tt>src/lib/dbd_ado/ADO.rb</tt> to <tt>X:/Ruby/lib/ruby/site_ruby/1.8/DBD/ADO/ADO.rb</tt>
158
+ #
159
+ # You will more than likely need to create the ADO directory. Once you've installed
160
+ # that file, you are ready to go.
161
+ #
162
+ # In ODBC mode, the adapter requires the ODBC support in the DBI module which requires
163
+ # the Ruby ODBC module. Ruby ODBC 0.996 was used in development and testing,
164
+ # and it is available at http://www.ch-werner.de/rubyodbc/
165
+ #
166
+ # Options:
167
+ #
168
+ # * <tt>:mode</tt> -- ADO or ODBC. Defaults to ADO.
169
+ # * <tt>:username</tt> -- Defaults to sa.
170
+ # * <tt>:password</tt> -- Defaults to empty string.
171
+ # * <tt>:windows_auth</tt> -- Defaults to "User ID=#{username};Password=#{password}"
172
+ #
173
+ # ADO specific options:
174
+ #
175
+ # * <tt>:host</tt> -- Defaults to localhost.
176
+ # * <tt>:database</tt> -- The name of the database. No default, must be provided.
177
+ # * <tt>:windows_auth</tt> -- Use windows authentication instead of username/password.
178
+ #
179
+ # ODBC specific options:
180
+ #
181
+ # * <tt>:dsn</tt> -- Defaults to nothing.
182
+ #
183
+ class SQLServerAdapter < AbstractAdapter
184
+
185
+ ADAPTER_NAME = 'SQLServer'.freeze
186
+ VERSION = '2.2.18'.freeze
187
+ DATABASE_VERSION_REGEXP = /Microsoft SQL Server\s+(\d{4})/
188
+ SUPPORTED_VERSIONS = [2000,2005,2008].freeze
189
+ LIMITABLE_TYPES = ['string','integer','float','char','nchar','varchar','nvarchar'].freeze
190
+
191
+ LOST_CONNECTION_EXCEPTIONS = [DBI::DatabaseError, DBI::InterfaceError]
192
+ LOST_CONNECTION_MESSAGES = [
193
+ 'Communication link failure',
194
+ 'Read from the server failed',
195
+ 'Write to the server failed',
196
+ 'Database connection was already closed']
197
+
198
+ cattr_accessor :native_text_database_type, :native_binary_database_type, :native_string_database_type,
199
+ :log_info_schema_queries, :enable_default_unicode_types, :auto_connect
200
+
201
+ class << self
202
+
203
+ def type_limitable?(type)
204
+ LIMITABLE_TYPES.include?(type.to_s)
205
+ end
206
+
207
+ end
208
+
209
+ def initialize(logger, connection_options)
210
+ @connection_options = connection_options
211
+ connect
212
+ super(raw_connection, logger)
213
+ initialize_sqlserver_caches
214
+ unless SUPPORTED_VERSIONS.include?(database_year)
215
+ raise NotImplementedError, "Currently, only #{SUPPORTED_VERSIONS.to_sentence} are supported."
216
+ end
217
+ end
218
+
219
+ # ABSTRACT ADAPTER =========================================#
220
+
221
+ def adapter_name
222
+ ADAPTER_NAME
223
+ end
224
+
225
+ def supports_migrations?
226
+ true
227
+ end
228
+
229
+ def supports_ddl_transactions?
230
+ true
231
+ end
232
+
233
+ def supports_savepoints?
234
+ true
235
+ end
236
+
237
+ def database_version
238
+ @database_version ||= info_schema_query { select_value('SELECT @@version') }
239
+ end
240
+
241
+ def database_year
242
+ DATABASE_VERSION_REGEXP.match(database_version)[1].to_i
243
+ end
244
+
245
+ def sqlserver?
246
+ true
247
+ end
248
+
249
+ def sqlserver_2000?
250
+ database_year == 2000
251
+ end
252
+
253
+ def sqlserver_2005?
254
+ database_year == 2005
255
+ end
256
+
257
+ def sqlserver_2008?
258
+ database_year == 2008
259
+ end
260
+
261
+ def version
262
+ self.class::VERSION
263
+ end
264
+
265
+ def inspect
266
+ "#<#{self.class} version: #{version}, year: #{database_year}, connection_options: #{@connection_options.inspect}>"
267
+ end
268
+
269
+ def auto_connect
270
+ @@auto_connect.is_a?(FalseClass) ? false : true
271
+ end
272
+
273
+ def native_string_database_type
274
+ @@native_string_database_type || (enable_default_unicode_types ? 'nvarchar' : 'varchar')
275
+ end
276
+
277
+ def native_text_database_type
278
+ @@native_text_database_type ||
279
+ if sqlserver_2005? || sqlserver_2008?
280
+ enable_default_unicode_types ? 'nvarchar(max)' : 'varchar(max)'
281
+ else
282
+ enable_default_unicode_types ? 'ntext' : 'text'
283
+ end
284
+ end
285
+
286
+ def native_binary_database_type
287
+ @@native_binary_database_type || ((sqlserver_2005? || sqlserver_2008?) ? 'varbinary(max)' : 'image')
288
+ end
289
+
290
+ # QUOTING ==================================================#
291
+
292
+ def quote(value, column = nil)
293
+ case value
294
+ when String, ActiveSupport::Multibyte::Chars
295
+ if column && column.type == :binary
296
+ column.class.string_to_binary(value)
297
+ elsif column && column.respond_to?(:is_utf8?) && column.is_utf8?
298
+ quoted_utf8_value(value)
299
+ else
300
+ super
301
+ end
302
+ else
303
+ super
304
+ end
305
+ end
306
+
307
+ def quote_string(string)
308
+ string.to_s.gsub(/\'/, "''")
309
+ end
310
+
311
+ def quote_column_name(column_name)
312
+ column_name.to_s.split('.').map{ |name| "[#{name}]" }.join('.')
313
+ end
314
+
315
+ def quote_table_name(table_name)
316
+ return table_name if table_name =~ /^\[.*\]$/
317
+ quote_column_name(table_name)
318
+ end
319
+
320
+ def quoted_true
321
+ '1'
322
+ end
323
+
324
+ def quoted_false
325
+ '0'
326
+ end
327
+
328
+ def quoted_date(value)
329
+ if value.acts_like?(:time) && value.respond_to?(:usec)
330
+ "#{super}.#{sprintf("%03d",value.usec/1000)}"
331
+ else
332
+ super
333
+ end
334
+ end
335
+
336
+ def quoted_utf8_value(value)
337
+ "N'#{quote_string(value)}'"
338
+ end
339
+
340
+ # REFERENTIAL INTEGRITY ====================================#
341
+
342
+ def disable_referential_integrity(&block)
343
+ do_execute "EXEC sp_MSForEachTable 'ALTER TABLE ? NOCHECK CONSTRAINT ALL'"
344
+ yield
345
+ ensure
346
+ do_execute "EXEC sp_MSForEachTable 'ALTER TABLE ? CHECK CONSTRAINT ALL'"
347
+ end
348
+
349
+ # CONNECTION MANAGEMENT ====================================#
350
+
351
+ def active?
352
+ raw_connection.execute("SELECT 1").finish
353
+ true
354
+ rescue *LOST_CONNECTION_EXCEPTIONS
355
+ false
356
+ end
357
+
358
+ def reconnect!
359
+ disconnect!
360
+ connect
361
+ active?
362
+ end
363
+
364
+ def disconnect!
365
+ raw_connection.disconnect rescue nil
366
+ end
367
+
368
+ def finish_statement_handle(handle)
369
+ handle.finish if handle && handle.respond_to?(:finish) && !handle.finished?
370
+ handle
371
+ end
372
+
373
+ # DATABASE STATEMENTS ======================================#
374
+
375
+ def user_options
376
+ info_schema_query do
377
+ select_rows("dbcc useroptions").inject(HashWithIndifferentAccess.new) do |values,row|
378
+ set_option = row[0].gsub(/\s+/,'_')
379
+ user_value = row[1]
380
+ values[set_option] = user_value
381
+ values
382
+ end
383
+ end
384
+ end
385
+
386
+ VALID_ISOLATION_LEVELS = ["READ COMMITTED", "READ UNCOMMITTED", "REPEATABLE READ", "SERIALIZABLE", "SNAPSHOT"]
387
+
388
+ def run_with_isolation_level(isolation_level)
389
+ raise ArgumentError, "Invalid isolation level, #{isolation_level}. Supported levels include #{VALID_ISOLATION_LEVELS.to_sentence}." if !VALID_ISOLATION_LEVELS.include?(isolation_level.upcase)
390
+ initial_isolation_level = user_options[:isolation_level] || "READ COMMITTED"
391
+ do_execute "SET TRANSACTION ISOLATION LEVEL #{isolation_level}"
392
+ begin
393
+ yield
394
+ ensure
395
+ do_execute "SET TRANSACTION ISOLATION LEVEL #{initial_isolation_level}"
396
+ end if block_given?
397
+ end
398
+
399
+ def select_rows(sql, name = nil)
400
+ raw_select(sql,name).last
401
+ end
402
+
403
+ def execute(sql, name = nil, &block)
404
+ if table_name = query_requires_identity_insert?(sql)
405
+ handle = with_identity_insert_enabled(table_name) { raw_execute(sql,name,&block) }
406
+ else
407
+ handle = raw_execute(sql,name,&block)
408
+ end
409
+ finish_statement_handle(handle)
410
+ end
411
+
412
+ def execute_procedure(proc_name, *variables)
413
+ vars = variables.map{ |v| quote(v) }.join(', ')
414
+ sql = "EXEC #{proc_name} #{vars}".strip
415
+ select(sql,'Execute Procedure',true).inject([]) do |results,row|
416
+ results << row.with_indifferent_access
417
+ end
418
+ end
419
+
420
+ def outside_transaction?
421
+ info_schema_query { select_value("SELECT @@TRANCOUNT") == 0 }
422
+ end
423
+
424
+ def begin_db_transaction
425
+ do_execute "BEGIN TRANSACTION"
426
+ end
427
+
428
+ def commit_db_transaction
429
+ do_execute "COMMIT TRANSACTION"
430
+ end
431
+
432
+ def rollback_db_transaction
433
+ do_execute "ROLLBACK TRANSACTION" rescue nil
434
+ end
435
+
436
+ def create_savepoint
437
+ do_execute "SAVE TRANSACTION #{current_savepoint_name}"
438
+ end
439
+
440
+ def release_savepoint
441
+ end
442
+
443
+ def rollback_to_savepoint
444
+ do_execute "ROLLBACK TRANSACTION #{current_savepoint_name}"
445
+ end
446
+
447
+ def add_limit_offset!(sql, options)
448
+ # Validate and/or convert integers for :limit and :offets options.
449
+ if options[:offset]
450
+ raise ArgumentError, "offset should have a limit" unless options[:limit]
451
+ unless options[:offset].kind_of?(Integer)
452
+ if options[:offset] =~ /^\d+$/
453
+ options[:offset] = options[:offset].to_i
454
+ else
455
+ raise ArgumentError, "offset should be an integer"
456
+ end
457
+ end
458
+ end
459
+ if options[:limit] && !(options[:limit].kind_of?(Integer))
460
+ if options[:limit] =~ /^\d+$/
461
+ options[:limit] = options[:limit].to_i
462
+ else
463
+ raise ArgumentError, "limit should be an integer"
464
+ end
465
+ end
466
+ # The business of adding limit/offset
467
+ if options[:limit] and options[:offset]
468
+ tally_sql = "SELECT count(*) as TotalRows from (#{sql.sub(/\bSELECT(\s+DISTINCT)?\b/i, "SELECT#{$1} TOP 1000000000")}) tally"
469
+ add_lock! tally_sql, options
470
+ total_rows = select_value(tally_sql).to_i
471
+ if (options[:limit] + options[:offset]) >= total_rows
472
+ options[:limit] = (total_rows - options[:offset] >= 0) ? (total_rows - options[:offset]) : 0
473
+ end
474
+ # Make sure we do not need a special limit/offset for association limiting. http://gist.github.com/25118
475
+ add_limit_offset_for_association_limiting!(sql,options) and return if sql_for_association_limiting?(sql)
476
+ # Wrap the SQL query in a bunch of outer SQL queries that emulate proper LIMIT,OFFSET support.
477
+ sql.sub!(/^\s*SELECT(\s+DISTINCT)?/i, "SELECT * FROM (SELECT TOP #{options[:limit]} * FROM (SELECT#{$1} TOP #{options[:limit] + options[:offset]}")
478
+ sql << ") AS tmp1"
479
+ if options[:order]
480
+ order = options[:order].split(',').map do |field|
481
+ order_by_column, order_direction = field.split(" ")
482
+ order_by_column = quote_column_name(order_by_column)
483
+ # Investigate the SQL query to figure out if the order_by_column has been renamed.
484
+ if sql =~ /#{Regexp.escape(order_by_column)} AS (t\d_r\d\d?)/
485
+ # Fx "[foo].[bar] AS t4_r2" was found in the SQL. Use the column alias (ie 't4_r2') for the subsequent orderings
486
+ order_by_column = $1
487
+ elsif order_by_column =~ /\w+\.\[?(\w+)\]?/
488
+ order_by_column = $1
489
+ else
490
+ # It doesn't appear that the column name has been renamed as part of the query. Use just the column
491
+ # name rather than the full identifier for the outer queries.
492
+ order_by_column = order_by_column.split('.').last
493
+ end
494
+ # Put the column name and eventual direction back together
495
+ [order_by_column, order_direction].join(' ').strip
496
+ end.join(', ')
497
+ sql << " ORDER BY #{change_order_direction(order)}) AS tmp2 ORDER BY #{order}"
498
+ else
499
+ sql << ") AS tmp2"
500
+ end
501
+ elsif options[:limit] && sql !~ /^\s*SELECT (@@|COUNT\()/i
502
+ if md = sql.match(/^(\s*SELECT)(\s+DISTINCT)?(.*)/im)
503
+ sql.replace "#{md[1]}#{md[2]} TOP #{options[:limit]}#{md[3]}"
504
+ else
505
+ # Account for building SQL fragments without SELECT yet. See #update_all and #limited_update_conditions.
506
+ sql.replace "TOP #{options[:limit]} #{sql}"
507
+ end
508
+ end
509
+ end
510
+
511
+ def add_lock!(sql, options)
512
+ # http://blog.sqlauthority.com/2007/04/27/sql-server-2005-locking-hints-and-examples/
513
+ return unless options[:lock]
514
+ lock_type = options[:lock] == true ? 'WITH(HOLDLOCK, ROWLOCK)' : options[:lock]
515
+ sql.gsub! %r|LEFT OUTER JOIN\s+(.*?)\s+ON|im, "LEFT OUTER JOIN \\1 #{lock_type} ON"
516
+ sql.gsub! %r{FROM\s([\w\[\]\.]+)}im, "FROM \\1 #{lock_type}"
517
+ end
518
+
519
+ def empty_insert_statement(table_name)
520
+ "INSERT INTO #{quote_table_name(table_name)} DEFAULT VALUES"
521
+ end
522
+
523
+ def case_sensitive_equality_operator
524
+ "COLLATE Latin1_General_CS_AS ="
525
+ end
526
+
527
+ def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
528
+ match_data = where_sql.match(/(.*)WHERE/)
529
+ limit = match_data[1]
530
+ where_sql.sub!(limit,'')
531
+ "WHERE #{quoted_primary_key} IN (SELECT #{limit} #{quoted_primary_key} FROM #{quoted_table_name} #{where_sql})"
532
+ end
533
+
534
+ # SCHEMA STATEMENTS ========================================#
535
+
536
+ def native_database_types
537
+ {
538
+ :primary_key => "int NOT NULL IDENTITY(1, 1) PRIMARY KEY",
539
+ :string => { :name => native_string_database_type, :limit => 255 },
540
+ :text => { :name => native_text_database_type },
541
+ :integer => { :name => "int", :limit => 4 },
542
+ :float => { :name => "float", :limit => 8 },
543
+ :decimal => { :name => "decimal" },
544
+ :datetime => { :name => "datetime" },
545
+ :timestamp => { :name => "datetime" },
546
+ :time => { :name => "datetime" },
547
+ :date => { :name => "datetime" },
548
+ :binary => { :name => native_binary_database_type },
549
+ :boolean => { :name => "bit"},
550
+ # These are custom types that may move somewhere else for good schema_dumper.rb hacking to output them.
551
+ :char => { :name => 'char' },
552
+ :varchar_max => { :name => 'varchar(max)' },
553
+ :nchar => { :name => "nchar" },
554
+ :nvarchar => { :name => "nvarchar", :limit => 255 },
555
+ :nvarchar_max => { :name => "nvarchar(max)" },
556
+ :ntext => { :name => "ntext" }
557
+ }
558
+ end
559
+
560
+ def table_alias_length
561
+ 128
562
+ end
563
+
564
+ def tables(name = nil)
565
+ info_schema_query do
566
+ select_values "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME <> 'dtproperties'"
567
+ end
568
+ end
569
+
570
+ def views(name = nil)
571
+ @sqlserver_views_cache ||=
572
+ info_schema_query { select_values("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_NAME NOT IN ('sysconstraints','syssegments')") }
573
+ end
574
+
575
+ def view_information(table_name)
576
+ table_name = unqualify_table_name(table_name)
577
+ @sqlserver_view_information_cache[table_name] ||= begin
578
+ view_info = info_schema_query { select_one("SELECT * FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_NAME = '#{table_name}'") }
579
+ if view_info
580
+ if view_info['VIEW_DEFINITION'].blank? || view_info['VIEW_DEFINITION'].length == 4000
581
+ view_info['VIEW_DEFINITION'] = info_schema_query { select_values("EXEC sp_helptext #{table_name}").join }
582
+ end
583
+ end
584
+ view_info
585
+ end
586
+ end
587
+
588
+ def view_table_name(table_name)
589
+ view_info = view_information(table_name)
590
+ view_info ? get_table_name(view_info['VIEW_DEFINITION']) : table_name
591
+ end
592
+
593
+ def table_exists?(table_name)
594
+ super || tables.include?(unqualify_table_name(table_name)) || views.include?(table_name.to_s)
595
+ end
596
+
597
+ def indexes(table_name, name = nil)
598
+ unquoted_table_name = unqualify_table_name(table_name)
599
+ select("EXEC sp_helpindex #{quote_table_name(unquoted_table_name)}",name).inject([]) do |indexes,index|
600
+ if index['index_description'] =~ /primary key/
601
+ indexes
602
+ else
603
+ name = index['index_name']
604
+ unique = index['index_description'] =~ /unique/
605
+ columns = index['index_keys'].split(',').map do |column|
606
+ column.strip!
607
+ column.gsub! '(-)', '' if column.ends_with?('(-)')
608
+ column
609
+ end
610
+ indexes << IndexDefinition.new(table_name, name, unique, columns)
611
+ end
612
+ end
613
+ end
614
+
615
+ def columns(table_name, name = nil)
616
+ return [] if table_name.blank?
617
+ cache_key = unqualify_table_name(table_name)
618
+ @sqlserver_columns_cache[cache_key] ||= column_definitions(table_name).collect do |ci|
619
+ sqlserver_options = ci.except(:name,:default_value,:type,:null)
620
+ SQLServerColumn.new ci[:name], ci[:default_value], ci[:type], ci[:null], sqlserver_options
621
+ end
622
+ end
623
+
624
+ def create_table(table_name, options = {})
625
+ super
626
+ remove_sqlserver_columns_cache_for(table_name)
627
+ end
628
+
629
+ def rename_table(table_name, new_name)
630
+ do_execute "EXEC sp_rename '#{table_name}', '#{new_name}'"
631
+ end
632
+
633
+ def drop_table(table_name, options = {})
634
+ super
635
+ remove_sqlserver_columns_cache_for(table_name)
636
+ end
637
+
638
+ def add_column(table_name, column_name, type, options = {})
639
+ super
640
+ remove_sqlserver_columns_cache_for(table_name)
641
+ end
642
+
643
+ def remove_column(table_name, *column_names)
644
+ column_names.flatten.each do |column_name|
645
+ remove_check_constraints(table_name, column_name)
646
+ remove_default_constraint(table_name, column_name)
647
+ remove_indexes(table_name, column_name)
648
+ do_execute "ALTER TABLE #{quote_table_name(table_name)} DROP COLUMN #{quote_column_name(column_name)}"
649
+ end
650
+ remove_sqlserver_columns_cache_for(table_name)
651
+ end
652
+
653
+ def change_column(table_name, column_name, type, options = {})
654
+ sql_commands = []
655
+ 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])}"
656
+ change_column_sql << " NOT NULL" if options[:null] == false
657
+ sql_commands << change_column_sql
658
+ if options_include_default?(options)
659
+ remove_default_constraint(table_name, column_name)
660
+ 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)}"
661
+ end
662
+ sql_commands.each { |c| do_execute(c) }
663
+ remove_sqlserver_columns_cache_for(table_name)
664
+ end
665
+
666
+ def change_column_default(table_name, column_name, default)
667
+ remove_default_constraint(table_name, column_name)
668
+ 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)}"
669
+ remove_sqlserver_columns_cache_for(table_name)
670
+ end
671
+
672
+ def rename_column(table_name, column_name, new_column_name)
673
+ column_for(table_name,column_name)
674
+ do_execute "EXEC sp_rename '#{table_name}.#{column_name}', '#{new_column_name}', 'COLUMN'"
675
+ remove_sqlserver_columns_cache_for(table_name)
676
+ end
677
+
678
+ def remove_index(table_name, options = {})
679
+ do_execute "DROP INDEX #{table_name}.#{quote_column_name(index_name(table_name, options))}"
680
+ end
681
+
682
+ def type_to_sql(type, limit = nil, precision = nil, scale = nil)
683
+ limit = nil unless self.class.type_limitable?(type)
684
+ case type.to_s
685
+ when 'integer'
686
+ case limit
687
+ when 1..2 then 'smallint'
688
+ when 3..4, nil then 'integer'
689
+ when 5..8 then 'bigint'
690
+ else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
691
+ end
692
+ else
693
+ super
694
+ end
695
+ end
696
+
697
+ def add_order_by_for_association_limiting!(sql, options)
698
+ # Disertation http://gist.github.com/24073
699
+ # Information http://weblogs.sqlteam.com/jeffs/archive/2007/12/13/select-distinct-order-by-error.aspx
700
+ return sql if options[:order].blank?
701
+ columns = sql.match(/SELECT\s+DISTINCT(.*)FROM/)[1].strip
702
+ sql.sub!(/SELECT\s+DISTINCT/,'SELECT')
703
+ sql << "GROUP BY #{columns} ORDER BY #{order_to_min_set(options[:order])}"
704
+ end
705
+
706
+ def change_column_null(table_name, column_name, null, default = nil)
707
+ column = column_for(table_name,column_name)
708
+ unless null || default.nil?
709
+ do_execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
710
+ end
711
+ sql = "ALTER TABLE #{table_name} ALTER COLUMN #{quote_column_name(column_name)} #{type_to_sql column.type, column.limit, column.precision, column.scale}"
712
+ sql << " NOT NULL" unless null
713
+ do_execute sql
714
+ end
715
+
716
+ def pk_and_sequence_for(table_name)
717
+ idcol = identity_column(table_name)
718
+ idcol ? [idcol.name,nil] : nil
719
+ end
720
+
721
+ # RAKE UTILITY METHODS =====================================#
722
+
723
+ def recreate_database(name)
724
+ existing_database = current_database.to_s
725
+ if name.to_s == existing_database
726
+ do_execute 'USE master'
727
+ end
728
+ drop_database(name)
729
+ create_database(name)
730
+ ensure
731
+ do_execute "USE #{existing_database}" if name.to_s == existing_database
732
+ end
733
+
734
+ def drop_database(name)
735
+ retry_count = 0
736
+ max_retries = 1
737
+ begin
738
+ do_execute "DROP DATABASE #{name}"
739
+ rescue ActiveRecord::StatementInvalid => err
740
+ # Remove existing connections and rollback any transactions if we received the message
741
+ # 'Cannot drop the database 'test' because it is currently in use'
742
+ if err.message =~ /because it is currently in use/
743
+ raise if retry_count >= max_retries
744
+ retry_count += 1
745
+ remove_database_connections_and_rollback(name)
746
+ retry
747
+ else
748
+ raise
749
+ end
750
+ end
751
+ end
752
+
753
+ def create_database(name)
754
+ do_execute "CREATE DATABASE #{name}"
755
+ end
756
+
757
+ def current_database
758
+ select_value 'SELECT DB_NAME()'
759
+ end
760
+
761
+ def remove_database_connections_and_rollback(name)
762
+ # This should disconnect all other users and rollback any transactions for SQL 2000 and 2005
763
+ # http://sqlserver2000.databases.aspfaq.com/how-do-i-drop-a-sql-server-database.html
764
+ do_execute "ALTER DATABASE #{name} SET SINGLE_USER WITH ROLLBACK IMMEDIATE"
765
+ end
766
+
767
+
768
+
769
+ protected
770
+
771
+ # CONNECTION MANAGEMENT ====================================#
772
+
773
+ def connect
774
+ driver_url, username, password = @connection_options
775
+ @connection = DBI.connect(driver_url, username, password)
776
+ configure_connection
777
+ rescue
778
+ raise unless @auto_connecting
779
+ end
780
+
781
+ def configure_connection
782
+ raw_connection['AutoCommit'] = true
783
+ end
784
+
785
+ def with_auto_reconnect
786
+ begin
787
+ yield
788
+ rescue *LOST_CONNECTION_EXCEPTIONS => e
789
+ if LOST_CONNECTION_MESSAGES.any? { |lcm| e.message =~ Regexp.new(lcm,Regexp::IGNORECASE) }
790
+ retry if auto_reconnected?
791
+ end
792
+ raise
793
+ end
794
+ end
795
+
796
+ def auto_reconnected?
797
+ return false unless auto_connect
798
+ @auto_connecting = true
799
+ count = 0
800
+ while count <= 5
801
+ sleep 2** count
802
+ ActiveRecord::Base.did_retry_sqlserver_connection(self,count)
803
+ return true if reconnect!
804
+ count += 1
805
+ end
806
+ ActiveRecord::Base.did_lose_sqlserver_connection(self)
807
+ false
808
+ ensure
809
+ @auto_connecting = false
810
+ end
811
+
812
+ # DATABASE STATEMENTS ======================================
813
+
814
+ def select(sql, name = nil, ignore_special_columns = false)
815
+ repair_special_columns(sql) unless ignore_special_columns
816
+ fields, rows = raw_select(sql,name)
817
+ rows.inject([]) do |results,row|
818
+ row_hash = {}
819
+ fields.each_with_index do |f, i|
820
+ row_hash[f] = row[i]
821
+ end
822
+ results << row_hash
823
+ end
824
+ end
825
+
826
+ def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
827
+ super || select_value("SELECT SCOPE_IDENTITY() AS Ident")
828
+ end
829
+
830
+ def update_sql(sql, name = nil)
831
+ execute(sql, name)
832
+ select_value('SELECT @@ROWCOUNT AS AffectedRows')
833
+ end
834
+
835
+ def info_schema_query
836
+ log_info_schema_queries ? yield : ActiveRecord::Base.silence{ yield }
837
+ end
838
+
839
+ def raw_execute(sql, name = nil, &block)
840
+ log(sql, name) do
841
+ if block_given?
842
+ with_auto_reconnect { raw_connection.execute(sql) { |handle| yield(handle) } }
843
+ else
844
+ with_auto_reconnect { raw_connection.execute(sql) }
845
+ end
846
+ end
847
+ end
848
+
849
+ def without_type_conversion
850
+ raw_connection.convert_types = false if raw_connection.respond_to?(:convert_types=)
851
+ yield
852
+ ensure
853
+ raw_connection.convert_types = true if raw_connection.respond_to?(:convert_types=)
854
+ end
855
+
856
+ def do_execute(sql,name=nil)
857
+ log(sql, name || 'EXECUTE') do
858
+ with_auto_reconnect { raw_connection.do(sql) }
859
+ end
860
+ end
861
+
862
+ def raw_select(sql, name = nil)
863
+ handle = raw_execute(sql,name)
864
+ fields = handle.column_names
865
+ results = handle_as_array(handle)
866
+ rows = results.inject([]) do |rows,row|
867
+ row.each_with_index do |value, i|
868
+ # DEPRECATED in DBI 0.4.0 and above. Remove when 0.2.2 and lower is no longer supported.
869
+ if value.is_a? DBI::Timestamp
870
+ row[i] = value.to_sqlserver_string
871
+ end
872
+ end
873
+ rows << row
874
+ end
875
+ return fields, rows
876
+ end
877
+
878
+ def handle_as_array(handle)
879
+ array = handle.inject([]) do |rows,row|
880
+ rows << row.inject([]){ |values,value| values << value }
881
+ end
882
+ finish_statement_handle(handle)
883
+ array
884
+ end
885
+
886
+ def add_limit_offset_for_association_limiting!(sql, options)
887
+ sql.replace %|
888
+ SET NOCOUNT ON
889
+ DECLARE @row_number TABLE (row int identity(1,1), id int)
890
+ INSERT INTO @row_number (id)
891
+ #{sql}
892
+ SET NOCOUNT OFF
893
+ SELECT id FROM (
894
+ SELECT TOP #{options[:limit]} * FROM (
895
+ SELECT TOP #{options[:limit] + options[:offset]} * FROM @row_number ORDER BY row
896
+ ) AS tmp1 ORDER BY row DESC
897
+ ) AS tmp2 ORDER BY row
898
+ |.gsub(/[ \t\r\n]+/,' ')
899
+ end
900
+
901
+ # SCHEMA STATEMENTS ========================================#
902
+
903
+ def remove_check_constraints(table_name, column_name)
904
+ 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)}'") }
905
+ constraints.each do |constraint|
906
+ do_execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{quote_column_name(constraint)}"
907
+ end
908
+ end
909
+
910
+ def remove_default_constraint(table_name, column_name)
911
+ 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")
912
+ constraints.each do |constraint|
913
+ do_execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{quote_column_name(constraint)}"
914
+ end
915
+ end
916
+
917
+ def remove_indexes(table_name, column_name)
918
+ indexes(table_name).select{ |index| index.columns.include?(column_name.to_s) }.each do |index|
919
+ remove_index(table_name, {:name => index.name})
920
+ end
921
+ end
922
+
923
+ def default_name(table_name, column_name)
924
+ "DF_#{table_name}_#{column_name}"
925
+ end
926
+
927
+ # IDENTITY INSERTS =========================================#
928
+
929
+ def with_identity_insert_enabled(table_name, &block)
930
+ table_name = quote_table_name(table_name_or_views_table_name(table_name))
931
+ set_identity_insert(table_name, true)
932
+ yield
933
+ ensure
934
+ set_identity_insert(table_name, false)
935
+ end
936
+
937
+ def set_identity_insert(table_name, enable = true)
938
+ sql = "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}"
939
+ do_execute(sql,'IDENTITY_INSERT')
940
+ rescue Exception => e
941
+ raise ActiveRecordError, "IDENTITY_INSERT could not be turned #{enable ? 'ON' : 'OFF'} for table #{table_name}"
942
+ end
943
+
944
+ def query_requires_identity_insert?(sql)
945
+ if insert_sql?(sql)
946
+ table_name = get_table_name(sql)
947
+ id_column = identity_column(table_name)
948
+ id_column && sql =~ /INSERT[^(]+\([^)]*\[#{id_column.name}\][^)]*\)/i ? table_name : false
949
+ else
950
+ false
951
+ end
952
+ end
953
+
954
+ def identity_column(table_name)
955
+ columns(table_name).detect(&:is_identity?)
956
+ end
957
+
958
+ def table_name_or_views_table_name(table_name)
959
+ unquoted_table_name = unqualify_table_name(table_name)
960
+ views.include?(unquoted_table_name) ? view_table_name(unquoted_table_name) : unquoted_table_name
961
+ end
962
+
963
+ # HELPER METHODS ===========================================#
964
+
965
+ def insert_sql?(sql)
966
+ !(sql =~ /^\s*INSERT/i).nil?
967
+ end
968
+
969
+ def unqualify_table_name(table_name)
970
+ table_name.to_s.split('.').last.gsub(/[\[\]]/,'')
971
+ end
972
+
973
+ def unqualify_db_name(table_name)
974
+ table_names = table_name.to_s.split('.')
975
+ table_names.length == 3 ? table_names.first.tr('[]','') : nil
976
+ end
977
+
978
+ def get_table_name(sql)
979
+ if sql =~ /^\s*insert\s+into\s+([^\(\s]+)\s*|^\s*update\s+([^\(\s]+)\s*/i
980
+ $1 || $2
981
+ elsif sql =~ /from\s+([^\(\s]+)\s*/i
982
+ $1
983
+ else
984
+ nil
985
+ end
986
+ end
987
+
988
+ def orders_and_dirs_set(order)
989
+ orders = order.sub('ORDER BY','').split(',').map(&:strip).reject(&:blank?)
990
+ orders_dirs = orders.map do |ord|
991
+ dir = nil
992
+ ord.sub!(/\b(asc|desc)$/i) do |match|
993
+ if match
994
+ dir = match.upcase.strip
995
+ ''
996
+ end
997
+ end
998
+ [ord.strip, dir]
999
+ end
1000
+ end
1001
+
1002
+ def views_real_column_name(table_name,column_name)
1003
+ view_definition = view_information(table_name)['VIEW_DEFINITION']
1004
+ match_data = view_definition.match(/([\w-]*)\s+as\s+#{column_name}/im)
1005
+ match_data ? match_data[1] : column_name
1006
+ end
1007
+
1008
+ def order_to_min_set(order)
1009
+ orders_dirs = orders_and_dirs_set(order)
1010
+ orders_dirs.map do |o,d|
1011
+ "MIN(#{o}) #{d}".strip
1012
+ end.join(', ')
1013
+ end
1014
+
1015
+ def sql_for_association_limiting?(sql)
1016
+ if md = sql.match(/^\s*SELECT(.*)FROM.*GROUP BY.*ORDER BY.*/im)
1017
+ select_froms = md[1].split(',')
1018
+ select_froms.size == 1 && !select_froms.first.include?('*')
1019
+ end
1020
+ end
1021
+
1022
+ def remove_sqlserver_columns_cache_for(table_name)
1023
+ cache_key = unqualify_table_name(table_name)
1024
+ @sqlserver_columns_cache[cache_key] = nil
1025
+ initialize_sqlserver_caches(false)
1026
+ end
1027
+
1028
+ def initialize_sqlserver_caches(reset_columns=true)
1029
+ @sqlserver_columns_cache = {} if reset_columns
1030
+ @sqlserver_views_cache = nil
1031
+ @sqlserver_view_information_cache = {}
1032
+ end
1033
+
1034
+ def column_definitions(table_name)
1035
+ db_name = unqualify_db_name(table_name)
1036
+ table_name = unqualify_table_name(table_name)
1037
+ sql = %{
1038
+ SELECT
1039
+ columns.TABLE_NAME as table_name,
1040
+ columns.COLUMN_NAME as name,
1041
+ columns.DATA_TYPE as type,
1042
+ columns.COLUMN_DEFAULT as default_value,
1043
+ columns.NUMERIC_SCALE as numeric_scale,
1044
+ columns.NUMERIC_PRECISION as numeric_precision,
1045
+ CASE
1046
+ WHEN columns.DATA_TYPE IN ('nchar','nvarchar') THEN columns.CHARACTER_MAXIMUM_LENGTH
1047
+ ELSE COL_LENGTH(columns.TABLE_NAME, columns.COLUMN_NAME)
1048
+ END as length,
1049
+ CASE
1050
+ WHEN columns.IS_NULLABLE = 'YES' THEN 1
1051
+ ELSE NULL
1052
+ end as is_nullable,
1053
+ CASE
1054
+ WHEN COLUMNPROPERTY(OBJECT_ID(columns.TABLE_NAME), columns.COLUMN_NAME, 'IsIdentity') = 0 THEN NULL
1055
+ ELSE 1
1056
+ END as is_identity
1057
+ FROM #{db_name}INFORMATION_SCHEMA.COLUMNS columns
1058
+ WHERE columns.TABLE_NAME = '#{table_name}'
1059
+ ORDER BY columns.ordinal_position
1060
+ }.gsub(/[ \t\r\n]+/,' ')
1061
+ results = info_schema_query { without_type_conversion{ select(sql,nil,true) } }
1062
+ results.collect do |ci|
1063
+ ci.symbolize_keys!
1064
+ ci[:type] = case ci[:type]
1065
+ when /^bit|image|text|ntext|datetime$/
1066
+ ci[:type]
1067
+ when /^numeric|decimal$/i
1068
+ "#{ci[:type]}(#{ci[:numeric_precision]},#{ci[:numeric_scale]})"
1069
+ when /^char|nchar|varchar|nvarchar|varbinary|bigint|int|smallint$/
1070
+ ci[:length].to_i == -1 ? "#{ci[:type]}(max)" : "#{ci[:type]}(#{ci[:length]})"
1071
+ else
1072
+ ci[:type]
1073
+ end
1074
+ if ci[:default_value].nil? && views.include?(table_name)
1075
+ real_table_name = table_name_or_views_table_name(table_name)
1076
+ real_column_name = views_real_column_name(table_name,ci[:name])
1077
+ 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}'"
1078
+ ci[:default_value] = info_schema_query { without_type_conversion{ select_value(col_default_sql) } }
1079
+ end
1080
+ ci[:default_value] = case ci[:default_value]
1081
+ when nil, '(null)', '(NULL)'
1082
+ nil
1083
+ else
1084
+ ci[:default_value].match(/\A\(+N?'?(.*?)'?\)+\Z/)[1]
1085
+ end
1086
+ ci[:null] = ci[:is_nullable].to_i == 1 ; ci.delete(:is_nullable)
1087
+ ci
1088
+ end
1089
+ end
1090
+
1091
+ def column_for(table_name, column_name)
1092
+ unless column = columns(table_name).detect { |c| c.name == column_name.to_s }
1093
+ raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
1094
+ end
1095
+ column
1096
+ end
1097
+
1098
+ def change_order_direction(order)
1099
+ order.split(",").collect {|fragment|
1100
+ case fragment
1101
+ when /\bDESC\b/i then fragment.gsub(/\bDESC\b/i, "ASC")
1102
+ when /\bASC\b/i then fragment.gsub(/\bASC\b/i, "DESC")
1103
+ else String.new(fragment).split(',').join(' DESC,') + ' DESC'
1104
+ end
1105
+ }.join(",")
1106
+ end
1107
+
1108
+ def special_columns(table_name)
1109
+ columns(table_name).select(&:is_special?).map(&:name)
1110
+ end
1111
+
1112
+ def repair_special_columns(sql)
1113
+ special_cols = special_columns(get_table_name(sql))
1114
+ for col in special_cols.to_a
1115
+ sql.gsub!(/((\.|\s|\()\[?#{col.to_s}\]?)\s?=\s?/, '\1 LIKE ')
1116
+ sql.gsub!(/ORDER BY #{col.to_s}/i, '')
1117
+ end
1118
+ sql
1119
+ end
1120
+
1121
+ end #class SQLServerAdapter < AbstractAdapter
1122
+
1123
+ end #module ConnectionAdapters
1124
+
1125
+ end #module ActiveRecord
1126
+