andyjeffries-rubyrep 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (125) hide show
  1. data/History.txt +83 -0
  2. data/License.txt +20 -0
  3. data/Manifest.txt +151 -0
  4. data/README.txt +37 -0
  5. data/bin/rubyrep +8 -0
  6. data/lib/rubyrep.rb +72 -0
  7. data/lib/rubyrep/base_runner.rb +195 -0
  8. data/lib/rubyrep/command_runner.rb +144 -0
  9. data/lib/rubyrep/committers/buffered_committer.rb +151 -0
  10. data/lib/rubyrep/committers/committers.rb +152 -0
  11. data/lib/rubyrep/configuration.rb +275 -0
  12. data/lib/rubyrep/connection_extenders/connection_extenders.rb +165 -0
  13. data/lib/rubyrep/connection_extenders/jdbc_extender.rb +65 -0
  14. data/lib/rubyrep/connection_extenders/mysql_extender.rb +59 -0
  15. data/lib/rubyrep/connection_extenders/postgresql_extender.rb +277 -0
  16. data/lib/rubyrep/database_proxy.rb +52 -0
  17. data/lib/rubyrep/direct_table_scan.rb +75 -0
  18. data/lib/rubyrep/generate_runner.rb +105 -0
  19. data/lib/rubyrep/initializer.rb +39 -0
  20. data/lib/rubyrep/log_helper.rb +30 -0
  21. data/lib/rubyrep/logged_change.rb +160 -0
  22. data/lib/rubyrep/logged_change_loader.rb +197 -0
  23. data/lib/rubyrep/noisy_connection.rb +80 -0
  24. data/lib/rubyrep/proxied_table_scan.rb +171 -0
  25. data/lib/rubyrep/proxy_block_cursor.rb +145 -0
  26. data/lib/rubyrep/proxy_connection.rb +431 -0
  27. data/lib/rubyrep/proxy_cursor.rb +44 -0
  28. data/lib/rubyrep/proxy_row_cursor.rb +43 -0
  29. data/lib/rubyrep/proxy_runner.rb +89 -0
  30. data/lib/rubyrep/replication_difference.rb +100 -0
  31. data/lib/rubyrep/replication_extenders/mysql_replication.rb +271 -0
  32. data/lib/rubyrep/replication_extenders/postgresql_replication.rb +236 -0
  33. data/lib/rubyrep/replication_extenders/replication_extenders.rb +26 -0
  34. data/lib/rubyrep/replication_helper.rb +142 -0
  35. data/lib/rubyrep/replication_initializer.rb +327 -0
  36. data/lib/rubyrep/replication_run.rb +142 -0
  37. data/lib/rubyrep/replication_runner.rb +166 -0
  38. data/lib/rubyrep/replicators/replicators.rb +42 -0
  39. data/lib/rubyrep/replicators/two_way_replicator.rb +361 -0
  40. data/lib/rubyrep/scan_progress_printers/progress_bar.rb +65 -0
  41. data/lib/rubyrep/scan_progress_printers/scan_progress_printers.rb +65 -0
  42. data/lib/rubyrep/scan_report_printers/scan_detail_reporter.rb +111 -0
  43. data/lib/rubyrep/scan_report_printers/scan_report_printers.rb +67 -0
  44. data/lib/rubyrep/scan_report_printers/scan_summary_reporter.rb +75 -0
  45. data/lib/rubyrep/scan_runner.rb +25 -0
  46. data/lib/rubyrep/session.rb +230 -0
  47. data/lib/rubyrep/sync_helper.rb +121 -0
  48. data/lib/rubyrep/sync_runner.rb +31 -0
  49. data/lib/rubyrep/syncers/syncers.rb +112 -0
  50. data/lib/rubyrep/syncers/two_way_syncer.rb +174 -0
  51. data/lib/rubyrep/table_scan.rb +54 -0
  52. data/lib/rubyrep/table_scan_helper.rb +46 -0
  53. data/lib/rubyrep/table_sorter.rb +70 -0
  54. data/lib/rubyrep/table_spec_resolver.rb +142 -0
  55. data/lib/rubyrep/table_sync.rb +90 -0
  56. data/lib/rubyrep/task_sweeper.rb +77 -0
  57. data/lib/rubyrep/trigger_mode_switcher.rb +63 -0
  58. data/lib/rubyrep/type_casting_cursor.rb +31 -0
  59. data/lib/rubyrep/uninstall_runner.rb +93 -0
  60. data/lib/rubyrep/version.rb +9 -0
  61. data/rubyrep +8 -0
  62. data/rubyrep.bat +4 -0
  63. data/setup.rb +1585 -0
  64. data/spec/base_runner_spec.rb +218 -0
  65. data/spec/buffered_committer_spec.rb +274 -0
  66. data/spec/command_runner_spec.rb +145 -0
  67. data/spec/committers_spec.rb +178 -0
  68. data/spec/configuration_spec.rb +203 -0
  69. data/spec/connection_extender_interface_spec.rb +141 -0
  70. data/spec/connection_extenders_registration_spec.rb +164 -0
  71. data/spec/database_proxy_spec.rb +48 -0
  72. data/spec/database_rake_spec.rb +40 -0
  73. data/spec/db_specific_connection_extenders_spec.rb +34 -0
  74. data/spec/db_specific_replication_extenders_spec.rb +38 -0
  75. data/spec/direct_table_scan_spec.rb +61 -0
  76. data/spec/dolphins.jpg +0 -0
  77. data/spec/generate_runner_spec.rb +84 -0
  78. data/spec/initializer_spec.rb +46 -0
  79. data/spec/log_helper_spec.rb +39 -0
  80. data/spec/logged_change_loader_spec.rb +68 -0
  81. data/spec/logged_change_spec.rb +470 -0
  82. data/spec/noisy_connection_spec.rb +78 -0
  83. data/spec/postgresql_replication_spec.rb +48 -0
  84. data/spec/postgresql_schema_support_spec.rb +212 -0
  85. data/spec/postgresql_support_spec.rb +63 -0
  86. data/spec/progress_bar_spec.rb +77 -0
  87. data/spec/proxied_table_scan_spec.rb +151 -0
  88. data/spec/proxy_block_cursor_spec.rb +197 -0
  89. data/spec/proxy_connection_spec.rb +423 -0
  90. data/spec/proxy_cursor_spec.rb +56 -0
  91. data/spec/proxy_row_cursor_spec.rb +66 -0
  92. data/spec/proxy_runner_spec.rb +70 -0
  93. data/spec/replication_difference_spec.rb +161 -0
  94. data/spec/replication_extender_interface_spec.rb +367 -0
  95. data/spec/replication_extenders_spec.rb +32 -0
  96. data/spec/replication_helper_spec.rb +178 -0
  97. data/spec/replication_initializer_spec.rb +509 -0
  98. data/spec/replication_run_spec.rb +443 -0
  99. data/spec/replication_runner_spec.rb +254 -0
  100. data/spec/replicators_spec.rb +36 -0
  101. data/spec/rubyrep_spec.rb +8 -0
  102. data/spec/scan_detail_reporter_spec.rb +119 -0
  103. data/spec/scan_progress_printers_spec.rb +68 -0
  104. data/spec/scan_report_printers_spec.rb +67 -0
  105. data/spec/scan_runner_spec.rb +50 -0
  106. data/spec/scan_summary_reporter_spec.rb +61 -0
  107. data/spec/session_spec.rb +253 -0
  108. data/spec/spec.opts +1 -0
  109. data/spec/spec_helper.rb +305 -0
  110. data/spec/strange_name_support_spec.rb +135 -0
  111. data/spec/sync_helper_spec.rb +169 -0
  112. data/spec/sync_runner_spec.rb +78 -0
  113. data/spec/syncers_spec.rb +171 -0
  114. data/spec/table_scan_helper_spec.rb +36 -0
  115. data/spec/table_scan_spec.rb +49 -0
  116. data/spec/table_sorter_spec.rb +30 -0
  117. data/spec/table_spec_resolver_spec.rb +111 -0
  118. data/spec/table_sync_spec.rb +140 -0
  119. data/spec/task_sweeper_spec.rb +47 -0
  120. data/spec/trigger_mode_switcher_spec.rb +83 -0
  121. data/spec/two_way_replicator_spec.rb +721 -0
  122. data/spec/two_way_syncer_spec.rb +256 -0
  123. data/spec/type_casting_cursor_spec.rb +50 -0
  124. data/spec/uninstall_runner_spec.rb +93 -0
  125. metadata +190 -0
@@ -0,0 +1,151 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ include RR
4
+
5
+ describe ProxiedTableScan do
6
+ before(:each) do
7
+ Initializer.configuration = deep_copy(proxied_config)
8
+
9
+ # Small block size necessary to exercize all code paths in ProxiedTableScan
10
+ # even when only using tables with very small number of records.
11
+ Initializer.configuration.options[:proxy_block_size] = 2
12
+
13
+ ensure_proxy
14
+ end
15
+
16
+ it "initialize should raise exception if session is not proxied" do
17
+ session = Session.new standard_config
18
+ lambda { ProxiedTableScan.new session, 'dummy_table' } \
19
+ .should raise_error(RuntimeError, /only works with proxied sessions/)
20
+ end
21
+
22
+ it "initialize should cache the primary keys" do
23
+ session = Session.new
24
+ scan = ProxiedTableScan.new session, 'scanner_records'
25
+ scan.primary_key_names.should == ['id']
26
+ end
27
+
28
+ it "initialize should raise exception if table doesn't have primary keys" do
29
+ session = Session.new
30
+ lambda {ProxiedTableScan.new session, 'extender_without_key'} \
31
+ .should raise_error(RuntimeError, /.*extender_without_key.*primary key/)
32
+ end
33
+
34
+ it "block_size should return the :proxy_block_size value of the session options" do
35
+ ProxiedTableScan.new(Session.new, 'scanner_records').block_size \
36
+ .should == 2
37
+ end
38
+
39
+ it "block_size should return the matching table specific option if available" do
40
+ config = Initializer.configuration
41
+ old_table_specific_options = config.tables_with_options
42
+ begin
43
+ config.options = {:proxy_block_size => 2}
44
+ config.include_tables 'scanner_records', {:proxy_block_size => 3}
45
+ ProxiedTableScan.new(Session.new(config), 'scanner_records').block_size \
46
+ .should == 3
47
+ ensure
48
+ config.instance_eval {@tables_with_options = old_table_specific_options}
49
+ end
50
+ end
51
+
52
+ # Creates, prepares and returns a +ProxyBlockCursor+ for the given database
53
+ # +connection+ and +table+.
54
+ # Sets the ProxyBlockCursor#max_row_cache_size as per method parameter.
55
+ def get_block_cursor(connection, table, max_row_cache_size = 1000000)
56
+ cursor = ProxyBlockCursor.new connection, table
57
+ cursor.max_row_cache_size = max_row_cache_size
58
+ cursor.prepare_fetch
59
+ cursor.checksum :proxy_block_size => 1000
60
+ cursor
61
+ end
62
+
63
+ it "compare_blocks should compare all the records in the range" do
64
+ session = Session.new
65
+
66
+ left_cursor = get_block_cursor session.left, 'scanner_records'
67
+ right_cursor = get_block_cursor session.right, 'scanner_records'
68
+
69
+ scan = ProxiedTableScan.new session, 'scanner_records'
70
+ diff = []
71
+ scan.compare_blocks(left_cursor, right_cursor) do |type, row|
72
+ diff.push [type, row]
73
+ end
74
+ # in this scenario the right table has the 'highest' data,
75
+ # so 'right-sided' data are already implicitely tested here
76
+ diff.should == [
77
+ [:conflict, [
78
+ {'id' => 2, 'name' => 'Bob - left database version'},
79
+ {'id' => 2, 'name' => 'Bob - right database version'}]],
80
+ [:left, {'id' => 3, 'name' => 'Charlie - exists in left database only'}],
81
+ [:right, {'id' => 4, 'name' => 'Dave - exists in right database only'}],
82
+ [:left, {'id' => 5, 'name' => 'Eve - exists in left database only'}],
83
+ [:right, {'id' => 6, 'name' => 'Fred - exists in right database only'}]
84
+ ]
85
+ end
86
+
87
+ it "compare_blocks should destroy the created cursors" do
88
+ session = Session.new
89
+
90
+ left_cursor = get_block_cursor session.left, 'scanner_records', 0
91
+ right_cursor = get_block_cursor session.right, 'scanner_records', 0
92
+
93
+ scan = ProxiedTableScan.new session, 'scanner_records'
94
+ scan.compare_blocks(left_cursor, right_cursor) { |type, row| }
95
+
96
+ session.left.cursors.should == {}
97
+ session.right.cursors.should == {}
98
+ end
99
+
100
+ it "run should only call compare single rows if there are different block checksums" do
101
+ config = deep_copy(proxied_config)
102
+ config.right = config.left
103
+ session = Session.new config
104
+ scan = ProxiedTableScan.new session, 'scanner_records'
105
+ scan.should_not_receive(:compare_blocks)
106
+ diff = []
107
+ scan.run do |type, row|
108
+ diff.push [type,row]
109
+ end
110
+ diff.should == []
111
+ end
112
+
113
+ it "run should compare all the records in the table" do
114
+ session = Session.new
115
+ scan = ProxiedTableScan.new session, 'scanner_records'
116
+ diff = []
117
+ scan.run do |type, row|
118
+ diff.push [type, row]
119
+ end
120
+ # in this scenario the right table has the 'highest' data,
121
+ # so 'right-sided' data are already implicitely tested here
122
+ diff.should == [
123
+ [:conflict, [
124
+ {'id' => 2, 'name' => 'Bob - left database version'},
125
+ {'id' => 2, 'name' => 'Bob - right database version'}]],
126
+ [:left, {'id' => 3, 'name' => 'Charlie - exists in left database only'}],
127
+ [:right, {'id' => 4, 'name' => 'Dave - exists in right database only'}],
128
+ [:left, {'id' => 5, 'name' => 'Eve - exists in left database only'}],
129
+ [:right, {'id' => 6, 'name' => 'Fred - exists in right database only'}]
130
+ ]
131
+ end
132
+
133
+ it "run should update the progress" do
134
+ session = Session.new
135
+ scan = ProxiedTableScan.new session, 'scanner_records'
136
+ number_steps = 0
137
+ scan.should_receive(:update_progress).any_number_of_times do |steps|
138
+ number_steps += steps
139
+ end
140
+ scan.run {|_, _|}
141
+ number_steps.should == 8
142
+ end
143
+
144
+ it "run should update the progress even if there are no records" do
145
+ # it should do that to ensure the progress bar is printed
146
+ scan = ProxiedTableScan.new Session.new, 'extender_no_record'
147
+ scan.should_receive(:update_progress).at_least(:once)
148
+ scan.run {|_, _|}
149
+ end
150
+ end
151
+
@@ -0,0 +1,197 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ include RR
4
+
5
+ describe ProxyBlockCursor do
6
+ before(:each) do
7
+ @session = create_mock_proxy_connection 'dummy_table', ['dummy_id']
8
+ @cursor = ProxyBlockCursor.new @session, 'dummy_table'
9
+ end
10
+
11
+ it "initialize should super to ProxyCursor" do
12
+ @cursor.table.should == 'dummy_table'
13
+ end
14
+
15
+ it "next? should return true if there is an already loaded unprocessed row" do
16
+ @cursor.last_row = :dummy_row
17
+ @cursor.next?.should be_true
18
+ end
19
+
20
+ it "next? should return true if the database cursor has more rows" do
21
+ table_cursor = mock("DBCursor")
22
+ table_cursor.should_receive(:next?).and_return(true)
23
+ @cursor.cursor = table_cursor
24
+
25
+ @cursor.next?.should be_true
26
+ end
27
+
28
+ it "next? should return false if there are no loaded or unloaded unprocessed rows" do
29
+ table_cursor = mock("DBCursor")
30
+ table_cursor.should_receive(:next?).and_return(false)
31
+ @cursor.cursor = table_cursor
32
+
33
+ @cursor.next?.should be_false
34
+ end
35
+
36
+ it "next_row should return last loaded unprocessed row or nil if there is none" do
37
+ @cursor.last_row = :dummy_row
38
+
39
+ @cursor.next_row.should == :dummy_row
40
+ @cursor.last_row.should be_nil
41
+ end
42
+
43
+ it "next_row should return next row in database if there is no loaded unprocessed row available" do
44
+ table_cursor = mock("DBCursor")
45
+ table_cursor.should_receive(:next_row).and_return(:dummy_row)
46
+ @cursor.cursor = table_cursor
47
+
48
+ @cursor.next_row.should == :dummy_row
49
+ end
50
+
51
+ it "reset_checksum should create a new empty SHA1 digest" do
52
+ @cursor.digest = :dummy_digest
53
+ @cursor.reset_checksum
54
+ @cursor.digest.should be_an_instance_of(Digest::SHA1)
55
+ end
56
+
57
+ it "reset_checksum should reset block variables" do
58
+ @cursor.reset_checksum
59
+ @cursor.row_checksums.should == []
60
+ @cursor.current_row_cache_size.should == 0
61
+ @cursor.row_cache.should == {}
62
+
63
+ end
64
+
65
+ it "update_checksum should update the existing digests" do
66
+ dummy_row1 = {'dummy_id' => 'dummy_value1'}
67
+ dummy_row2 = {'dummy_id' => 'dummy_value2'}
68
+
69
+ @cursor.reset_checksum
70
+ @cursor.update_checksum dummy_row1
71
+ @cursor.update_checksum dummy_row2
72
+
73
+ @cursor.current_checksum.should == Digest::SHA1.hexdigest(Marshal.dump(dummy_row1) + Marshal.dump(dummy_row2))
74
+ @cursor.row_checksums.should == [
75
+ {:row_keys => dummy_row1, :checksum => Digest::SHA1.hexdigest(Marshal.dump(dummy_row1))},
76
+ {:row_keys => dummy_row2, :checksum => Digest::SHA1.hexdigest(Marshal.dump(dummy_row2))},
77
+ ]
78
+
79
+ @cursor.row_cache.should == {
80
+ Digest::SHA1.hexdigest(Marshal.dump(dummy_row1)) => Marshal.dump(dummy_row1),
81
+ Digest::SHA1.hexdigest(Marshal.dump(dummy_row2)) => Marshal.dump(dummy_row2)
82
+ }
83
+ end
84
+
85
+ it "retrieve_row_cache should retrieve the specified elements" do
86
+ @cursor.row_cache = {'dummy_checksum' => 'bla'}
87
+ @cursor.retrieve_row_cache(['non_cached_row_checksum', 'dummy_checksum']).should ==
88
+ {'dummy_checksum' => 'bla'}
89
+ end
90
+
91
+ it "current_checksum should return the current checksum" do
92
+ digest = mock("Digest")
93
+ digest.should_receive(:hexdigest).and_return(:dummy_checksum)
94
+ @cursor.digest = digest
95
+
96
+ @cursor.current_checksum.should == :dummy_checksum
97
+ end
98
+
99
+ it "checksum should reset the current digest" do
100
+ @cursor.reset_checksum # need to call it now so that for the call to checksum it can be mocked
101
+ @cursor.should_receive(:reset_checksum)
102
+ @cursor.should_receive(:next?).and_return(false)
103
+ @cursor.checksum :proxy_block_size => 1
104
+ end
105
+
106
+ it "checksum should complain if neither :proxy_block_size nor :max_row are provided" do
107
+ lambda {@cursor.checksum}.should raise_error(
108
+ RuntimeError, 'options must include either :proxy_block_size or :max_row')
109
+ end
110
+
111
+ it "checksum should verify options" do
112
+ lambda {@cursor.checksum}.should raise_error(
113
+ RuntimeError, 'options must include either :proxy_block_size or :max_row')
114
+ lambda {@cursor.checksum(:proxy_block_size => 0)}.should raise_error(
115
+ RuntimeError, ':proxy_block_size must be greater than 0')
116
+ end
117
+
118
+ it "checksum should read maximum :proxy_block_size rows" do
119
+ session = ProxyConnection.new proxied_config.left
120
+
121
+ cursor = ProxyBlockCursor.new session, 'scanner_records'
122
+ cursor.prepare_fetch
123
+
124
+ last_row, = cursor.checksum :proxy_block_size => 2
125
+ last_row.should == {'id' => 2}
126
+
127
+ last_row, = cursor.checksum :proxy_block_size => 1000
128
+ last_row.should == {'id' => 5}
129
+ end
130
+
131
+ it "checksum should read up to the specified :max_row" do
132
+ session = ProxyConnection.new proxied_config.left
133
+
134
+ cursor = ProxyBlockCursor.new session, 'scanner_records'
135
+ cursor.prepare_fetch
136
+
137
+ last_row, = cursor.checksum :max_row => {'id' => 2}
138
+ last_row.should == {'id' => 2}
139
+ last_row, = cursor.checksum :max_row => {'id' => 1000}
140
+ last_row.should == {'id' => 5}
141
+ end
142
+
143
+ it "checksum called with :proxy_block_size should return the correct checksum" do
144
+ session = ProxyConnection.new proxied_config.left
145
+
146
+ cursor = ProxyBlockCursor.new session, 'scanner_records'
147
+ cursor.prepare_fetch
148
+
149
+ last_row , checksum = cursor.checksum :proxy_block_size => 2
150
+
151
+ expected_checksum = Digest::SHA1.hexdigest(
152
+ Marshal.dump('id' => 1, 'name' => 'Alice - exists in both databases') +
153
+ Marshal.dump('id' => 2, 'name' => 'Bob - left database version')
154
+ )
155
+
156
+ checksum.should == expected_checksum
157
+ end
158
+
159
+ it "checksum called with :max_row should return the correct checksum" do
160
+ session = ProxyConnection.new proxied_config.left
161
+
162
+ cursor = ProxyBlockCursor.new session, 'scanner_records'
163
+ cursor.prepare_fetch
164
+
165
+ last_row , checksum = cursor.checksum :max_row => {'id' => 2}
166
+
167
+ expected_checksum = Digest::SHA1.hexdigest(
168
+ Marshal.dump('id' => 1, 'name' => 'Alice - exists in both databases') +
169
+ Marshal.dump('id' => 2, 'name' => 'Bob - left database version')
170
+ )
171
+
172
+ checksum.should == expected_checksum
173
+ end
174
+
175
+ it "checksum called with :proxy_block_size should return the correct row count" do
176
+ session = ProxyConnection.new proxied_config.left
177
+
178
+ cursor = ProxyBlockCursor.new session, 'scanner_records'
179
+ cursor.prepare_fetch
180
+
181
+ _ , _, row_count = cursor.checksum :proxy_block_size => 2
182
+
183
+ row_count.should == 2
184
+ end
185
+
186
+ it "checksum called with :max_row should return the correct row count" do
187
+ session = ProxyConnection.new proxied_config.left
188
+
189
+ cursor = ProxyBlockCursor.new session, 'scanner_records'
190
+ cursor.prepare_fetch
191
+
192
+ _ , _, row_count = cursor.checksum :max_row => {'id' => 2}
193
+
194
+ row_count.should == 2
195
+ end
196
+
197
+ end
@@ -0,0 +1,423 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ include RR
4
+
5
+ describe ProxyConnection do
6
+ before(:each) do
7
+ Initializer.configuration = proxied_config
8
+ @connection = ProxyConnection.new Initializer.configuration.left
9
+ end
10
+
11
+ it "initialize should connect to the database" do
12
+ (!!@connection.connection.active?).should == true
13
+ end
14
+
15
+ it "initialize should store the configuratin" do
16
+ @connection.config.should == Initializer.configuration.left
17
+ end
18
+
19
+ it "destroy should disconnect from the database" do
20
+ if ActiveSupport.const_defined?(:Notifications)
21
+ ConnectionExtenders::install_logger @connection.connection, :logger => StringIO.new
22
+ log_subscriber = @connection.connection.log_subscriber
23
+
24
+ ActiveSupport::Notifications.notifier.listeners_for("sql.active_record").should include(log_subscriber)
25
+ end
26
+
27
+ @connection.destroy
28
+
29
+ if ActiveSupport.const_defined?(:Notifications)
30
+ ActiveSupport::Notifications.notifier.listeners_for("sql.active_record").should_not include(log_subscriber)
31
+ @connection.connection.log_subscriber.should be_nil
32
+ end
33
+
34
+ (!!@connection.connection.active?).should == false
35
+ end
36
+
37
+ it "cursors should return the current cursor hash or an empty hash if nil" do
38
+ @connection.cursors.should == {}
39
+ @connection.cursors[:dummy_cursor] = :dummy_cursor
40
+ @connection.cursors.should == {:dummy_cursor => :dummy_cursor}
41
+ end
42
+
43
+ it "save_cursor should register the provided cursor" do
44
+ @connection.save_cursor :dummy_cursor
45
+
46
+ @connection.cursors[:dummy_cursor].should == :dummy_cursor
47
+ end
48
+
49
+ it "destroy should destroy and unregister any stored cursors" do
50
+ cursor = mock("Cursor")
51
+ cursor.should_receive(:destroy)
52
+
53
+ @connection.save_cursor cursor
54
+ @connection.destroy
55
+
56
+ @connection.cursors.should == {}
57
+ end
58
+
59
+ it "destroy_cursor should destroy and unregister the provided cursor" do
60
+ cursor = mock("Cursor")
61
+ cursor.should_receive(:destroy)
62
+
63
+ @connection.save_cursor cursor
64
+ @connection.destroy_cursor cursor
65
+
66
+ @connection.cursors.should == {}
67
+ end
68
+
69
+ it "create_cursor should create and register the cursor and initiate row fetching" do
70
+ cursor = @connection.create_cursor(
71
+ ProxyRowCursor,
72
+ 'scanner_records',
73
+ :from => {'id' => 2},
74
+ :to => {'id' => 2}
75
+ )
76
+
77
+ cursor.should be_an_instance_of(ProxyRowCursor)
78
+ cursor.next_row_keys_and_checksum[0].should == {'id' => 2} # verify that 'from' range was used
79
+ cursor.next?.should be_false # verify that 'to' range was used
80
+ end
81
+
82
+ it "column_names should return the column names of the specified table" do
83
+ @connection.column_names('scanner_records').should == ['id', 'name']
84
+ end
85
+
86
+ it "column_names should cache the column names" do
87
+ @connection.column_names('scanner_records')
88
+ @connection.column_names('scanner_text_key')
89
+ @connection.connection.should_not_receive(:columns)
90
+ @connection.column_names('scanner_records').should == ['id', 'name']
91
+ end
92
+
93
+ it "primary_key_names should return the correct primary keys" do
94
+ @connection.primary_key_names('scanner_records').should == ['id']
95
+ end
96
+
97
+ it "primary_key_names should return the manual primary keys if they exist" do
98
+ @connection.stub!(:manual_primary_keys).
99
+ and_return({'scanner_records' => ['manual_key']})
100
+ @connection.primary_key_names('scanner_records').should == ['manual_key']
101
+ end
102
+
103
+ it "primary_key_names should not cache or manually overwrite if :raw option is given" do
104
+ @connection.stub!(:manual_primary_keys).
105
+ and_return({'scanner_records' => ['manual_key']})
106
+ key1 = @connection.primary_key_names('scanner_records', :raw => true)
107
+ key1.should == ['id']
108
+
109
+ key2 = @connection.primary_key_names('scanner_records', :raw => true)
110
+ key1.__id__.should_not == key2.__id__
111
+ end
112
+
113
+ it "primary_key_names should cache the primary primary keys" do
114
+ @connection.connection.should_receive(:primary_key_names) \
115
+ .with('dummy_table').once.and_return(['dummy_key'])
116
+ @connection.connection.should_receive(:primary_key_names) \
117
+ .with('dummy_table2').once.and_return(['dummy_key2'])
118
+
119
+ @connection.primary_key_names('dummy_table').should == ['dummy_key']
120
+ @connection.primary_key_names('dummy_table2').should == ['dummy_key2']
121
+ @connection.primary_key_names('dummy_table').should == ['dummy_key']
122
+ end
123
+
124
+ # Note:
125
+ # Additional select_cursor tests are executed via
126
+ # 'db_specific_connection_extenders_spec.rb'
127
+ # (To verify the behaviour for all supported databases)
128
+
129
+ it "select_cursor should return the result fetcher" do
130
+ fetcher = @connection.select_cursor(:table => 'scanner_records', :type_cast => false)
131
+ fetcher.connection.should == @connection
132
+ fetcher.options.should == {:table => 'scanner_records', :type_cast => false}
133
+ end
134
+
135
+ it "select_cursor should return a type casting cursor if :type_cast option is specified" do
136
+ fetcher = @connection.select_cursor(:table => 'scanner_records', :type_cast => true)
137
+ fetcher.should be_an_instance_of(TypeCastingCursor)
138
+ end
139
+
140
+ it "table_select_query should handle queries without any conditions" do
141
+ @connection.table_select_query('scanner_records') \
142
+ .should =~ sql_to_regexp("\
143
+ select 'id', 'name' from 'scanner_records'\
144
+ order by 'id'")
145
+ end
146
+
147
+ it "table_select_query should handle queries with only a from condition" do
148
+ @connection.table_select_query('scanner_records', :from => {'id' => 1}) \
149
+ .should =~ sql_to_regexp("\
150
+ select 'id', 'name' from 'scanner_records' \
151
+ where ('id') >= (1) order by 'id'")
152
+ end
153
+
154
+ it "table_select_query should handle queries with an exclusive from condition" do
155
+ @connection.table_select_query(
156
+ 'scanner_records',
157
+ :from => {'id' => 1},
158
+ :exclude_starting_row => true
159
+ ).should =~ sql_to_regexp("\
160
+ select 'id', 'name' from 'scanner_records' \
161
+ where ('id') > (1) order by 'id'")
162
+ end
163
+
164
+ it "table_select_query should handle queries with only a to condition" do
165
+ @connection.table_select_query('scanner_text_key', :to => {'text_id' => 'k1'}) \
166
+ .should =~ sql_to_regexp("\
167
+ select 'text_id', 'name' from 'scanner_text_key' \
168
+ where ('text_id') <= ('k1') order by 'text_id'")
169
+ end
170
+
171
+ it "table_select_query should handle queries with both from and to conditions" do
172
+ @connection.table_select_query('scanner_records',
173
+ :from => {'id' => 0}, :to => {'id' => 1}) \
174
+ .should =~ sql_to_regexp("\
175
+ select 'id', 'name' from 'scanner_records' \
176
+ where ('id') >= (0) and ('id') <= (1) order by 'id'")
177
+ end
178
+
179
+ it "table_select_query should handle queries for specific rows" do
180
+ @connection.table_select_query('scanner_records',
181
+ :row_keys => [{'id' => 0}, {'id' => 1}]) \
182
+ .should =~ sql_to_regexp("\
183
+ select 'id', 'name' from 'scanner_records' \
184
+ where ('id') in ((0), (1)) order by 'id'")
185
+ end
186
+
187
+ it "table_select_query should handle queries for specific rows with the row array actually being empty" do
188
+ @connection.table_select_query('scanner_records', :row_keys => []) \
189
+ .should =~ sql_to_regexp("\
190
+ select 'id', 'name' from 'scanner_records' \
191
+ where false order by 'id'")
192
+ end
193
+
194
+ it "table_select_query should handle queries for specific rows in combination with other conditions" do
195
+ @connection.table_select_query('scanner_records',
196
+ :from => {'id' => 0},
197
+ :row_keys => [{'id' => 1}, {'id' => 2}]) \
198
+ .should =~ sql_to_regexp("\
199
+ select 'id', 'name' from 'scanner_records' \
200
+ where ('id') >= (0) and ('id') in ((1), (2)) order by 'id'")
201
+ end
202
+
203
+ it "table_select_query should handle tables with combined primary keys" do
204
+ @connection.table_select_query('extender_combined_key',
205
+ :from => {'first_id' => 0, 'second_id' => 1},
206
+ :to => {'first_id' => 2, 'second_id' => 3}) \
207
+ .should =~ sql_to_regexp("\
208
+ select 'first_id', 'second_id', 'name' from 'extender_combined_key' \
209
+ where ('first_id', 'second_id') >= (0, 1) \
210
+ and ('first_id', 'second_id') <= (2, 3) \
211
+ order by 'first_id', 'second_id'")
212
+ end
213
+
214
+ it "table_select_query should quote column values" do
215
+ select_options = {:from => {'text_id' => 'a'}, :to => {'text_id' => 'b'}}
216
+
217
+ @connection.table_select_query('scanner_text_key', select_options) \
218
+ .should match(/'a'.*'b'/)
219
+
220
+ # additional check that the quoted query actually works
221
+ cursor = ProxyCursor.new(@connection, 'scanner_text_key')
222
+ results = cursor.prepare_fetch(select_options)
223
+ results.next_row.should == {'text_id' => 'a', 'name' => 'Alice'}
224
+ results.next_row.should == {'text_id' => 'b', 'name' => 'Bob'}
225
+ results.next?.should be_false
226
+ end
227
+
228
+ it "table_insert_query should return the correct SQL query" do
229
+ @connection.table_insert_query('scanner_records', 'name' => 'bla') \
230
+ .should =~ sql_to_regexp(%q!insert into "scanner_records"("name") values("bla")!)
231
+ end
232
+
233
+ it "insert_record should insert the specified record" do
234
+ @connection.begin_db_transaction
235
+ begin
236
+ @connection.insert_record('scanner_records', 'id' => 9, 'name' => 'bla')
237
+ @connection.select_record(
238
+ :table => 'scanner_records',
239
+ :row_keys => ['id' => 9]
240
+ ).should == {'id' => 9, 'name' => 'bla'}
241
+ ensure
242
+ @connection.rollback_db_transaction
243
+ end
244
+ end
245
+
246
+ it "insert_record should handle combined primary keys" do
247
+ @connection.begin_db_transaction
248
+ begin
249
+ @connection.insert_record('extender_combined_key', 'first_id' => 8, 'second_id' => '9')
250
+ @connection.select_record(
251
+ :table => 'extender_combined_key',
252
+ :row_keys => ['first_id' => 8, 'second_id' => 9]
253
+ ).should == {'first_id' => 8, 'second_id' => 9, 'name' => nil}
254
+ ensure
255
+ @connection.rollback_db_transaction
256
+ end
257
+ end
258
+
259
+ it "insert_record should write nil values correctly" do
260
+ @connection.begin_db_transaction
261
+ begin
262
+ @connection.insert_record('extender_combined_key', 'first_id' => 8, 'second_id' => '9', 'name' => nil)
263
+ @connection.select_record(
264
+ :table => 'extender_combined_key',
265
+ :row_keys => ['first_id' => 8, 'second_id' => 9]
266
+ ).should == {'first_id' => 8, 'second_id' => 9, "name" => nil}
267
+ ensure
268
+ @connection.rollback_db_transaction
269
+ end
270
+ end
271
+
272
+ it "insert_record should also insert uncommon data types correctly" do
273
+ @connection.begin_db_transaction
274
+ begin
275
+ test_data = {
276
+ 'id' => 2,
277
+ 'decimal_test' => 1.234,
278
+ 'timestamp' => Time.local(2008,"jun",9,20,15,1),
279
+ 'multi_byte' => "よろしくお願(ねが)いします yoroshiku onegai shimasu: I humbly ask for your favor.",
280
+ 'binary_test' => Marshal.dump(['bla',:dummy,1,2,3]),
281
+ 'text_test' => 'dummy text'
282
+ }
283
+ @connection.insert_record('extender_type_check', test_data)
284
+
285
+ cursor = @connection.select_cursor(
286
+ :table => 'extender_type_check',
287
+ :row_keys => [{'id' => 2}],
288
+ :type_cast => true
289
+ )
290
+ result_data = cursor.next_row
291
+ result_data.should == test_data
292
+ ensure
293
+ @connection.rollback_db_transaction
294
+ end
295
+ end
296
+
297
+ it "table_update_query should return the correct SQL query" do
298
+ @connection.table_update_query('scanner_records', 'id' => 1) \
299
+ .should =~ sql_to_regexp(%q!update "scanner_records" set "id" = 1 where ("id") = (1)!)
300
+ end
301
+
302
+ it "update_record should update the specified record" do
303
+ @connection.begin_db_transaction
304
+ begin
305
+ @connection.update_record('scanner_records', 'id' => 1, 'name' => 'update_test')
306
+ @connection.select_record(
307
+ :table => "scanner_records",
308
+ :row_keys => ['id' => 1]
309
+ ).should == {'id' => 1, 'name' => 'update_test'}
310
+ ensure
311
+ @connection.rollback_db_transaction
312
+ end
313
+ end
314
+
315
+ it "update_record should return the number of updated records" do
316
+ @connection.begin_db_transaction
317
+ begin
318
+ @connection.
319
+ update_record('scanner_records', 'id' => 1, 'name' => 'update_test').
320
+ should == 1
321
+ @connection.
322
+ update_record('scanner_records', 'id' => 0, 'name' => 'update_test').
323
+ should == 0
324
+ ensure
325
+ @connection.rollback_db_transaction
326
+ end
327
+ end
328
+
329
+ it "update_record should handle combined primary keys" do
330
+ @connection.begin_db_transaction
331
+ begin
332
+ @connection.update_record('extender_combined_key', 'first_id' => 1, 'second_id' => '1', 'name' => 'xy')
333
+ @connection.select_record(
334
+ :table => 'extender_combined_key',
335
+ :row_keys => ['first_id' => 1, 'second_id' => 1]
336
+ ).should == {'first_id' => 1, 'second_id' => 1, 'name' => 'xy'}
337
+ ensure
338
+ @connection.rollback_db_transaction
339
+ end
340
+ end
341
+
342
+ it "update_record should handle key changes" do
343
+ @connection.begin_db_transaction
344
+ begin
345
+ @connection.update_record 'extender_combined_key',
346
+ {'first_id' => '8', 'second_id' => '9', 'name' => 'xy'},
347
+ {'first_id' => '1', 'second_id' => '1'}
348
+ @connection.select_record(
349
+ :table => 'extender_combined_key',
350
+ :row_keys => ['first_id' => 8, 'second_id' => 9]
351
+ ).should == {'first_id' => 8, 'second_id' => 9, 'name' => 'xy'}
352
+ ensure
353
+ @connection.rollback_db_transaction
354
+ end
355
+ end
356
+
357
+ it "update_record should write nil values correctly" do
358
+ @connection.begin_db_transaction
359
+ begin
360
+ @connection.update_record('extender_combined_key', 'first_id' => 1, 'second_id' => '1', 'name' => nil)
361
+ @connection.select_record(
362
+ :table => 'extender_combined_key',
363
+ :row_keys => ['first_id' => 1, 'second_id' => 1]
364
+ ).should == {'first_id' => 1, 'second_id' => 1, 'name' => nil}
365
+ ensure
366
+ @connection.rollback_db_transaction
367
+ end
368
+ end
369
+
370
+ it "update_record should also update uncommon data types correctly" do
371
+ @connection.begin_db_transaction
372
+ begin
373
+ test_data = {
374
+ 'id' => 1,
375
+ 'decimal_test' => 0.234,
376
+ 'timestamp' => Time.local(2009,"jun",9,20,15,1),
377
+ 'multi_byte' => "よろしくお願(ねが)いします yoroshiku onegai shimasu: I humbly ask for your favor. bla",
378
+ 'binary_test' => Marshal.dump(['bla',:dummy,1,2,3,4]),
379
+ 'text_test' => 'dummy text bla'
380
+ }
381
+ @connection.update_record('extender_type_check', test_data)
382
+
383
+ @connection.select_record(
384
+ :table => "extender_type_check",
385
+ :row_keys => ["id" => 1]
386
+ ).should == test_data
387
+ ensure
388
+ @connection.rollback_db_transaction
389
+ end
390
+ end
391
+
392
+ it "table_delete_query should return the correct SQL query" do
393
+ @connection.table_delete_query('scanner_records', 'id' => 1) \
394
+ .should =~ sql_to_regexp(%q!delete from "scanner_records" where ("id") = (1)!)
395
+ end
396
+
397
+ it "delete_record should delete the specified record" do
398
+ @connection.begin_db_transaction
399
+ begin
400
+ @connection.delete_record('extender_combined_key', 'first_id' => 1, 'second_id' => '1', 'name' => 'xy')
401
+ @connection.select_one(
402
+ "select first_id, second_id, name
403
+ from extender_combined_key where (first_id, second_id) = (1, 1)") \
404
+ .should be_nil
405
+ ensure
406
+ @connection.rollback_db_transaction
407
+ end
408
+ end
409
+
410
+ it "delete_record should return the number of deleted records" do
411
+ @connection.begin_db_transaction
412
+ begin
413
+ @connection.
414
+ delete_record('extender_combined_key', 'first_id' => 1, 'second_id' => '1').
415
+ should == 1
416
+ @connection.
417
+ delete_record('extender_combined_key', 'first_id' => 1, 'second_id' => '0').
418
+ should == 0
419
+ ensure
420
+ @connection.rollback_db_transaction
421
+ end
422
+ end
423
+ end