aq1018-sqlserver-2000-2008-adpater 0.0.2

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