aq1018-sqlserver-2000-2008-adpater 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1,55 @@
1
+
2
+ MASTER
3
+
4
+ *
5
+
6
+
7
+ * 2.2.5 (January 4th, 2009)
8
+
9
+ * Added a log_info_schema_queries class attribute and make all queries to INFORMATION_SCHEMA silent by
10
+ default. [Ken Collins]
11
+
12
+ * Fix millisecond support in datetime columns. ODBC::Timestamp incorrectly takes SQL Server milliseconds
13
+ and applies them as nanoseconds. We cope with this at the DBI layer by using SQLServerDBI::Type::SqlserverTimestamp
14
+ class to parse the before type cast value appropriately. Also update the adapters #quoted_date method
15
+ to work more simply by converting ruby's #usec milliseconds to SQL Server microseconds. [Ken Collins]
16
+
17
+ * Core extensions for ActiveRecord now reflect on the connection before doing SQL Server things. Now
18
+ this adapter is compatible for using with other adapters. [Ken Collins]
19
+
20
+
21
+ * 2.2.4 (December 5th, 2008)
22
+
23
+ * Fix a type left in #views_real_column_name. Also cache #view_information lookups. [Ken Collins]
24
+
25
+
26
+ * 2.2.3 (December 5th, 2008)
27
+
28
+ * Changing back to using real table name in column_definitions. Makes sure views get back only the columns
29
+ that are defined for them with correct names, etc. Now supporting views by looking for NULL default and
30
+ then if table name is a view, perform a targeted with sub select to the real table name and column name
31
+ to find true default. [Ken Collins]
32
+
33
+ * Ensure that add_limit_offset! does not alter sub queries. [Erik Bryn]
34
+
35
+
36
+ 2.2.2 (December 2nd, 2008)
37
+
38
+ * Add support for view defaults by making column_definitions use real table name for schema info. [Ken Collins]
39
+
40
+ * Include version in connection method and inspection. [Ken Collins]
41
+
42
+
43
+ 2.2.1 (November 25th, 2008)
44
+
45
+ * Add identity insert support for views. Cache #views so that identity #table_name_or_views_table_name
46
+ will run quickly. [Ken Collins]
47
+
48
+ * Add views support. ActiveRecord classes can use views. The connection now has a #views method and
49
+ #table_exists? will not fall back to checking views too. [Ken Collins]
50
+
51
+
52
+ 2.2.0 (November 21st, 2008)
53
+
54
+ * Release for rails 2.2.2. Many many changes. [Ken Collins], [Murray Steele], [Shawn Balestracci], [Joe Rafaniello]
55
+
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008
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/README ADDED
File without changes
@@ -0,0 +1,60 @@
1
+ == Creating the test database
2
+
3
+ The default names for the test databases are "activerecord_unittest" and
4
+ "activerecord_unittest2". If you want to use another database name then be sure
5
+ to update the connection adapter setups you want to test with in
6
+ test/connections/<your database>/connection.rb.
7
+
8
+
9
+ == Requirements
10
+
11
+ The following gems need to be installed. Make sure you have gems.github.com as a
12
+ source. http://github.com/blog/97-github-loves-rubygems-1-2
13
+
14
+ * gem install thoughtbot-shoulda -s http://gems.github.com
15
+ * gem install mocha
16
+
17
+ The tests of this adapter depend on the existence of rails checkout. All the tests
18
+ defined by rails are re-used. For this to work the following directory structure
19
+ is assumed to exist:
20
+
21
+ #{RAILS_ROOT}/vendor/plugins/adapters/sqlserver
22
+ #{RAILS_ROOT}/vendor/rails/activerecord/test
23
+
24
+ Define a user named 'rails' in SQL Server with all privileges granted. Use an empty
25
+ password for user 'rails', or alternatively use the OSQLPASSWORD environment variable
26
+ which allows you to set a default password for the current session.
27
+
28
+ Then run "rake create_databases".
29
+
30
+
31
+ == Running with Rake
32
+
33
+ The easiest way to run the unit tests is through Rake. Either run "rake test_sqlserver"
34
+ or "rake test_sqlserver_odbc". For more information, checkout the full array
35
+ of rake tasks with "rake -T"
36
+
37
+ Rake can be found at http://rake.rubyforge.org
38
+
39
+
40
+ == Running with Autotest
41
+
42
+ Using autotest is easy, just run "autotest" and the tests will run continually in the
43
+ same order as the rake test command. By default autotest will use ODBC connection. If
44
+ you want to change this you can edit the autotest/sqlserver.rb file and set odbc_mode
45
+ to false.
46
+
47
+ Lastly, you can run autotest on just the adapter specific tests with "autotest sqlserver".
48
+ This will continuously run ONLY the SQL Sever specific behavior tests which are much
49
+ quicker to run than the entire active record test suite.
50
+
51
+
52
+ == Running by hand
53
+
54
+ Unit tests are located in test directory. If you only want to run a single test suite,
55
+ you can do so with:
56
+
57
+ rake test_sqlserver TEST=base_test.rb
58
+
59
+ That'll run the base suite using the SQLServer-Ruby adapter.
60
+
data/Rakefile ADDED
@@ -0,0 +1,44 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+
5
+
6
+ desc 'Create the SQL Server test databases'
7
+ task :create_databases do
8
+ # Define a user named 'rails' in SQL Server with all privileges granted
9
+ # Use an empty password for user 'rails', or alternatively use the OSQLPASSWORD environment variable
10
+ # which allows you to set a default password for the current session.
11
+ %x( osql -S 192.168.0.99 -U rails -Q "create database activerecord_unittest" -P )
12
+ %x( osql -S 192.168.0.99 -U rails -Q "create database activerecord_unittest2" -P )
13
+ %x( osql -S 192.168.0.99 -U rails -d activerecord_unittest -Q "exec sp_grantdbaccess 'rails'" -P )
14
+ %x( osql -S 192.168.0.99 -U rails -d activerecord_unittest2 -Q "exec sp_grantdbaccess 'rails'" -P )
15
+ %x( osql -S 192.168.0.99 -U rails -d activerecord_unittest -Q "grant BACKUP DATABASE, BACKUP LOG, CREATE DEFAULT, CREATE FUNCTION, CREATE PROCEDURE, CREATE RULE, CREATE TABLE, CREATE VIEW to 'rails';" -P )
16
+ %x( osql -S 192.168.0.99 -U rails -d activerecord_unittest2 -Q "grant BACKUP DATABASE, BACKUP LOG, CREATE DEFAULT, CREATE FUNCTION, CREATE PROCEDURE, CREATE RULE, CREATE TABLE, CREATE VIEW to 'rails';" -P )
17
+ end
18
+
19
+ desc 'Drop the SQL Server test databases'
20
+ task :drop_databases do
21
+ %x( osql -S 192.168.0.99 -U rails -Q "drop database activerecord_unittest" -P )
22
+ %x( osql -S 192.168.0.99 -U rails -Q "drop database activerecord_unittest2" -P )
23
+ end
24
+
25
+ desc 'Recreate the SQL Server test databases'
26
+ task :recreate_databases => [:drop_databases, :create_databases]
27
+
28
+
29
+ for adapter in %w( sqlserver sqlserver_odbc )
30
+ Rake::TestTask.new("test_#{adapter}") { |t|
31
+ t.libs << "test"
32
+ t.libs << "test/connections/native_#{adapter}"
33
+ t.libs << "../../rails/activerecord/test/"
34
+ t.test_files = (
35
+ Dir.glob("test/cases/**/*_test_sqlserver.rb").sort +
36
+ Dir.glob("../../rails/activerecord/test/**/*_test.rb").sort )
37
+ t.verbose = true
38
+ }
39
+
40
+ namespace adapter do
41
+ task :test => "test_#{adapter}"
42
+ end
43
+ end
44
+
@@ -0,0 +1,984 @@
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
+ conn = DBI.connect(driver_url, username, password)
27
+ conn["AutoCommit"] = true
28
+ ConnectionAdapters::SQLServerAdapter.new(conn, logger, [driver_url, username, password])
29
+ end
30
+
31
+ end
32
+
33
+ module ConnectionAdapters
34
+
35
+ class SQLServerColumn < Column
36
+
37
+ def initialize(name, default, sql_type = nil, null = true, sqlserver_options = {})
38
+ @sqlserver_options = sqlserver_options
39
+ super(name, default, sql_type, null)
40
+ end
41
+
42
+ class << self
43
+
44
+ def string_to_binary(value)
45
+ "0x#{value.unpack("H*")[0]}"
46
+ end
47
+
48
+ def binary_to_string(value)
49
+ value =~ /[^[:xdigit:]]/ ? value : [value].pack('H*')
50
+ end
51
+
52
+ end
53
+
54
+ def is_identity?
55
+ @sqlserver_options[:is_identity]
56
+ end
57
+
58
+ def is_special?
59
+ # TODO: Not sure if these should be added: varbinary(max), nchar, nvarchar(max)
60
+ sql_type =~ /^text|ntext|image$/
61
+ end
62
+
63
+ def is_utf8?
64
+ sql_type =~ /nvarchar|ntext|nchar/i
65
+ end
66
+
67
+ def table_name
68
+ @sqlserver_options[:table_name]
69
+ end
70
+
71
+ def table_klass
72
+ @table_klass ||= table_name.classify.constantize rescue nil
73
+ (@table_klass && @table_klass < ActiveRecord::Base) ? @table_klass : nil
74
+ end
75
+
76
+ private
77
+
78
+ def extract_limit(sql_type)
79
+ case sql_type
80
+ when /^smallint/i
81
+ 2
82
+ when /^int/i
83
+ 4
84
+ when /^bigint/i
85
+ 8
86
+ when /\(max\)/, /decimal/, /numeric/
87
+ nil
88
+ else
89
+ super
90
+ end
91
+ end
92
+
93
+ def simplified_type(field_type)
94
+ case field_type
95
+ when /real/i then :float
96
+ when /money/i then :decimal
97
+ when /image/i then :binary
98
+ when /bit/i then :boolean
99
+ when /uniqueidentifier/i then :string
100
+ when /datetime/i then simplified_datetime
101
+ else super
102
+ end
103
+ end
104
+
105
+ def simplified_datetime
106
+ if table_klass && table_klass.coerced_sqlserver_date_columns.include?(name)
107
+ :date
108
+ elsif table_klass && table_klass.coerced_sqlserver_time_columns.include?(name)
109
+ :time
110
+ else
111
+ :datetime
112
+ end
113
+ end
114
+
115
+ end #SQLServerColumn
116
+
117
+ # In ADO mode, this adapter will ONLY work on Windows systems, since it relies on
118
+ # Win32OLE, which, to my knowledge, is only available on Windows.
119
+ #
120
+ # This mode also relies on the ADO support in the DBI module. If you are using the
121
+ # one-click installer of Ruby, then you already have DBI installed, but the ADO module
122
+ # is *NOT* installed. You will need to get the latest source distribution of Ruby-DBI
123
+ # from http://ruby-dbi.sourceforge.net/ unzip it, and copy the file from
124
+ # <tt>src/lib/dbd_ado/ADO.rb</tt> to <tt>X:/Ruby/lib/ruby/site_ruby/1.8/DBD/ADO/ADO.rb</tt>
125
+ #
126
+ # You will more than likely need to create the ADO directory. Once you've installed
127
+ # that file, you are ready to go.
128
+ #
129
+ # In ODBC mode, the adapter requires the ODBC support in the DBI module which requires
130
+ # the Ruby ODBC module. Ruby ODBC 0.996 was used in development and testing,
131
+ # and it is available at http://www.ch-werner.de/rubyodbc/
132
+ #
133
+ # Options:
134
+ #
135
+ # * <tt>:mode</tt> -- ADO or ODBC. Defaults to ADO.
136
+ # * <tt>:username</tt> -- Defaults to sa.
137
+ # * <tt>:password</tt> -- Defaults to empty string.
138
+ # * <tt>:windows_auth</tt> -- Defaults to "User ID=#{username};Password=#{password}"
139
+ #
140
+ # ADO specific options:
141
+ #
142
+ # * <tt>:host</tt> -- Defaults to localhost.
143
+ # * <tt>:database</tt> -- The name of the database. No default, must be provided.
144
+ # * <tt>:windows_auth</tt> -- Use windows authentication instead of username/password.
145
+ #
146
+ # ODBC specific options:
147
+ #
148
+ # * <tt>:dsn</tt> -- Defaults to nothing.
149
+ #
150
+ class SQLServerAdapter < AbstractAdapter
151
+
152
+ ADAPTER_NAME = 'SQLServer'.freeze
153
+ VERSION = '2.2.5'.freeze
154
+ DATABASE_VERSION_REGEXP = /Microsoft SQL Server\s+(\d{4})/
155
+ SUPPORTED_VERSIONS = [2000,2005,2008].freeze
156
+ LIMITABLE_TYPES = ['string','integer','float','char','nchar','varchar','nvarchar'].freeze
157
+
158
+ cattr_accessor :native_text_database_type, :native_binary_database_type, :log_info_schema_queries
159
+
160
+ class << self
161
+
162
+ def type_limitable?(type)
163
+ LIMITABLE_TYPES.include?(type.to_s)
164
+ end
165
+
166
+ end
167
+
168
+ def initialize(connection, logger, connection_options=nil)
169
+ super(connection, logger)
170
+ @connection_options = connection_options
171
+ initialize_sqlserver_caches
172
+ unless SUPPORTED_VERSIONS.include?(database_year)
173
+ raise NotImplementedError, "Currently, only #{SUPPORTED_VERSIONS.to_sentence} are supported."
174
+ end
175
+ end
176
+
177
+ # ABSTRACT ADAPTER =========================================#
178
+
179
+ def adapter_name
180
+ ADAPTER_NAME
181
+ end
182
+
183
+ def supports_migrations?
184
+ true
185
+ end
186
+
187
+ def supports_ddl_transactions?
188
+ true
189
+ end
190
+
191
+ def database_version
192
+ @database_version ||= info_schema_query { select_value('SELECT @@version') }
193
+ end
194
+
195
+ def database_year
196
+ DATABASE_VERSION_REGEXP.match(database_version)[1].to_i
197
+ end
198
+
199
+ def sqlserver?
200
+ true
201
+ end
202
+
203
+ def sqlserver_2000?
204
+ database_year == 2000
205
+ end
206
+
207
+ def sqlserver_2005?
208
+ database_year == 2005
209
+ end
210
+
211
+ def sqlserver_2008?
212
+ database_year == 2008
213
+ end
214
+
215
+ def version
216
+ self.class::VERSION
217
+ end
218
+
219
+ def inspect
220
+ "#<#{self.class} version: #{version}, year: #{database_year}, connection_options: #{@connection_options.inspect}>"
221
+ end
222
+
223
+ def native_text_database_type
224
+ self.class.native_text_database_type || ((sqlserver_2005? || sqlserver_2008?) ? 'varchar(max)' : 'text')
225
+ end
226
+
227
+ def native_binary_database_type
228
+ self.class.native_binary_database_type || ((sqlserver_2005? || sqlserver_2008?) ? 'varbinary(max)' : 'image')
229
+ end
230
+
231
+ # QUOTING ==================================================#
232
+
233
+ def quote(value, column = nil)
234
+ case value
235
+ when String, ActiveSupport::Multibyte::Chars
236
+ if column && column.type == :binary
237
+ column.class.string_to_binary(value)
238
+ elsif column && column.respond_to?(:is_utf8?) && column.is_utf8?
239
+ quoted_utf8_value(value)
240
+ else
241
+ super
242
+ end
243
+ else
244
+ super
245
+ end
246
+ end
247
+
248
+ def quote_string(string)
249
+ string.to_s.gsub(/\'/, "''")
250
+ end
251
+
252
+ def quote_column_name(column_name)
253
+ column_name.to_s.split('.').map{ |name| "[#{name}]" }.join('.')
254
+ end
255
+
256
+ def quote_table_name(table_name)
257
+ return table_name if table_name =~ /^\[.*\]$/
258
+ quote_column_name(table_name)
259
+ end
260
+
261
+ def quoted_true
262
+ '1'
263
+ end
264
+
265
+ def quoted_false
266
+ '0'
267
+ end
268
+
269
+ def quoted_date(value)
270
+ if value.acts_like?(:time) && value.respond_to?(:usec)
271
+ "#{super}.#{sprintf("%03d",value.usec/1000)}"
272
+ else
273
+ super
274
+ end
275
+ end
276
+
277
+ def quoted_utf8_value(value)
278
+ "N'#{quote_string(value)}'"
279
+ end
280
+
281
+ # REFERENTIAL INTEGRITY ====================================#
282
+
283
+ def disable_referential_integrity(&block)
284
+ do_execute "EXEC sp_MSForEachTable 'ALTER TABLE ? NOCHECK CONSTRAINT ALL'"
285
+ yield
286
+ ensure
287
+ do_execute "EXEC sp_MSForEachTable 'ALTER TABLE ? CHECK CONSTRAINT ALL'"
288
+ end
289
+
290
+ # CONNECTION MANAGEMENT ====================================#
291
+
292
+ def active?
293
+ raw_connection.execute("SELECT 1").finish
294
+ true
295
+ rescue DBI::DatabaseError, DBI::InterfaceError
296
+ false
297
+ end
298
+
299
+ def reconnect!
300
+ disconnect!
301
+ @connection = DBI.connect(*@connection_options)
302
+ rescue DBI::DatabaseError => e
303
+ @logger.warn "#{adapter_name} reconnection failed: #{e.message}" if @logger
304
+ false
305
+ end
306
+
307
+ def disconnect!
308
+ raw_connection.disconnect rescue nil
309
+ end
310
+
311
+ def finish_statement_handle(handle)
312
+ handle.finish if handle && handle.respond_to?(:finish) && !handle.finished?
313
+ handle
314
+ end
315
+
316
+ # DATABASE STATEMENTS ======================================#
317
+
318
+ def select_rows(sql, name = nil)
319
+ raw_select(sql,name).last
320
+ end
321
+
322
+ def execute(sql, name = nil, &block)
323
+ if table_name = query_requires_identity_insert?(sql)
324
+ handle = with_identity_insert_enabled(table_name) { raw_execute(sql,name,&block) }
325
+ else
326
+ handle = raw_execute(sql,name,&block)
327
+ end
328
+ finish_statement_handle(handle)
329
+ end
330
+
331
+ def begin_db_transaction
332
+ do_execute "BEGIN TRANSACTION"
333
+ end
334
+
335
+ def commit_db_transaction
336
+ do_execute "COMMIT TRANSACTION"
337
+ end
338
+
339
+ def rollback_db_transaction
340
+ do_execute "ROLLBACK TRANSACTION" rescue nil
341
+ end
342
+
343
+ def add_limit_offset!(sql, options)
344
+ # Validate and/or convert integers for :limit and :offets options.
345
+ if options[:offset]
346
+ raise ArgumentError, "offset should have a limit" unless options[:limit]
347
+ unless options[:offset].kind_of?(Integer)
348
+ if options[:offset] =~ /^\d+$/
349
+ options[:offset] = options[:offset].to_i
350
+ else
351
+ raise ArgumentError, "offset should be an integer"
352
+ end
353
+ end
354
+ end
355
+
356
+
357
+ if options[:limit] && !(options[:limit].kind_of?(Integer))
358
+ if options[:limit] =~ /^\d+$/
359
+ options[:limit] = options[:limit].to_i
360
+ else
361
+ raise ArgumentError, "limit should be an integer"
362
+ end
363
+ end
364
+
365
+
366
+ # The business of adding limit/offset
367
+ if options[:limit] and options[:offset]
368
+
369
+ # adjust :limit to accommodate edge cases where there is not enough rows to reach the limit.
370
+ # Do we even need this??
371
+
372
+ total_rows = select_value("SELECT count(*) as TotalRows from (#{sql.sub(/\bSELECT(\s+DISTINCT)?\b/i, "SELECT#{$1} TOP 1000000000")}) tally").to_i
373
+ if (options[:limit] + options[:offset]) >= total_rows
374
+ options[:limit] = (total_rows - options[:offset] >= 0) ? (total_rows - options[:offset]) : 0
375
+ end
376
+
377
+ # Make sure we do not need a special limit/offset for association limiting. http://gist.github.com/25118
378
+ add_limit_offset_for_association_limiting!(sql,options) and return if sql_for_association_limiting?(sql)
379
+
380
+ # Wrap the SQL query in a bunch of outer SQL queries that emulate proper LIMIT,OFFSET support.
381
+ #
382
+ sql.sub!(/^\s*SELECT(\s+DISTINCT)?/i, "SELECT * FROM (SELECT TOP #{options[:limit]} * FROM (SELECT#{$1} TOP #{options[:limit] + options[:offset]}")
383
+
384
+
385
+ sql << ") AS tmp1"
386
+
387
+ if options[:order]
388
+ order = options[:order].split(',').map do |field|
389
+ order_by_column, order_direction = field.split(" ")
390
+ order_by_column = quote_column_name(order_by_column)
391
+ # Investigate the SQL query to figure out if the order_by_column has been renamed.
392
+ if sql =~ /#{Regexp.escape(order_by_column)} AS (t\d_r\d\d?)/
393
+ # Fx "[foo].[bar] AS t4_r2" was found in the SQL. Use the column alias (ie 't4_r2') for the subsequent orderings
394
+ order_by_column = $1
395
+ elsif order_by_column =~ /\w+\.\[?(\w+)\]?/
396
+ order_by_column = $1
397
+ else
398
+ # It doesn't appear that the column name has been renamed as part of the query. Use just the column
399
+ # name rather than the full identifier for the outer queries.
400
+ order_by_column = order_by_column.split('.').last
401
+ end
402
+ # Put the column name and eventual direction back together
403
+ [order_by_column, order_direction].join(' ').strip
404
+ end.join(', ')
405
+ sql << " ORDER BY #{change_order_direction(order)}) AS tmp2 ORDER BY #{order}"
406
+ else
407
+ sql << ") AS tmp2"
408
+ end
409
+ elsif options[:limit] && sql !~ /^\s*SELECT (@@|COUNT\()/i
410
+ if md = sql.match(/^(\s*SELECT)(\s+DISTINCT)?(.*)/im)
411
+ sql.replace "#{md[1]}#{md[2]} TOP #{options[:limit]}#{md[3]}"
412
+ else
413
+ # Account for building SQL fragments without SELECT yet. See #update_all and #limited_update_conditions.
414
+ sql.replace "TOP #{options[:limit]} #{sql}"
415
+ end
416
+ end
417
+ end
418
+
419
+ def add_lock!(sql, options)
420
+ # http://blog.sqlauthority.com/2007/04/27/sql-server-2005-locking-hints-and-examples/
421
+ return unless options[:lock]
422
+ lock_type = options[:lock] == true ? 'WITH(HOLDLOCK, ROWLOCK)' : options[:lock]
423
+ from_table = sql.match(/FROM(.*)WHERE/im)[1]
424
+ sql.sub! from_table, "#{from_table}#{lock_type} "
425
+ end
426
+
427
+ def empty_insert_statement(table_name)
428
+ "INSERT INTO #{quote_table_name(table_name)} DEFAULT VALUES"
429
+ end
430
+
431
+ def case_sensitive_equality_operator
432
+ "COLLATE Latin1_General_CS_AS ="
433
+ end
434
+
435
+ def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
436
+ match_data = where_sql.match(/(.*)WHERE/)
437
+ limit = match_data[1]
438
+ where_sql.sub!(limit,'')
439
+ "WHERE #{quoted_primary_key} IN (SELECT #{limit} #{quoted_primary_key} FROM #{quoted_table_name} #{where_sql})"
440
+ end
441
+
442
+ # SCHEMA STATEMENTS ========================================#
443
+
444
+ def native_database_types
445
+ {
446
+ :primary_key => "int NOT NULL IDENTITY(1, 1) PRIMARY KEY",
447
+ :string => { :name => "varchar", :limit => 255 },
448
+ :text => { :name => native_text_database_type },
449
+ :integer => { :name => "int", :limit => 4 },
450
+ :float => { :name => "float", :limit => 8 },
451
+ :decimal => { :name => "decimal" },
452
+ :datetime => { :name => "datetime" },
453
+ :timestamp => { :name => "datetime" },
454
+ :time => { :name => "datetime" },
455
+ :date => { :name => "datetime" },
456
+ :binary => { :name => native_binary_database_type },
457
+ :boolean => { :name => "bit"},
458
+ # These are custom types that may move somewhere else for good schema_dumper.rb hacking to output them.
459
+ :char => { :name => 'char' },
460
+ :varchar_max => { :name => 'varchar(max)' },
461
+ :nchar => { :name => "nchar" },
462
+ :nvarchar => { :name => "nvarchar", :limit => 255 },
463
+ :nvarchar_max => { :name => "nvarchar(max)" },
464
+ :ntext => { :name => "ntext" }
465
+ }
466
+ end
467
+
468
+ def table_alias_length
469
+ 128
470
+ end
471
+
472
+ def tables(name = nil)
473
+ info_schema_query do
474
+ select_values "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME <> 'dtproperties'"
475
+ end
476
+ end
477
+
478
+ def views(name = nil)
479
+ @sqlserver_views_cache ||=
480
+ info_schema_query { select_values("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_NAME NOT IN ('sysconstraints','syssegments')") }
481
+ end
482
+
483
+ def view_information(table_name)
484
+ table_name = unqualify_table_name(table_name)
485
+ @sqlserver_view_information_cache[table_name] ||=
486
+ info_schema_query { select_one("SELECT * FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_NAME = '#{table_name}'") }
487
+ end
488
+
489
+ def view_table_name(table_name)
490
+ view_info = view_information(table_name)
491
+ view_info ? get_table_name(view_info['VIEW_DEFINITION']) : table_name
492
+ end
493
+
494
+ def table_exists?(table_name)
495
+ super || views.include?(table_name.to_s)
496
+ end
497
+
498
+ def indexes(table_name, name = nil)
499
+ select("EXEC sp_helpindex #{quote_table_name(table_name)}",name).inject([]) do |indexes,index|
500
+ if index['index_description'] =~ /primary key/
501
+ indexes
502
+ else
503
+ name = index['index_name']
504
+ unique = index['index_description'] =~ /unique/
505
+ columns = index['index_keys'].split(',').map do |column|
506
+ column.strip!
507
+ column.gsub! '(-)', '' if column.ends_with?('(-)')
508
+ column
509
+ end
510
+ indexes << IndexDefinition.new(table_name, name, unique, columns)
511
+ end
512
+ end
513
+ end
514
+
515
+ def columns(table_name, name = nil)
516
+ return [] if table_name.blank?
517
+ cache_key = unqualify_table_name(table_name)
518
+ @sqlserver_columns_cache[cache_key] ||= column_definitions(table_name).collect do |ci|
519
+ sqlserver_options = ci.except(:name,:default_value,:type,:null)
520
+ SQLServerColumn.new ci[:name], ci[:default_value], ci[:type], ci[:null], sqlserver_options
521
+ end
522
+ end
523
+
524
+ def create_table(table_name, options = {})
525
+ super
526
+ remove_sqlserver_columns_cache_for(table_name)
527
+ end
528
+
529
+ def rename_table(table_name, new_name)
530
+ do_execute "EXEC sp_rename '#{table_name}', '#{new_name}'"
531
+ end
532
+
533
+ def drop_table(table_name, options = {})
534
+ super
535
+ remove_sqlserver_columns_cache_for(table_name)
536
+ end
537
+
538
+ def add_column(table_name, column_name, type, options = {})
539
+ super
540
+ remove_sqlserver_columns_cache_for(table_name)
541
+ end
542
+
543
+ def remove_column(table_name, *column_names)
544
+ column_names.flatten.each do |column_name|
545
+ remove_check_constraints(table_name, column_name)
546
+ remove_default_constraint(table_name, column_name)
547
+ remove_indexes(table_name, column_name)
548
+ do_execute "ALTER TABLE #{quote_table_name(table_name)} DROP COLUMN #{quote_column_name(column_name)}"
549
+ end
550
+ remove_sqlserver_columns_cache_for(table_name)
551
+ end
552
+
553
+ def change_column(table_name, column_name, type, options = {})
554
+ sql_commands = []
555
+ 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])}"
556
+ change_column_sql << " NOT NULL" if options[:null] == false
557
+ sql_commands << change_column_sql
558
+ if options_include_default?(options)
559
+ remove_default_constraint(table_name, column_name)
560
+ 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)}"
561
+ end
562
+ sql_commands.each { |c| do_execute(c) }
563
+ remove_sqlserver_columns_cache_for(table_name)
564
+ end
565
+
566
+ def change_column_default(table_name, column_name, default)
567
+ remove_default_constraint(table_name, column_name)
568
+ 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)}"
569
+ remove_sqlserver_columns_cache_for(table_name)
570
+ end
571
+
572
+ def rename_column(table_name, column_name, new_column_name)
573
+ column_for(table_name,column_name)
574
+ do_execute "EXEC sp_rename '#{table_name}.#{column_name}', '#{new_column_name}', 'COLUMN'"
575
+ remove_sqlserver_columns_cache_for(table_name)
576
+ end
577
+
578
+ def remove_index(table_name, options = {})
579
+ do_execute "DROP INDEX #{table_name}.#{quote_column_name(index_name(table_name, options))}"
580
+ end
581
+
582
+ def type_to_sql(type, limit = nil, precision = nil, scale = nil)
583
+ limit = nil unless self.class.type_limitable?(type)
584
+ case type.to_s
585
+ when 'integer'
586
+ case limit
587
+ when 1..2 then 'smallint'
588
+ when 3..4, nil then 'integer'
589
+ when 5..8 then 'bigint'
590
+ else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
591
+ end
592
+ else
593
+ super
594
+ end
595
+ end
596
+
597
+ def add_order_by_for_association_limiting!(sql, options)
598
+ # Disertation http://gist.github.com/24073
599
+ # Information http://weblogs.sqlteam.com/jeffs/archive/2007/12/13/select-distinct-order-by-error.aspx
600
+ return sql if options[:order].blank?
601
+ columns = sql.match(/SELECT\s+DISTINCT(.*)FROM/)[1].strip
602
+ sql.sub!(/SELECT\s+DISTINCT/,'SELECT')
603
+ sql << "GROUP BY #{columns} ORDER BY #{order_to_min_set(options[:order])}"
604
+ end
605
+
606
+ def change_column_null(table_name, column_name, null, default = nil)
607
+ column = column_for(table_name,column_name)
608
+ unless null || default.nil?
609
+ do_execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
610
+ end
611
+ sql = "ALTER TABLE #{table_name} ALTER COLUMN #{quote_column_name(column_name)} #{type_to_sql column.type, column.limit, column.precision, column.scale}"
612
+ sql << " NOT NULL" unless null
613
+ do_execute sql
614
+ end
615
+
616
+ def pk_and_sequence_for(table_name)
617
+ idcol = identity_column(table_name)
618
+ idcol ? [idcol.name,nil] : nil
619
+ end
620
+
621
+ # RAKE UTILITY METHODS =====================================#
622
+
623
+ def recreate_database(name)
624
+ existing_database = current_database.to_s
625
+ if name.to_s == existing_database
626
+ do_execute 'USE master'
627
+ end
628
+ drop_database(name)
629
+ create_database(name)
630
+ ensure
631
+ do_execute "USE #{existing_database}" if name.to_s == existing_database
632
+ end
633
+
634
+ def drop_database(name)
635
+ retry_count = 0
636
+ max_retries = 1
637
+ begin
638
+ do_execute "DROP DATABASE #{name}"
639
+ rescue ActiveRecord::StatementInvalid => err
640
+ # Remove existing connections and rollback any transactions if we received the message
641
+ # 'Cannot drop the database 'test' because it is currently in use'
642
+ if err.message =~ /because it is currently in use/
643
+ raise if retry_count >= max_retries
644
+ retry_count += 1
645
+ remove_database_connections_and_rollback(name)
646
+ retry
647
+ else
648
+ raise
649
+ end
650
+ end
651
+ end
652
+
653
+ def create_database(name)
654
+ do_execute "CREATE DATABASE #{name}"
655
+ end
656
+
657
+ def current_database
658
+ select_value 'SELECT DB_NAME()'
659
+ end
660
+
661
+ def remove_database_connections_and_rollback(name)
662
+ # This should disconnect all other users and rollback any transactions for SQL 2000 and 2005
663
+ # http://sqlserver2000.databases.aspfaq.com/how-do-i-drop-a-sql-server-database.html
664
+ do_execute "ALTER DATABASE #{name} SET SINGLE_USER WITH ROLLBACK IMMEDIATE"
665
+ end
666
+
667
+
668
+
669
+ protected
670
+
671
+ # DATABASE STATEMENTS ======================================
672
+
673
+ def select(sql, name = nil, ignore_special_columns = false)
674
+ repair_special_columns(sql) unless ignore_special_columns
675
+ fields, rows = raw_select(sql,name)
676
+ rows.inject([]) do |results,row|
677
+ row_hash = {}
678
+ fields.each_with_index do |f, i|
679
+ row_hash[f] = row[i]
680
+ end
681
+ results << row_hash
682
+ end
683
+ end
684
+
685
+ def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
686
+ super || select_value("SELECT SCOPE_IDENTITY() AS Ident")
687
+ end
688
+
689
+ def update_sql(sql, name = nil)
690
+ execute(sql, name)
691
+ select_value('SELECT @@ROWCOUNT AS AffectedRows')
692
+ end
693
+
694
+ def info_schema_query
695
+ log_info_schema_queries ? yield : ActiveRecord::Base.silence{ yield }
696
+ end
697
+
698
+ def raw_execute(sql, name = nil, &block)
699
+ log(sql, name) do
700
+ if block_given?
701
+ raw_connection.execute(sql) { |handle| yield(handle) }
702
+ else
703
+ raw_connection.execute(sql)
704
+ end
705
+ end
706
+ end
707
+
708
+ def without_type_conversion
709
+ raw_connection.convert_types = false if raw_connection.respond_to?(:convert_types=)
710
+ yield
711
+ ensure
712
+ raw_connection.convert_types = true if raw_connection.respond_to?(:convert_types=)
713
+ end
714
+
715
+ def do_execute(sql,name=nil)
716
+ log(sql, name || 'EXECUTE') do
717
+ raw_connection.do(sql)
718
+ end
719
+ end
720
+
721
+ def raw_select(sql, name = nil)
722
+ handle = raw_execute(sql,name)
723
+ fields = handle.column_names
724
+ results = handle_as_array(handle)
725
+ rows = results.inject([]) do |rows,row|
726
+ row.each_with_index do |value, i|
727
+ # DEPRECATED in DBI 0.4.0 and above. Remove when 0.2.2 and lower is no longer supported.
728
+ if value.is_a? DBI::Timestamp
729
+ row[i] = value.to_sqlserver_string
730
+ end
731
+ end
732
+ rows << row
733
+ end
734
+ return fields, rows
735
+ end
736
+
737
+ def handle_as_array(handle)
738
+ array = handle.inject([]) do |rows,row|
739
+ rows << row.inject([]){ |values,value| values << value }
740
+ end
741
+ finish_statement_handle(handle)
742
+ array
743
+ end
744
+
745
+ def add_limit_offset_for_association_limiting!(sql, options)
746
+ sql.replace %|
747
+ SET NOCOUNT ON
748
+ DECLARE @row_number TABLE (row int identity(1,1), id int)
749
+ INSERT INTO @row_number (id)
750
+ #{sql}
751
+ SET NOCOUNT OFF
752
+ SELECT id FROM (
753
+ SELECT TOP #{options[:limit]} * FROM (
754
+ SELECT TOP #{options[:limit] + options[:offset]} * FROM @row_number ORDER BY row
755
+ ) AS tmp1 ORDER BY row DESC
756
+ ) AS tmp2 ORDER BY row
757
+ |.gsub(/[ \t\r\n]+/,' ')
758
+ end
759
+
760
+ # SCHEMA STATEMENTS ========================================#
761
+
762
+ def remove_check_constraints(table_name, column_name)
763
+ 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)}'") }
764
+ constraints.each do |constraint|
765
+ do_execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{quote_column_name(constraint)}"
766
+ end
767
+ end
768
+
769
+ def remove_default_constraint(table_name, column_name)
770
+ 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")
771
+ constraints.each do |constraint|
772
+ do_execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{quote_column_name(constraint)}"
773
+ end
774
+ end
775
+
776
+ def remove_indexes(table_name, column_name)
777
+ indexes(table_name).select{ |index| index.columns.include?(column_name.to_s) }.each do |index|
778
+ remove_index(table_name, {:name => index.name})
779
+ end
780
+ end
781
+
782
+ def default_name(table_name, column_name)
783
+ "DF_#{table_name}_#{column_name}"
784
+ end
785
+
786
+ # IDENTITY INSERTS =========================================#
787
+
788
+ def with_identity_insert_enabled(table_name, &block)
789
+ table_name = quote_table_name(table_name_or_views_table_name(table_name))
790
+ set_identity_insert(table_name, true)
791
+ yield
792
+ ensure
793
+ set_identity_insert(table_name, false)
794
+ end
795
+
796
+ def set_identity_insert(table_name, enable = true)
797
+ sql = "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}"
798
+ do_execute(sql,'IDENTITY_INSERT')
799
+ rescue Exception => e
800
+ raise ActiveRecordError, "IDENTITY_INSERT could not be turned #{enable ? 'ON' : 'OFF'} for table #{table_name}"
801
+ end
802
+
803
+ def query_requires_identity_insert?(sql)
804
+ if insert_sql?(sql)
805
+ table_name = get_table_name(sql)
806
+ id_column = identity_column(table_name)
807
+ id_column && sql =~ /INSERT[^(]+\([^)]*\[#{id_column.name}\][^)]*\)/i ? table_name : false
808
+ else
809
+ false
810
+ end
811
+ end
812
+
813
+ def identity_column(table_name)
814
+ columns(table_name).detect(&:is_identity?)
815
+ end
816
+
817
+ def table_name_or_views_table_name(table_name)
818
+ unquoted_table_name = unqualify_table_name(table_name)
819
+ views.include?(unquoted_table_name) ? view_table_name(unquoted_table_name) : unquoted_table_name
820
+ end
821
+
822
+ # HELPER METHODS ===========================================#
823
+
824
+ def insert_sql?(sql)
825
+ !(sql =~ /^\s*INSERT/i).nil?
826
+ end
827
+
828
+ def unqualify_table_name(table_name)
829
+ table_name.to_s.split('.').last.gsub(/[\[\]]/,'')
830
+ end
831
+
832
+ def unqualify_db_name(table_name)
833
+ table_names = table_name.to_s.split('.')
834
+ table_names.length == 3 ? table_names.first.tr('[]','') : nil
835
+ end
836
+
837
+ def get_table_name(sql)
838
+ if sql =~ /^\s*insert\s+into\s+([^\(\s]+)\s*|^\s*update\s+([^\(\s]+)\s*/i
839
+ $1 || $2
840
+ elsif sql =~ /from\s+([^\(\s]+)\s*/i
841
+ $1
842
+ else
843
+ nil
844
+ end
845
+ end
846
+
847
+ def orders_and_dirs_set(order)
848
+ orders = order.sub('ORDER BY','').split(',').map(&:strip).reject(&:blank?)
849
+ orders_dirs = orders.map do |ord|
850
+ dir = nil
851
+ if match_data = ord.match(/\b(asc|desc)$/i)
852
+ dir = match_data[1]
853
+ ord.sub!(dir,'').strip!
854
+ dir.upcase!
855
+ end
856
+ [ord,dir]
857
+ end
858
+ end
859
+
860
+ def views_real_column_name(table_name,column_name)
861
+ view_definition = view_information(table_name)['VIEW_DEFINITION']
862
+ match_data = view_definition.match(/([\w-]*)\s+as\s+#{column_name}/im)
863
+ match_data ? match_data[1] : column_name
864
+ end
865
+
866
+ def order_to_min_set(order)
867
+ orders_dirs = orders_and_dirs_set(order)
868
+ orders_dirs.map do |o,d|
869
+ "MIN(#{o}) #{d}".strip
870
+ end.join(', ')
871
+ end
872
+
873
+ def sql_for_association_limiting?(sql)
874
+ if md = sql.match(/^\s*SELECT(.*)FROM.*GROUP BY.*ORDER BY.*/im)
875
+ select_froms = md[1].split(',')
876
+ select_froms.size == 1 && !select_froms.first.include?('*')
877
+ end
878
+ end
879
+
880
+ def remove_sqlserver_columns_cache_for(table_name)
881
+ cache_key = unqualify_table_name(table_name)
882
+ @sqlserver_columns_cache[cache_key] = nil
883
+ initialize_sqlserver_caches(false)
884
+ end
885
+
886
+ def initialize_sqlserver_caches(reset_columns=true)
887
+ @sqlserver_columns_cache = {} if reset_columns
888
+ @sqlserver_views_cache = nil
889
+ @sqlserver_view_information_cache = {}
890
+ end
891
+
892
+ def column_definitions(table_name)
893
+ db_name = unqualify_db_name(table_name)
894
+ table_name = unqualify_table_name(table_name)
895
+ sql = %{
896
+ SELECT
897
+ columns.TABLE_NAME as table_name,
898
+ columns.COLUMN_NAME as name,
899
+ columns.DATA_TYPE as type,
900
+ columns.COLUMN_DEFAULT as default_value,
901
+ columns.NUMERIC_SCALE as numeric_scale,
902
+ columns.NUMERIC_PRECISION as numeric_precision,
903
+ CASE
904
+ WHEN columns.DATA_TYPE IN ('nchar','nvarchar') THEN columns.CHARACTER_MAXIMUM_LENGTH
905
+ ELSE COL_LENGTH(columns.TABLE_NAME, columns.COLUMN_NAME)
906
+ END as length,
907
+ CASE
908
+ WHEN columns.IS_NULLABLE = 'YES' THEN 1
909
+ ELSE NULL
910
+ end as is_nullable,
911
+ CASE
912
+ WHEN COLUMNPROPERTY(OBJECT_ID(columns.TABLE_NAME), columns.COLUMN_NAME, 'IsIdentity') = 0 THEN NULL
913
+ ELSE 1
914
+ END as is_identity
915
+ FROM #{db_name}INFORMATION_SCHEMA.COLUMNS columns
916
+ WHERE columns.TABLE_NAME = '#{table_name}'
917
+ ORDER BY columns.ordinal_position
918
+ }.gsub(/[ \t\r\n]+/,' ')
919
+ results = info_schema_query { without_type_conversion{ select(sql,nil,true) } }
920
+ results.collect do |ci|
921
+ ci.symbolize_keys!
922
+ ci[:type] = case ci[:type]
923
+ when /^bit|image|text|ntext|datetime$/
924
+ ci[:type]
925
+ when /^numeric|decimal$/i
926
+ "#{ci[:type]}(#{ci[:numeric_precision]},#{ci[:numeric_scale]})"
927
+ when /^char|nchar|varchar|nvarchar|varbinary|bigint|int|smallint$/
928
+ ci[:length].to_i == -1 ? "#{ci[:type]}(max)" : "#{ci[:type]}(#{ci[:length]})"
929
+ else
930
+ ci[:type]
931
+ end
932
+ if ci[:default_value].nil? && views.include?(table_name)
933
+ real_table_name = table_name_or_views_table_name(table_name)
934
+ real_column_name = views_real_column_name(table_name,ci[:name])
935
+ 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}'"
936
+ ci[:default_value] = info_schema_query { without_type_conversion{ select_value(col_default_sql) } }
937
+ end
938
+ ci[:default_value] = case ci[:default_value]
939
+ when nil, '(null)', '(NULL)'
940
+ nil
941
+ else
942
+ ci[:default_value].match(/\A\(+N?'?(.*?)'?\)+\Z/)[1]
943
+ end
944
+ ci[:null] = ci[:is_nullable].to_i == 1 ; ci.delete(:is_nullable)
945
+ ci
946
+ end
947
+ end
948
+
949
+ def column_for(table_name, column_name)
950
+ unless column = columns(table_name).detect { |c| c.name == column_name.to_s }
951
+ raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
952
+ end
953
+ column
954
+ end
955
+
956
+ def change_order_direction(order)
957
+ order.split(",").collect {|fragment|
958
+ case fragment
959
+ when /\bDESC\b/i then fragment.gsub(/\bDESC\b/i, "ASC")
960
+ when /\bASC\b/i then fragment.gsub(/\bASC\b/i, "DESC")
961
+ else String.new(fragment).split(',').join(' DESC,') + ' DESC'
962
+ end
963
+ }.join(",")
964
+ end
965
+
966
+ def special_columns(table_name)
967
+ columns(table_name).select(&:is_special?).map(&:name)
968
+ end
969
+
970
+ def repair_special_columns(sql)
971
+ special_cols = special_columns(get_table_name(sql))
972
+ for col in special_cols.to_a
973
+ sql.gsub!(/((\.|\s|\()\[?#{col.to_s}\]?)\s?=\s?/, '\1 LIKE ')
974
+ sql.gsub!(/ORDER BY #{col.to_s}/i, '')
975
+ end
976
+ sql
977
+ end
978
+
979
+ end #class SQLServerAdapter < AbstractAdapter
980
+
981
+ end #module ConnectionAdapters
982
+
983
+ end #module ActiveRecord
984
+