rubyrep 1.0.0
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.
- data/History.txt +4 -0
- data/License.txt +20 -0
- data/Manifest.txt +137 -0
- data/README.txt +37 -0
- data/Rakefile +30 -0
- data/bin/rubyrep +8 -0
- data/config/hoe.rb +72 -0
- data/config/mysql_config.rb +25 -0
- data/config/postgres_config.rb +21 -0
- data/config/proxied_test_config.rb +14 -0
- data/config/redmine_config.rb +17 -0
- data/config/rep_config.rb +20 -0
- data/config/requirements.rb +32 -0
- data/config/test_config.rb +20 -0
- data/lib/rubyrep/base_runner.rb +195 -0
- data/lib/rubyrep/command_runner.rb +144 -0
- data/lib/rubyrep/committers/buffered_committer.rb +140 -0
- data/lib/rubyrep/committers/committers.rb +146 -0
- data/lib/rubyrep/configuration.rb +240 -0
- data/lib/rubyrep/connection_extenders/connection_extenders.rb +133 -0
- data/lib/rubyrep/connection_extenders/jdbc_extender.rb +284 -0
- data/lib/rubyrep/connection_extenders/mysql_extender.rb +168 -0
- data/lib/rubyrep/connection_extenders/postgresql_extender.rb +261 -0
- data/lib/rubyrep/database_proxy.rb +52 -0
- data/lib/rubyrep/direct_table_scan.rb +75 -0
- data/lib/rubyrep/generate_runner.rb +105 -0
- data/lib/rubyrep/initializer.rb +39 -0
- data/lib/rubyrep/logged_change.rb +326 -0
- data/lib/rubyrep/proxied_table_scan.rb +171 -0
- data/lib/rubyrep/proxy_block_cursor.rb +145 -0
- data/lib/rubyrep/proxy_connection.rb +318 -0
- data/lib/rubyrep/proxy_cursor.rb +44 -0
- data/lib/rubyrep/proxy_row_cursor.rb +43 -0
- data/lib/rubyrep/proxy_runner.rb +89 -0
- data/lib/rubyrep/replication_difference.rb +91 -0
- data/lib/rubyrep/replication_extenders/mysql_replication.rb +271 -0
- data/lib/rubyrep/replication_extenders/postgresql_replication.rb +204 -0
- data/lib/rubyrep/replication_extenders/replication_extenders.rb +26 -0
- data/lib/rubyrep/replication_helper.rb +104 -0
- data/lib/rubyrep/replication_initializer.rb +307 -0
- data/lib/rubyrep/replication_run.rb +48 -0
- data/lib/rubyrep/replication_runner.rb +138 -0
- data/lib/rubyrep/replicators/replicators.rb +37 -0
- data/lib/rubyrep/replicators/two_way_replicator.rb +334 -0
- data/lib/rubyrep/scan_progress_printers/progress_bar.rb +65 -0
- data/lib/rubyrep/scan_progress_printers/scan_progress_printers.rb +65 -0
- data/lib/rubyrep/scan_report_printers/scan_detail_reporter.rb +111 -0
- data/lib/rubyrep/scan_report_printers/scan_report_printers.rb +67 -0
- data/lib/rubyrep/scan_report_printers/scan_summary_reporter.rb +75 -0
- data/lib/rubyrep/scan_runner.rb +25 -0
- data/lib/rubyrep/session.rb +177 -0
- data/lib/rubyrep/sync_helper.rb +111 -0
- data/lib/rubyrep/sync_runner.rb +31 -0
- data/lib/rubyrep/syncers/syncers.rb +112 -0
- data/lib/rubyrep/syncers/two_way_syncer.rb +174 -0
- data/lib/rubyrep/table_scan.rb +54 -0
- data/lib/rubyrep/table_scan_helper.rb +38 -0
- data/lib/rubyrep/table_sorter.rb +70 -0
- data/lib/rubyrep/table_spec_resolver.rb +136 -0
- data/lib/rubyrep/table_sync.rb +68 -0
- data/lib/rubyrep/trigger_mode_switcher.rb +63 -0
- data/lib/rubyrep/type_casting_cursor.rb +31 -0
- data/lib/rubyrep/uninstall_runner.rb +92 -0
- data/lib/rubyrep/version.rb +9 -0
- data/lib/rubyrep.rb +68 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/script/txt2html +74 -0
- data/setup.rb +1585 -0
- data/sims/performance/big_rep_spec.rb +100 -0
- data/sims/performance/big_scan_spec.rb +57 -0
- data/sims/performance/big_sync_spec.rb +141 -0
- data/sims/performance/performance.rake +228 -0
- data/sims/sim_helper.rb +24 -0
- data/spec/base_runner_spec.rb +218 -0
- data/spec/buffered_committer_spec.rb +271 -0
- data/spec/command_runner_spec.rb +145 -0
- data/spec/committers_spec.rb +174 -0
- data/spec/configuration_spec.rb +198 -0
- data/spec/connection_extender_interface_spec.rb +138 -0
- data/spec/connection_extenders_registration_spec.rb +129 -0
- data/spec/database_proxy_spec.rb +48 -0
- data/spec/database_rake_spec.rb +40 -0
- data/spec/db_specific_connection_extenders_spec.rb +34 -0
- data/spec/db_specific_replication_extenders_spec.rb +38 -0
- data/spec/direct_table_scan_spec.rb +61 -0
- data/spec/generate_runner_spec.rb +84 -0
- data/spec/initializer_spec.rb +46 -0
- data/spec/logged_change_spec.rb +480 -0
- data/spec/postgresql_replication_spec.rb +48 -0
- data/spec/postgresql_support_spec.rb +57 -0
- data/spec/progress_bar_spec.rb +77 -0
- data/spec/proxied_table_scan_spec.rb +151 -0
- data/spec/proxy_block_cursor_spec.rb +197 -0
- data/spec/proxy_connection_spec.rb +399 -0
- data/spec/proxy_cursor_spec.rb +56 -0
- data/spec/proxy_row_cursor_spec.rb +66 -0
- data/spec/proxy_runner_spec.rb +70 -0
- data/spec/replication_difference_spec.rb +160 -0
- data/spec/replication_extender_interface_spec.rb +365 -0
- data/spec/replication_extenders_spec.rb +32 -0
- data/spec/replication_helper_spec.rb +121 -0
- data/spec/replication_initializer_spec.rb +477 -0
- data/spec/replication_run_spec.rb +166 -0
- data/spec/replication_runner_spec.rb +213 -0
- data/spec/replicators_spec.rb +31 -0
- data/spec/rubyrep_spec.rb +8 -0
- data/spec/scan_detail_reporter_spec.rb +119 -0
- data/spec/scan_progress_printers_spec.rb +68 -0
- data/spec/scan_report_printers_spec.rb +67 -0
- data/spec/scan_runner_spec.rb +50 -0
- data/spec/scan_summary_reporter_spec.rb +61 -0
- data/spec/session_spec.rb +212 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +295 -0
- data/spec/sync_helper_spec.rb +157 -0
- data/spec/sync_runner_spec.rb +78 -0
- data/spec/syncers_spec.rb +171 -0
- data/spec/table_scan_helper_spec.rb +29 -0
- data/spec/table_scan_spec.rb +49 -0
- data/spec/table_sorter_spec.rb +31 -0
- data/spec/table_spec_resolver_spec.rb +102 -0
- data/spec/table_sync_spec.rb +84 -0
- data/spec/trigger_mode_switcher_spec.rb +83 -0
- data/spec/two_way_replicator_spec.rb +551 -0
- data/spec/two_way_syncer_spec.rb +256 -0
- data/spec/type_casting_cursor_spec.rb +50 -0
- data/spec/uninstall_runner_spec.rb +86 -0
- data/tasks/database.rake +439 -0
- data/tasks/deployment.rake +29 -0
- data/tasks/environment.rake +9 -0
- data/tasks/java.rake +37 -0
- data/tasks/redmine_test.rake +47 -0
- data/tasks/rspec.rake +68 -0
- data/tasks/rubyrep.tailor +18 -0
- data/tasks/stats.rake +19 -0
- data/tasks/task_helper.rb +20 -0
- data.tar.gz.sig +0 -0
- metadata +243 -0
- metadata.gz.sig +0 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
require 'time'
|
|
2
|
+
|
|
3
|
+
# A cursor to iterate over the records returned by select_cursor.
|
|
4
|
+
# Only one row is kept in memory at a time.
|
|
5
|
+
class PGresult
|
|
6
|
+
# Returns true if there are more rows to read.
|
|
7
|
+
def next?
|
|
8
|
+
@current_row_num ||= 0
|
|
9
|
+
@num_rows ||= self.ntuples()
|
|
10
|
+
@current_row_num < @num_rows
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Returns the row as a column => value hash and moves the cursor to the next row.
|
|
14
|
+
def next_row
|
|
15
|
+
raise("no more rows available") unless next?
|
|
16
|
+
row = {}
|
|
17
|
+
@fields ||= self.fields
|
|
18
|
+
@fields.each_with_index do |field, field_index|
|
|
19
|
+
if self.getisnull(@current_row_num, field_index)
|
|
20
|
+
value = nil
|
|
21
|
+
else
|
|
22
|
+
value = self.getvalue @current_row_num, field_index
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
row[@fields[field_index]] = value
|
|
26
|
+
end
|
|
27
|
+
@current_row_num += 1
|
|
28
|
+
row
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Hack:
|
|
33
|
+
# For some reasons these methods were removed in Rails 2.2.2, thus breaking
|
|
34
|
+
# the binary and multi-lingual data loading.
|
|
35
|
+
# So here they are again.
|
|
36
|
+
module ActiveRecord
|
|
37
|
+
module ConnectionAdapters
|
|
38
|
+
# PostgreSQL-specific extensions to column definitions in a table.
|
|
39
|
+
class PostgreSQLColumn < Column #:nodoc:
|
|
40
|
+
|
|
41
|
+
# Escapes binary strings for bytea input to the database.
|
|
42
|
+
def self.string_to_binary(value)
|
|
43
|
+
if PGconn.respond_to?(:escape_bytea)
|
|
44
|
+
self.class.module_eval do
|
|
45
|
+
define_method(:string_to_binary) do |value|
|
|
46
|
+
PGconn.escape_bytea(value) if value
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
else
|
|
50
|
+
self.class.module_eval do
|
|
51
|
+
define_method(:string_to_binary) do |value|
|
|
52
|
+
if value
|
|
53
|
+
result = ''
|
|
54
|
+
value.each_byte { |c| result << sprintf('\\\\%03o', c) }
|
|
55
|
+
result
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
self.class.string_to_binary(value)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Unescapes bytea output from a database to the binary string it represents.
|
|
64
|
+
def self.binary_to_string(value)
|
|
65
|
+
# In each case, check if the value actually is escaped PostgreSQL bytea output
|
|
66
|
+
# or an unescaped Active Record attribute that was just written.
|
|
67
|
+
if PGconn.respond_to?(:unescape_bytea)
|
|
68
|
+
self.class.module_eval do
|
|
69
|
+
define_method(:binary_to_string) do |value|
|
|
70
|
+
if value =~ /\\\d{3}/
|
|
71
|
+
PGconn.unescape_bytea(value)
|
|
72
|
+
else
|
|
73
|
+
value
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
else
|
|
78
|
+
self.class.module_eval do
|
|
79
|
+
define_method(:binary_to_string) do |value|
|
|
80
|
+
if value =~ /\\\d{3}/
|
|
81
|
+
result = ''
|
|
82
|
+
i, max = 0, value.size
|
|
83
|
+
while i < max
|
|
84
|
+
char = value[i]
|
|
85
|
+
if char == ?\\
|
|
86
|
+
if value[i+1] == ?\\
|
|
87
|
+
char = ?\\
|
|
88
|
+
i += 1
|
|
89
|
+
else
|
|
90
|
+
char = value[i+1..i+3].oct
|
|
91
|
+
i += 3
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
result << char
|
|
95
|
+
i += 1
|
|
96
|
+
end
|
|
97
|
+
result
|
|
98
|
+
else
|
|
99
|
+
value
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
self.class.binary_to_string(value)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
module RR
|
|
111
|
+
module ConnectionExtenders
|
|
112
|
+
|
|
113
|
+
# Fetches results from a PostgreSQL cursor object.
|
|
114
|
+
class PostgreSQLFetcher
|
|
115
|
+
|
|
116
|
+
# The current database connection
|
|
117
|
+
attr_accessor :connection
|
|
118
|
+
|
|
119
|
+
# Name of the cursor from which to fetch
|
|
120
|
+
attr_accessor :cursor_name
|
|
121
|
+
|
|
122
|
+
# Number of rows to be read at once
|
|
123
|
+
attr_accessor :row_buffer_size
|
|
124
|
+
|
|
125
|
+
# Creates a new fetcher.
|
|
126
|
+
# * +connection+: the current database connection
|
|
127
|
+
# * +cursor_name+: name of the cursor from which to fetch
|
|
128
|
+
# * +row_buffer_size+: number of records to read at once
|
|
129
|
+
def initialize(connection, cursor_name, row_buffer_size)
|
|
130
|
+
self.connection = connection
|
|
131
|
+
self.cursor_name = cursor_name
|
|
132
|
+
self.row_buffer_size = row_buffer_size
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Executes the specified SQL staements, returning the result
|
|
136
|
+
def execute(sql)
|
|
137
|
+
connection.execute sql
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Returns true if there are more rows to read.
|
|
141
|
+
def next?
|
|
142
|
+
@current_result ||= execute("FETCH FORWARD #{row_buffer_size} FROM #{cursor_name}")
|
|
143
|
+
@current_result.next?
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Returns the row as a column => value hash and moves the cursor to the next row.
|
|
147
|
+
def next_row
|
|
148
|
+
raise("no more rows available") unless next?
|
|
149
|
+
row = @current_result.next_row
|
|
150
|
+
unless @current_result.next?
|
|
151
|
+
@current_result.clear
|
|
152
|
+
@current_result = nil
|
|
153
|
+
end
|
|
154
|
+
row
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Closes the cursor and frees up all ressources
|
|
158
|
+
def clear
|
|
159
|
+
if @current_result
|
|
160
|
+
@current_result.clear
|
|
161
|
+
@current_result = nil
|
|
162
|
+
end
|
|
163
|
+
result = execute("CLOSE #{cursor_name}")
|
|
164
|
+
result.clear if result
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Provides various PostgreSQL specific functionality required by Rubyrep.
|
|
169
|
+
module PostgreSQLExtender
|
|
170
|
+
RR::ConnectionExtenders.register :postgresql => self
|
|
171
|
+
|
|
172
|
+
# Executes the given sql query with the otional name written in the
|
|
173
|
+
# ActiveRecord log file.
|
|
174
|
+
#
|
|
175
|
+
# :+row_buffer_size+ controls how many records are ready into memory at a
|
|
176
|
+
# time. Implemented using the PostgeSQL "DECLARE CURSOR" and "FETCH" constructs.
|
|
177
|
+
# This is necessary as the postgresql driver always reads the
|
|
178
|
+
# complete resultset into memory.
|
|
179
|
+
#
|
|
180
|
+
# Returns the results as a Cursor object supporting
|
|
181
|
+
# * next? - returns true if there are more rows to read
|
|
182
|
+
# * next_row - returns the row as a column => value hash and moves the cursor to the next row
|
|
183
|
+
# * clear - clearing the cursor (making allocated memory available for GC)
|
|
184
|
+
def select_cursor(sql, row_buffer_size = 1000)
|
|
185
|
+
cursor_name = "RR_#{Time.now.to_i}#{rand(1_000_000)}"
|
|
186
|
+
execute("DECLARE #{cursor_name} NO SCROLL CURSOR WITH HOLD FOR " + sql)
|
|
187
|
+
PostgreSQLFetcher.new(self, cursor_name, row_buffer_size)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# Returns an ordered list of primary key column names of the given table
|
|
191
|
+
def primary_key_names(table)
|
|
192
|
+
row = self.select_one(<<-end_sql)
|
|
193
|
+
SELECT relname
|
|
194
|
+
FROM pg_class
|
|
195
|
+
WHERE relname = '#{table}'
|
|
196
|
+
end_sql
|
|
197
|
+
if row.nil?
|
|
198
|
+
raise "table '#{table}' does not exist"
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
row = self.select_one(<<-end_sql)
|
|
202
|
+
SELECT cons.conkey
|
|
203
|
+
FROM pg_class rel
|
|
204
|
+
JOIN pg_constraint cons ON (rel.oid = cons.conrelid)
|
|
205
|
+
WHERE cons.contype = 'p' AND rel.relname = '#{table}'
|
|
206
|
+
end_sql
|
|
207
|
+
if row.nil?
|
|
208
|
+
return []
|
|
209
|
+
end
|
|
210
|
+
column_parray = row['conkey']
|
|
211
|
+
|
|
212
|
+
# Change a Postgres Array of attribute numbers
|
|
213
|
+
# (returned in String form, e. g.: "{1,2}") into an array of Integers
|
|
214
|
+
column_ids = column_parray.sub(/^\{(.*)\}$/,'\1').split(',').map {|a| a.to_i}
|
|
215
|
+
|
|
216
|
+
columns = {}
|
|
217
|
+
rows = self.select_all(<<-end_sql)
|
|
218
|
+
SELECT attnum, attname
|
|
219
|
+
FROM pg_class rel
|
|
220
|
+
JOIN pg_constraint cons ON (rel.oid = cons.conrelid)
|
|
221
|
+
JOIN pg_attribute attr ON (rel.oid = attr.attrelid and attr.attnum = any (cons.conkey))
|
|
222
|
+
WHERE cons.contype = 'p' AND rel.relname = '#{table}'
|
|
223
|
+
end_sql
|
|
224
|
+
sorted_columns = []
|
|
225
|
+
if not rows.nil?
|
|
226
|
+
rows.each() {|r| columns[r['attnum'].to_i] = r['attname']}
|
|
227
|
+
sorted_columns = column_ids.map {|column_id| columns[column_id]}
|
|
228
|
+
end
|
|
229
|
+
sorted_columns
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Returns for each given table, which other tables it references via
|
|
233
|
+
# foreign key constraints.
|
|
234
|
+
# * tables: an array of table names
|
|
235
|
+
# Returns: a hash with
|
|
236
|
+
# * key: name of the referencing table
|
|
237
|
+
# * value: an array of names of referenced tables
|
|
238
|
+
def referenced_tables(tables)
|
|
239
|
+
rows = self.select_all(<<-end_sql)
|
|
240
|
+
select distinct referencing.relname as referencing_table, referenced.relname as referenced_table
|
|
241
|
+
from pg_class referencing
|
|
242
|
+
left join pg_constraint on referencing.oid = pg_constraint.conrelid
|
|
243
|
+
left join pg_class referenced on pg_constraint.confrelid = referenced.oid
|
|
244
|
+
where referencing.relkind='r'
|
|
245
|
+
and referencing.relname in ('#{tables.join("', '")}')
|
|
246
|
+
end_sql
|
|
247
|
+
result = {}
|
|
248
|
+
rows.each do |row|
|
|
249
|
+
unless result.include? row['referencing_table']
|
|
250
|
+
result[row['referencing_table']] = []
|
|
251
|
+
end
|
|
252
|
+
if row['referenced_table'] != nil
|
|
253
|
+
result[row['referencing_table']] << row['referenced_table']
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
result
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/..'
|
|
2
|
+
|
|
3
|
+
require 'drb'
|
|
4
|
+
|
|
5
|
+
require 'rubyrep'
|
|
6
|
+
|
|
7
|
+
module RR
|
|
8
|
+
# The proxy to a remote database connection
|
|
9
|
+
class DatabaseProxy
|
|
10
|
+
|
|
11
|
+
# Ensure that the proxy object always stays on server side and only remote
|
|
12
|
+
# references are returned to the client.
|
|
13
|
+
include DRbUndumped
|
|
14
|
+
|
|
15
|
+
# Default tcp port to listen on
|
|
16
|
+
DEFAULT_PORT = 9876
|
|
17
|
+
|
|
18
|
+
# A simple Hash to hold Session object
|
|
19
|
+
# Purpose: preventing them from being garbage collected when they are only referenced through Drb
|
|
20
|
+
attr_accessor :session_register
|
|
21
|
+
|
|
22
|
+
def initialize
|
|
23
|
+
self.session_register = {}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Create a ProxyConnection according to provided configuration Hash.
|
|
27
|
+
# +config+ is a hash as described by ActiveRecord::Base#establish_connection
|
|
28
|
+
def create_session(config)
|
|
29
|
+
session = ProxyConnection.new config
|
|
30
|
+
self.session_register[session] = session
|
|
31
|
+
session
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Destroys the given session from the session register
|
|
35
|
+
def destroy_session(session)
|
|
36
|
+
session.destroy
|
|
37
|
+
session_register.delete session
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Returns 'pong'. Used to verify that a working proxy is running.
|
|
41
|
+
def ping
|
|
42
|
+
'pong'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Terminates this proxy
|
|
46
|
+
def terminate!
|
|
47
|
+
# AL: The only way I could find to kill the main thread from a sub thread
|
|
48
|
+
Thread.main.raise SystemExit
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module RR
|
|
2
|
+
|
|
3
|
+
# Scans two tables for differences.
|
|
4
|
+
# Doesn't have any reporting functionality by itself.
|
|
5
|
+
# Instead DirectTableScan#run yields all the differences for the caller to do with as it pleases.
|
|
6
|
+
# Usage:
|
|
7
|
+
# 1. Create a new DirectTableScan object and hand it all necessary information
|
|
8
|
+
# 2. Call DirectTableScan#run to do the actual comparison
|
|
9
|
+
# 3. The block handed to DirectTableScan#run receives all differences
|
|
10
|
+
class DirectTableScan < TableScan
|
|
11
|
+
include TableScanHelper
|
|
12
|
+
|
|
13
|
+
# The TypeCastingCursor for the left table
|
|
14
|
+
attr_accessor :left_caster
|
|
15
|
+
|
|
16
|
+
# The TypeCastingCursor for the right table
|
|
17
|
+
attr_accessor :right_caster
|
|
18
|
+
|
|
19
|
+
# Creates a new DirectTableScan instance
|
|
20
|
+
# * session: a Session object representing the current database session
|
|
21
|
+
# * left_table: name of the table in the left database
|
|
22
|
+
# * right_table: name of the table in the right database. If not given, same like left_table
|
|
23
|
+
def initialize(session, left_table, right_table = nil)
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Runs the table scan.
|
|
28
|
+
# Calls the block for every found difference.
|
|
29
|
+
# Differences are yielded with 2 parameters
|
|
30
|
+
# * type: describes the difference, either :left (row only in left table), :right (row only in right table) or :conflict
|
|
31
|
+
# * row: For :left or :right cases a hash describing the row; for :conflict an array of left and right row.
|
|
32
|
+
# A row is a hash of column_name => value pairs.
|
|
33
|
+
def run(&blck)
|
|
34
|
+
left_cursor = right_cursor = nil
|
|
35
|
+
left_cursor = session.left.select_cursor(
|
|
36
|
+
:table => left_table,
|
|
37
|
+
:row_buffer_size => scan_options[:row_buffer_size],
|
|
38
|
+
:type_cast => true
|
|
39
|
+
)
|
|
40
|
+
right_cursor = session.right.select_cursor(
|
|
41
|
+
:table => right_table,
|
|
42
|
+
:row_buffer_size => scan_options[:row_buffer_size],
|
|
43
|
+
:type_cast => true
|
|
44
|
+
)
|
|
45
|
+
left_row = right_row = nil
|
|
46
|
+
update_progress 0 # ensures progress bar is printed even if there are no records
|
|
47
|
+
while left_row or right_row or left_cursor.next? or right_cursor.next?
|
|
48
|
+
# if there is no current left row, _try_ to load the next one
|
|
49
|
+
left_row ||= left_cursor.next_row if left_cursor.next?
|
|
50
|
+
# if there is no current right row, _try_ to load the next one
|
|
51
|
+
right_row ||= right_cursor.next_row if right_cursor.next?
|
|
52
|
+
rank = rank_rows left_row, right_row
|
|
53
|
+
case rank
|
|
54
|
+
when -1
|
|
55
|
+
yield :left, left_row
|
|
56
|
+
left_row = nil
|
|
57
|
+
update_progress 1
|
|
58
|
+
when 1
|
|
59
|
+
yield :right, right_row
|
|
60
|
+
right_row = nil
|
|
61
|
+
update_progress 1
|
|
62
|
+
when 0
|
|
63
|
+
update_progress 2
|
|
64
|
+
if not left_row == right_row
|
|
65
|
+
yield :conflict, [left_row, right_row]
|
|
66
|
+
end
|
|
67
|
+
left_row = right_row = nil
|
|
68
|
+
end
|
|
69
|
+
# check for corresponding right rows
|
|
70
|
+
end
|
|
71
|
+
ensure
|
|
72
|
+
[left_cursor, right_cursor].each {|cursor| cursor.clear if cursor}
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
|
|
5
|
+
module RR
|
|
6
|
+
# This class implements the functionality of the 'generate' command.
|
|
7
|
+
class GenerateRunner
|
|
8
|
+
|
|
9
|
+
CONFIG_TEMPLATE = <<EOF
|
|
10
|
+
RR::Initializer::run do |config|
|
|
11
|
+
config.left = {
|
|
12
|
+
:adapter => 'postgresql', # or 'mysql'
|
|
13
|
+
:database => 'SCOTT',
|
|
14
|
+
:username => 'scott',
|
|
15
|
+
:password => 'tiger',
|
|
16
|
+
:host => '172.16.1.1'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
config.right = {
|
|
20
|
+
:adapter => 'postgresql',
|
|
21
|
+
:database => 'SCOTT',
|
|
22
|
+
:username => 'scott',
|
|
23
|
+
:password => 'tiger',
|
|
24
|
+
:host => '172.16.1.2'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
config.include_tables 'dept'
|
|
28
|
+
config.include_tables /^e/ # regexp matching all tables starting with e
|
|
29
|
+
# config.include_tables /./ # regexp matching all tables in the database
|
|
30
|
+
end
|
|
31
|
+
EOF
|
|
32
|
+
|
|
33
|
+
CommandRunner.register 'generate' => {
|
|
34
|
+
:command => self,
|
|
35
|
+
:description => 'Generates a configuration file template'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Provided options. Possible values:
|
|
39
|
+
# * +:config_file+: path to config file
|
|
40
|
+
attr_accessor :options
|
|
41
|
+
|
|
42
|
+
# Parses the given command line parameter array.
|
|
43
|
+
# Returns the status (as per UNIX conventions: 1 if parameters were invalid,
|
|
44
|
+
# 0 otherwise)
|
|
45
|
+
def process_options(args)
|
|
46
|
+
status = 0
|
|
47
|
+
self.options = {}
|
|
48
|
+
|
|
49
|
+
parser = OptionParser.new do |opts|
|
|
50
|
+
opts.banner = <<EOS
|
|
51
|
+
Usage: #{$0} generate [file_name]
|
|
52
|
+
|
|
53
|
+
Generates a configuration file template under name [file_name].
|
|
54
|
+
EOS
|
|
55
|
+
opts.separator ""
|
|
56
|
+
opts.separator " Specific options:"
|
|
57
|
+
|
|
58
|
+
opts.on_tail("--help", "Show this message") do
|
|
59
|
+
$stderr.puts opts
|
|
60
|
+
self.options = nil
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
begin
|
|
65
|
+
unprocessed_args = parser.parse!(args)
|
|
66
|
+
if options # this will be +nil+ if the --help option is specified
|
|
67
|
+
raise("Please specify the name of the configuration file") if unprocessed_args.empty?
|
|
68
|
+
options[:file_name] = unprocessed_args[0]
|
|
69
|
+
end
|
|
70
|
+
rescue Exception => e
|
|
71
|
+
$stderr.puts "Command line parsing failed: #{e}"
|
|
72
|
+
$stderr.puts parser.help
|
|
73
|
+
self.options = nil
|
|
74
|
+
status = 1
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
return status
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Generates a configuration file template.
|
|
81
|
+
def execute
|
|
82
|
+
if File.exists?(options[:file_name])
|
|
83
|
+
raise("Cowardly refuse to overwrite existing file '#{options[:file_name]}'")
|
|
84
|
+
end
|
|
85
|
+
File.open(options[:file_name], 'w') do |f|
|
|
86
|
+
f.write CONFIG_TEMPLATE
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Entry points for executing a processing run.
|
|
91
|
+
# args: the array of command line options that were provided by the user.
|
|
92
|
+
def self.run(args)
|
|
93
|
+
runner = new
|
|
94
|
+
|
|
95
|
+
status = runner.process_options(args)
|
|
96
|
+
if runner.options
|
|
97
|
+
runner.execute
|
|
98
|
+
end
|
|
99
|
+
status
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module RR
|
|
2
|
+
|
|
3
|
+
# The settings of the current deployment are passed to Rubyrep through the
|
|
4
|
+
# Initializer::run method.
|
|
5
|
+
# This method yields a Configuration object for overwriting of the default
|
|
6
|
+
# settings.
|
|
7
|
+
# Accordingly a configuration file should look something like this:
|
|
8
|
+
#
|
|
9
|
+
# Rubyrep::Initializer.run do |config|
|
|
10
|
+
# config.left = ...
|
|
11
|
+
# end
|
|
12
|
+
class Initializer
|
|
13
|
+
|
|
14
|
+
# Sets a new Configuration object
|
|
15
|
+
# Current configuration values are lost and replaced with the default
|
|
16
|
+
# settings.
|
|
17
|
+
def self.reset
|
|
18
|
+
@@configuration = Configuration.new
|
|
19
|
+
end
|
|
20
|
+
reset
|
|
21
|
+
|
|
22
|
+
# Returns the current Configuration object
|
|
23
|
+
def self.configuration
|
|
24
|
+
@@configuration
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Allows direct overwriting of the Configuration
|
|
28
|
+
def self.configuration=(configuration)
|
|
29
|
+
@@configuration = configuration
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Yields the current Configuration object to enable overwriting of
|
|
33
|
+
# configuration values.
|
|
34
|
+
# Refer to the Initializer class documentation for a usage example.
|
|
35
|
+
def self.run
|
|
36
|
+
yield configuration
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|