andyjeffries-rubyrep 1.2.1

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 (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