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,42 @@
1
+ require 'cases/sqlserver_helper'
2
+ require 'models/author'
3
+ require 'models/post'
4
+ require 'models/comment'
5
+
6
+ class EagerAssociationTestSqlserver < ActiveRecord::TestCase
7
+ end
8
+
9
+ class EagerAssociationTest < ActiveRecord::TestCase
10
+
11
+ COERCED_TESTS = [
12
+ :test_count_with_include,
13
+ :test_eager_with_has_many_and_limit_and_high_offset_and_multiple_array_conditions,
14
+ :test_eager_with_has_many_and_limit_and_high_offset_and_multiple_hash_conditions
15
+ ]
16
+
17
+ include SqlserverCoercedTest
18
+
19
+ fixtures :authors, :posts, :comments
20
+
21
+ def test_coerced_test_count_with_include
22
+ assert_equal 3, authors(:david).posts_with_comments.count(:conditions => "len(comments.body) > 15")
23
+ end
24
+
25
+ def test_coerced_eager_with_has_many_and_limit_and_high_offset_and_multiple_array_conditions
26
+ assert_queries(2) do
27
+ posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10,
28
+ :conditions => [ "authors.name = ? and comments.body = ?", 'David', 'go crazy' ])
29
+ assert_equal 0, posts.size
30
+ end
31
+ end
32
+
33
+ def test_coerced_eager_with_has_many_and_limit_and_high_offset_and_multiple_hash_conditions
34
+ assert_queries(2) do
35
+ posts = Post.find(:all, :include => [ :author, :comments ], :limit => 2, :offset => 10,
36
+ :conditions => { 'authors.name' => 'David', 'comments.body' => 'go crazy' })
37
+ assert_equal 0, posts.size
38
+ end
39
+ end unless active_record_2_point_2?
40
+
41
+
42
+ end
@@ -0,0 +1,33 @@
1
+ require 'cases/sqlserver_helper'
2
+
3
+ class ExecuteProcedureTestSqlserver < ActiveRecord::TestCase
4
+
5
+ def setup
6
+ @klass = ActiveRecord::Base
7
+ end
8
+
9
+ should 'execute a simple procedure' do
10
+ tables = @klass.execute_procedure :sp_tables
11
+ assert_instance_of Array, tables
12
+ assert_instance_of HashWithIndifferentAccess, tables.first
13
+ end
14
+
15
+ should 'take parameter arguments' do
16
+ tables = @klass.execute_procedure :sp_tables, 'sql_server_chronics'
17
+ table_info = tables.first
18
+ assert_equal 1, tables.size
19
+ assert_equal 'activerecord_unittest', table_info[:TABLE_QUALIFIER], "Table Info: #{table_info.inspect}"
20
+ assert_equal 'TABLE', table_info[:TABLE_TYPE], "Table Info: #{table_info.inspect}"
21
+ end
22
+
23
+ should 'quote bind vars correctly' do
24
+ assert_sql(/EXEC sp_tables '%sql_server%', NULL, NULL, NULL, 1/) do
25
+ @klass.execute_procedure :sp_tables, '%sql_server%', nil, nil, nil, true
26
+ end if sqlserver_2005? || sqlserver_2008?
27
+ assert_sql(/EXEC sp_tables '%sql_server%', NULL, NULL, NULL/) do
28
+ @klass.execute_procedure :sp_tables, '%sql_server%', nil, nil, nil
29
+ end if sqlserver_2000?
30
+ end
31
+
32
+
33
+ end
@@ -0,0 +1,28 @@
1
+ require 'cases/sqlserver_helper'
2
+ require 'models/company'
3
+
4
+ class InheritanceTestSqlserver < ActiveRecord::TestCase
5
+ end
6
+
7
+ class InheritanceTest < ActiveRecord::TestCase
8
+
9
+ COERCED_TESTS = [
10
+ :test_eager_load_belongs_to_primary_key_quoting,
11
+ :test_a_bad_type_column
12
+ ]
13
+
14
+ include SqlserverCoercedTest
15
+
16
+ def test_coerced_test_eager_load_belongs_to_primary_key_quoting
17
+ assert_sql(/\(\[companies\].\[id\] = 1\)/) do
18
+ Account.find(1, :include => :firm)
19
+ end
20
+ end
21
+
22
+ def test_coerced_test_a_bad_type_column
23
+ Company.connection.insert "INSERT INTO [companies] ([id], #{QUOTED_TYPE}, [name]) VALUES(100, 'bad_class!', 'Not happening')"
24
+ assert_raises(ActiveRecord::SubclassNotFound) { Company.find(100) }
25
+ end
26
+
27
+
28
+ end
@@ -0,0 +1,28 @@
1
+ require 'cases/sqlserver_helper'
2
+ require 'models/developer'
3
+
4
+ class MethodScopingTestSqlServer < ActiveRecord::TestCase
5
+ end
6
+
7
+ class NestedScopingTest < ActiveRecord::TestCase
8
+
9
+ COERCED_TESTS = [:test_merged_scoped_find]
10
+
11
+ include SqlserverCoercedTest
12
+
13
+ fixtures :developers
14
+
15
+ def test_coerced_test_merged_scoped_find
16
+ poor_jamis = developers(:poor_jamis)
17
+ Developer.with_scope(:find => { :conditions => "salary < 100000" }) do
18
+ Developer.with_scope(:find => { :offset => 1, :order => 'id asc' }) do
19
+ assert_sql /ORDER BY id ASC/ do
20
+ assert_equal(poor_jamis, Developer.find(:first, :order => 'id asc'))
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ end
27
+
28
+
@@ -0,0 +1,93 @@
1
+ require 'cases/sqlserver_helper'
2
+ require 'models/person'
3
+
4
+ class MigrationTestSqlserver < ActiveRecord::TestCase
5
+
6
+ context 'For transactions' do
7
+
8
+ setup do
9
+ @connection = ActiveRecord::Base.connection
10
+ @trans_test_table1 = 'sqlserver_trans_table1'
11
+ @trans_test_table2 = 'sqlserver_trans_table2'
12
+ @trans_tables = [@trans_test_table1,@trans_test_table2]
13
+ end
14
+
15
+ teardown do
16
+ @trans_tables.each do |table_name|
17
+ ActiveRecord::Migration.drop_table(table_name) if @connection.tables.include?(table_name)
18
+ end
19
+ end
20
+
21
+ should 'not create a tables if error in migrations' do
22
+ begin
23
+ ActiveRecord::Migrator.up(SQLSERVER_MIGRATIONS_ROOT+'/transaction_table')
24
+ rescue Exception => e
25
+ assert_match %r|this and all later migrations canceled|, e.message
26
+ end
27
+ assert_does_not_contain @trans_test_table1, @connection.tables
28
+ assert_does_not_contain @trans_test_table2, @connection.tables
29
+ end
30
+
31
+ end
32
+
33
+
34
+ end
35
+
36
+
37
+ class MigrationTest < ActiveRecord::TestCase
38
+
39
+ COERCED_TESTS = [:test_add_column_not_null_without_default]
40
+
41
+ include SqlserverCoercedTest
42
+
43
+ def test_coerced_test_add_column_not_null_without_default
44
+ Person.connection.create_table :testings do |t|
45
+ t.column :foo, :string
46
+ t.column :bar, :string, :null => false
47
+ end
48
+ assert_raises(ActiveRecord::StatementInvalid) do
49
+ Person.connection.execute "INSERT INTO [testings] ([foo], [bar]) VALUES ('hello', NULL)"
50
+ end
51
+ ensure
52
+ Person.connection.drop_table :testings rescue nil
53
+ end
54
+
55
+ end
56
+
57
+ class ChangeTableMigrationsTest < ActiveRecord::TestCase
58
+
59
+ COERCED_TESTS = [:test_string_creates_string_column]
60
+
61
+ include SqlserverCoercedTest
62
+
63
+ def setup
64
+ @connection = Person.connection
65
+ @connection.create_table :delete_me, :force => true do |t|
66
+ end
67
+ end
68
+
69
+ def teardown
70
+ @connection.drop_table :delete_me rescue nil
71
+ end
72
+
73
+ def test_coerced_string_creates_string_column
74
+ with_sqlserver_change_table do |t|
75
+ @connection.expects(:add_column).with(:delete_me, :foo, sqlserver_string_column, {})
76
+ @connection.expects(:add_column).with(:delete_me, :bar, sqlserver_string_column, {})
77
+ t.string :foo, :bar
78
+ end
79
+ end
80
+
81
+ protected
82
+
83
+ def with_sqlserver_change_table
84
+ @connection.change_table :delete_me do |t|
85
+ yield t
86
+ end
87
+ end
88
+
89
+ def sqlserver_string_column
90
+ "#{@connection.native_string_database_type}(255)"
91
+ end
92
+
93
+ end
@@ -0,0 +1,108 @@
1
+ require 'cases/sqlserver_helper'
2
+ require 'models/book'
3
+
4
+ class OffsetAndLimitTestSqlserver < ActiveRecord::TestCase
5
+
6
+ class Account < ActiveRecord::Base; end
7
+
8
+ def setup
9
+ @connection = ActiveRecord::Base.connection
10
+ end
11
+
12
+ context 'When selecting with limit' do
13
+
14
+ setup do
15
+ @select_sql = 'SELECT * FROM schema'
16
+ end
17
+
18
+ should 'alter SQL to limit number of records returned' do
19
+ options = { :limit => 10 }
20
+ assert_equal('SELECT TOP 10 * FROM schema', @connection.add_limit_offset!(@select_sql, options))
21
+ end
22
+
23
+ should 'only allow integers for limit' do
24
+ options = { :limit => 'ten' }
25
+ assert_raise(ArgumentError) {@connection.add_limit_offset!(@select_sql, options) }
26
+ end
27
+
28
+ should 'convert strings which look like integers to integers' do
29
+ options = { :limit => '42' }
30
+ assert_nothing_raised(ArgumentError) {@connection.add_limit_offset!(@select_sql, options)}
31
+ end
32
+
33
+ should 'not allow sql injection via limit' do
34
+ options = { :limit => '1 * FROM schema; DELETE * FROM table; SELECT TOP 10 *'}
35
+ assert_raise(ArgumentError) { @connection.add_limit_offset!(@select_sql, options) }
36
+ end
37
+
38
+ end
39
+
40
+ context 'When selecting with limit and offset' do
41
+
42
+ setup do
43
+ @select_sql = 'SELECT * FROM books'
44
+ @subquery_select_sql = 'SELECT *, (SELECT TOP 1 id FROM books) AS book_id FROM books'
45
+ @books = (1..10).map {|i| Book.create!}
46
+ end
47
+
48
+ teardown do
49
+ @books.each {|b| b.destroy}
50
+ end
51
+
52
+ should 'have limit if offset is passed' do
53
+ options = { :offset => 1 }
54
+ assert_raise(ArgumentError) { @connection.add_limit_offset!(@select_sql, options) }
55
+ end
56
+
57
+ should 'only allow integers for offset' do
58
+ options = { :limit => 10, :offset => 'five' }
59
+ assert_raise(ArgumentError) { @connection.add_limit_offset!(@select_sql, options)}
60
+ end
61
+
62
+ should 'convert strings which look like integers to integers' do
63
+ options = { :limit => 10, :offset => '5' }
64
+ assert_nothing_raised(ArgumentError) {@connection.add_limit_offset!(@select_sql, options)}
65
+ end
66
+
67
+ should 'alter SQL to limit number of records returned offset by specified amount' do
68
+ options = { :limit => 3, :offset => 5 }
69
+ expected_sql = "SELECT * FROM (SELECT TOP 3 * FROM (SELECT TOP 8 * FROM books) AS tmp1) AS tmp2"
70
+ assert_equal(expected_sql, @connection.add_limit_offset!(@select_sql, options))
71
+ end
72
+
73
+ should 'add locks to deepest sub select in limit offset sql that has a limited tally' do
74
+ options = { :limit => 3, :offset => 5, :lock => 'WITH (NOLOCK)' }
75
+ expected_sql = "SELECT * FROM (SELECT TOP 3 * FROM (SELECT TOP 8 * FROM books WITH (NOLOCK)) AS tmp1) AS tmp2"
76
+ @connection.add_limit_offset! @select_sql, options
77
+ assert_equal expected_sql, @connection.add_lock!(@select_sql,options)
78
+ end
79
+
80
+ # Not really sure what an offset sql injection might look like
81
+ should 'not allow sql injection via offset' do
82
+ options = { :limit => 10, :offset => '1 * FROM schema; DELETE * FROM table; SELECT TOP 10 *'}
83
+ assert_raise(ArgumentError) { @connection.add_limit_offset!(@select_sql, options) }
84
+ end
85
+
86
+ should 'not create invalid SQL with subquery SELECTs with TOP' do
87
+ options = { :limit => 5, :offset => 1 }
88
+ expected_sql = "SELECT * FROM (SELECT TOP 5 * FROM (SELECT TOP 6 *, (SELECT TOP 1 id FROM books) AS book_id FROM books) AS tmp1) AS tmp2"
89
+ assert_equal expected_sql, @connection.add_limit_offset!(@subquery_select_sql,options)
90
+ end
91
+
92
+ should 'add lock hints to tally sql if :lock option is present' do
93
+ assert_sql %r|SELECT TOP 1000000000 \* FROM \[people\] WITH \(NOLOCK\)| do
94
+ Person.all :limit => 5, :offset => 1, :lock => 'WITH (NOLOCK)'
95
+ end
96
+ end
97
+
98
+ should 'not add lock hints to tally sql if there is no :lock option' do
99
+ assert_sql %r|\(SELECT TOP 1000000000 \* FROM \[people\] \)| do
100
+ Person.all :limit => 5, :offset => 1
101
+ end
102
+ end
103
+
104
+ end
105
+
106
+
107
+ end
108
+
@@ -0,0 +1,125 @@
1
+ require 'cases/sqlserver_helper'
2
+ require 'models/person'
3
+ require 'models/reader'
4
+
5
+ class PessimisticLockingTestSqlserver < ActiveRecord::TestCase
6
+
7
+ self.use_transactional_fixtures = false
8
+ fixtures :people, :readers
9
+
10
+ def setup
11
+ Person.columns; Reader.columns # Avoid introspection queries during tests.
12
+ end
13
+
14
+ context 'For simple finds with default lock option' do
15
+
16
+ should 'lock with simple find' do
17
+ assert_nothing_raised do
18
+ Person.transaction do
19
+ Person.find 1, :lock => true
20
+ end
21
+ end
22
+ end
23
+
24
+ should 'lock with scoped find' do
25
+ assert_nothing_raised do
26
+ Person.transaction do
27
+ Person.with_scope(:find => { :lock => true }) do
28
+ Person.find 1
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ should 'lock with eager find' do
35
+ assert_nothing_raised do
36
+ Person.transaction do
37
+ Person.find 1, :include => :readers, :lock => true
38
+ end
39
+ end
40
+ end
41
+
42
+ should 'reload with lock when #lock! called' do
43
+ assert_nothing_raised do
44
+ Person.transaction do
45
+ person = Person.find 1
46
+ old, person.first_name = person.first_name, 'fooman'
47
+ person.lock!
48
+ assert_equal old, person.first_name
49
+ end
50
+ end
51
+ end
52
+
53
+ should 'simply add lock to find all' do
54
+ assert_sql %r|SELECT \* FROM \[people\] WITH \(NOLOCK\)| do
55
+ Person.all(:lock => 'WITH (NOLOCK)')
56
+ end
57
+ end
58
+
59
+ end
60
+
61
+ context 'For paginated finds' do
62
+
63
+ setup do
64
+ 20.times { |n| Person.create!(:first_name => "Thing_#{n}") }
65
+ end
66
+
67
+ should 'cope with un-locked paginated results' do
68
+ tally_not_locked = %r|SELECT count\(\*\) as TotalRows from \(SELECT TOP 1000000000 \* FROM \[people\]\s+WITH \(NOLOCK\) \) tally|
69
+ inner_tmp_not_locked = %r|SELECT TOP 15 \* FROM \[people\] WITH \(NOLOCK\)|
70
+ # Currently association limiting is not locked like the parent.
71
+ association_limiting_not_locked = %r|SELECT \[readers\]\.\* FROM \[readers\] WITH \(NOLOCK\) WHERE \(\[readers\]\.person_id IN \(1,2,3,4,5\)\)|
72
+ assert_sql(tally_not_locked,inner_tmp_not_locked) do
73
+ Person.all(:include => :readers, :lock => 'WITH (NOLOCK)', :limit => 5, :offset => 10)
74
+ end
75
+ end
76
+
77
+ end
78
+
79
+
80
+ context 'For dueling concurrent connections' do
81
+
82
+ use_concurrent_connections
83
+
84
+ should 'no locks does not wait' do
85
+ first, second = duel { Person.find 1 }
86
+ assert first.end > second.end
87
+ end
88
+
89
+ should 'that second lock waits' do
90
+ assert [0.2, 1, 5].any? { |zzz|
91
+ first, second = duel(zzz) { Person.find 1, :lock => true }
92
+ second.end > first.end
93
+ }
94
+ end
95
+
96
+ end
97
+
98
+
99
+ protected
100
+
101
+ def duel(zzz = 5)
102
+ t0, t1, t2, t3 = nil, nil, nil, nil
103
+ a = Thread.new do
104
+ t0 = Time.now
105
+ Person.transaction do
106
+ yield
107
+ sleep zzz # block thread 2 for zzz seconds
108
+ end
109
+ t1 = Time.now
110
+ end
111
+ b = Thread.new do
112
+ sleep zzz / 2.0 # ensure thread 1 tx starts first
113
+ t2 = Time.now
114
+ Person.transaction { yield }
115
+ t3 = Time.now
116
+ end
117
+ a.join
118
+ b.join
119
+ assert t1 > t0 + zzz
120
+ assert t2 > t0
121
+ assert t3 > t2
122
+ [t0.to_f..t1.to_f, t2.to_f..t3.to_f]
123
+ end
124
+
125
+ end