artpop-2000-2005-adapter 2.2.15

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