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,174 @@
|
|
|
1
|
+
module RR
|
|
2
|
+
module Syncers
|
|
3
|
+
# This syncer implements a two way sync.
|
|
4
|
+
# Syncer options relevant for this syncer:
|
|
5
|
+
# * :+left_record_handling+, :+right_record_handling+:
|
|
6
|
+
# Handling of records only existing only in the named database.
|
|
7
|
+
# Can be any of the following:
|
|
8
|
+
# * :+ignore+: No action.
|
|
9
|
+
# * :+delete+: Delete from the source database.
|
|
10
|
+
# * :+insert+: Insert in the target database. *Default* *Setting*
|
|
11
|
+
# * +Proc+ object:
|
|
12
|
+
# If a Proc object is given, it is responsible for dealing with the
|
|
13
|
+
# record. Called with the following parameters:
|
|
14
|
+
# * sync_helper: The current SyncHelper instance.
|
|
15
|
+
# * type: :+left+ or :+right+ to designate source database
|
|
16
|
+
# * row: column_name => value hash representing the row
|
|
17
|
+
# * :+sync_conflict_handling+:
|
|
18
|
+
# Handling of conflicting records. Can be any of the following:
|
|
19
|
+
# * :+ignore+: No action. *Default* *Setting*
|
|
20
|
+
# * :+left_wins+: Update right database with the field values in the left db.
|
|
21
|
+
# * :+right_wins+: Update left database with the field values in the right db.
|
|
22
|
+
# * +Proc+ object:
|
|
23
|
+
# If a Proc object is given, it is responsible for dealing with the
|
|
24
|
+
# record. Called with the following parameters:
|
|
25
|
+
# * sync_helper: The current SyncHelper instance.
|
|
26
|
+
# * type: always :+conflict+
|
|
27
|
+
# * rows: A two element array of rows (column_name => value hashes).
|
|
28
|
+
# First left, than right record.
|
|
29
|
+
# * :+logged_sync_events+:
|
|
30
|
+
# Specifies which types of syncs are logged.
|
|
31
|
+
# Is either a single value or an array of multiple ones.
|
|
32
|
+
# Default: [:ignored_conflicts]
|
|
33
|
+
# Possible values:
|
|
34
|
+
# * :+ignored_changes+: log ignored (but not synced) non-conflict changes
|
|
35
|
+
# * :+all_changes+: log all non-conflict changes
|
|
36
|
+
# * :+ignored_conflicts+: log ignored (but not synced) conflicts
|
|
37
|
+
# * :+all_conflicts+: log all conflicts
|
|
38
|
+
#
|
|
39
|
+
# Example of using a Proc object:
|
|
40
|
+
# lambda do |sync_helper, type, row|
|
|
41
|
+
# # delete records existing only in the left database.
|
|
42
|
+
# sync_helper.delete(type, row) if type == :left
|
|
43
|
+
# end
|
|
44
|
+
class TwoWaySyncer
|
|
45
|
+
|
|
46
|
+
# Register the syncer
|
|
47
|
+
Syncers.register :two_way => self
|
|
48
|
+
|
|
49
|
+
# The current SyncHelper object
|
|
50
|
+
attr_accessor :sync_helper
|
|
51
|
+
|
|
52
|
+
# Provides default option for the syncer. Optional.
|
|
53
|
+
# Returns a hash with key => value pairs.
|
|
54
|
+
def self.default_options
|
|
55
|
+
{
|
|
56
|
+
:left_record_handling => :insert,
|
|
57
|
+
:right_record_handling => :insert,
|
|
58
|
+
:sync_conflict_handling => :ignore,
|
|
59
|
+
:logged_sync_events => [:ignored_conflicts]
|
|
60
|
+
}
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Verifies if the given :+left_record_handling+ / :+right_record_handling+
|
|
64
|
+
# option is valid.
|
|
65
|
+
# Raises an ArgumentError if option is invalid
|
|
66
|
+
def validate_left_right_record_handling_option(option)
|
|
67
|
+
unless option.respond_to? :call
|
|
68
|
+
unless [:ignore, :delete, :insert].include? option
|
|
69
|
+
raise ArgumentError.new("#{option.inspect} not a valid :left_record_handling / :right_record_handling option")
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Verifies if the given :+sync_conflict_handling+ option is valid.
|
|
75
|
+
# Raises an ArgumentError if option is invalid
|
|
76
|
+
def validate_conflict_handling_option(option)
|
|
77
|
+
unless option.respond_to? :call
|
|
78
|
+
unless [:ignore, :right_wins, :left_wins].include? option
|
|
79
|
+
raise ArgumentError.new("#{option.inspect} not a valid :sync_conflict_handling option")
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Verifies if the given :+replication_logging+ option /options is / are valid.
|
|
85
|
+
# Raises an ArgumentError if invalid
|
|
86
|
+
def validate_logging_options(options)
|
|
87
|
+
values = [options].flatten # ensure that I have an array
|
|
88
|
+
values.each do |value|
|
|
89
|
+
unless [:ignored_changes, :all_changes, :ignored_conflicts, :all_conflicts].include? value
|
|
90
|
+
raise ArgumentError.new("#{value.inspect} not a valid :logged_sync_events option")
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Initializes the syncer
|
|
96
|
+
# * sync_helper:
|
|
97
|
+
# The SyncHelper object provided information and utility functions.
|
|
98
|
+
# Raises an ArgumentError if any of the option in sync_helper.sync_options
|
|
99
|
+
# is invalid.
|
|
100
|
+
def initialize(sync_helper)
|
|
101
|
+
validate_left_right_record_handling_option sync_helper.sync_options[:left_record_handling]
|
|
102
|
+
validate_left_right_record_handling_option sync_helper.sync_options[:right_record_handling]
|
|
103
|
+
validate_conflict_handling_option sync_helper.sync_options[:sync_conflict_handling]
|
|
104
|
+
validate_logging_options sync_helper.sync_options[:logged_sync_events]
|
|
105
|
+
|
|
106
|
+
self.sync_helper = sync_helper
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Sync type descriptions that are written into the event log
|
|
110
|
+
TYPE_DESCRIPTIONS = {
|
|
111
|
+
:left => 'left_record',
|
|
112
|
+
:right => 'right_record',
|
|
113
|
+
:conflict => 'conflict'
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
# Returns the :logged_sync_events option values.
|
|
117
|
+
def log_option_values
|
|
118
|
+
@log_option_values ||= [sync_helper.sync_options[:logged_sync_events]].flatten
|
|
119
|
+
end
|
|
120
|
+
private :log_option_values
|
|
121
|
+
|
|
122
|
+
# Logs a sync event into the event log table as per configuration options.
|
|
123
|
+
# * +type+: Refer to DirectTableScan#run for a description
|
|
124
|
+
# * +action+: the sync action that is executed
|
|
125
|
+
# (The :+left_record_handling+ / :+right_record_handling+ or
|
|
126
|
+
# :+sync_conflict_handling+ option)
|
|
127
|
+
# * +row+: Refer to DirectTableScan#run for a description
|
|
128
|
+
def log_sync_outcome(type, action, row)
|
|
129
|
+
if type == :conflict
|
|
130
|
+
return unless log_option_values.include?(:all_conflicts) or log_option_values.include?(:ignored_conflicts)
|
|
131
|
+
return if action != :ignore and not log_option_values.include?(:all_conflicts)
|
|
132
|
+
row = row[0] # Extract left row from row array
|
|
133
|
+
else
|
|
134
|
+
return unless log_option_values.include?(:all_changes) or log_option_values.include?(:ignored_changes)
|
|
135
|
+
return if action != :ignore and not log_option_values.include?(:all_changes)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
sync_helper.log_sync_outcome row, TYPE_DESCRIPTIONS[type], action
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Called to sync the provided difference.
|
|
142
|
+
# See DirectTableScan#run for a description of the +type+ and +row+ parameters.
|
|
143
|
+
def sync_difference(type, row)
|
|
144
|
+
if type == :left or type == :right
|
|
145
|
+
option_key = type == :left ? :left_record_handling : :right_record_handling
|
|
146
|
+
option = sync_helper.sync_options[option_key]
|
|
147
|
+
log_sync_outcome type, option, row unless option.respond_to?(:call)
|
|
148
|
+
if option == :ignore
|
|
149
|
+
# nothing to do
|
|
150
|
+
elsif option == :delete
|
|
151
|
+
sync_helper.delete_record type, row
|
|
152
|
+
elsif option == :insert
|
|
153
|
+
target = (type == :left ? :right : :left)
|
|
154
|
+
sync_helper.insert_record target, row
|
|
155
|
+
else #option must be a Proc
|
|
156
|
+
option.call sync_helper, type, row
|
|
157
|
+
end
|
|
158
|
+
else
|
|
159
|
+
option = sync_helper.sync_options[:sync_conflict_handling]
|
|
160
|
+
log_sync_outcome type, option, row unless option.respond_to?(:call)
|
|
161
|
+
if option == :ignore
|
|
162
|
+
# nothing to do
|
|
163
|
+
elsif option == :right_wins
|
|
164
|
+
sync_helper.update_record :left, row[1]
|
|
165
|
+
elsif option == :left_wins
|
|
166
|
+
sync_helper.update_record :right, row[0]
|
|
167
|
+
else #option must be a Proc
|
|
168
|
+
option.call sync_helper, type, row
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module RR
|
|
2
|
+
|
|
3
|
+
# Shared functionality for DirectTableScan and ProxiedTableScan
|
|
4
|
+
class TableScan
|
|
5
|
+
include TableScanHelper
|
|
6
|
+
|
|
7
|
+
# The current Session object
|
|
8
|
+
attr_accessor :session
|
|
9
|
+
|
|
10
|
+
# Name of the left table
|
|
11
|
+
attr_accessor :left_table
|
|
12
|
+
|
|
13
|
+
# Name of the right table
|
|
14
|
+
attr_accessor :right_table
|
|
15
|
+
|
|
16
|
+
# Cached array of primary key names
|
|
17
|
+
attr_accessor :primary_key_names
|
|
18
|
+
|
|
19
|
+
# Receives the active ScanProgressPrinters class
|
|
20
|
+
attr_accessor :progress_printer
|
|
21
|
+
|
|
22
|
+
# Returns a hash of scan options for this table scan.
|
|
23
|
+
def scan_options
|
|
24
|
+
@scan_options ||= session.configuration.options_for_table(left_table)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Inform new progress to progress printer
|
|
28
|
+
# +steps+ is the number of processed records.
|
|
29
|
+
def update_progress(steps)
|
|
30
|
+
return unless progress_printer
|
|
31
|
+
unless @progress_printer_instance
|
|
32
|
+
total_records =
|
|
33
|
+
session.left.select_one("select count(*) as n from #{left_table}")['n'].to_i +
|
|
34
|
+
session.right.select_one("select count(*) as n from #{right_table}")['n'].to_i
|
|
35
|
+
@progress_printer_instance = progress_printer.new(total_records, session, left_table, right_table)
|
|
36
|
+
end
|
|
37
|
+
@progress_printer_instance.step(steps)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Creates a new DirectTableScan instance
|
|
41
|
+
# * session: a Session object representing the current database session
|
|
42
|
+
# * left_table: name of the table in the left database
|
|
43
|
+
# * right_table: name of the table in the right database. If not given, same like left_table
|
|
44
|
+
def initialize(session, left_table, right_table = nil)
|
|
45
|
+
if session.left.primary_key_names(left_table).empty?
|
|
46
|
+
raise "Table '#{left_table}' doesn't have a primary key. Cannot scan."
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
self.session, self.left_table, self.right_table = session, left_table, right_table
|
|
50
|
+
self.right_table ||= self.left_table
|
|
51
|
+
self.primary_key_names = session.left.primary_key_names left_table
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/..'
|
|
2
|
+
|
|
3
|
+
require 'rubyrep'
|
|
4
|
+
|
|
5
|
+
module RR
|
|
6
|
+
|
|
7
|
+
# Some helper functions that are of use to all TableScan classes
|
|
8
|
+
module TableScanHelper
|
|
9
|
+
# Compares the primary keys of left_row and right_row to determine their rank.
|
|
10
|
+
# Assumes there is a function primary_key_names returning the array of primary keys
|
|
11
|
+
# that are relevant for this comparison
|
|
12
|
+
#
|
|
13
|
+
# Assumes that at least one of left_row and right_row is not nil
|
|
14
|
+
# A nil row counts as infinite.
|
|
15
|
+
# E. g. left_row is something and right_row is nil ==> left_row is smaller ==> return -1
|
|
16
|
+
def rank_rows(left_row, right_row)
|
|
17
|
+
raise "At least one of left_row and right_row must not be nil!" unless left_row or right_row
|
|
18
|
+
return -1 unless right_row
|
|
19
|
+
return 1 unless left_row
|
|
20
|
+
rank = 0
|
|
21
|
+
primary_key_names.any? do |key|
|
|
22
|
+
rank = left_row[key] <=> right_row[key]
|
|
23
|
+
rank != 0
|
|
24
|
+
end
|
|
25
|
+
rank
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Returns the correct class for the table scan based on the type of the
|
|
29
|
+
# session (proxied or direct).
|
|
30
|
+
def self.scan_class(session)
|
|
31
|
+
if session.proxied?
|
|
32
|
+
ProxiedTableScan
|
|
33
|
+
else
|
|
34
|
+
DirectTableScan
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/..'
|
|
2
|
+
|
|
3
|
+
require 'tsort'
|
|
4
|
+
require 'rubyrep'
|
|
5
|
+
|
|
6
|
+
module RR
|
|
7
|
+
# This class sorts a given list of tables so that tables referencing other
|
|
8
|
+
# tables via foreign keys are placed behind those referenced tables.
|
|
9
|
+
#
|
|
10
|
+
# Rationale:
|
|
11
|
+
# If tables are sorted in that sequence, the risk of foreign key violations is
|
|
12
|
+
# smaller.
|
|
13
|
+
class TableSorter
|
|
14
|
+
include TSort
|
|
15
|
+
|
|
16
|
+
# The active +Session+
|
|
17
|
+
attr_accessor :session
|
|
18
|
+
|
|
19
|
+
# The list of table names to be ordered
|
|
20
|
+
attr_accessor :tables
|
|
21
|
+
|
|
22
|
+
# The table dependencies.
|
|
23
|
+
# Format as described e. g. here: PostgreSQLExtender#referenced_tables
|
|
24
|
+
def referenced_tables
|
|
25
|
+
unless @referenced_tables
|
|
26
|
+
@referenced_tables = session.left.referenced_tables(tables)
|
|
27
|
+
|
|
28
|
+
# Strip away all unrelated tables
|
|
29
|
+
@referenced_tables.each_pair do |table, references|
|
|
30
|
+
references.delete_if do |reference|
|
|
31
|
+
not tables.include? reference
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
@referenced_tables
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Yields each table.
|
|
39
|
+
# For details see standard library: TSort#sort_each_node.
|
|
40
|
+
def tsort_each_node
|
|
41
|
+
referenced_tables.each_key do |table|
|
|
42
|
+
yield table
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Yields all tables that are references by +table+.
|
|
47
|
+
def tsort_each_child(table)
|
|
48
|
+
referenced_tables[table].each do |reference|
|
|
49
|
+
yield reference
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def sort
|
|
54
|
+
# Note:
|
|
55
|
+
# We should not use TSort#tsort as this one throws an exception if
|
|
56
|
+
# there are cyclic redundancies.
|
|
57
|
+
# (Our goal is to just get the best ordering that is possible and then
|
|
58
|
+
# take our chances.)
|
|
59
|
+
strongly_connected_components.flatten
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Initializes the TableSorter
|
|
63
|
+
# * session: The active +Session+ instance
|
|
64
|
+
# * tables: an array of table names
|
|
65
|
+
def initialize(session, tables)
|
|
66
|
+
self.session = session
|
|
67
|
+
self.tables = tables
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
module RR
|
|
2
|
+
|
|
3
|
+
# Resolves table specifications as provided e. g. in the command line of rrscan
|
|
4
|
+
class TableSpecResolver
|
|
5
|
+
|
|
6
|
+
# The +Session+ instance from which the table specifications are resolved.
|
|
7
|
+
attr_accessor :session
|
|
8
|
+
|
|
9
|
+
# Returns the array of tables of the specified database. Caches the table array.
|
|
10
|
+
# * database: either :+left+ or :+right+
|
|
11
|
+
def tables(database)
|
|
12
|
+
@table_cache ||= {}
|
|
13
|
+
unless @table_cache[database]
|
|
14
|
+
@table_cache[database] = session.send(database).tables
|
|
15
|
+
end
|
|
16
|
+
@table_cache[database]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Creates a resolver that works based on the given +Session+ instance.
|
|
20
|
+
def initialize(session)
|
|
21
|
+
self.session = session
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Returns all those tables from the given table_pairs that do not exist.
|
|
25
|
+
# * +table_pairs+: same as described at #table_pairs_without_excluded
|
|
26
|
+
#
|
|
27
|
+
# Returns:
|
|
28
|
+
# A hash with keys :+left+ and +:right+, with the value for each key being
|
|
29
|
+
# an array of non-existing tables for the according database.
|
|
30
|
+
# The keys only exist if there are according missing tables.
|
|
31
|
+
def non_existing_tables(table_pairs)
|
|
32
|
+
[:left, :right].inject({}) do |memo, database|
|
|
33
|
+
found_tables = table_pairs.inject([]) do |phantom_tables, table_pair|
|
|
34
|
+
phantom_tables << table_pair[database] unless tables(database).include?(table_pair[database])
|
|
35
|
+
phantom_tables
|
|
36
|
+
end
|
|
37
|
+
memo[database] = found_tables unless found_tables.empty?
|
|
38
|
+
memo
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Resolves the given array of table specificifications.
|
|
43
|
+
# Table specifications are either
|
|
44
|
+
# * strings as produced by BaseRunner#get_options or
|
|
45
|
+
# * actual regular expressions
|
|
46
|
+
# If +excluded_table_specs+ is provided, removes all tables that match it
|
|
47
|
+
# (even if otherwise matching +included_table_specs+).
|
|
48
|
+
#
|
|
49
|
+
# If +verify+ is +true+, raises an exception if any non-existing tables are
|
|
50
|
+
# specified.
|
|
51
|
+
#
|
|
52
|
+
# Returns an array of table name pairs in Hash form.
|
|
53
|
+
# For example something like
|
|
54
|
+
# [{:left => 'my_table', :right => 'my_table_backup'}]
|
|
55
|
+
#
|
|
56
|
+
# Takes care that a table is only returned once.
|
|
57
|
+
def resolve(included_table_specs, excluded_table_specs = [], verify = true)
|
|
58
|
+
table_pairs = expand_table_specs(included_table_specs)
|
|
59
|
+
table_pairs = table_pairs_without_duplicates(table_pairs)
|
|
60
|
+
table_pairs = table_pairs_without_excluded(table_pairs, excluded_table_specs)
|
|
61
|
+
|
|
62
|
+
if verify
|
|
63
|
+
non_existing_tables = non_existing_tables(table_pairs)
|
|
64
|
+
unless non_existing_tables.empty?
|
|
65
|
+
raise "non-existing tables specified: #{non_existing_tables.inspect}"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
table_pairs
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Helper for #resolve
|
|
73
|
+
# Takes the specified table_specifications and expands it into an array of
|
|
74
|
+
# according table pairs.
|
|
75
|
+
# Returns the result
|
|
76
|
+
# Refer to #resolve for a full description of parameters and result.
|
|
77
|
+
def expand_table_specs(table_specs)
|
|
78
|
+
table_pairs = []
|
|
79
|
+
table_specs.each do |table_spec|
|
|
80
|
+
|
|
81
|
+
# If it is a regexp, convert it in an according string
|
|
82
|
+
table_spec = table_spec.inspect if table_spec.kind_of? Regexp
|
|
83
|
+
|
|
84
|
+
case table_spec
|
|
85
|
+
when /^\/.*\/$/ # matches e. g. '/^user/'
|
|
86
|
+
table_spec = table_spec.sub(/^\/(.*)\/$/,'\1') # remove leading and trailing slash
|
|
87
|
+
matching_tables = tables(:left).grep(Regexp.new(table_spec, Regexp::IGNORECASE, 'U'))
|
|
88
|
+
matching_tables.each do |table|
|
|
89
|
+
table_pairs << {:left => table, :right => table}
|
|
90
|
+
end
|
|
91
|
+
when /.+,.+/ # matches e. g. 'users,users_backup'
|
|
92
|
+
pair = table_spec.match(/(.*),(.*)/)[1..2].map { |str| str.strip }
|
|
93
|
+
table_pairs << {:left => pair[0], :right => pair[1]}
|
|
94
|
+
else # everything else: just a normal table
|
|
95
|
+
table_pairs << {:left => table_spec.strip, :right => table_spec.strip}
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
table_pairs
|
|
99
|
+
end
|
|
100
|
+
private :expand_table_specs
|
|
101
|
+
|
|
102
|
+
# Helper for #resolve
|
|
103
|
+
# Takes given table_pairs and removes all tables that are excluded.
|
|
104
|
+
# Returns the result.
|
|
105
|
+
# Both the given and the returned table_pairs is an array of hashes with
|
|
106
|
+
# * :+left+: name of the left table
|
|
107
|
+
# * :+right+: name of the corresponding right table
|
|
108
|
+
# +excluded_table_specs+ is the array of table specifications to be excluded.
|
|
109
|
+
def table_pairs_without_excluded(table_pairs, excluded_table_specs)
|
|
110
|
+
excluded_tables = expand_table_specs(excluded_table_specs).map do |table_pair|
|
|
111
|
+
table_pair[:left]
|
|
112
|
+
end
|
|
113
|
+
table_pairs.select {|table_pair| not excluded_tables.include? table_pair[:left]}
|
|
114
|
+
end
|
|
115
|
+
private :table_pairs_without_excluded
|
|
116
|
+
|
|
117
|
+
# Helper for #resolve
|
|
118
|
+
# Takes given table_pairs and removes all duplicates.
|
|
119
|
+
# Returns the result.
|
|
120
|
+
# Both the given and the returned table_pairs is an array of hashes with
|
|
121
|
+
# * :+left+: name of the left table
|
|
122
|
+
# * :+right+: name of the corresponding right table
|
|
123
|
+
def table_pairs_without_duplicates(table_pairs)
|
|
124
|
+
processed_left_tables = {}
|
|
125
|
+
resulting_table_pairs = []
|
|
126
|
+
table_pairs.each do |table_pair|
|
|
127
|
+
unless processed_left_tables.include? table_pair[:left]
|
|
128
|
+
resulting_table_pairs << table_pair
|
|
129
|
+
processed_left_tables[table_pair[:left]] = true
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
resulting_table_pairs
|
|
133
|
+
end
|
|
134
|
+
private :table_pairs_without_duplicates
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module RR
|
|
2
|
+
|
|
3
|
+
# Synchronizes the data of two tables.
|
|
4
|
+
class TableSync < TableScan
|
|
5
|
+
|
|
6
|
+
# Instance of SyncHelper
|
|
7
|
+
attr_accessor :helper
|
|
8
|
+
|
|
9
|
+
# Returns a hash of sync options for this table sync.
|
|
10
|
+
def sync_options
|
|
11
|
+
@sync_options ||= session.configuration.options_for_table(left_table)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Creates a new TableSync instance
|
|
15
|
+
# * session: a Session object representing the current database session
|
|
16
|
+
# * left_table: name of the table in the left database
|
|
17
|
+
# * right_table: name of the table in the right database. If not given, same like left_table
|
|
18
|
+
def initialize(session, left_table, right_table = nil)
|
|
19
|
+
super
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Executes the specified sync hook
|
|
23
|
+
# * +hook_id+: either :+before_table_sync+ or :+after_table_sync+
|
|
24
|
+
def execute_sync_hook(hook_id)
|
|
25
|
+
hook = sync_options[hook_id]
|
|
26
|
+
if hook
|
|
27
|
+
if hook.respond_to?(:call)
|
|
28
|
+
hook.call(helper)
|
|
29
|
+
else
|
|
30
|
+
[:left, :right].each do |database|
|
|
31
|
+
session.send(database).execute hook
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Executes the table sync. If a block is given, yields each difference with
|
|
38
|
+
# the following 2 parameters
|
|
39
|
+
# * +type+
|
|
40
|
+
# * +row+
|
|
41
|
+
# Purpose: enable display of progress information.
|
|
42
|
+
# See DirectTableScan#run for full description of yielded parameters.
|
|
43
|
+
def run
|
|
44
|
+
success = false
|
|
45
|
+
|
|
46
|
+
scan_class = TableScanHelper.scan_class(session)
|
|
47
|
+
scan = scan_class.new(session, left_table, right_table)
|
|
48
|
+
scan.progress_printer = progress_printer
|
|
49
|
+
|
|
50
|
+
self.helper = SyncHelper.new(self)
|
|
51
|
+
syncer = Syncers.configured_syncer(sync_options).new(helper)
|
|
52
|
+
|
|
53
|
+
execute_sync_hook :before_table_sync
|
|
54
|
+
|
|
55
|
+
scan.run do |type, row|
|
|
56
|
+
yield type, row if block_given? # To enable progress reporting
|
|
57
|
+
syncer.sync_difference type, row
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
execute_sync_hook :after_table_sync
|
|
61
|
+
|
|
62
|
+
success = true # considered to be successful if we get till here
|
|
63
|
+
ensure
|
|
64
|
+
helper.finalize success if helper
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require 'set'
|
|
2
|
+
|
|
3
|
+
module RR
|
|
4
|
+
|
|
5
|
+
# Switches rubyrep triggers between "exclude rubyrep activity" modes.
|
|
6
|
+
class TriggerModeSwitcher
|
|
7
|
+
|
|
8
|
+
# Keeps track of all the triggers.
|
|
9
|
+
# This is a hash with 2 keys: :+left+ and :+right+.
|
|
10
|
+
# Each of these entries is a Set containing table names.
|
|
11
|
+
def triggers
|
|
12
|
+
@triggers ||= {
|
|
13
|
+
:left => Set.new,
|
|
14
|
+
:right => Set.new
|
|
15
|
+
}
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# The active Session
|
|
19
|
+
attr_accessor :session
|
|
20
|
+
|
|
21
|
+
def initialize(session)
|
|
22
|
+
self.session = session
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Does the actual switching of the trigger mode.
|
|
26
|
+
# * +database+: either :+left+ or :+right+
|
|
27
|
+
# * +table+: name of the table
|
|
28
|
+
# * +exclude_rr_activity+: the new trigger mode (either +true+ or +false+)
|
|
29
|
+
def switch_trigger_mode(database, table, exclude_rr_activity)
|
|
30
|
+
options = session.configuration.options
|
|
31
|
+
if session.send(database).replication_trigger_exists? "#{options[:rep_prefix]}_#{table}", table
|
|
32
|
+
params = {
|
|
33
|
+
:trigger_name => "#{options[:rep_prefix]}_#{table}",
|
|
34
|
+
:table => table,
|
|
35
|
+
:keys => session.send(database).primary_key_names(table),
|
|
36
|
+
:log_table => "#{options[:rep_prefix]}_pending_changes",
|
|
37
|
+
:activity_table => "#{options[:rep_prefix]}_running_flags",
|
|
38
|
+
:key_sep => options[:key_sep],
|
|
39
|
+
:exclude_rr_activity => exclude_rr_activity,
|
|
40
|
+
}
|
|
41
|
+
session.send(database).create_or_replace_replication_trigger_function(params)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Switches the trigger of the named table to "exclude rubyrep activity" mode.
|
|
46
|
+
# Only switches if it didn't do so already for the table.
|
|
47
|
+
# * +database+: either :+left+ or :+right+
|
|
48
|
+
# * +table+: name of the table
|
|
49
|
+
def exclude_rr_activity(database, table)
|
|
50
|
+
switch_trigger_mode(database, table, true) if triggers[database].add? table
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Restores all switched triggers to not exclude rubyrep activity
|
|
54
|
+
def restore_triggers
|
|
55
|
+
[:left, :right].each do |database|
|
|
56
|
+
triggers[database].each do |table|
|
|
57
|
+
switch_trigger_mode database, table, false
|
|
58
|
+
end
|
|
59
|
+
triggers[database].clear
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module RR
|
|
2
|
+
# Provides functionality to cast a query result value into the correct ruby type.
|
|
3
|
+
# Requires originating table and column to be known.
|
|
4
|
+
class TypeCastingCursor
|
|
5
|
+
|
|
6
|
+
# Delegate the uninteresting methods to the original cursor
|
|
7
|
+
def next?; org_cursor.next? end
|
|
8
|
+
def clear; org_cursor.clear end
|
|
9
|
+
|
|
10
|
+
# The original cursor object
|
|
11
|
+
attr_accessor :org_cursor
|
|
12
|
+
|
|
13
|
+
# A column_name => Column cache
|
|
14
|
+
attr_accessor :columns
|
|
15
|
+
|
|
16
|
+
# Creates a new TypeCastingCursor based on provided database connection and table name
|
|
17
|
+
# for the provided database query cursor
|
|
18
|
+
def initialize(connection, table, cursor)
|
|
19
|
+
self.org_cursor = cursor
|
|
20
|
+
self.columns = {}
|
|
21
|
+
connection.columns(table).each {|c| columns[c.name] = c}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Reads the next row from the original cursor and returns the row with the type casted row values.
|
|
25
|
+
def next_row
|
|
26
|
+
row = org_cursor.next_row
|
|
27
|
+
row.each {|column, value| row[column] = columns[column].type_cast value}
|
|
28
|
+
row
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|