rainux-2000-2005-adapter 2.2.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. data/CHANGELOG +142 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.rdoc +157 -0
  4. data/RUNNING_UNIT_TESTS +60 -0
  5. data/Rakefile +52 -0
  6. data/autotest/discover.rb +4 -0
  7. data/autotest/railssqlserver.rb +16 -0
  8. data/autotest/sqlserver.rb +54 -0
  9. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +1017 -0
  10. data/lib/core_ext/active_record.rb +150 -0
  11. data/lib/core_ext/dbi.rb +85 -0
  12. data/lib/rails-sqlserver-2000-2005-adapter.rb +1 -0
  13. data/test/cases/aaaa_create_tables_test_sqlserver.rb +19 -0
  14. data/test/cases/adapter_test_sqlserver.rb +627 -0
  15. data/test/cases/attribute_methods_test_sqlserver.rb +33 -0
  16. data/test/cases/basics_test_sqlserver.rb +21 -0
  17. data/test/cases/calculations_test_sqlserver.rb +20 -0
  18. data/test/cases/column_test_sqlserver.rb +264 -0
  19. data/test/cases/connection_test_sqlserver.rb +103 -0
  20. data/test/cases/eager_association_test_sqlserver.rb +42 -0
  21. data/test/cases/execute_procedure_test_sqlserver.rb +33 -0
  22. data/test/cases/inheritance_test_sqlserver.rb +28 -0
  23. data/test/cases/method_scoping_test_sqlserver.rb +28 -0
  24. data/test/cases/migration_test_sqlserver.rb +93 -0
  25. data/test/cases/offset_and_limit_test_sqlserver.rb +89 -0
  26. data/test/cases/pessimistic_locking_test_sqlserver.rb +125 -0
  27. data/test/cases/query_cache_test_sqlserver.rb +24 -0
  28. data/test/cases/schema_dumper_test_sqlserver.rb +61 -0
  29. data/test/cases/specific_schema_test_sqlserver.rb +26 -0
  30. data/test/cases/sqlserver_helper.rb +119 -0
  31. data/test/cases/table_name_test_sqlserver.rb +22 -0
  32. data/test/cases/transaction_test_sqlserver.rb +93 -0
  33. data/test/cases/unicode_test_sqlserver.rb +44 -0
  34. data/test/connections/native_sqlserver/connection.rb +23 -0
  35. data/test/connections/native_sqlserver_odbc/connection.rb +25 -0
  36. data/test/migrations/transaction_table/1_table_will_never_be_created.rb +11 -0
  37. data/test/schema/sqlserver_specific_schema.rb +88 -0
  38. metadata +96 -0
@@ -0,0 +1,54 @@
1
+ require 'autotest'
2
+ require 'activesupport'
3
+
4
+ class Autotest::Sqlserver < Autotest
5
+
6
+ def initialize
7
+ super
8
+
9
+ odbc_mode = true
10
+
11
+ clear_mappings
12
+
13
+ self.libs = [
14
+ "lib",
15
+ "test",
16
+ "test/connections/native_sqlserver#{odbc_mode ? '_odbc' : ''}",
17
+ "../../../rails/activerecord/test/"
18
+ ].join(File::PATH_SEPARATOR)
19
+
20
+ @test_sqlserver_file_match = %r%^test/cases/.*_test_sqlserver\.rb$%
21
+
22
+ add_exception %r%^\./(?:autotest)%
23
+ add_exception %r%^\./(.*LICENSE|debug.log|README.*|CHANGELOG.*)$%i
24
+
25
+ # Any *_test_sqlserver file saved runs that file
26
+ self.add_mapping @test_sqlserver_file_match do |filename, matchs|
27
+ filename
28
+ end
29
+
30
+ # If any the adapter changes
31
+ # the test directory, ofcourse having _test.rb at the end, will run that test.
32
+ self.add_mapping(%r%^lib/(.*)\.rb$%) do |filename, matchs|
33
+ files_matching @test_sqlserver_file_match
34
+ end
35
+
36
+ # If any core file like the test helper, connections, fixtures, migratinos,
37
+ # and other support files, then run all matching *_test_sqlserver files.
38
+ add_mapping %r%^test/(cases/(sqlserver_helper)\.rb|connections|fixtures|migrations|schema/.*)% do
39
+ files_matching @test_sqlserver_file_match
40
+ end
41
+
42
+ end
43
+
44
+ # Have to use a custom reorder method since the normal :alpha for Autotest would put the
45
+ # files with ../ in the path before others.
46
+ def reorder(files_to_test)
47
+ ar_tests, sqlsvr_tests = files_to_test.partition { |k,v| k.starts_with?('../../../') }
48
+ ar_tests.sort! { |a,b| a[0] <=> b[0] }
49
+ sqlsvr_tests.sort! { |a,b| a[0] <=> b[0] }
50
+ sqlsvr_tests + ar_tests
51
+ end
52
+
53
+ end
54
+
@@ -0,0 +1,1017 @@
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.15'.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, :native_string_database_type,
159
+ :log_info_schema_queries, :enable_default_unicode_types
160
+
161
+ class << self
162
+
163
+ def type_limitable?(type)
164
+ LIMITABLE_TYPES.include?(type.to_s)
165
+ end
166
+
167
+ end
168
+
169
+ def initialize(connection, logger, connection_options=nil)
170
+ super(connection, logger)
171
+ @connection_options = connection_options
172
+ initialize_sqlserver_caches
173
+ unless SUPPORTED_VERSIONS.include?(database_year)
174
+ raise NotImplementedError, "Currently, only #{SUPPORTED_VERSIONS.to_sentence} are supported."
175
+ end
176
+ end
177
+
178
+ # ABSTRACT ADAPTER =========================================#
179
+
180
+ def adapter_name
181
+ ADAPTER_NAME
182
+ end
183
+
184
+ def supports_migrations?
185
+ true
186
+ end
187
+
188
+ def supports_ddl_transactions?
189
+ true
190
+ end
191
+
192
+ def supports_savepoints?
193
+ true
194
+ end
195
+
196
+ def database_version
197
+ @database_version ||= info_schema_query { select_value('SELECT @@version') }
198
+ end
199
+
200
+ def database_year
201
+ DATABASE_VERSION_REGEXP.match(database_version)[1].to_i
202
+ end
203
+
204
+ def sqlserver?
205
+ true
206
+ end
207
+
208
+ def sqlserver_2000?
209
+ database_year == 2000
210
+ end
211
+
212
+ def sqlserver_2005?
213
+ database_year == 2005
214
+ end
215
+
216
+ def sqlserver_2008?
217
+ database_year == 2008
218
+ end
219
+
220
+ def version
221
+ self.class::VERSION
222
+ end
223
+
224
+ def inspect
225
+ "#<#{self.class} version: #{version}, year: #{database_year}, connection_options: #{@connection_options.inspect}>"
226
+ end
227
+
228
+ def native_string_database_type
229
+ @@native_string_database_type || (enable_default_unicode_types ? 'nvarchar' : 'varchar')
230
+ end
231
+
232
+ def native_text_database_type
233
+ @@native_text_database_type ||
234
+ if (sqlserver_2005? || sqlserver_2008?)
235
+ enable_default_unicode_types ? 'nvarchar(max)' : 'varchar(max)'
236
+ else
237
+ enable_default_unicode_types ? 'ntext' : 'text'
238
+ end
239
+ end
240
+
241
+ def native_binary_database_type
242
+ @@native_binary_database_type || ((sqlserver_2005? || sqlserver_2008?) ? 'varbinary(max)' : 'image')
243
+ end
244
+
245
+ # QUOTING ==================================================#
246
+
247
+ def quote(value, column = nil)
248
+ case value
249
+ when String, ActiveSupport::Multibyte::Chars
250
+ if column && column.type == :binary
251
+ column.class.string_to_binary(value)
252
+ elsif column && column.respond_to?(:is_utf8?) && column.is_utf8?
253
+ quoted_utf8_value(value)
254
+ else
255
+ super
256
+ end
257
+ else
258
+ super
259
+ end
260
+ end
261
+
262
+ def quote_string(string)
263
+ string.to_s.gsub(/\'/, "''")
264
+ end
265
+
266
+ def quote_column_name(column_name)
267
+ column_name.to_s.split('.').map{ |name| "[#{name}]" }.join('.')
268
+ end
269
+
270
+ def quote_table_name(table_name)
271
+ return table_name if table_name =~ /^\[.*\]$/
272
+ quote_column_name(table_name)
273
+ end
274
+
275
+ def quoted_true
276
+ '1'
277
+ end
278
+
279
+ def quoted_false
280
+ '0'
281
+ end
282
+
283
+ def quoted_date(value)
284
+ if value.acts_like?(:time) && value.respond_to?(:usec)
285
+ "#{super}.#{sprintf("%03d",value.usec/1000)}"
286
+ else
287
+ super
288
+ end
289
+ end
290
+
291
+ def quoted_utf8_value(value)
292
+ "N'#{quote_string(value)}'"
293
+ end
294
+
295
+ # REFERENTIAL INTEGRITY ====================================#
296
+
297
+ def disable_referential_integrity(&block)
298
+ do_execute "EXEC sp_MSForEachTable 'ALTER TABLE ? NOCHECK CONSTRAINT ALL'"
299
+ yield
300
+ ensure
301
+ do_execute "EXEC sp_MSForEachTable 'ALTER TABLE ? CHECK CONSTRAINT ALL'"
302
+ end
303
+
304
+ # CONNECTION MANAGEMENT ====================================#
305
+
306
+ def active?
307
+ raw_connection.execute("SELECT 1").finish
308
+ true
309
+ rescue DBI::DatabaseError, DBI::InterfaceError
310
+ false
311
+ end
312
+
313
+ def reconnect!
314
+ disconnect!
315
+ @connection = DBI.connect(*@connection_options)
316
+ rescue DBI::DatabaseError => e
317
+ @logger.warn "#{adapter_name} reconnection failed: #{e.message}" if @logger
318
+ false
319
+ end
320
+
321
+ def disconnect!
322
+ raw_connection.disconnect rescue nil
323
+ end
324
+
325
+ def finish_statement_handle(handle)
326
+ handle.finish if handle && handle.respond_to?(:finish) && !handle.finished?
327
+ handle
328
+ end
329
+
330
+ # DATABASE STATEMENTS ======================================#
331
+
332
+ def select_rows(sql, name = nil)
333
+ raw_select(sql,name).last
334
+ end
335
+
336
+ def execute(sql, name = nil, &block)
337
+ if table_name = query_requires_identity_insert?(sql)
338
+ handle = with_identity_insert_enabled(table_name) { raw_execute(sql,name,&block) }
339
+ else
340
+ handle = raw_execute(sql,name,&block)
341
+ end
342
+ finish_statement_handle(handle)
343
+ end
344
+
345
+ def execute_procedure(proc_name, *variables)
346
+ vars = variables.map{ |v| quote(v) }.join(', ')
347
+ sql = "EXEC #{proc_name} #{vars}".strip
348
+ select(sql,'Execute Procedure',true).inject([]) do |results,row|
349
+ results << row.with_indifferent_access
350
+ end
351
+ end
352
+
353
+ def outside_transaction?
354
+ info_schema_query { select_value("SELECT @@TRANCOUNT") == 0 }
355
+ end
356
+
357
+ def begin_db_transaction
358
+ do_execute "BEGIN TRANSACTION"
359
+ end
360
+
361
+ def commit_db_transaction
362
+ do_execute "COMMIT TRANSACTION"
363
+ end
364
+
365
+ def rollback_db_transaction
366
+ do_execute "ROLLBACK TRANSACTION" rescue nil
367
+ end
368
+
369
+ def create_savepoint
370
+ do_execute "SAVE TRANSACTION #{current_savepoint_name}"
371
+ end
372
+
373
+ def release_savepoint
374
+ end
375
+
376
+ def rollback_to_savepoint
377
+ do_execute "ROLLBACK TRANSACTION #{current_savepoint_name}"
378
+ end
379
+
380
+ def add_limit_offset!(sql, options)
381
+ # Validate and/or convert integers for :limit and :offets options.
382
+ if options[:offset]
383
+ raise ArgumentError, "offset should have a limit" unless options[:limit]
384
+ unless options[:offset].kind_of?(Integer)
385
+ if options[:offset] =~ /^\d+$/
386
+ options[:offset] = options[:offset].to_i
387
+ else
388
+ raise ArgumentError, "offset should be an integer"
389
+ end
390
+ end
391
+ end
392
+ if options[:limit] && !(options[:limit].kind_of?(Integer))
393
+ if options[:limit] =~ /^\d+$/
394
+ options[:limit] = options[:limit].to_i
395
+ else
396
+ raise ArgumentError, "limit should be an integer"
397
+ end
398
+ end
399
+ # The business of adding limit/offset
400
+ if options[:limit] and options[:offset]
401
+ tally_sql = "SELECT count(*) as TotalRows from (#{sql.sub(/\bSELECT(\s+DISTINCT)?\b/i, "SELECT#{$1} TOP 1000000000")}) tally"
402
+ add_lock! tally_sql, :lock => 'WITH (NOLOCK)'
403
+ total_rows = select_value(tally_sql).to_i
404
+ if (options[:limit] + options[:offset]) >= total_rows
405
+ options[:limit] = (total_rows - options[:offset] >= 0) ? (total_rows - options[:offset]) : 0
406
+ end
407
+ # Make sure we do not need a special limit/offset for association limiting. http://gist.github.com/25118
408
+ add_limit_offset_for_association_limiting!(sql,options) and return if sql_for_association_limiting?(sql)
409
+ # Wrap the SQL query in a bunch of outer SQL queries that emulate proper LIMIT,OFFSET support.
410
+ sql.sub!(/^\s*SELECT(\s+DISTINCT)?/i, "SELECT * FROM (SELECT TOP #{options[:limit]} * FROM (SELECT#{$1} TOP #{options[:limit] + options[:offset]}")
411
+ sql << ") AS tmp1"
412
+ if options[:order]
413
+ order = options[:order].split(',').map do |field|
414
+ order_by_column, order_direction = field.split(" ")
415
+ order_by_column = quote_column_name(order_by_column)
416
+ # Investigate the SQL query to figure out if the order_by_column has been renamed.
417
+ if sql =~ /#{Regexp.escape(order_by_column)} AS (t\d_r\d\d?)/
418
+ # Fx "[foo].[bar] AS t4_r2" was found in the SQL. Use the column alias (ie 't4_r2') for the subsequent orderings
419
+ order_by_column = $1
420
+ elsif order_by_column =~ /\w+\.\[?(\w+)\]?/
421
+ order_by_column = $1
422
+ else
423
+ # It doesn't appear that the column name has been renamed as part of the query. Use just the column
424
+ # name rather than the full identifier for the outer queries.
425
+ order_by_column = order_by_column.split('.').last
426
+ end
427
+ # Put the column name and eventual direction back together
428
+ [order_by_column, order_direction].join(' ').strip
429
+ end.join(', ')
430
+ sql << " ORDER BY #{change_order_direction(order)}) AS tmp2 ORDER BY #{order}"
431
+ else
432
+ sql << ") AS tmp2"
433
+ end
434
+ elsif options[:limit] && sql !~ /^\s*SELECT (@@|COUNT\()/i
435
+ if md = sql.match(/^(\s*SELECT)(\s+DISTINCT)?(.*)/im)
436
+ sql.replace "#{md[1]}#{md[2]} TOP #{options[:limit]}#{md[3]}"
437
+ else
438
+ # Account for building SQL fragments without SELECT yet. See #update_all and #limited_update_conditions.
439
+ sql.replace "TOP #{options[:limit]} #{sql}"
440
+ end
441
+ end
442
+ end
443
+
444
+ def add_lock!(sql, options)
445
+ # http://blog.sqlauthority.com/2007/04/27/sql-server-2005-locking-hints-and-examples/
446
+ return unless options[:lock]
447
+ lock_type = options[:lock] == true ? 'WITH(HOLDLOCK, ROWLOCK)' : options[:lock]
448
+ sql.gsub! %r|LEFT OUTER JOIN\s+(.*?)\s+ON|im, "LEFT OUTER JOIN \\1 #{lock_type} ON"
449
+ sql.gsub! %r{FROM\s([\w\[\]\.]+)}im, "FROM \\1 #{lock_type}"
450
+ end
451
+
452
+ def empty_insert_statement(table_name)
453
+ "INSERT INTO #{quote_table_name(table_name)} DEFAULT VALUES"
454
+ end
455
+
456
+ def case_sensitive_equality_operator
457
+ "COLLATE Latin1_General_CS_AS ="
458
+ end
459
+
460
+ def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
461
+ match_data = where_sql.match(/(.*)WHERE/)
462
+ limit = match_data[1]
463
+ where_sql.sub!(limit,'')
464
+ "WHERE #{quoted_primary_key} IN (SELECT #{limit} #{quoted_primary_key} FROM #{quoted_table_name} #{where_sql})"
465
+ end
466
+
467
+ # SCHEMA STATEMENTS ========================================#
468
+
469
+ def native_database_types
470
+ {
471
+ :primary_key => "int NOT NULL IDENTITY(1, 1) PRIMARY KEY",
472
+ :string => { :name => native_string_database_type, :limit => 255 },
473
+ :text => { :name => native_text_database_type },
474
+ :integer => { :name => "int", :limit => 4 },
475
+ :float => { :name => "float", :limit => 8 },
476
+ :decimal => { :name => "decimal" },
477
+ :datetime => { :name => "datetime" },
478
+ :timestamp => { :name => "datetime" },
479
+ :time => { :name => "datetime" },
480
+ :date => { :name => "datetime" },
481
+ :binary => { :name => native_binary_database_type },
482
+ :boolean => { :name => "bit"},
483
+ # These are custom types that may move somewhere else for good schema_dumper.rb hacking to output them.
484
+ :char => { :name => 'char' },
485
+ :varchar_max => { :name => 'varchar(max)' },
486
+ :nchar => { :name => "nchar" },
487
+ :nvarchar => { :name => "nvarchar", :limit => 255 },
488
+ :nvarchar_max => { :name => "nvarchar(max)" },
489
+ :ntext => { :name => "ntext" }
490
+ }
491
+ end
492
+
493
+ def table_alias_length
494
+ 128
495
+ end
496
+
497
+ def tables(name = nil)
498
+ info_schema_query do
499
+ select_values "SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_NAME <> 'dtproperties'"
500
+ end
501
+ end
502
+
503
+ def views(name = nil)
504
+ @sqlserver_views_cache ||=
505
+ info_schema_query { select_values("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_NAME NOT IN ('sysconstraints','syssegments')") }
506
+ end
507
+
508
+ def view_information(table_name)
509
+ table_name = unqualify_table_name(table_name)
510
+ @sqlserver_view_information_cache[table_name] ||= begin
511
+ view_info = info_schema_query { select_one("SELECT * FROM INFORMATION_SCHEMA.VIEWS WHERE TABLE_NAME = '#{table_name}'") }
512
+ if view_info
513
+ if view_info['VIEW_DEFINITION'].blank? || view_info['VIEW_DEFINITION'].length == 4000
514
+ view_info['VIEW_DEFINITION'] = info_schema_query { select_values("EXEC sp_helptext #{table_name}").join }
515
+ end
516
+ end
517
+ view_info
518
+ end
519
+ end
520
+
521
+ def view_table_name(table_name)
522
+ view_info = view_information(table_name)
523
+ view_info ? get_table_name(view_info['VIEW_DEFINITION']) : table_name
524
+ end
525
+
526
+ def table_exists?(table_name)
527
+ super || tables.include?(unqualify_table_name(table_name)) || views.include?(table_name.to_s)
528
+ end
529
+
530
+ def indexes(table_name, name = nil)
531
+ unquoted_table_name = unqualify_table_name(table_name)
532
+ select("EXEC sp_helpindex #{quote_table_name(unquoted_table_name)}",name).inject([]) do |indexes,index|
533
+ if index['index_description'] =~ /primary key/
534
+ indexes
535
+ else
536
+ name = index['index_name']
537
+ unique = index['index_description'] =~ /unique/
538
+ columns = index['index_keys'].split(',').map do |column|
539
+ column.strip!
540
+ column.gsub! '(-)', '' if column.ends_with?('(-)')
541
+ column
542
+ end
543
+ indexes << IndexDefinition.new(table_name, name, unique, columns)
544
+ end
545
+ end
546
+ end
547
+
548
+ def columns(table_name, name = nil)
549
+ return [] if table_name.blank?
550
+ cache_key = unqualify_table_name(table_name)
551
+ @sqlserver_columns_cache[cache_key] ||= column_definitions(table_name).collect do |ci|
552
+ sqlserver_options = ci.except(:name,:default_value,:type,:null)
553
+ SQLServerColumn.new ci[:name], ci[:default_value], ci[:type], ci[:null], sqlserver_options
554
+ end
555
+ end
556
+
557
+ def create_table(table_name, options = {})
558
+ super
559
+ remove_sqlserver_columns_cache_for(table_name)
560
+ end
561
+
562
+ def rename_table(table_name, new_name)
563
+ do_execute "EXEC sp_rename '#{table_name}', '#{new_name}'"
564
+ end
565
+
566
+ def drop_table(table_name, options = {})
567
+ super
568
+ remove_sqlserver_columns_cache_for(table_name)
569
+ end
570
+
571
+ def add_column(table_name, column_name, type, options = {})
572
+ super
573
+ remove_sqlserver_columns_cache_for(table_name)
574
+ end
575
+
576
+ def remove_column(table_name, *column_names)
577
+ column_names.flatten.each do |column_name|
578
+ remove_check_constraints(table_name, column_name)
579
+ remove_default_constraint(table_name, column_name)
580
+ remove_indexes(table_name, column_name)
581
+ do_execute "ALTER TABLE #{quote_table_name(table_name)} DROP COLUMN #{quote_column_name(column_name)}"
582
+ end
583
+ remove_sqlserver_columns_cache_for(table_name)
584
+ end
585
+
586
+ def change_column(table_name, column_name, type, options = {})
587
+ sql_commands = []
588
+ 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])}"
589
+ change_column_sql << " NOT NULL" if options[:null] == false
590
+ sql_commands << change_column_sql
591
+ if options_include_default?(options)
592
+ remove_default_constraint(table_name, column_name)
593
+ 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)}"
594
+ end
595
+ sql_commands.each { |c| do_execute(c) }
596
+ remove_sqlserver_columns_cache_for(table_name)
597
+ end
598
+
599
+ def change_column_default(table_name, column_name, default)
600
+ remove_default_constraint(table_name, column_name)
601
+ 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)}"
602
+ remove_sqlserver_columns_cache_for(table_name)
603
+ end
604
+
605
+ def rename_column(table_name, column_name, new_column_name)
606
+ column_for(table_name,column_name)
607
+ do_execute "EXEC sp_rename '#{table_name}.#{column_name}', '#{new_column_name}', 'COLUMN'"
608
+ remove_sqlserver_columns_cache_for(table_name)
609
+ end
610
+
611
+ def remove_index(table_name, options = {})
612
+ do_execute "DROP INDEX #{table_name}.#{quote_column_name(index_name(table_name, options))}"
613
+ end
614
+
615
+ def type_to_sql(type, limit = nil, precision = nil, scale = nil)
616
+ limit = nil unless self.class.type_limitable?(type)
617
+ case type.to_s
618
+ when 'integer'
619
+ case limit
620
+ when 1..2 then 'smallint'
621
+ when 3..4, nil then 'integer'
622
+ when 5..8 then 'bigint'
623
+ else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.")
624
+ end
625
+ else
626
+ super
627
+ end
628
+ end
629
+
630
+ def add_order_by_for_association_limiting!(sql, options)
631
+ # Disertation http://gist.github.com/24073
632
+ # Information http://weblogs.sqlteam.com/jeffs/archive/2007/12/13/select-distinct-order-by-error.aspx
633
+ return sql if options[:order].blank?
634
+ columns = sql.match(/SELECT\s+DISTINCT(.*)FROM/)[1].strip
635
+ sql.sub!(/SELECT\s+DISTINCT/,'SELECT')
636
+ sql << "GROUP BY #{columns} ORDER BY #{order_to_min_set(options[:order])}"
637
+ end
638
+
639
+ def change_column_null(table_name, column_name, null, default = nil)
640
+ column = column_for(table_name,column_name)
641
+ unless null || default.nil?
642
+ do_execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
643
+ end
644
+ sql = "ALTER TABLE #{table_name} ALTER COLUMN #{quote_column_name(column_name)} #{type_to_sql column.type, column.limit, column.precision, column.scale}"
645
+ sql << " NOT NULL" unless null
646
+ do_execute sql
647
+ end
648
+
649
+ def pk_and_sequence_for(table_name)
650
+ idcol = identity_column(table_name)
651
+ idcol ? [idcol.name,nil] : nil
652
+ end
653
+
654
+ # RAKE UTILITY METHODS =====================================#
655
+
656
+ def recreate_database(name)
657
+ existing_database = current_database.to_s
658
+ if name.to_s == existing_database
659
+ do_execute 'USE master'
660
+ end
661
+ drop_database(name)
662
+ create_database(name)
663
+ ensure
664
+ do_execute "USE #{existing_database}" if name.to_s == existing_database
665
+ end
666
+
667
+ def drop_database(name)
668
+ retry_count = 0
669
+ max_retries = 1
670
+ begin
671
+ do_execute "DROP DATABASE #{name}"
672
+ rescue ActiveRecord::StatementInvalid => err
673
+ # Remove existing connections and rollback any transactions if we received the message
674
+ # 'Cannot drop the database 'test' because it is currently in use'
675
+ if err.message =~ /because it is currently in use/
676
+ raise if retry_count >= max_retries
677
+ retry_count += 1
678
+ remove_database_connections_and_rollback(name)
679
+ retry
680
+ else
681
+ raise
682
+ end
683
+ end
684
+ end
685
+
686
+ def create_database(name)
687
+ do_execute "CREATE DATABASE #{name}"
688
+ end
689
+
690
+ def current_database
691
+ select_value 'SELECT DB_NAME()'
692
+ end
693
+
694
+ def remove_database_connections_and_rollback(name)
695
+ # This should disconnect all other users and rollback any transactions for SQL 2000 and 2005
696
+ # http://sqlserver2000.databases.aspfaq.com/how-do-i-drop-a-sql-server-database.html
697
+ do_execute "ALTER DATABASE #{name} SET SINGLE_USER WITH ROLLBACK IMMEDIATE"
698
+ end
699
+
700
+
701
+
702
+ protected
703
+
704
+ # DATABASE STATEMENTS ======================================
705
+
706
+ def select(sql, name = nil, ignore_special_columns = false)
707
+ repair_special_columns(sql) unless ignore_special_columns
708
+ fields, rows = raw_select(sql,name)
709
+ rows.inject([]) do |results,row|
710
+ row_hash = {}
711
+ fields.each_with_index do |f, i|
712
+ row_hash[f] = row[i]
713
+ end
714
+ results << row_hash
715
+ end
716
+ end
717
+
718
+ def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
719
+ super || select_value("SELECT SCOPE_IDENTITY() AS Ident")
720
+ end
721
+
722
+ def update_sql(sql, name = nil)
723
+ execute(sql, name)
724
+ select_value('SELECT @@ROWCOUNT AS AffectedRows')
725
+ end
726
+
727
+ def info_schema_query
728
+ log_info_schema_queries ? yield : ActiveRecord::Base.silence{ yield }
729
+ end
730
+
731
+ def raw_execute(sql, name = nil, &block)
732
+ log(sql, name) do
733
+ if block_given?
734
+ raw_connection.execute(sql) { |handle| yield(handle) }
735
+ else
736
+ raw_connection.execute(sql)
737
+ end
738
+ end
739
+ end
740
+
741
+ def without_type_conversion
742
+ raw_connection.convert_types = false if raw_connection.respond_to?(:convert_types=)
743
+ yield
744
+ ensure
745
+ raw_connection.convert_types = true if raw_connection.respond_to?(:convert_types=)
746
+ end
747
+
748
+ def do_execute(sql,name=nil)
749
+ log(sql, name || 'EXECUTE') do
750
+ raw_connection.do(sql)
751
+ end
752
+ end
753
+
754
+ def raw_select(sql, name = nil)
755
+ handle = raw_execute(sql,name)
756
+ fields = handle.column_names
757
+ results = handle_as_array(handle)
758
+ rows = results.inject([]) do |rows,row|
759
+ row.each_with_index do |value, i|
760
+ # DEPRECATED in DBI 0.4.0 and above. Remove when 0.2.2 and lower is no longer supported.
761
+ if value.is_a? DBI::Timestamp
762
+ row[i] = value.to_sqlserver_string
763
+ end
764
+ end
765
+ rows << row
766
+ end
767
+ return fields, rows
768
+ end
769
+
770
+ def handle_as_array(handle)
771
+ array = handle.inject([]) do |rows,row|
772
+ rows << row.inject([]){ |values,value| values << value }
773
+ end
774
+ finish_statement_handle(handle)
775
+ array
776
+ end
777
+
778
+ def add_limit_offset_for_association_limiting!(sql, options)
779
+ sql.replace %|
780
+ SET NOCOUNT ON
781
+ DECLARE @row_number TABLE (row int identity(1,1), id int)
782
+ INSERT INTO @row_number (id)
783
+ #{sql}
784
+ SET NOCOUNT OFF
785
+ SELECT id FROM (
786
+ SELECT TOP #{options[:limit]} * FROM (
787
+ SELECT TOP #{options[:limit] + options[:offset]} * FROM @row_number ORDER BY row
788
+ ) AS tmp1 ORDER BY row DESC
789
+ ) AS tmp2 ORDER BY row
790
+ |.gsub(/[ \t\r\n]+/,' ')
791
+ end
792
+
793
+ # SCHEMA STATEMENTS ========================================#
794
+
795
+ def remove_check_constraints(table_name, column_name)
796
+ 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)}'") }
797
+ constraints.each do |constraint|
798
+ do_execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{quote_column_name(constraint)}"
799
+ end
800
+ end
801
+
802
+ def remove_default_constraint(table_name, column_name)
803
+ 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")
804
+ constraints.each do |constraint|
805
+ do_execute "ALTER TABLE #{quote_table_name(table_name)} DROP CONSTRAINT #{quote_column_name(constraint)}"
806
+ end
807
+ end
808
+
809
+ def remove_indexes(table_name, column_name)
810
+ indexes(table_name).select{ |index| index.columns.include?(column_name.to_s) }.each do |index|
811
+ remove_index(table_name, {:name => index.name})
812
+ end
813
+ end
814
+
815
+ def default_name(table_name, column_name)
816
+ "DF_#{table_name}_#{column_name}"
817
+ end
818
+
819
+ # IDENTITY INSERTS =========================================#
820
+
821
+ def with_identity_insert_enabled(table_name, &block)
822
+ table_name = quote_table_name(table_name_or_views_table_name(table_name))
823
+ set_identity_insert(table_name, true)
824
+ yield
825
+ ensure
826
+ set_identity_insert(table_name, false)
827
+ end
828
+
829
+ def set_identity_insert(table_name, enable = true)
830
+ sql = "SET IDENTITY_INSERT #{table_name} #{enable ? 'ON' : 'OFF'}"
831
+ do_execute(sql,'IDENTITY_INSERT')
832
+ rescue Exception => e
833
+ raise ActiveRecordError, "IDENTITY_INSERT could not be turned #{enable ? 'ON' : 'OFF'} for table #{table_name}"
834
+ end
835
+
836
+ def query_requires_identity_insert?(sql)
837
+ if insert_sql?(sql)
838
+ table_name = get_table_name(sql)
839
+ id_column = identity_column(table_name)
840
+ id_column && sql =~ /INSERT[^(]+\([^)]*\[#{id_column.name}\][^)]*\)/i ? table_name : false
841
+ else
842
+ false
843
+ end
844
+ end
845
+
846
+ def identity_column(table_name)
847
+ columns(table_name).detect(&:is_identity?)
848
+ end
849
+
850
+ def table_name_or_views_table_name(table_name)
851
+ unquoted_table_name = unqualify_table_name(table_name)
852
+ views.include?(unquoted_table_name) ? view_table_name(unquoted_table_name) : unquoted_table_name
853
+ end
854
+
855
+ # HELPER METHODS ===========================================#
856
+
857
+ def insert_sql?(sql)
858
+ !(sql =~ /^\s*INSERT/i).nil?
859
+ end
860
+
861
+ def unqualify_table_name(table_name)
862
+ table_name.to_s.split('.').last.gsub(/[\[\]]/,'')
863
+ end
864
+
865
+ def unqualify_db_name(table_name)
866
+ table_names = table_name.to_s.split('.')
867
+ table_names.length == 3 ? table_names.first.tr('[]','') : nil
868
+ end
869
+
870
+ def get_table_name(sql)
871
+ if sql =~ /^\s*insert\s+into\s+([^\(\s]+)\s*|^\s*update\s+([^\(\s]+)\s*/i
872
+ $1 || $2
873
+ elsif sql =~ /from\s+([^\(\s]+)\s*/i
874
+ $1
875
+ else
876
+ nil
877
+ end
878
+ end
879
+
880
+ def orders_and_dirs_set(order)
881
+ orders = order.sub('ORDER BY','').split(',').map(&:strip).reject(&:blank?)
882
+ orders_dirs = orders.map do |ord|
883
+ dir = nil
884
+ if match_data = ord.match(/\b(asc|desc)$/i)
885
+ dir = match_data[1]
886
+ ord.sub!(dir,'').strip!
887
+ dir.upcase!
888
+ end
889
+ [ord,dir]
890
+ end
891
+ end
892
+
893
+ def views_real_column_name(table_name,column_name)
894
+ view_definition = view_information(table_name)['VIEW_DEFINITION']
895
+ match_data = view_definition.match(/([\w-]*)\s+as\s+#{column_name}/im)
896
+ match_data ? match_data[1] : column_name
897
+ end
898
+
899
+ def order_to_min_set(order)
900
+ orders_dirs = orders_and_dirs_set(order)
901
+ orders_dirs.map do |o,d|
902
+ "MIN(#{o}) #{d}".strip
903
+ end.join(', ')
904
+ end
905
+
906
+ def sql_for_association_limiting?(sql)
907
+ if md = sql.match(/^\s*SELECT(.*)FROM.*GROUP BY.*ORDER BY.*/im)
908
+ select_froms = md[1].split(',')
909
+ select_froms.size == 1 && !select_froms.first.include?('*')
910
+ end
911
+ end
912
+
913
+ def remove_sqlserver_columns_cache_for(table_name)
914
+ cache_key = unqualify_table_name(table_name)
915
+ @sqlserver_columns_cache[cache_key] = nil
916
+ initialize_sqlserver_caches(false)
917
+ end
918
+
919
+ def initialize_sqlserver_caches(reset_columns=true)
920
+ @sqlserver_columns_cache = {} if reset_columns
921
+ @sqlserver_views_cache = nil
922
+ @sqlserver_view_information_cache = {}
923
+ end
924
+
925
+ def column_definitions(table_name)
926
+ db_name = unqualify_db_name(table_name)
927
+ table_name = unqualify_table_name(table_name)
928
+ sql = %{
929
+ SELECT
930
+ columns.TABLE_NAME as table_name,
931
+ columns.COLUMN_NAME as name,
932
+ columns.DATA_TYPE as type,
933
+ columns.COLUMN_DEFAULT as default_value,
934
+ columns.NUMERIC_SCALE as numeric_scale,
935
+ columns.NUMERIC_PRECISION as numeric_precision,
936
+ CASE
937
+ WHEN columns.DATA_TYPE IN ('nchar','nvarchar') THEN columns.CHARACTER_MAXIMUM_LENGTH
938
+ ELSE COL_LENGTH(columns.TABLE_NAME, columns.COLUMN_NAME)
939
+ END as length,
940
+ CASE
941
+ WHEN columns.IS_NULLABLE = 'YES' THEN 1
942
+ ELSE NULL
943
+ end as is_nullable,
944
+ CASE
945
+ WHEN COLUMNPROPERTY(OBJECT_ID(columns.TABLE_NAME), columns.COLUMN_NAME, 'IsIdentity') = 0 THEN NULL
946
+ ELSE 1
947
+ END as is_identity
948
+ FROM #{db_name}INFORMATION_SCHEMA.COLUMNS columns
949
+ WHERE columns.TABLE_NAME = '#{table_name}'
950
+ ORDER BY columns.ordinal_position
951
+ }.gsub(/[ \t\r\n]+/,' ')
952
+ results = info_schema_query { without_type_conversion{ select(sql,nil,true) } }
953
+ results.collect do |ci|
954
+ ci.symbolize_keys!
955
+ ci[:type] = case ci[:type]
956
+ when /^bit|image|text|ntext|datetime$/
957
+ ci[:type]
958
+ when /^numeric|decimal$/i
959
+ "#{ci[:type]}(#{ci[:numeric_precision]},#{ci[:numeric_scale]})"
960
+ when /^char|nchar|varchar|nvarchar|varbinary|bigint|int|smallint$/
961
+ ci[:length].to_i == -1 ? "#{ci[:type]}(max)" : "#{ci[:type]}(#{ci[:length]})"
962
+ else
963
+ ci[:type]
964
+ end
965
+ if ci[:default_value].nil? && views.include?(table_name)
966
+ real_table_name = table_name_or_views_table_name(table_name)
967
+ real_column_name = views_real_column_name(table_name,ci[:name])
968
+ 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}'"
969
+ ci[:default_value] = info_schema_query { without_type_conversion{ select_value(col_default_sql) } }
970
+ end
971
+ ci[:default_value] = case ci[:default_value]
972
+ when nil, '(null)', '(NULL)'
973
+ nil
974
+ else
975
+ ci[:default_value].match(/\A\(+N?'?(.*?)'?\)+\Z/)[1]
976
+ end
977
+ ci[:null] = ci[:is_nullable].to_i == 1 ; ci.delete(:is_nullable)
978
+ ci
979
+ end
980
+ end
981
+
982
+ def column_for(table_name, column_name)
983
+ unless column = columns(table_name).detect { |c| c.name == column_name.to_s }
984
+ raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
985
+ end
986
+ column
987
+ end
988
+
989
+ def change_order_direction(order)
990
+ order.split(",").collect {|fragment|
991
+ case fragment
992
+ when /\bDESC\b/i then fragment.gsub(/\bDESC\b/i, "ASC")
993
+ when /\bASC\b/i then fragment.gsub(/\bASC\b/i, "DESC")
994
+ else String.new(fragment).split(',').join(' DESC,') + ' DESC'
995
+ end
996
+ }.join(",")
997
+ end
998
+
999
+ def special_columns(table_name)
1000
+ columns(table_name).select(&:is_special?).map(&:name)
1001
+ end
1002
+
1003
+ def repair_special_columns(sql)
1004
+ special_cols = special_columns(get_table_name(sql))
1005
+ for col in special_cols.to_a
1006
+ sql.gsub!(/((\.|\s|\()\[?#{col.to_s}\]?)\s?=\s?/, '\1 LIKE ')
1007
+ sql.gsub!(/ORDER BY #{col.to_s}/i, '')
1008
+ end
1009
+ sql
1010
+ end
1011
+
1012
+ end #class SQLServerAdapter < AbstractAdapter
1013
+
1014
+ end #module ConnectionAdapters
1015
+
1016
+ end #module ActiveRecord
1017
+