activerecord-sqlserver-adapter 2.2.18

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 (37) hide show
  1. data/CHANGELOG +175 -0
  2. data/MIT-LICENSE +20 -0
  3. data/Manifest +36 -0
  4. data/README.rdoc +175 -0
  5. data/RUNNING_UNIT_TESTS +60 -0
  6. data/Rakefile +18 -0
  7. data/lib/active_record/connection_adapters/sqlserver_adapter.rb +1126 -0
  8. data/lib/activerecord-sqlserver-adapter.rb +1 -0
  9. data/lib/core_ext/active_record.rb +133 -0
  10. data/lib/core_ext/dbi.rb +85 -0
  11. data/tasks/sqlserver.rake +31 -0
  12. data/test/cases/aaaa_create_tables_test_sqlserver.rb +19 -0
  13. data/test/cases/adapter_test_sqlserver.rb +707 -0
  14. data/test/cases/attribute_methods_test_sqlserver.rb +33 -0
  15. data/test/cases/basics_test_sqlserver.rb +21 -0
  16. data/test/cases/calculations_test_sqlserver.rb +20 -0
  17. data/test/cases/column_test_sqlserver.rb +264 -0
  18. data/test/cases/connection_test_sqlserver.rb +142 -0
  19. data/test/cases/eager_association_test_sqlserver.rb +42 -0
  20. data/test/cases/execute_procedure_test_sqlserver.rb +33 -0
  21. data/test/cases/inheritance_test_sqlserver.rb +28 -0
  22. data/test/cases/method_scoping_test_sqlserver.rb +28 -0
  23. data/test/cases/migration_test_sqlserver.rb +93 -0
  24. data/test/cases/offset_and_limit_test_sqlserver.rb +108 -0
  25. data/test/cases/pessimistic_locking_test_sqlserver.rb +125 -0
  26. data/test/cases/query_cache_test_sqlserver.rb +24 -0
  27. data/test/cases/schema_dumper_test_sqlserver.rb +72 -0
  28. data/test/cases/specific_schema_test_sqlserver.rb +57 -0
  29. data/test/cases/sqlserver_helper.rb +123 -0
  30. data/test/cases/table_name_test_sqlserver.rb +22 -0
  31. data/test/cases/transaction_test_sqlserver.rb +93 -0
  32. data/test/cases/unicode_test_sqlserver.rb +50 -0
  33. data/test/connections/native_sqlserver/connection.rb +23 -0
  34. data/test/connections/native_sqlserver_odbc/connection.rb +25 -0
  35. data/test/migrations/transaction_table/1_table_will_never_be_created.rb +11 -0
  36. data/test/schema/sqlserver_specific_schema.rb +91 -0
  37. metadata +120 -0
@@ -0,0 +1 @@
1
+ require 'active_record/connection_adapters/sqlserver_adapter'
@@ -0,0 +1,133 @@
1
+ require 'active_record/version'
2
+
3
+ module ActiveRecord
4
+ module ConnectionAdapters
5
+ module SQLServerActiveRecordExtensions
6
+
7
+ def self.included(klass)
8
+ klass.extend ClassMethods
9
+ class << klass
10
+ alias_method_chain :reset_column_information, :sqlserver_cache_support
11
+ alias_method_chain :add_order!, :sqlserver_unique_checking
12
+ end
13
+ end
14
+
15
+ module ClassMethods
16
+
17
+ def execute_procedure(proc_name, *variables)
18
+ if connection.respond_to?(:execute_procedure)
19
+ connection.execute_procedure(proc_name,*variables)
20
+ else
21
+ []
22
+ end
23
+ end
24
+
25
+ def coerce_sqlserver_date(*attributes)
26
+ write_inheritable_attribute :coerced_sqlserver_date_columns, Set.new(attributes.map(&:to_s))
27
+ end
28
+
29
+ def coerce_sqlserver_time(*attributes)
30
+ write_inheritable_attribute :coerced_sqlserver_time_columns, Set.new(attributes.map(&:to_s))
31
+ end
32
+
33
+ def coerced_sqlserver_date_columns
34
+ read_inheritable_attribute(:coerced_sqlserver_date_columns) || []
35
+ end
36
+
37
+ def coerced_sqlserver_time_columns
38
+ read_inheritable_attribute(:coerced_sqlserver_time_columns) || []
39
+ end
40
+
41
+ def reset_column_information_with_sqlserver_cache_support
42
+ connection.send(:initialize_sqlserver_caches) if connection.respond_to?(:sqlserver?)
43
+ reset_column_information_without_sqlserver_cache_support
44
+ end
45
+
46
+ private
47
+
48
+ def add_order_with_sqlserver_unique_checking!(sql, order, scope = :auto)
49
+ if connection.respond_to?(:sqlserver?)
50
+ order_sql = ''
51
+ add_order_without_sqlserver_unique_checking!(order_sql, order, scope)
52
+ unless order_sql.blank?
53
+ unique_order_hash = {}
54
+ select_table_name = connection.send(:get_table_name,sql)
55
+ select_table_name.tr!('[]','') if select_table_name
56
+ orders_and_dirs_set = connection.send(:orders_and_dirs_set,order_sql)
57
+ unique_order_sql = orders_and_dirs_set.inject([]) do |array,order_dir|
58
+ ord, dir = order_dir
59
+ ord_tn_and_cn = ord.to_s.split('.').map{|o|o.tr('[]','')}
60
+ ord_table_name, ord_column_name = if ord_tn_and_cn.size > 1
61
+ ord_tn_and_cn
62
+ else
63
+ [nil, ord_tn_and_cn.first]
64
+ end
65
+ if (ord_table_name && ord_table_name == select_table_name && unique_order_hash[ord_column_name]) || unique_order_hash[ord_column_name]
66
+ array
67
+ else
68
+ unique_order_hash[ord_column_name] = true
69
+ array << "#{ord} #{dir}".strip
70
+ end
71
+ end.join(', ')
72
+ sql << " ORDER BY #{unique_order_sql}"
73
+ end
74
+ else
75
+ add_order_without_sqlserver_unique_checking!(sql, order, scope)
76
+ end
77
+ end
78
+
79
+ end
80
+
81
+ end
82
+ end
83
+ end
84
+
85
+ ActiveRecord::Base.send :include, ActiveRecord::ConnectionAdapters::SQLServerActiveRecordExtensions
86
+
87
+
88
+
89
+
90
+ if ActiveRecord::VERSION::MAJOR == 2 && ActiveRecord::VERSION::MINOR >= 3
91
+
92
+ require 'active_record/associations'
93
+ module ActiveRecord
94
+ module ConnectionAdapters
95
+ module SQLServerJoinAssociationChanges
96
+
97
+ def self.included(klass)
98
+ klass.class_eval do
99
+ include InstanceMethods
100
+ alias_method_chain :aliased_table_name_for, :sqlserver_support
101
+ end
102
+ end
103
+
104
+ module InstanceMethods
105
+
106
+ protected
107
+
108
+ # An exact copy, except this method has a Regexp escape on the quoted table name.
109
+ def aliased_table_name_for_with_sqlserver_support(name,suffix=nil)
110
+ if !parent.table_joins.blank? && parent.table_joins.to_s.downcase =~ %r{join(\s+\w+)?\s+#{Regexp.escape(active_record.connection.quote_table_name(name.downcase))}\son}i
111
+ @join_dependency.table_aliases[name] += 1
112
+ end
113
+ unless @join_dependency.table_aliases[name].zero?
114
+ # if the table name has been used, then use an alias
115
+ name = active_record.connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}"
116
+ table_index = @join_dependency.table_aliases[name]
117
+ @join_dependency.table_aliases[name] += 1
118
+ name = name[0..active_record.connection.table_alias_length-3] + "_#{table_index+1}" if table_index > 0
119
+ else
120
+ @join_dependency.table_aliases[name] += 1
121
+ end
122
+ name
123
+ end
124
+
125
+ end
126
+
127
+ end
128
+ end
129
+ end
130
+ ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation.send :include, ActiveRecord::ConnectionAdapters::SQLServerJoinAssociationChanges
131
+
132
+ end
133
+
@@ -0,0 +1,85 @@
1
+
2
+ module SQLServerDBI
3
+
4
+ module Timestamp
5
+ # Deprecated DBI. See documentation for Type::SqlserverTimestamp which
6
+ # this method tries to mimic as ODBC is still going to convert SQL Server
7
+ # milliconds to whole number representation of nanoseconds.
8
+ def to_sqlserver_string
9
+ datetime, nanoseconds = to_s.split('.')
10
+ "#{datetime}.#{sprintf("%03d",nanoseconds.to_i/1000000)}"
11
+ end
12
+ end
13
+
14
+ module Type
15
+
16
+ # Make sure we get DBI::Type::Timestamp returning a string NOT a time object
17
+ # that represents what is in the DB before type casting while letting core
18
+ # ActiveRecord do the reset. It is assumed that DBI is using ODBC connections
19
+ # and that ODBC::Timestamp is taking the native milliseconds that SQL Server
20
+ # stores and returning them incorrect using ODBC::Timestamp#fraction which is
21
+ # nanoseconds. Below shows the incorrect ODBC::Timestamp represented by DBI
22
+ # and the conversion we expect to have in the DB before type casting.
23
+ #
24
+ # "1985-04-15 00:00:00 0" # => "1985-04-15 00:00:00.000"
25
+ # "2008-11-08 10:24:36 30000000" # => "2008-11-08 10:24:36.003"
26
+ # "2008-11-08 10:24:36 123000000" # => "2008-11-08 10:24:36.123"
27
+ class SqlserverTimestamp
28
+ def self.parse(obj)
29
+ return nil if ::DBI::Type::Null.parse(obj).nil?
30
+ date, time, nanoseconds = obj.split(' ')
31
+ "#{date} #{time}.#{sprintf("%03d",nanoseconds.to_i/1000000)}"
32
+ end
33
+ end
34
+
35
+ # The adapter and rails will parse our floats, decimals, and money field correctly
36
+ # from a string. Do not let the DBI::Type classes create Float/BigDecimal objects
37
+ # for us. Trust rails .type_cast to do what it is built to do.
38
+ class SqlserverForcedString
39
+ def self.parse(obj)
40
+ return nil if ::DBI::Type::Null.parse(obj).nil?
41
+ obj.to_s
42
+ end
43
+ end
44
+
45
+ end
46
+
47
+ module TypeUtil
48
+
49
+ def self.included(klass)
50
+ klass.extend ClassMethods
51
+ class << klass
52
+ alias_method_chain :type_name_to_module, :sqlserver_types
53
+ end
54
+ end
55
+
56
+ module ClassMethods
57
+
58
+ # Capture all types classes that we need to handle directly for SQL Server
59
+ # and allow normal processing for those that we do not.
60
+ def type_name_to_module_with_sqlserver_types(type_name)
61
+ case type_name
62
+ when /^timestamp$/i
63
+ DBI::Type::SqlserverTimestamp
64
+ when /^float|decimal|money$/i
65
+ DBI::Type::SqlserverForcedString
66
+ else
67
+ type_name_to_module_without_sqlserver_types(type_name)
68
+ end
69
+ end
70
+
71
+ end
72
+
73
+ end
74
+
75
+
76
+ end
77
+
78
+
79
+ if defined?(DBI::TypeUtil)
80
+ DBI::Type.send :include, SQLServerDBI::Type
81
+ DBI::TypeUtil.send :include, SQLServerDBI::TypeUtil
82
+ elsif defined?(DBI::Timestamp) # DEPRECATED in DBI 0.4.0 and above. Remove when 0.2.2 and lower is no longer supported.
83
+ DBI::Timestamp.send :include, SQLServerDBI::Timestamp
84
+ end
85
+
@@ -0,0 +1,31 @@
1
+
2
+ namespace :sqlserver do
3
+
4
+ ['sqlserver','sqlserver_odbc'].each do |adapter|
5
+
6
+ Rake::TestTask.new("test_#{adapter}") do |t|
7
+ t.libs << "test"
8
+ t.libs << "test/connections/native_#{adapter}"
9
+ t.libs << "../../../rails/activerecord/test/"
10
+ t.test_files = (
11
+ Dir.glob("test/cases/**/*_test_sqlserver.rb").sort +
12
+ Dir.glob("../../../rails/activerecord/test/**/*_test.rb").sort )
13
+ t.verbose = true
14
+ end
15
+
16
+ namespace adapter do
17
+ task :test => "test_#{adapter}"
18
+ end
19
+
20
+ end
21
+
22
+ desc 'Test with unicode types enabled.'
23
+ task :test_unicode_types do
24
+ ENV['ENABLE_DEFAULT_UNICODE_TYPES'] = 'true'
25
+ test = Rake::Task['test_sqlserver_odbc']
26
+ test.invoke
27
+ end
28
+
29
+
30
+ end
31
+
@@ -0,0 +1,19 @@
1
+ # The filename begins with "aaaa" to ensure this is the first test.
2
+ require 'cases/sqlserver_helper'
3
+
4
+ class AAAACreateTablesTestSqlserver < ActiveRecord::TestCase
5
+ self.use_transactional_fixtures = false
6
+
7
+ should 'load activerecord schema' do
8
+ schema_file = "#{ACTIVERECORD_TEST_ROOT}/schema/schema.rb"
9
+ eval(File.read(schema_file))
10
+ assert true
11
+ end
12
+
13
+ should 'load sqlserver specific schema' do
14
+ sqlserver_specific_schema_file = "#{SQLSERVER_SCHEMA_ROOT}/sqlserver_specific_schema.rb"
15
+ eval(File.read(sqlserver_specific_schema_file))
16
+ assert true
17
+ end
18
+
19
+ end
@@ -0,0 +1,707 @@
1
+ require 'cases/sqlserver_helper'
2
+ require 'models/task'
3
+ require 'models/reply'
4
+ require 'models/joke'
5
+ require 'models/subscriber'
6
+
7
+ class AdapterTestSqlserver < ActiveRecord::TestCase
8
+
9
+ fixtures :tasks
10
+
11
+ def setup
12
+ @connection = ActiveRecord::Base.connection
13
+ @basic_insert_sql = "INSERT INTO [funny_jokes] ([name]) VALUES('Knock knock')"
14
+ @basic_update_sql = "UPDATE [customers] SET [address_street] = NULL WHERE [id] = 2"
15
+ @basic_select_sql = "SELECT * FROM [customers] WHERE ([customers].[id] = 1)"
16
+ end
17
+
18
+ context 'For abstract behavior' do
19
+
20
+ should 'have a 128 max #table_alias_length' do
21
+ assert @connection.table_alias_length <= 128
22
+ end
23
+
24
+ should 'raise invalid statement error' do
25
+ assert_raise(ActiveRecord::StatementInvalid) { Topic.connection.update("UPDATE XXX") }
26
+ end
27
+
28
+ should 'be our adapter_name' do
29
+ assert_equal 'SQLServer', @connection.adapter_name
30
+ end
31
+
32
+ should 'include version in inspect' do
33
+ assert_match(/version\: \d.\d.\d/,@connection.inspect)
34
+ end
35
+
36
+ should 'support migrations' do
37
+ assert @connection.supports_migrations?
38
+ end
39
+
40
+ should 'support DDL in transactions' do
41
+ assert @connection.supports_ddl_transactions?
42
+ end
43
+
44
+ should 'allow owner table name prefixs like dbo. to still allow table_exists? to return true' do
45
+ begin
46
+ assert_equal 'tasks', Task.table_name
47
+ assert Task.table_exists?
48
+ Task.table_name = 'dbo.tasks'
49
+ assert Task.table_exists?, 'Tasks table name of dbo.tasks should return true for exists.'
50
+ ensure
51
+ Task.table_name = 'tasks'
52
+ end
53
+ end
54
+
55
+ context 'for database version' do
56
+
57
+ setup do
58
+ @version_regexp = ActiveRecord::ConnectionAdapters::SQLServerAdapter::DATABASE_VERSION_REGEXP
59
+ @supported_version = ActiveRecord::ConnectionAdapters::SQLServerAdapter::SUPPORTED_VERSIONS
60
+ @sqlserver_2000_string = "Microsoft SQL Server 2000 - 8.00.2039 (Intel X86)"
61
+ @sqlserver_2005_string = "Microsoft SQL Server 2005 - 9.00.3215.00 (Intel X86)"
62
+ @sqlserver_2008_string = "Microsoft SQL Server 2008 (RTM) - 10.0.1600.22 (Intel X86)"
63
+ end
64
+
65
+ should 'return a string from #database_version that matches class regexp' do
66
+ assert_match @version_regexp, @connection.database_version
67
+ end
68
+
69
+ should 'return a 4 digit year fixnum for #database_year' do
70
+ assert_instance_of Fixnum, @connection.database_year
71
+ assert_contains @supported_version, @connection.database_year
72
+ end
73
+
74
+ should 'return true to #sqlserver_2000?' do
75
+ @connection.stubs(:database_version).returns(@sqlserver_2000_string)
76
+ assert @connection.sqlserver_2000?
77
+ end
78
+
79
+ should 'return true to #sqlserver_2005?' do
80
+ @connection.stubs(:database_version).returns(@sqlserver_2005_string)
81
+ assert @connection.sqlserver_2005?
82
+ end
83
+
84
+ should 'return true to #sqlserver_2008?' do
85
+ @connection.stubs(:database_version).returns(@sqlserver_2008_string)
86
+ assert @connection.sqlserver_2008?
87
+ end
88
+
89
+ end
90
+
91
+ context 'for #unqualify_table_name and #unqualify_db_name' do
92
+
93
+ setup do
94
+ @expected_table_name = 'baz'
95
+ @expected_db_name = 'foo'
96
+ @first_second_table_names = ['[baz]','baz','[bar].[baz]','bar.baz']
97
+ @third_table_names = ['[foo].[bar].[baz]','foo.bar.baz']
98
+ @qualifed_table_names = @first_second_table_names + @third_table_names
99
+ end
100
+
101
+ should 'return clean table_name from #unqualify_table_name' do
102
+ @qualifed_table_names.each do |qtn|
103
+ assert_equal @expected_table_name,
104
+ @connection.send(:unqualify_table_name,qtn),
105
+ "This qualifed_table_name #{qtn} did not unqualify correctly."
106
+ end
107
+ end
108
+
109
+ should 'return nil from #unqualify_db_name when table_name is less than 2 qualified' do
110
+ @first_second_table_names.each do |qtn|
111
+ assert_equal nil, @connection.send(:unqualify_db_name,qtn),
112
+ "This qualifed_table_name #{qtn} did not return nil."
113
+ end
114
+ end
115
+
116
+ should 'return clean db_name from #unqualify_db_name when table is thrid level qualified' do
117
+ @third_table_names.each do |qtn|
118
+ assert_equal @expected_db_name,
119
+ @connection.send(:unqualify_db_name,qtn),
120
+ "This qualifed_table_name #{qtn} did not unqualify the db_name correctly."
121
+ end
122
+ end
123
+
124
+ end
125
+
126
+ should 'return true to #insert_sql? for inserts only' do
127
+ assert @connection.send(:insert_sql?,'INSERT...')
128
+ assert !@connection.send(:insert_sql?,'UPDATE...')
129
+ assert !@connection.send(:insert_sql?,'SELECT...')
130
+ end
131
+
132
+ context 'for #sql_for_association_limiting?' do
133
+
134
+ should 'return false for simple selects with no GROUP BY and ORDER BY' do
135
+ assert !sql_for_association_limiting?("SELECT * FROM [posts]")
136
+ end
137
+
138
+ should 'return true to single SELECT, ideally a table/primarykey, that also has a GROUP BY and ORDER BY' do
139
+ assert sql_for_association_limiting?("SELECT [posts].id FROM...GROUP BY [posts].id ORDER BY MIN(posts.id)")
140
+ end
141
+
142
+ should 'return false to single * wildcard SELECT that also has a GROUP BY and ORDER BY' do
143
+ assert !sql_for_association_limiting?("SELECT * FROM...GROUP BY [posts].id ORDER BY MIN(posts.id)")
144
+ end
145
+
146
+ should 'return false to multiple columns in the select even when GROUP BY and ORDER BY are present' do
147
+ sql = "SELECT [accounts].credit_limit, firm_id FROM...GROUP BY firm_id ORDER BY firm_id"
148
+ assert !sql_for_association_limiting?(sql)
149
+ end
150
+
151
+ end
152
+
153
+ context 'for #get_table_name' do
154
+
155
+ should 'return quoted table name from basic INSERT, UPDATE and SELECT statements' do
156
+ assert_equal '[funny_jokes]', @connection.send(:get_table_name,@basic_insert_sql)
157
+ assert_equal '[customers]', @connection.send(:get_table_name,@basic_update_sql)
158
+ assert_equal '[customers]', @connection.send(:get_table_name,@basic_select_sql)
159
+ end
160
+
161
+ end
162
+
163
+ context 'dealing with various orders SQL snippets' do
164
+
165
+ setup do
166
+ @single_order = 'comments.id'
167
+ @single_order_with_desc = 'comments.id DESC'
168
+ @two_orders = 'comments.id, comments.post_id'
169
+ @two_orders_with_asc = 'comments.id, comments.post_id ASC'
170
+ @two_orders_with_desc_and_asc = 'comments.id DESC, comments.post_id ASC'
171
+ @two_duplicate_order_with_dif_dir = "id, id DESC"
172
+ end
173
+
174
+ should 'convert to an 2D array of column/direction arrays using #orders_and_dirs_set' do
175
+ assert_equal [['comments.id',nil]], orders_and_dirs_set('ORDER BY comments.id'), 'Needs to remove ORDER BY'
176
+ assert_equal [['comments.id',nil]], orders_and_dirs_set(@single_order)
177
+ assert_equal [['comments.id',nil],['comments.post_id',nil]], orders_and_dirs_set(@two_orders)
178
+ assert_equal [['comments.id',nil],['comments.post_id','ASC']], orders_and_dirs_set(@two_orders_with_asc)
179
+ assert_equal [['id',nil],['id','DESC']], orders_and_dirs_set(@two_duplicate_order_with_dif_dir)
180
+ end
181
+
182
+ should 'remove duplicate or maintain the same order by statements giving precedence to first using #add_order! method chain extension' do
183
+ assert_equal ' ORDER BY comments.id', add_order!(@single_order)
184
+ assert_equal ' ORDER BY comments.id DESC', add_order!(@single_order_with_desc)
185
+ assert_equal ' ORDER BY comments.id, comments.post_id', add_order!(@two_orders)
186
+ assert_equal ' ORDER BY comments.id DESC, comments.post_id ASC', add_order!(@two_orders_with_desc_and_asc)
187
+ assert_equal 'SELECT * FROM [developers] ORDER BY id', add_order!('id, developers.id DESC','SELECT * FROM [developers]')
188
+ assert_equal 'SELECT * FROM [developers] ORDER BY [developers].[id] DESC', add_order!('[developers].[id] DESC, id','SELECT * FROM [developers]')
189
+ end
190
+
191
+ should 'take all types of order options and convert them to MIN functions using #order_to_min_set' do
192
+ assert_equal 'MIN(comments.id)', order_to_min_set(@single_order)
193
+ assert_equal 'MIN(comments.id), MIN(comments.post_id)', order_to_min_set(@two_orders)
194
+ assert_equal 'MIN(comments.id) DESC', order_to_min_set(@single_order_with_desc)
195
+ assert_equal 'MIN(comments.id), MIN(comments.post_id) ASC', order_to_min_set(@two_orders_with_asc)
196
+ assert_equal 'MIN(comments.id) DESC, MIN(comments.post_id) ASC', order_to_min_set(@two_orders_with_desc_and_asc)
197
+ end
198
+
199
+ end
200
+
201
+ context 'with different language' do
202
+
203
+ teardown do
204
+ @connection.execute("SET LANGUAGE us_english") rescue nil
205
+ end
206
+
207
+ should_eventually 'do a date insertion when language is german' do
208
+ @connection.execute("SET LANGUAGE deutsch")
209
+ assert_nothing_raised do
210
+ Task.create(:starting => Time.utc(2000, 1, 31, 5, 42, 0), :ending => Date.new(2006, 12, 31))
211
+ end
212
+ end
213
+
214
+ end
215
+
216
+ context 'testing #enable_default_unicode_types configuration' do
217
+
218
+ should 'use non-unicode types when set to false' do
219
+ with_enable_default_unicode_types(false) do
220
+ if sqlserver_2000?
221
+ assert_equal 'varchar', @connection.native_string_database_type
222
+ assert_equal 'text', @connection.native_text_database_type
223
+ elsif sqlserver_2005?
224
+ assert_equal 'varchar', @connection.native_string_database_type
225
+ assert_equal 'varchar(max)', @connection.native_text_database_type
226
+ end
227
+ end
228
+ end
229
+
230
+ should 'use unicode types when set to true' do
231
+ with_enable_default_unicode_types(true) do
232
+ if sqlserver_2000?
233
+ assert_equal 'nvarchar', @connection.native_string_database_type
234
+ assert_equal 'ntext', @connection.native_text_database_type
235
+ elsif sqlserver_2005?
236
+ assert_equal 'nvarchar', @connection.native_string_database_type
237
+ assert_equal 'nvarchar(max)', @connection.native_text_database_type
238
+ end
239
+ end
240
+ end
241
+
242
+ end
243
+
244
+
245
+ end
246
+
247
+ context 'For chronic data types' do
248
+
249
+ context 'with a usec' do
250
+
251
+ setup do
252
+ @time = Time.now
253
+ @db_datetime_003 = '2012-11-08 10:24:36.003'
254
+ @db_datetime_123 = '2012-11-08 10:24:36.123'
255
+ @all_datetimes = [@db_datetime_003, @db_datetime_123]
256
+ @all_datetimes.each do |datetime|
257
+ @connection.execute("INSERT INTO [sql_server_chronics] ([datetime]) VALUES('#{datetime}')")
258
+ end
259
+ end
260
+
261
+ teardown do
262
+ @all_datetimes.each do |datetime|
263
+ @connection.execute("DELETE FROM [sql_server_chronics] WHERE [datetime] = '#{datetime}'")
264
+ end
265
+ end
266
+
267
+ context 'finding existing DB objects' do
268
+
269
+ should 'find 003 millisecond in the DB with before and after casting' do
270
+ existing_003 = SqlServerChronic.find_by_datetime!(@db_datetime_003)
271
+ assert_equal @db_datetime_003, existing_003.datetime_before_type_cast
272
+ assert_equal 3000, existing_003.datetime.usec, 'A 003 millisecond in SQL Server is 3000 microseconds'
273
+ end
274
+
275
+ should 'find 123 millisecond in the DB with before and after casting' do
276
+ existing_123 = SqlServerChronic.find_by_datetime!(@db_datetime_123)
277
+ assert_equal @db_datetime_123, existing_123.datetime_before_type_cast
278
+ assert_equal 123000, existing_123.datetime.usec, 'A 123 millisecond in SQL Server is 123000 microseconds'
279
+ end
280
+
281
+ end
282
+
283
+ context 'saving new datetime objects' do
284
+
285
+ should 'truncate 123456 usec to just 123 in the DB cast back to 123000' do
286
+ @time.stubs(:usec).returns(123456)
287
+ saved = SqlServerChronic.create!(:datetime => @time).reload
288
+ assert_equal '123', saved.datetime_before_type_cast.split('.')[1]
289
+ assert_equal 123000, saved.datetime.usec
290
+ end
291
+
292
+ should 'truncate 3001 usec to just 003 in the DB cast back to 3000' do
293
+ @time.stubs(:usec).returns(3001)
294
+ saved = SqlServerChronic.create!(:datetime => @time).reload
295
+ assert_equal '003', saved.datetime_before_type_cast.split('.')[1]
296
+ assert_equal 3000, saved.datetime.usec
297
+ end
298
+
299
+ end
300
+
301
+ end
302
+
303
+ end
304
+
305
+ context 'For identity inserts' do
306
+
307
+ setup do
308
+ @identity_insert_sql = "INSERT INTO [funny_jokes] ([id],[name]) VALUES(420,'Knock knock')"
309
+ end
310
+
311
+ should 'return quoted table_name to #query_requires_identity_insert? when INSERT sql contains id_column' do
312
+ assert_equal '[funny_jokes]', @connection.send(:query_requires_identity_insert?,@identity_insert_sql)
313
+ end
314
+
315
+ should 'return false to #query_requires_identity_insert? for normal SQL' do
316
+ [@basic_insert_sql, @basic_update_sql, @basic_select_sql].each do |sql|
317
+ assert !@connection.send(:query_requires_identity_insert?,sql), "SQL was #{sql}"
318
+ end
319
+ end
320
+
321
+ should 'find identity column using #identity_column' do
322
+ joke_id_column = Joke.columns.detect { |c| c.name == 'id' }
323
+ assert_equal joke_id_column, @connection.send(:identity_column,Joke.table_name)
324
+ end
325
+
326
+ should 'return nil when calling #identity_column for a table_name with no identity' do
327
+ assert_nil @connection.send(:identity_column,Subscriber.table_name)
328
+ end
329
+
330
+ end
331
+
332
+ context 'For Quoting' do
333
+
334
+ should 'return 1 for #quoted_true' do
335
+ assert_equal '1', @connection.quoted_true
336
+ end
337
+
338
+ should 'return 0 for #quoted_false' do
339
+ assert_equal '0', @connection.quoted_false
340
+ end
341
+
342
+ should 'not escape backslash characters like abstract adapter' do
343
+ string_with_backslashs = "\\n"
344
+ assert_equal string_with_backslashs, @connection.quote_string(string_with_backslashs)
345
+ end
346
+
347
+ should 'quote column names with brackets' do
348
+ assert_equal '[foo]', @connection.quote_column_name(:foo)
349
+ assert_equal '[foo]', @connection.quote_column_name('foo')
350
+ assert_equal '[foo].[bar]', @connection.quote_column_name('foo.bar')
351
+ end
352
+
353
+ should 'quote table names like columns' do
354
+ assert_equal '[foo].[bar]', @connection.quote_column_name('foo.bar')
355
+ assert_equal '[foo].[bar].[baz]', @connection.quote_column_name('foo.bar.baz')
356
+ end
357
+
358
+ end
359
+
360
+ context 'When disableing referential integrity' do
361
+
362
+ setup do
363
+ @parent = FkTestHasPk.create!
364
+ @member = FkTestHasFk.create!(:fk_id => @parent.id)
365
+ end
366
+
367
+ should 'NOT ALLOW by default the deletion of a referenced parent' do
368
+ FkTestHasPk.connection.disable_referential_integrity { }
369
+ assert_raise(ActiveRecord::StatementInvalid) { @parent.destroy }
370
+ end
371
+
372
+ should 'ALLOW deletion of referenced parent using #disable_referential_integrity block' do
373
+ FkTestHasPk.connection.disable_referential_integrity { @parent.destroy }
374
+ end
375
+
376
+ should 'again NOT ALLOW deletion of referenced parent after #disable_referential_integrity block' do
377
+ assert_raise(ActiveRecord::StatementInvalid) do
378
+ FkTestHasPk.connection.disable_referential_integrity { }
379
+ @parent.destroy
380
+ end
381
+ end
382
+
383
+ end
384
+
385
+ context 'For DatabaseStatements' do
386
+
387
+ context "finding out what user_options are available" do
388
+
389
+ should "run the database consistency checker useroptions command" do
390
+ @connection.expects(:select_rows).with(regexp_matches(/^dbcc\s+useroptions$/i)).returns []
391
+ @connection.user_options
392
+ end
393
+
394
+ should "return a underscored key hash with indifferent access of the results" do
395
+ @connection.expects(:select_rows).with(regexp_matches(/^dbcc\s+useroptions$/i)).returns [['some', 'thing'], ['isolation level', 'read uncommitted']]
396
+ uo = @connection.user_options
397
+ assert_equal 2, uo.keys.size
398
+ assert_equal 'thing', uo['some']
399
+ assert_equal 'thing', uo[:some]
400
+ assert_equal 'read uncommitted', uo['isolation_level']
401
+ assert_equal 'read uncommitted', uo[:isolation_level]
402
+ end
403
+
404
+ end
405
+
406
+ context "altering isolation levels" do
407
+
408
+ should "barf if the requested isolation level is not valid" do
409
+ assert_raise(ArgumentError) do
410
+ @connection.run_with_isolation_level 'INVALID ISOLATION LEVEL' do; end
411
+ end
412
+ end
413
+
414
+ context "with a valid isolation level" do
415
+
416
+ setup do
417
+ @t1 = tasks(:first_task)
418
+ @t2 = tasks(:another_task)
419
+ assert @t1, 'Tasks :first_task should be in AR fixtures'
420
+ assert @t2, 'Tasks :another_task should be in AR fixtures'
421
+ good_isolation_level = @connection.user_options[:isolation_level].blank? || @connection.user_options[:isolation_level] =~ /read committed/i
422
+ assert good_isolation_level, "User isolation level is not at a happy starting place: #{@connection.user_options[:isolation_level].inspect}"
423
+ end
424
+
425
+ should 'allow #run_with_isolation_level to not take a block to set it' do
426
+ begin
427
+ @connection.run_with_isolation_level 'READ UNCOMMITTED'
428
+ assert_match %r|read uncommitted|i, @connection.user_options[:isolation_level]
429
+ ensure
430
+ @connection.run_with_isolation_level 'READ COMMITTED'
431
+ end
432
+ end
433
+
434
+ should 'return block value using #run_with_isolation_level' do
435
+ assert_same_elements Task.find(:all), @connection.run_with_isolation_level('READ UNCOMMITTED') { Task.find(:all) }
436
+ end
437
+
438
+ should 'pass a read uncommitted isolation level test' do
439
+ assert_nil @t2.starting, 'Fixture should have this empty.'
440
+ begin
441
+ Task.transaction do
442
+ @t2.starting = Time.now
443
+ @t2.save
444
+ @dirty_t2 = @connection.run_with_isolation_level('READ UNCOMMITTED') { Task.find(@t2.id) }
445
+ raise ActiveRecord::ActiveRecordError
446
+ end
447
+ rescue
448
+ 'Do Nothing'
449
+ end
450
+ assert @dirty_t2, 'Should have a Task record from within block above.'
451
+ assert @dirty_t2.starting, 'Should have a dirty date.'
452
+ assert_nil Task.find(@t2.id).starting, 'Should be nil again from botched transaction above.'
453
+ end unless active_record_2_point_2? # Transactions in tests are a bit screwy in 2.2.
454
+
455
+ end
456
+
457
+ end
458
+
459
+ end
460
+
461
+ context 'For SchemaStatements' do
462
+
463
+ context 'returning from #type_to_sql' do
464
+
465
+ should 'create integers when no limit supplied' do
466
+ assert_equal 'integer', @connection.type_to_sql(:integer)
467
+ end
468
+
469
+ should 'create integers when limit is 4' do
470
+ assert_equal 'integer', @connection.type_to_sql(:integer, 4)
471
+ end
472
+
473
+ should 'create integers when limit is 3' do
474
+ assert_equal 'integer', @connection.type_to_sql(:integer, 3)
475
+ end
476
+
477
+ should 'create smallints when limit is less than 3' do
478
+ assert_equal 'smallint', @connection.type_to_sql(:integer, 2)
479
+ assert_equal 'smallint', @connection.type_to_sql(:integer, 1)
480
+ end
481
+
482
+ should 'create bigints when limit is greateer than 4' do
483
+ assert_equal 'bigint', @connection.type_to_sql(:integer, 5)
484
+ assert_equal 'bigint', @connection.type_to_sql(:integer, 6)
485
+ assert_equal 'bigint', @connection.type_to_sql(:integer, 7)
486
+ assert_equal 'bigint', @connection.type_to_sql(:integer, 8)
487
+ end
488
+
489
+ end
490
+
491
+ end
492
+
493
+ context 'For indexes' do
494
+
495
+ setup do
496
+ @desc_index_name = 'idx_credit_limit_test_desc'
497
+ @connection.execute "CREATE INDEX #{@desc_index_name} ON accounts (credit_limit DESC)"
498
+ end
499
+
500
+ teardown do
501
+ @connection.execute "DROP INDEX accounts.#{@desc_index_name}"
502
+ end
503
+
504
+ should 'have indexes with descending order' do
505
+ assert @connection.indexes('accounts').detect { |i| i.name == @desc_index_name }
506
+ end
507
+
508
+ end
509
+
510
+ context 'For views' do
511
+
512
+ context 'using @connection.views' do
513
+
514
+ should 'return an array' do
515
+ assert_instance_of Array, @connection.views
516
+ end
517
+
518
+ should 'find CustomersView table name' do
519
+ assert_contains @connection.views, 'customers_view'
520
+ end
521
+
522
+ should 'not contain system views' do
523
+ systables = ['sysconstraints','syssegments']
524
+ systables.each do |systable|
525
+ assert !@connection.views.include?(systable), "This systable #{systable} should not be in the views array."
526
+ end
527
+ end
528
+
529
+ should 'allow the connection.view_information method to return meta data on the view' do
530
+ view_info = @connection.view_information('customers_view')
531
+ assert_equal('customers_view', view_info['TABLE_NAME'])
532
+ assert_match(/CREATE VIEW customers_view/, view_info['VIEW_DEFINITION'])
533
+ end
534
+
535
+ should 'allow the connection.view_table_name method to return true table_name for the view' do
536
+ assert_equal 'customers', @connection.view_table_name('customers_view')
537
+ assert_equal 'topics', @connection.view_table_name('topics'), 'No view here, the same table name should come back.'
538
+ end
539
+
540
+ end
541
+
542
+ context 'used by a class for table_name' do
543
+
544
+ context 'with same column names' do
545
+
546
+ should 'have matching column objects' do
547
+ columns = ['id','name','balance']
548
+ assert !CustomersView.columns.blank?
549
+ assert_equal columns.size, CustomersView.columns.size
550
+ columns.each do |colname|
551
+ assert_instance_of ActiveRecord::ConnectionAdapters::SQLServerColumn,
552
+ CustomersView.columns_hash[colname],
553
+ "Column name #{colname.inspect} was not found in these columns #{CustomersView.columns.map(&:name).inspect}"
554
+ end
555
+ end
556
+
557
+ should 'find identity column' do
558
+ assert CustomersView.columns_hash['id'].primary
559
+ assert CustomersView.columns_hash['id'].is_identity?
560
+ end
561
+
562
+ should 'find default values' do
563
+ assert_equal 0, CustomersView.new.balance
564
+ end
565
+
566
+ should 'respond true to table_exists?' do
567
+ assert CustomersView.table_exists?
568
+ end
569
+
570
+ should 'have correct table name for all column objects' do
571
+ assert CustomersView.columns.all?{ |c| c.table_name == 'customers_view' },
572
+ CustomersView.columns.map(&:table_name).inspect
573
+ end
574
+
575
+ end
576
+
577
+ context 'with aliased column names' do
578
+
579
+ should 'have matching column objects' do
580
+ columns = ['id','pretend_null']
581
+ assert !StringDefaultsView.columns.blank?
582
+ assert_equal columns.size, StringDefaultsView.columns.size
583
+ columns.each do |colname|
584
+ assert_instance_of ActiveRecord::ConnectionAdapters::SQLServerColumn,
585
+ StringDefaultsView.columns_hash[colname],
586
+ "Column name #{colname.inspect} was not found in these columns #{StringDefaultsView.columns.map(&:name).inspect}"
587
+ end
588
+ end
589
+
590
+ should 'find identity column' do
591
+ assert StringDefaultsView.columns_hash['id'].primary
592
+ assert StringDefaultsView.columns_hash['id'].is_identity?
593
+ end
594
+
595
+ should 'find default values' do
596
+ assert_equal 'null', StringDefaultsView.new.pretend_null,
597
+ StringDefaultsView.columns_hash['pretend_null'].inspect
598
+ end
599
+
600
+ should 'respond true to table_exists?' do
601
+ assert StringDefaultsView.table_exists?
602
+ end
603
+
604
+ should 'have correct table name for all column objects' do
605
+ assert StringDefaultsView.columns.all?{ |c| c.table_name == 'string_defaults_view' },
606
+ StringDefaultsView.columns.map(&:table_name).inspect
607
+ end
608
+
609
+ end
610
+
611
+ end
612
+
613
+ context 'doing identity inserts' do
614
+
615
+ setup do
616
+ @view_insert_sql = "INSERT INTO [customers_view] ([id],[name],[balance]) VALUES (420,'Microsoft',0)"
617
+ end
618
+
619
+ should 'respond true/tablename to #query_requires_identity_insert?' do
620
+ assert_equal '[customers_view]', @connection.send(:query_requires_identity_insert?,@view_insert_sql)
621
+ end
622
+
623
+ should 'be able to do an identity insert' do
624
+ assert_nothing_raised { @connection.execute(@view_insert_sql) }
625
+ assert CustomersView.find(420)
626
+ end
627
+
628
+ end
629
+
630
+ context 'that have more than 4000 chars for their defintion' do
631
+
632
+ should 'cope with null returned for the defintion' do
633
+ assert_nothing_raised() { StringDefaultsBigView.columns }
634
+ end
635
+
636
+ should 'using alternate view defintion still be able to find real default' do
637
+ assert_equal 'null', StringDefaultsBigView.new.pretend_null,
638
+ StringDefaultsBigView.columns_hash['pretend_null'].inspect
639
+ end
640
+
641
+ end
642
+
643
+ end
644
+
645
+
646
+
647
+ private
648
+
649
+ def sql_for_association_limiting?(sql)
650
+ @connection.send :sql_for_association_limiting?, sql
651
+ end
652
+
653
+ def orders_and_dirs_set(order)
654
+ @connection.send :orders_and_dirs_set, order
655
+ end
656
+
657
+ def add_order!(order,sql='')
658
+ ActiveRecord::Base.send :add_order!, sql, order, nil
659
+ sql
660
+ end
661
+
662
+ def order_to_min_set(order)
663
+ @connection.send :order_to_min_set, order
664
+ end
665
+
666
+ def with_enable_default_unicode_types(setting)
667
+ old_setting = ActiveRecord::ConnectionAdapters::SQLServerAdapter.enable_default_unicode_types
668
+ old_text = ActiveRecord::ConnectionAdapters::SQLServerAdapter.native_text_database_type
669
+ old_string = ActiveRecord::ConnectionAdapters::SQLServerAdapter.native_string_database_type
670
+ ActiveRecord::ConnectionAdapters::SQLServerAdapter.enable_default_unicode_types = setting
671
+ ActiveRecord::ConnectionAdapters::SQLServerAdapter.native_text_database_type = nil
672
+ ActiveRecord::ConnectionAdapters::SQLServerAdapter.native_string_database_type = nil
673
+ yield
674
+ ensure
675
+ ActiveRecord::ConnectionAdapters::SQLServerAdapter.enable_default_unicode_types = old_setting
676
+ ActiveRecord::ConnectionAdapters::SQLServerAdapter.native_text_database_type = old_text
677
+ ActiveRecord::ConnectionAdapters::SQLServerAdapter.native_string_database_type = old_string
678
+ end
679
+
680
+ end
681
+
682
+
683
+ class AdapterTest < ActiveRecord::TestCase
684
+
685
+ COERCED_TESTS = [
686
+ :test_add_limit_offset_should_sanitize_sql_injection_for_limit_without_comas,
687
+ :test_add_limit_offset_should_sanitize_sql_injection_for_limit_with_comas
688
+ ]
689
+
690
+ include SqlserverCoercedTest
691
+
692
+ def test_coerced_test_add_limit_offset_should_sanitize_sql_injection_for_limit_without_comas
693
+ sql_inject = "1 select * from schema"
694
+ connection = ActiveRecord::Base.connection
695
+ assert_raise(ArgumentError) { connection.add_limit_offset!("", :limit=>sql_inject) }
696
+ assert_raise(ArgumentError) { connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7) }
697
+ end
698
+
699
+ def test_coerced_test_add_limit_offset_should_sanitize_sql_injection_for_limit_with_comas
700
+ sql_inject = "1, 7 procedure help()"
701
+ connection = ActiveRecord::Base.connection
702
+ assert_raise(ArgumentError) { connection.add_limit_offset!("", :limit=>sql_inject) }
703
+ assert_raise(ArgumentError) { connection.add_limit_offset!("", :limit=> '1 ; DROP TABLE USERS', :offset=>7) }
704
+ assert_raise(ArgumentError) { connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7) }
705
+ end
706
+
707
+ end