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,318 @@
|
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/..'
|
|
2
|
+
|
|
3
|
+
require 'drb'
|
|
4
|
+
|
|
5
|
+
require 'rubyrep'
|
|
6
|
+
require 'forwardable'
|
|
7
|
+
|
|
8
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
|
9
|
+
|
|
10
|
+
module RR
|
|
11
|
+
|
|
12
|
+
# This class represents a remote activerecord database connection.
|
|
13
|
+
# Normally created by DatabaseProxy
|
|
14
|
+
class ProxyConnection
|
|
15
|
+
extend Forwardable
|
|
16
|
+
|
|
17
|
+
# The database connection
|
|
18
|
+
attr_accessor :connection
|
|
19
|
+
|
|
20
|
+
# A hash as described by ActiveRecord::Base#establish_connection
|
|
21
|
+
attr_accessor :config
|
|
22
|
+
|
|
23
|
+
# Forward certain methods to the proxied database connection
|
|
24
|
+
def_delegators :connection,
|
|
25
|
+
:columns, :quote_column_name,
|
|
26
|
+
:quote_table_name, :execute,
|
|
27
|
+
:select_one, :select_all, :tables,
|
|
28
|
+
:begin_db_transaction, :rollback_db_transaction, :commit_db_transaction,
|
|
29
|
+
:referenced_tables,
|
|
30
|
+
:create_or_replace_replication_trigger_function,
|
|
31
|
+
:create_replication_trigger, :drop_replication_trigger, :replication_trigger_exists?,
|
|
32
|
+
:sequence_values, :update_sequences, :clear_sequence_setup,
|
|
33
|
+
:create_table, :drop_table, :add_big_primary_key
|
|
34
|
+
|
|
35
|
+
# Caching the primary keys. This is a hash with
|
|
36
|
+
# * key: table name
|
|
37
|
+
# * value: array of primary key names
|
|
38
|
+
attr_accessor :primary_key_names_cache
|
|
39
|
+
|
|
40
|
+
# Hash to register cursors.
|
|
41
|
+
# Purpose:
|
|
42
|
+
# Objects only referenced remotely via DRb can be garbage collected.
|
|
43
|
+
# We register them in this hash to protect them from unintended garbage collection.
|
|
44
|
+
attr_accessor :cursors
|
|
45
|
+
|
|
46
|
+
# 2-level Hash of table_name => column_name => Column objects.
|
|
47
|
+
attr_accessor :table_columns
|
|
48
|
+
|
|
49
|
+
# Hash of table_name => array of column names pairs.
|
|
50
|
+
attr_accessor :table_column_names
|
|
51
|
+
|
|
52
|
+
# A hash of manually overwritten primary keys:
|
|
53
|
+
# * key: table_name
|
|
54
|
+
# * value: array of primary key names
|
|
55
|
+
attr_accessor :manual_primary_keys
|
|
56
|
+
|
|
57
|
+
# Returns an array of primary key names for the given +table_name+.
|
|
58
|
+
# Caches the result for future calls. Allows manual overwrites through
|
|
59
|
+
# the Configuration options +:primary_key_names+ or :+primary_key_only_limit+.
|
|
60
|
+
#
|
|
61
|
+
# Parameters:
|
|
62
|
+
# * +table_name+: name of the table
|
|
63
|
+
# * +options+: An option hash with the following valid options:
|
|
64
|
+
# * :+raw+: if +true+, than don't use manual overwrites and don't cache
|
|
65
|
+
def primary_key_names(table_name, options = {})
|
|
66
|
+
return connection.primary_key_names(table_name) if options[:raw]
|
|
67
|
+
|
|
68
|
+
self.primary_key_names_cache ||= {}
|
|
69
|
+
result = primary_key_names_cache[table_name]
|
|
70
|
+
unless result
|
|
71
|
+
result = manual_primary_keys[table_name] || connection.primary_key_names(table_name)
|
|
72
|
+
primary_key_names_cache[table_name] = result
|
|
73
|
+
end
|
|
74
|
+
result
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Returns a Hash of currently registerred cursors
|
|
78
|
+
def cursors
|
|
79
|
+
@cursors ||= {}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Store a cursor in the register to protect it from the garbage collector.
|
|
83
|
+
def save_cursor(cursor)
|
|
84
|
+
cursors[cursor] = cursor
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# default number of records that are read into memory at a time during
|
|
88
|
+
# select queries
|
|
89
|
+
DEFAULT_ROW_BUFFER_SIZE = 1000
|
|
90
|
+
|
|
91
|
+
# Returns a cusor as produced by the #select_cursor method of the connection
|
|
92
|
+
# extenders.
|
|
93
|
+
#
|
|
94
|
+
# Two modes of operation: Either
|
|
95
|
+
# * execute the specified query (takes precedense) OR
|
|
96
|
+
# * first build the query based on options forwarded to #table_select_query
|
|
97
|
+
# +options+ is a hash with
|
|
98
|
+
# * :+query+: executes the given query
|
|
99
|
+
# * :+type_cast+: if +true+, build a type casting cursor around the result
|
|
100
|
+
# * :+table+: name of the table from which to read data
|
|
101
|
+
# * further options as taken by #table_select_query to build the query
|
|
102
|
+
# * :+row_buffer_size+:
|
|
103
|
+
# Integer controlling how many rows a read into memory at one time.
|
|
104
|
+
def select_cursor(options)
|
|
105
|
+
row_buffer_size = options[:row_buffer_size] || DEFAULT_ROW_BUFFER_SIZE
|
|
106
|
+
query = options[:query] || table_select_query(options[:table], options)
|
|
107
|
+
cursor = connection.select_cursor query, row_buffer_size
|
|
108
|
+
if options[:type_cast]
|
|
109
|
+
cursor = TypeCastingCursor.new(self, options[:table], cursor)
|
|
110
|
+
end
|
|
111
|
+
cursor
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Create a session on the proxy side according to provided configuration hash.
|
|
115
|
+
# +config+ is a hash as described by ActiveRecord::Base#establish_connection
|
|
116
|
+
def initialize(config)
|
|
117
|
+
self.connection = ConnectionExtenders.db_connect config
|
|
118
|
+
self.config = config
|
|
119
|
+
self.manual_primary_keys = {}
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Checks if the connection is still active and if not, reestablished it.
|
|
123
|
+
def refresh
|
|
124
|
+
unless self.connection.active?
|
|
125
|
+
self.connection = ConnectionExtenders.db_connect config
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Destroys the session
|
|
130
|
+
def destroy
|
|
131
|
+
self.connection.disconnect!
|
|
132
|
+
|
|
133
|
+
cursors.each_key do |cursor|
|
|
134
|
+
cursor.destroy
|
|
135
|
+
end
|
|
136
|
+
cursors.clear
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Quotes the given value. It is assumed that the value belongs to the specified column name and table name.
|
|
140
|
+
# Caches the column objects for higher speed.
|
|
141
|
+
def quote_value(table, column, value)
|
|
142
|
+
self.table_columns ||= {}
|
|
143
|
+
unless table_columns.include? table
|
|
144
|
+
table_columns[table] = {}
|
|
145
|
+
columns(table).each {|c| table_columns[table][c.name] = c}
|
|
146
|
+
end
|
|
147
|
+
connection.quote value, table_columns[table][column]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# Create a cursor for the given table.
|
|
151
|
+
# * +cursor_class+: should specify the Cursor class (e. g. ProxyBlockCursor or ProxyRowCursor).
|
|
152
|
+
# * +table+: name of the table
|
|
153
|
+
# * +options+: An option hash that is used to construct the SQL query. See ProxyCursor#construct_query for details.
|
|
154
|
+
def create_cursor(cursor_class, table, options = {})
|
|
155
|
+
cursor = cursor_class.new self, table
|
|
156
|
+
cursor.prepare_fetch options
|
|
157
|
+
save_cursor cursor
|
|
158
|
+
cursor
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Destroys the provided cursor and removes it from the register
|
|
162
|
+
def destroy_cursor(cursor)
|
|
163
|
+
cursor.destroy
|
|
164
|
+
cursors.delete cursor
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Returns an array of column names of the given table name.
|
|
168
|
+
# The array is ordered in the sequence as returned by the database.
|
|
169
|
+
# The result is cached for higher speed.
|
|
170
|
+
def column_names(table)
|
|
171
|
+
self.table_column_names ||= {}
|
|
172
|
+
unless table_column_names.include? table
|
|
173
|
+
table_column_names[table] = columns(table).map {|c| c.name}
|
|
174
|
+
end
|
|
175
|
+
table_column_names[table]
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Returns a list of quoted column names for the given +table+ as comma
|
|
179
|
+
# separated string.
|
|
180
|
+
def quote_column_list(table)
|
|
181
|
+
column_names(table).map do |column_name|
|
|
182
|
+
quote_column_name(column_name)
|
|
183
|
+
end.join(', ')
|
|
184
|
+
end
|
|
185
|
+
private :quote_column_list
|
|
186
|
+
|
|
187
|
+
# Returns a list of quoted primary key names for the given +table+ as comma
|
|
188
|
+
# separated string.
|
|
189
|
+
def quote_key_list(table)
|
|
190
|
+
primary_key_names(table).map do |column_name|
|
|
191
|
+
quote_column_name(column_name)
|
|
192
|
+
end.join(', ')
|
|
193
|
+
end
|
|
194
|
+
private :quote_key_list
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# Generates an sql condition string for the given +table+ based on
|
|
198
|
+
# * +row+: a hash of primary key => value pairs designating the target row
|
|
199
|
+
# * +condition+: the type of sql condition (something like '>=' or '=', etc.)
|
|
200
|
+
def row_condition(table, row, condition)
|
|
201
|
+
query_part = ""
|
|
202
|
+
query_part << ' (' << quote_key_list(table) << ') ' << condition
|
|
203
|
+
query_part << ' (' << primary_key_names(table).map do |key|
|
|
204
|
+
quote_value(table, key, row[key])
|
|
205
|
+
end.join(', ') << ')'
|
|
206
|
+
query_part
|
|
207
|
+
end
|
|
208
|
+
private :row_condition
|
|
209
|
+
|
|
210
|
+
# Returns an SQL query string for the given +table+ based on the provided +options+.
|
|
211
|
+
# +options+ is a hash that can contain any of the following:
|
|
212
|
+
# * :+from+: nil OR the hash of primary key => value pairs designating the start of the selection
|
|
213
|
+
# * :+exclude_starting_row+: if true, do not include the row specified by :+from+
|
|
214
|
+
# * :+to+: nil OR the hash of primary key => value pairs designating the end of the selection
|
|
215
|
+
# * :+row_keys+: an array of primary key => value hashes specify the target rows.
|
|
216
|
+
def table_select_query(table, options = {})
|
|
217
|
+
query = "select #{quote_column_list(table)}"
|
|
218
|
+
query << " from #{quote_table_name(table)}"
|
|
219
|
+
query << " where" if [:from, :to, :row_keys].any? {|key| options.include? key}
|
|
220
|
+
first_condition = true
|
|
221
|
+
if options[:from]
|
|
222
|
+
first_condition = false
|
|
223
|
+
matching_condition = options[:exclude_starting_row] ? '>' : '>='
|
|
224
|
+
query << row_condition(table, options[:from], matching_condition)
|
|
225
|
+
end
|
|
226
|
+
if options[:to]
|
|
227
|
+
query << ' and' unless first_condition
|
|
228
|
+
first_condition = false
|
|
229
|
+
query << row_condition(table, options[:to], '<=')
|
|
230
|
+
end
|
|
231
|
+
if options[:row_keys]
|
|
232
|
+
query << ' and' unless first_condition
|
|
233
|
+
if options[:row_keys].empty?
|
|
234
|
+
query << ' false'
|
|
235
|
+
else
|
|
236
|
+
query << ' (' << quote_key_list(table) << ') in ('
|
|
237
|
+
first_key = true
|
|
238
|
+
options[:row_keys].each do |row|
|
|
239
|
+
query << ', ' unless first_key
|
|
240
|
+
first_key = false
|
|
241
|
+
query << '(' << primary_key_names(table).map do |key|
|
|
242
|
+
quote_value(table, key, row[key])
|
|
243
|
+
end.join(', ') << ')'
|
|
244
|
+
end
|
|
245
|
+
query << ')'
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
query << " order by #{quote_key_list(table)}"
|
|
249
|
+
|
|
250
|
+
query
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Returns an SQL insert query for the given +table+ and +values+.
|
|
254
|
+
# +values+ is a hash of column_name => value pairs.
|
|
255
|
+
def table_insert_query(table, values)
|
|
256
|
+
query = "insert into #{quote_table_name(table)}"
|
|
257
|
+
query << '(' << values.keys.map do |column_name|
|
|
258
|
+
quote_column_name(column_name)
|
|
259
|
+
end.join(', ') << ') '
|
|
260
|
+
query << 'values(' << values.map do |column_name, value|
|
|
261
|
+
quote_value(table, column_name, value)
|
|
262
|
+
end.join(', ') << ')'
|
|
263
|
+
query
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Inserts the specified records into the named +table+.
|
|
267
|
+
# +values+ is a hash of column_name => value pairs.
|
|
268
|
+
def insert_record(table, values)
|
|
269
|
+
execute table_insert_query(table, values)
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Returns an SQL update query.
|
|
273
|
+
# * +table+: name of the target table
|
|
274
|
+
# * +values+: a hash of column_name => value pairs
|
|
275
|
+
# * +org_key+:
|
|
276
|
+
# A hash of column_name => value pairs. If +nil+, use the key specified by
|
|
277
|
+
# +values+ instead.
|
|
278
|
+
def table_update_query(table, values, org_key = nil)
|
|
279
|
+
org_key ||= values
|
|
280
|
+
query = "update #{quote_table_name(table)} set "
|
|
281
|
+
query << values.map do |column_name, value|
|
|
282
|
+
"#{quote_column_name(column_name)} = #{quote_value(table, column_name, value)}"
|
|
283
|
+
end.join(', ')
|
|
284
|
+
query << " where (" << quote_key_list(table) << ") = ("
|
|
285
|
+
query << primary_key_names(table).map do |key|
|
|
286
|
+
quote_value(table, key, org_key[key])
|
|
287
|
+
end.join(', ') << ")"
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
# Updates the specified records of the specified table.
|
|
291
|
+
# * +table+: name of the target table
|
|
292
|
+
# * +values+: a hash of column_name => value pairs.
|
|
293
|
+
# * +org_key+:
|
|
294
|
+
# A hash of column_name => value pairs. If +nil+, use the key specified by
|
|
295
|
+
# +values+ instead.
|
|
296
|
+
def update_record(table, values, org_key = nil)
|
|
297
|
+
execute table_update_query(table, values, org_key)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
# Returns an SQL delete query for the given +table+ and +values+
|
|
301
|
+
# +values+ is a hash of column_name => value pairs. (Only the primary key
|
|
302
|
+
# values will be used and must be included in the hash.)
|
|
303
|
+
def table_delete_query(table, values)
|
|
304
|
+
query = "delete from #{quote_table_name(table)}"
|
|
305
|
+
query << " where (" << quote_key_list(table) << ") = ("
|
|
306
|
+
query << primary_key_names(table).map do |key|
|
|
307
|
+
quote_value(table, key, values[key])
|
|
308
|
+
end.join(', ') << ")"
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Deletes the specified record from the named +table+.
|
|
312
|
+
# +values+ is a hash of column_name => value pairs. (Only the primary key
|
|
313
|
+
# values will be used and must be included in the hash.)
|
|
314
|
+
def delete_record(table, values)
|
|
315
|
+
execute table_delete_query(table, values)
|
|
316
|
+
end
|
|
317
|
+
end
|
|
318
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/..'
|
|
2
|
+
|
|
3
|
+
require 'rubyrep'
|
|
4
|
+
|
|
5
|
+
module RR
|
|
6
|
+
# Provides shared functionality for ProxyRowCursor and ProxyBlockCursor
|
|
7
|
+
class ProxyCursor
|
|
8
|
+
|
|
9
|
+
# The current ProxyConnection.
|
|
10
|
+
attr_accessor :connection
|
|
11
|
+
|
|
12
|
+
# The name of the current table.
|
|
13
|
+
attr_accessor :table
|
|
14
|
+
|
|
15
|
+
# Array of primary key names for current table.
|
|
16
|
+
attr_accessor :primary_key_names
|
|
17
|
+
|
|
18
|
+
# The current cursor.
|
|
19
|
+
attr_accessor :cursor
|
|
20
|
+
|
|
21
|
+
# Shared initializations
|
|
22
|
+
# * connection: the current proxy connection
|
|
23
|
+
# * table: table_name
|
|
24
|
+
def initialize(connection, table)
|
|
25
|
+
self.connection = connection
|
|
26
|
+
self.table = table
|
|
27
|
+
self.primary_key_names = connection.primary_key_names table
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Initiate a query for the specified row range.
|
|
31
|
+
# +options+: An option hash that is used to construct the SQL query. See ProxyCursor#construct_query for details.
|
|
32
|
+
def prepare_fetch(options = {})
|
|
33
|
+
self.cursor = connection.select_cursor(
|
|
34
|
+
options.merge(:table => table, :type_cast => true)
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Releases all ressources
|
|
39
|
+
def destroy
|
|
40
|
+
self.cursor.clear if self.cursor
|
|
41
|
+
self.cursor = nil
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/..'
|
|
2
|
+
|
|
3
|
+
require 'digest/sha1'
|
|
4
|
+
|
|
5
|
+
require 'rubyrep'
|
|
6
|
+
|
|
7
|
+
module RR
|
|
8
|
+
|
|
9
|
+
# This class is used to scan a given table range
|
|
10
|
+
# Can return rows either themselves or only their checksum
|
|
11
|
+
class ProxyRowCursor < ProxyCursor
|
|
12
|
+
|
|
13
|
+
# The column_name => value hash of the current row.
|
|
14
|
+
attr_accessor :current_row
|
|
15
|
+
|
|
16
|
+
# Creates a new cursor
|
|
17
|
+
# * session: the current proxy session
|
|
18
|
+
# * table: table_name
|
|
19
|
+
def initialize(session, table)
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Returns true if there are unprocessed rows in the table range
|
|
24
|
+
def next?
|
|
25
|
+
cursor.next?
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Returns the next row in cursor
|
|
29
|
+
def next_row
|
|
30
|
+
cursor.next_row
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Returns for the next row
|
|
34
|
+
# * a hash of :column_name => value pairs of the primary keys
|
|
35
|
+
# * checksum string for that row
|
|
36
|
+
def next_row_keys_and_checksum
|
|
37
|
+
self.current_row = cursor.next_row
|
|
38
|
+
keys = self.current_row.reject {|key, | not primary_key_names.include? key}
|
|
39
|
+
checksum = Digest::SHA1.hexdigest(Marshal.dump(self.current_row))
|
|
40
|
+
return keys, checksum
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
require 'drb'
|
|
5
|
+
|
|
6
|
+
module RR
|
|
7
|
+
# This class implements the functionality of the rrproxy.rb command.
|
|
8
|
+
class ProxyRunner
|
|
9
|
+
|
|
10
|
+
CommandRunner.register 'proxy' => {
|
|
11
|
+
:command => self,
|
|
12
|
+
:description => 'Proxies connections from rubyrep commands to the database'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
# Default options to start a DatabaseProxy server
|
|
16
|
+
DEFAULT_OPTIONS = {
|
|
17
|
+
:port => DatabaseProxy::DEFAULT_PORT,
|
|
18
|
+
:host => ''
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
# Parses the given command line parameter array.
|
|
22
|
+
# Returns
|
|
23
|
+
# * the options hash or nil if command line parsing failed
|
|
24
|
+
# * status (as per UNIX conventions: 1 if parameters were invalid, 0 otherwise)
|
|
25
|
+
def get_options(args)
|
|
26
|
+
options = DEFAULT_OPTIONS
|
|
27
|
+
status = 0
|
|
28
|
+
|
|
29
|
+
parser = OptionParser.new do |opts|
|
|
30
|
+
opts.banner = "Usage: #{$0} proxy [options]"
|
|
31
|
+
opts.separator ""
|
|
32
|
+
opts.separator "Specific options:"
|
|
33
|
+
|
|
34
|
+
opts.on("-h","--host", "=IP_ADDRESS", "IP address to listen on. Default: binds to all IP addresses of the computer") do |arg|
|
|
35
|
+
options[:host] = arg
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
opts.on("-p","--port", "=PORT_NUMBER", Integer, "TCP port to listen on. Default port: #{DatabaseProxy::DEFAULT_PORT}") do |arg|
|
|
39
|
+
options[:port] = arg
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
opts.on_tail("--help", "Show this message") do
|
|
43
|
+
$stderr.puts opts
|
|
44
|
+
options = nil
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
begin
|
|
49
|
+
parser.parse!(args)
|
|
50
|
+
rescue Exception => e
|
|
51
|
+
$stderr.puts "Command line parsing failed: #{e}"
|
|
52
|
+
$stderr.puts parser.help
|
|
53
|
+
options = nil
|
|
54
|
+
status = 1
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
return options, status
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Builds the druby URL from the given options and returns it
|
|
61
|
+
def build_url(options)
|
|
62
|
+
"druby://#{options[:host]}:#{options[:port]}"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Starts a proxy server under the given druby URL
|
|
66
|
+
def start_server(url)
|
|
67
|
+
proxy = DatabaseProxy.new
|
|
68
|
+
|
|
69
|
+
DRb.start_service(url, proxy)
|
|
70
|
+
DRb.thread.join
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Runs the ProxyRunner (processing of command line & starting of server)
|
|
74
|
+
# args: the array of command line options with which to start the server
|
|
75
|
+
def self.run(args)
|
|
76
|
+
runner = ProxyRunner.new
|
|
77
|
+
|
|
78
|
+
options, status = runner.get_options(args)
|
|
79
|
+
if options
|
|
80
|
+
url = runner.build_url(options)
|
|
81
|
+
runner.start_server(url)
|
|
82
|
+
end
|
|
83
|
+
status
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
module RR
|
|
2
|
+
|
|
3
|
+
# Describes a (record specific) difference between both databases as identifed
|
|
4
|
+
# via change log.
|
|
5
|
+
class ReplicationDifference
|
|
6
|
+
|
|
7
|
+
# The current Session.
|
|
8
|
+
attr_accessor :session
|
|
9
|
+
|
|
10
|
+
# The type of the difference. Either
|
|
11
|
+
# * :+left+: change in left database
|
|
12
|
+
# * :+right+: change in right database
|
|
13
|
+
# * :+conflict+: change in both databases
|
|
14
|
+
# * :+no_diff+: changes in both databases constitute no difference
|
|
15
|
+
attr_accessor :type
|
|
16
|
+
|
|
17
|
+
# A hash with keys :+left+ and / or :+right+.
|
|
18
|
+
# Hash values are LoggedChange instances.
|
|
19
|
+
def changes
|
|
20
|
+
@changes ||= {}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Creates a new ReplicationDifference instance.
|
|
24
|
+
# +session+ is the current Session.
|
|
25
|
+
def initialize(session)
|
|
26
|
+
self.session = session
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Should be set to +true+ if this ReplicationDifference instance was
|
|
30
|
+
# successfully loaded.
|
|
31
|
+
attr_writer :loaded
|
|
32
|
+
|
|
33
|
+
# Returns +true+ if a replication difference was loaded
|
|
34
|
+
def loaded?
|
|
35
|
+
@loaded
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Shortcut to calculate the "other" database.
|
|
39
|
+
OTHER_SIDE = {
|
|
40
|
+
:left => :right,
|
|
41
|
+
:right => :left
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Resulting diff type based on types of left changes (outer hash) and right
|
|
45
|
+
# changes (inner hash)
|
|
46
|
+
DIFF_TYPES = {
|
|
47
|
+
:insert => {:insert => :conflict, :update => :conflict, :delete => :conflict, :no_change => :left},
|
|
48
|
+
:update => {:insert => :conflict, :update => :conflict, :delete => :conflict, :no_change => :left},
|
|
49
|
+
:delete => {:insert => :conflict, :update => :conflict, :delete => :no_change, :no_change => :left},
|
|
50
|
+
:no_change => {:insert => :right, :update => :right, :delete => :right, :no_change => :no_change}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Amends a difference according to new entries in the change log table
|
|
54
|
+
def amend
|
|
55
|
+
session.reload_changes
|
|
56
|
+
changes[:left].load
|
|
57
|
+
changes[:right].load
|
|
58
|
+
self.type = DIFF_TYPES[changes[:left].type][changes[:right].type]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Loads a difference
|
|
62
|
+
def load
|
|
63
|
+
change_times = {}
|
|
64
|
+
[:left, :right].each do |database|
|
|
65
|
+
changes[database] = LoggedChange.new session, database
|
|
66
|
+
change_times[database] = changes[database].oldest_change_time
|
|
67
|
+
end
|
|
68
|
+
return if change_times[:left] == nil and change_times[:right] == nil
|
|
69
|
+
|
|
70
|
+
oldest = nil
|
|
71
|
+
[:left, :right].each do |database|
|
|
72
|
+
oldest = OTHER_SIDE[database] if change_times[database] == nil
|
|
73
|
+
end
|
|
74
|
+
oldest ||= change_times[:left] <= change_times[:right] ? :left : :right
|
|
75
|
+
changes[oldest].load_oldest
|
|
76
|
+
|
|
77
|
+
changes[OTHER_SIDE[oldest]].load_specified(
|
|
78
|
+
session.corresponding_table(oldest, changes[oldest].table),
|
|
79
|
+
changes[oldest].key)
|
|
80
|
+
|
|
81
|
+
self.type = DIFF_TYPES[changes[:left].type][changes[:right].type]
|
|
82
|
+
self.loaded = true
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Prevents session from going into YAML output
|
|
86
|
+
def to_yaml_properties
|
|
87
|
+
instance_variables.sort.reject {|var_name| var_name == '@session'}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
end
|
|
91
|
+
end
|