activerecord-sqlserver-adapter 2.2.18

Sign up to get free protection for your applications and to get access to all the features.
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
+