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,150 @@
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
+
134
+ module ActiveRecord
135
+ class Base
136
+ class << self
137
+ def add_limit!(sql, options, scope = :auto)
138
+ scope = scope(:find) if :auto == scope
139
+
140
+ if scope
141
+ options[:limit] ||= scope[:limit]
142
+ options[:offset] ||= scope[:offset]
143
+ options[:order] ||= scope[:order]
144
+ end
145
+
146
+ connection.add_limit_offset!(sql, options)
147
+ end
148
+ end
149
+ end
150
+ end
@@ -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 @@
1
+ require 'active_record/connection_adapters/sqlserver_adapter'
@@ -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,627 @@
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
+ def setup
10
+ @connection = ActiveRecord::Base.connection
11
+ @basic_insert_sql = "INSERT INTO [funny_jokes] ([name]) VALUES('Knock knock')"
12
+ @basic_update_sql = "UPDATE [customers] SET [address_street] = NULL WHERE [id] = 2"
13
+ @basic_select_sql = "SELECT * FROM [customers] WHERE ([customers].[id] = 1)"
14
+ end
15
+
16
+ context 'For abstract behavior' do
17
+
18
+ should 'have a 128 max #table_alias_length' do
19
+ assert @connection.table_alias_length <= 128
20
+ end
21
+
22
+ should 'raise invalid statement error' do
23
+ assert_raise(ActiveRecord::StatementInvalid) { Topic.connection.update("UPDATE XXX") }
24
+ end
25
+
26
+ should 'be our adapter_name' do
27
+ assert_equal 'SQLServer', @connection.adapter_name
28
+ end
29
+
30
+ should 'include version in inspect' do
31
+ assert_match(/version\: \d.\d.\d/,@connection.inspect)
32
+ end
33
+
34
+ should 'support migrations' do
35
+ assert @connection.supports_migrations?
36
+ end
37
+
38
+ should 'support DDL in transactions' do
39
+ assert @connection.supports_ddl_transactions?
40
+ end
41
+
42
+ should 'allow owner table name prefixs like dbo. to still allow table_exists? to return true' do
43
+ begin
44
+ assert_equal 'tasks', Task.table_name
45
+ assert Task.table_exists?
46
+ Task.table_name = 'dbo.tasks'
47
+ assert Task.table_exists?, 'Tasks table name of dbo.tasks should return true for exists.'
48
+ ensure
49
+ Task.table_name = 'tasks'
50
+ end
51
+ end
52
+
53
+ context 'for database version' do
54
+
55
+ setup do
56
+ @version_regexp = ActiveRecord::ConnectionAdapters::SQLServerAdapter::DATABASE_VERSION_REGEXP
57
+ @supported_version = ActiveRecord::ConnectionAdapters::SQLServerAdapter::SUPPORTED_VERSIONS
58
+ @sqlserver_2000_string = "Microsoft SQL Server 2000 - 8.00.2039 (Intel X86)"
59
+ @sqlserver_2005_string = "Microsoft SQL Server 2005 - 9.00.3215.00 (Intel X86)"
60
+ end
61
+
62
+ should 'return a string from #database_version that matches class regexp' do
63
+ assert_match @version_regexp, @connection.database_version
64
+ end
65
+
66
+ should 'return a 4 digit year fixnum for #database_year' do
67
+ assert_instance_of Fixnum, @connection.database_year
68
+ assert_contains @supported_version, @connection.database_year
69
+ end
70
+
71
+ should 'return true to #sqlserver_2000?' do
72
+ @connection.stubs(:database_version).returns(@sqlserver_2000_string)
73
+ assert @connection.sqlserver_2000?
74
+ end
75
+
76
+ should 'return true to #sqlserver_2005?' do
77
+ @connection.stubs(:database_version).returns(@sqlserver_2005_string)
78
+ assert @connection.sqlserver_2005?
79
+ end
80
+
81
+ end
82
+
83
+ context 'for #unqualify_table_name and #unqualify_db_name' do
84
+
85
+ setup do
86
+ @expected_table_name = 'baz'
87
+ @expected_db_name = 'foo'
88
+ @first_second_table_names = ['[baz]','baz','[bar].[baz]','bar.baz']
89
+ @third_table_names = ['[foo].[bar].[baz]','foo.bar.baz']
90
+ @qualifed_table_names = @first_second_table_names + @third_table_names
91
+ end
92
+
93
+ should 'return clean table_name from #unqualify_table_name' do
94
+ @qualifed_table_names.each do |qtn|
95
+ assert_equal @expected_table_name,
96
+ @connection.send(:unqualify_table_name,qtn),
97
+ "This qualifed_table_name #{qtn} did not unqualify correctly."
98
+ end
99
+ end
100
+
101
+ should 'return nil from #unqualify_db_name when table_name is less than 2 qualified' do
102
+ @first_second_table_names.each do |qtn|
103
+ assert_equal nil, @connection.send(:unqualify_db_name,qtn),
104
+ "This qualifed_table_name #{qtn} did not return nil."
105
+ end
106
+ end
107
+
108
+ should 'return clean db_name from #unqualify_db_name when table is thrid level qualified' do
109
+ @third_table_names.each do |qtn|
110
+ assert_equal @expected_db_name,
111
+ @connection.send(:unqualify_db_name,qtn),
112
+ "This qualifed_table_name #{qtn} did not unqualify the db_name correctly."
113
+ end
114
+ end
115
+
116
+ end
117
+
118
+ should 'return true to #insert_sql? for inserts only' do
119
+ assert @connection.send(:insert_sql?,'INSERT...')
120
+ assert !@connection.send(:insert_sql?,'UPDATE...')
121
+ assert !@connection.send(:insert_sql?,'SELECT...')
122
+ end
123
+
124
+ context 'for #sql_for_association_limiting?' do
125
+
126
+ should 'return false for simple selects with no GROUP BY and ORDER BY' do
127
+ assert !sql_for_association_limiting?("SELECT * FROM [posts]")
128
+ end
129
+
130
+ should 'return true to single SELECT, ideally a table/primarykey, that also has a GROUP BY and ORDER BY' do
131
+ assert sql_for_association_limiting?("SELECT [posts].id FROM...GROUP BY [posts].id ORDER BY MIN(posts.id)")
132
+ end
133
+
134
+ should 'return false to single * wildcard SELECT that also has a GROUP BY and ORDER BY' do
135
+ assert !sql_for_association_limiting?("SELECT * FROM...GROUP BY [posts].id ORDER BY MIN(posts.id)")
136
+ end
137
+
138
+ should 'return false to multiple columns in the select even when GROUP BY and ORDER BY are present' do
139
+ sql = "SELECT [accounts].credit_limit, firm_id FROM...GROUP BY firm_id ORDER BY firm_id"
140
+ assert !sql_for_association_limiting?(sql)
141
+ end
142
+
143
+ end
144
+
145
+ context 'for #get_table_name' do
146
+
147
+ should 'return quoted table name from basic INSERT, UPDATE and SELECT statements' do
148
+ assert_equal '[funny_jokes]', @connection.send(:get_table_name,@basic_insert_sql)
149
+ assert_equal '[customers]', @connection.send(:get_table_name,@basic_update_sql)
150
+ assert_equal '[customers]', @connection.send(:get_table_name,@basic_select_sql)
151
+ end
152
+
153
+ end
154
+
155
+ context 'dealing with various orders SQL snippets' do
156
+
157
+ setup do
158
+ @single_order = 'comments.id'
159
+ @single_order_with_desc = 'comments.id DESC'
160
+ @two_orders = 'comments.id, comments.post_id'
161
+ @two_orders_with_asc = 'comments.id, comments.post_id ASC'
162
+ @two_orders_with_desc_and_asc = 'comments.id DESC, comments.post_id ASC'
163
+ @two_duplicate_order_with_dif_dir = "id, id DESC"
164
+ end
165
+
166
+ should 'convert to an 2D array of column/direction arrays using #orders_and_dirs_set' do
167
+ assert_equal [['comments.id',nil]], orders_and_dirs_set('ORDER BY comments.id'), 'Needs to remove ORDER BY'
168
+ assert_equal [['comments.id',nil]], orders_and_dirs_set(@single_order)
169
+ assert_equal [['comments.id',nil],['comments.post_id',nil]], orders_and_dirs_set(@two_orders)
170
+ assert_equal [['comments.id',nil],['comments.post_id','ASC']], orders_and_dirs_set(@two_orders_with_asc)
171
+ assert_equal [['id',nil],['id','DESC']], orders_and_dirs_set(@two_duplicate_order_with_dif_dir)
172
+ end
173
+
174
+ should 'remove duplicate or maintain the same order by statements giving precedence to first using #add_order! method chain extension' do
175
+ assert_equal ' ORDER BY comments.id', add_order!(@single_order)
176
+ assert_equal ' ORDER BY comments.id DESC', add_order!(@single_order_with_desc)
177
+ assert_equal ' ORDER BY comments.id, comments.post_id', add_order!(@two_orders)
178
+ assert_equal ' ORDER BY comments.id DESC, comments.post_id ASC', add_order!(@two_orders_with_desc_and_asc)
179
+ assert_equal 'SELECT * FROM [developers] ORDER BY id', add_order!('id, developers.id DESC','SELECT * FROM [developers]')
180
+ assert_equal 'SELECT * FROM [developers] ORDER BY [developers].[id] DESC', add_order!('[developers].[id] DESC, id','SELECT * FROM [developers]')
181
+ end
182
+
183
+ should 'take all types of order options and convert them to MIN functions using #order_to_min_set' do
184
+ assert_equal 'MIN(comments.id)', order_to_min_set(@single_order)
185
+ assert_equal 'MIN(comments.id), MIN(comments.post_id)', order_to_min_set(@two_orders)
186
+ assert_equal 'MIN(comments.id) DESC', order_to_min_set(@single_order_with_desc)
187
+ assert_equal 'MIN(comments.id), MIN(comments.post_id) ASC', order_to_min_set(@two_orders_with_asc)
188
+ assert_equal 'MIN(comments.id) DESC, MIN(comments.post_id) ASC', order_to_min_set(@two_orders_with_desc_and_asc)
189
+ end
190
+
191
+ end
192
+
193
+ context 'with different language' do
194
+
195
+ teardown do
196
+ @connection.execute("SET LANGUAGE us_english") rescue nil
197
+ end
198
+
199
+ should_eventually 'do a date insertion when language is german' do
200
+ @connection.execute("SET LANGUAGE deutsch")
201
+ assert_nothing_raised do
202
+ Task.create(:starting => Time.utc(2000, 1, 31, 5, 42, 0), :ending => Date.new(2006, 12, 31))
203
+ end
204
+ end
205
+
206
+ end
207
+
208
+ context 'testing #enable_default_unicode_types configuration' do
209
+
210
+ should 'use non-unicode types when set to false' do
211
+ with_enable_default_unicode_types(false) do
212
+ if sqlserver_2000?
213
+ assert_equal 'varchar', @connection.native_string_database_type
214
+ assert_equal 'text', @connection.native_text_database_type
215
+ elsif sqlserver_2005?
216
+ assert_equal 'varchar', @connection.native_string_database_type
217
+ assert_equal 'varchar(max)', @connection.native_text_database_type
218
+ end
219
+ end
220
+ end
221
+
222
+ should 'use unicode types when set to true' do
223
+ with_enable_default_unicode_types(true) do
224
+ if sqlserver_2000?
225
+ assert_equal 'nvarchar', @connection.native_string_database_type
226
+ assert_equal 'ntext', @connection.native_text_database_type
227
+ elsif sqlserver_2005?
228
+ assert_equal 'nvarchar', @connection.native_string_database_type
229
+ assert_equal 'nvarchar(max)', @connection.native_text_database_type
230
+ end
231
+ end
232
+ end
233
+
234
+ end
235
+
236
+
237
+ end
238
+
239
+ context 'For chronic data types' do
240
+
241
+ context 'with a usec' do
242
+
243
+ setup do
244
+ @time = Time.now
245
+ @db_datetime_003 = '2012-11-08 10:24:36.003'
246
+ @db_datetime_123 = '2012-11-08 10:24:36.123'
247
+ @all_datetimes = [@db_datetime_003, @db_datetime_123]
248
+ @all_datetimes.each do |datetime|
249
+ @connection.execute("INSERT INTO [sql_server_chronics] ([datetime]) VALUES('#{datetime}')")
250
+ end
251
+ end
252
+
253
+ teardown do
254
+ @all_datetimes.each do |datetime|
255
+ @connection.execute("DELETE FROM [sql_server_chronics] WHERE [datetime] = '#{datetime}'")
256
+ end
257
+ end
258
+
259
+ context 'finding existing DB objects' do
260
+
261
+ should 'find 003 millisecond in the DB with before and after casting' do
262
+ existing_003 = SqlServerChronic.find_by_datetime!(@db_datetime_003)
263
+ assert_equal @db_datetime_003, existing_003.datetime_before_type_cast
264
+ assert_equal 3000, existing_003.datetime.usec, 'A 003 millisecond in SQL Server is 3000 microseconds'
265
+ end
266
+
267
+ should 'find 123 millisecond in the DB with before and after casting' do
268
+ existing_123 = SqlServerChronic.find_by_datetime!(@db_datetime_123)
269
+ assert_equal @db_datetime_123, existing_123.datetime_before_type_cast
270
+ assert_equal 123000, existing_123.datetime.usec, 'A 123 millisecond in SQL Server is 123000 microseconds'
271
+ end
272
+
273
+ end
274
+
275
+ context 'saving new datetime objects' do
276
+
277
+ should 'truncate 123456 usec to just 123 in the DB cast back to 123000' do
278
+ @time.stubs(:usec).returns(123456)
279
+ saved = SqlServerChronic.create!(:datetime => @time).reload
280
+ assert_equal '123', saved.datetime_before_type_cast.split('.')[1]
281
+ assert_equal 123000, saved.datetime.usec
282
+ end
283
+
284
+ should 'truncate 3001 usec to just 003 in the DB cast back to 3000' do
285
+ @time.stubs(:usec).returns(3001)
286
+ saved = SqlServerChronic.create!(:datetime => @time).reload
287
+ assert_equal '003', saved.datetime_before_type_cast.split('.')[1]
288
+ assert_equal 3000, saved.datetime.usec
289
+ end
290
+
291
+ end
292
+
293
+ end
294
+
295
+ end
296
+
297
+ context 'For identity inserts' do
298
+
299
+ setup do
300
+ @identity_insert_sql = "INSERT INTO [funny_jokes] ([id],[name]) VALUES(420,'Knock knock')"
301
+ end
302
+
303
+ should 'return quoted table_name to #query_requires_identity_insert? when INSERT sql contains id_column' do
304
+ assert_equal '[funny_jokes]', @connection.send(:query_requires_identity_insert?,@identity_insert_sql)
305
+ end
306
+
307
+ should 'return false to #query_requires_identity_insert? for normal SQL' do
308
+ [@basic_insert_sql, @basic_update_sql, @basic_select_sql].each do |sql|
309
+ assert !@connection.send(:query_requires_identity_insert?,sql), "SQL was #{sql}"
310
+ end
311
+ end
312
+
313
+ should 'find identity column using #identity_column' do
314
+ joke_id_column = Joke.columns.detect { |c| c.name == 'id' }
315
+ assert_equal joke_id_column, @connection.send(:identity_column,Joke.table_name)
316
+ end
317
+
318
+ should 'return nil when calling #identity_column for a table_name with no identity' do
319
+ assert_nil @connection.send(:identity_column,Subscriber.table_name)
320
+ end
321
+
322
+ end
323
+
324
+ context 'For Quoting' do
325
+
326
+ should 'return 1 for #quoted_true' do
327
+ assert_equal '1', @connection.quoted_true
328
+ end
329
+
330
+ should 'return 0 for #quoted_false' do
331
+ assert_equal '0', @connection.quoted_false
332
+ end
333
+
334
+ should 'not escape backslash characters like abstract adapter' do
335
+ string_with_backslashs = "\\n"
336
+ assert_equal string_with_backslashs, @connection.quote_string(string_with_backslashs)
337
+ end
338
+
339
+ should 'quote column names with brackets' do
340
+ assert_equal '[foo]', @connection.quote_column_name(:foo)
341
+ assert_equal '[foo]', @connection.quote_column_name('foo')
342
+ assert_equal '[foo].[bar]', @connection.quote_column_name('foo.bar')
343
+ end
344
+
345
+ should 'quote table names like columns' do
346
+ assert_equal '[foo].[bar]', @connection.quote_column_name('foo.bar')
347
+ assert_equal '[foo].[bar].[baz]', @connection.quote_column_name('foo.bar.baz')
348
+ end
349
+
350
+ end
351
+
352
+ context 'When disableing referential integrity' do
353
+
354
+ setup do
355
+ @parent = FkTestHasPk.create!
356
+ @member = FkTestHasFk.create!(:fk_id => @parent.id)
357
+ end
358
+
359
+ should 'NOT ALLOW by default the deletion of a referenced parent' do
360
+ FkTestHasPk.connection.disable_referential_integrity { }
361
+ assert_raise(ActiveRecord::StatementInvalid) { @parent.destroy }
362
+ end
363
+
364
+ should 'ALLOW deletion of referenced parent using #disable_referential_integrity block' do
365
+ FkTestHasPk.connection.disable_referential_integrity { @parent.destroy }
366
+ end
367
+
368
+ should 'again NOT ALLOW deletion of referenced parent after #disable_referential_integrity block' do
369
+ assert_raise(ActiveRecord::StatementInvalid) do
370
+ FkTestHasPk.connection.disable_referential_integrity { }
371
+ @parent.destroy
372
+ end
373
+ end
374
+
375
+ end
376
+
377
+ context 'For DatabaseStatements' do
378
+
379
+ end
380
+
381
+ context 'For SchemaStatements' do
382
+
383
+ context 'returning from #type_to_sql' do
384
+
385
+ should 'create integers when no limit supplied' do
386
+ assert_equal 'integer', @connection.type_to_sql(:integer)
387
+ end
388
+
389
+ should 'create integers when limit is 4' do
390
+ assert_equal 'integer', @connection.type_to_sql(:integer, 4)
391
+ end
392
+
393
+ should 'create integers when limit is 3' do
394
+ assert_equal 'integer', @connection.type_to_sql(:integer, 3)
395
+ end
396
+
397
+ should 'create smallints when limit is less than 3' do
398
+ assert_equal 'smallint', @connection.type_to_sql(:integer, 2)
399
+ assert_equal 'smallint', @connection.type_to_sql(:integer, 1)
400
+ end
401
+
402
+ should 'create bigints when limit is greateer than 4' do
403
+ assert_equal 'bigint', @connection.type_to_sql(:integer, 5)
404
+ assert_equal 'bigint', @connection.type_to_sql(:integer, 6)
405
+ assert_equal 'bigint', @connection.type_to_sql(:integer, 7)
406
+ assert_equal 'bigint', @connection.type_to_sql(:integer, 8)
407
+ end
408
+
409
+ end
410
+
411
+ end
412
+
413
+ context 'For indexes' do
414
+
415
+ setup do
416
+ @desc_index_name = 'idx_credit_limit_test_desc'
417
+ @connection.execute "CREATE INDEX #{@desc_index_name} ON accounts (credit_limit DESC)"
418
+ end
419
+
420
+ teardown do
421
+ @connection.execute "DROP INDEX accounts.#{@desc_index_name}"
422
+ end
423
+
424
+ should 'have indexes with descending order' do
425
+ assert @connection.indexes('accounts').detect { |i| i.name == @desc_index_name }
426
+ end
427
+
428
+ end
429
+
430
+ context 'For views' do
431
+
432
+ context 'using @connection.views' do
433
+
434
+ should 'return an array' do
435
+ assert_instance_of Array, @connection.views
436
+ end
437
+
438
+ should 'find CustomersView table name' do
439
+ assert_contains @connection.views, 'customers_view'
440
+ end
441
+
442
+ should 'not contain system views' do
443
+ systables = ['sysconstraints','syssegments']
444
+ systables.each do |systable|
445
+ assert !@connection.views.include?(systable), "This systable #{systable} should not be in the views array."
446
+ end
447
+ end
448
+
449
+ should 'allow the connection.view_information method to return meta data on the view' do
450
+ view_info = @connection.view_information('customers_view')
451
+ assert_equal('customers_view', view_info['TABLE_NAME'])
452
+ assert_match(/CREATE VIEW customers_view/, view_info['VIEW_DEFINITION'])
453
+ end
454
+
455
+ should 'allow the connection.view_table_name method to return true table_name for the view' do
456
+ assert_equal 'customers', @connection.view_table_name('customers_view')
457
+ assert_equal 'topics', @connection.view_table_name('topics'), 'No view here, the same table name should come back.'
458
+ end
459
+
460
+ end
461
+
462
+ context 'used by a class for table_name' do
463
+
464
+ context 'with same column names' do
465
+
466
+ should 'have matching column objects' do
467
+ columns = ['id','name','balance']
468
+ assert !CustomersView.columns.blank?
469
+ assert_equal columns.size, CustomersView.columns.size
470
+ columns.each do |colname|
471
+ assert_instance_of ActiveRecord::ConnectionAdapters::SQLServerColumn,
472
+ CustomersView.columns_hash[colname],
473
+ "Column name #{colname.inspect} was not found in these columns #{CustomersView.columns.map(&:name).inspect}"
474
+ end
475
+ end
476
+
477
+ should 'find identity column' do
478
+ assert CustomersView.columns_hash['id'].primary
479
+ assert CustomersView.columns_hash['id'].is_identity?
480
+ end
481
+
482
+ should 'find default values' do
483
+ assert_equal 0, CustomersView.new.balance
484
+ end
485
+
486
+ should 'respond true to table_exists?' do
487
+ assert CustomersView.table_exists?
488
+ end
489
+
490
+ should 'have correct table name for all column objects' do
491
+ assert CustomersView.columns.all?{ |c| c.table_name == 'customers_view' },
492
+ CustomersView.columns.map(&:table_name).inspect
493
+ end
494
+
495
+ end
496
+
497
+ context 'with aliased column names' do
498
+
499
+ should 'have matching column objects' do
500
+ columns = ['id','pretend_null']
501
+ assert !StringDefaultsView.columns.blank?
502
+ assert_equal columns.size, StringDefaultsView.columns.size
503
+ columns.each do |colname|
504
+ assert_instance_of ActiveRecord::ConnectionAdapters::SQLServerColumn,
505
+ StringDefaultsView.columns_hash[colname],
506
+ "Column name #{colname.inspect} was not found in these columns #{StringDefaultsView.columns.map(&:name).inspect}"
507
+ end
508
+ end
509
+
510
+ should 'find identity column' do
511
+ assert StringDefaultsView.columns_hash['id'].primary
512
+ assert StringDefaultsView.columns_hash['id'].is_identity?
513
+ end
514
+
515
+ should 'find default values' do
516
+ assert_equal 'null', StringDefaultsView.new.pretend_null,
517
+ StringDefaultsView.columns_hash['pretend_null'].inspect
518
+ end
519
+
520
+ should 'respond true to table_exists?' do
521
+ assert StringDefaultsView.table_exists?
522
+ end
523
+
524
+ should 'have correct table name for all column objects' do
525
+ assert StringDefaultsView.columns.all?{ |c| c.table_name == 'string_defaults_view' },
526
+ StringDefaultsView.columns.map(&:table_name).inspect
527
+ end
528
+
529
+ end
530
+
531
+ end
532
+
533
+ context 'doing identity inserts' do
534
+
535
+ setup do
536
+ @view_insert_sql = "INSERT INTO [customers_view] ([id],[name],[balance]) VALUES (420,'Microsoft',0)"
537
+ end
538
+
539
+ should 'respond true/tablename to #query_requires_identity_insert?' do
540
+ assert_equal '[customers_view]', @connection.send(:query_requires_identity_insert?,@view_insert_sql)
541
+ end
542
+
543
+ should 'be able to do an identity insert' do
544
+ assert_nothing_raised { @connection.execute(@view_insert_sql) }
545
+ assert CustomersView.find(420)
546
+ end
547
+
548
+ end
549
+
550
+ context 'that have more than 4000 chars for their defintion' do
551
+
552
+ should 'cope with null returned for the defintion' do
553
+ assert_nothing_raised() { StringDefaultsBigView.columns }
554
+ end
555
+
556
+ should 'using alternate view defintion still be able to find real default' do
557
+ assert_equal 'null', StringDefaultsBigView.new.pretend_null,
558
+ StringDefaultsBigView.columns_hash['pretend_null'].inspect
559
+ end
560
+
561
+ end
562
+
563
+ end
564
+
565
+
566
+
567
+ private
568
+
569
+ def sql_for_association_limiting?(sql)
570
+ @connection.send :sql_for_association_limiting?, sql
571
+ end
572
+
573
+ def orders_and_dirs_set(order)
574
+ @connection.send :orders_and_dirs_set, order
575
+ end
576
+
577
+ def add_order!(order,sql='')
578
+ ActiveRecord::Base.send :add_order!, sql, order, nil
579
+ sql
580
+ end
581
+
582
+ def order_to_min_set(order)
583
+ @connection.send :order_to_min_set, order
584
+ end
585
+
586
+ def with_enable_default_unicode_types(setting)
587
+ old_setting = ActiveRecord::ConnectionAdapters::SQLServerAdapter.enable_default_unicode_types
588
+ old_text = ActiveRecord::ConnectionAdapters::SQLServerAdapter.native_text_database_type
589
+ old_string = ActiveRecord::ConnectionAdapters::SQLServerAdapter.native_string_database_type
590
+ ActiveRecord::ConnectionAdapters::SQLServerAdapter.enable_default_unicode_types = setting
591
+ ActiveRecord::ConnectionAdapters::SQLServerAdapter.native_text_database_type = nil
592
+ ActiveRecord::ConnectionAdapters::SQLServerAdapter.native_string_database_type = nil
593
+ yield
594
+ ensure
595
+ ActiveRecord::ConnectionAdapters::SQLServerAdapter.enable_default_unicode_types = old_setting
596
+ ActiveRecord::ConnectionAdapters::SQLServerAdapter.native_text_database_type = old_text
597
+ ActiveRecord::ConnectionAdapters::SQLServerAdapter.native_string_database_type = old_string
598
+ end
599
+
600
+ end
601
+
602
+
603
+ class AdapterTest < ActiveRecord::TestCase
604
+
605
+ COERCED_TESTS = [
606
+ :test_add_limit_offset_should_sanitize_sql_injection_for_limit_without_comas,
607
+ :test_add_limit_offset_should_sanitize_sql_injection_for_limit_with_comas
608
+ ]
609
+
610
+ include SqlserverCoercedTest
611
+
612
+ def test_coerced_test_add_limit_offset_should_sanitize_sql_injection_for_limit_without_comas
613
+ sql_inject = "1 select * from schema"
614
+ connection = ActiveRecord::Base.connection
615
+ assert_raise(ArgumentError) { connection.add_limit_offset!("", :limit=>sql_inject) }
616
+ assert_raise(ArgumentError) { connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7) }
617
+ end
618
+
619
+ def test_coerced_test_add_limit_offset_should_sanitize_sql_injection_for_limit_with_comas
620
+ sql_inject = "1, 7 procedure help()"
621
+ connection = ActiveRecord::Base.connection
622
+ assert_raise(ArgumentError) { connection.add_limit_offset!("", :limit=>sql_inject) }
623
+ assert_raise(ArgumentError) { connection.add_limit_offset!("", :limit=> '1 ; DROP TABLE USERS', :offset=>7) }
624
+ assert_raise(ArgumentError) { connection.add_limit_offset!("", :limit=>sql_inject, :offset=>7) }
625
+ end
626
+
627
+ end