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.
Files changed (140) hide show
  1. data/History.txt +4 -0
  2. data/License.txt +20 -0
  3. data/Manifest.txt +137 -0
  4. data/README.txt +37 -0
  5. data/Rakefile +30 -0
  6. data/bin/rubyrep +8 -0
  7. data/config/hoe.rb +72 -0
  8. data/config/mysql_config.rb +25 -0
  9. data/config/postgres_config.rb +21 -0
  10. data/config/proxied_test_config.rb +14 -0
  11. data/config/redmine_config.rb +17 -0
  12. data/config/rep_config.rb +20 -0
  13. data/config/requirements.rb +32 -0
  14. data/config/test_config.rb +20 -0
  15. data/lib/rubyrep/base_runner.rb +195 -0
  16. data/lib/rubyrep/command_runner.rb +144 -0
  17. data/lib/rubyrep/committers/buffered_committer.rb +140 -0
  18. data/lib/rubyrep/committers/committers.rb +146 -0
  19. data/lib/rubyrep/configuration.rb +240 -0
  20. data/lib/rubyrep/connection_extenders/connection_extenders.rb +133 -0
  21. data/lib/rubyrep/connection_extenders/jdbc_extender.rb +284 -0
  22. data/lib/rubyrep/connection_extenders/mysql_extender.rb +168 -0
  23. data/lib/rubyrep/connection_extenders/postgresql_extender.rb +261 -0
  24. data/lib/rubyrep/database_proxy.rb +52 -0
  25. data/lib/rubyrep/direct_table_scan.rb +75 -0
  26. data/lib/rubyrep/generate_runner.rb +105 -0
  27. data/lib/rubyrep/initializer.rb +39 -0
  28. data/lib/rubyrep/logged_change.rb +326 -0
  29. data/lib/rubyrep/proxied_table_scan.rb +171 -0
  30. data/lib/rubyrep/proxy_block_cursor.rb +145 -0
  31. data/lib/rubyrep/proxy_connection.rb +318 -0
  32. data/lib/rubyrep/proxy_cursor.rb +44 -0
  33. data/lib/rubyrep/proxy_row_cursor.rb +43 -0
  34. data/lib/rubyrep/proxy_runner.rb +89 -0
  35. data/lib/rubyrep/replication_difference.rb +91 -0
  36. data/lib/rubyrep/replication_extenders/mysql_replication.rb +271 -0
  37. data/lib/rubyrep/replication_extenders/postgresql_replication.rb +204 -0
  38. data/lib/rubyrep/replication_extenders/replication_extenders.rb +26 -0
  39. data/lib/rubyrep/replication_helper.rb +104 -0
  40. data/lib/rubyrep/replication_initializer.rb +307 -0
  41. data/lib/rubyrep/replication_run.rb +48 -0
  42. data/lib/rubyrep/replication_runner.rb +138 -0
  43. data/lib/rubyrep/replicators/replicators.rb +37 -0
  44. data/lib/rubyrep/replicators/two_way_replicator.rb +334 -0
  45. data/lib/rubyrep/scan_progress_printers/progress_bar.rb +65 -0
  46. data/lib/rubyrep/scan_progress_printers/scan_progress_printers.rb +65 -0
  47. data/lib/rubyrep/scan_report_printers/scan_detail_reporter.rb +111 -0
  48. data/lib/rubyrep/scan_report_printers/scan_report_printers.rb +67 -0
  49. data/lib/rubyrep/scan_report_printers/scan_summary_reporter.rb +75 -0
  50. data/lib/rubyrep/scan_runner.rb +25 -0
  51. data/lib/rubyrep/session.rb +177 -0
  52. data/lib/rubyrep/sync_helper.rb +111 -0
  53. data/lib/rubyrep/sync_runner.rb +31 -0
  54. data/lib/rubyrep/syncers/syncers.rb +112 -0
  55. data/lib/rubyrep/syncers/two_way_syncer.rb +174 -0
  56. data/lib/rubyrep/table_scan.rb +54 -0
  57. data/lib/rubyrep/table_scan_helper.rb +38 -0
  58. data/lib/rubyrep/table_sorter.rb +70 -0
  59. data/lib/rubyrep/table_spec_resolver.rb +136 -0
  60. data/lib/rubyrep/table_sync.rb +68 -0
  61. data/lib/rubyrep/trigger_mode_switcher.rb +63 -0
  62. data/lib/rubyrep/type_casting_cursor.rb +31 -0
  63. data/lib/rubyrep/uninstall_runner.rb +92 -0
  64. data/lib/rubyrep/version.rb +9 -0
  65. data/lib/rubyrep.rb +68 -0
  66. data/script/destroy +14 -0
  67. data/script/generate +14 -0
  68. data/script/txt2html +74 -0
  69. data/setup.rb +1585 -0
  70. data/sims/performance/big_rep_spec.rb +100 -0
  71. data/sims/performance/big_scan_spec.rb +57 -0
  72. data/sims/performance/big_sync_spec.rb +141 -0
  73. data/sims/performance/performance.rake +228 -0
  74. data/sims/sim_helper.rb +24 -0
  75. data/spec/base_runner_spec.rb +218 -0
  76. data/spec/buffered_committer_spec.rb +271 -0
  77. data/spec/command_runner_spec.rb +145 -0
  78. data/spec/committers_spec.rb +174 -0
  79. data/spec/configuration_spec.rb +198 -0
  80. data/spec/connection_extender_interface_spec.rb +138 -0
  81. data/spec/connection_extenders_registration_spec.rb +129 -0
  82. data/spec/database_proxy_spec.rb +48 -0
  83. data/spec/database_rake_spec.rb +40 -0
  84. data/spec/db_specific_connection_extenders_spec.rb +34 -0
  85. data/spec/db_specific_replication_extenders_spec.rb +38 -0
  86. data/spec/direct_table_scan_spec.rb +61 -0
  87. data/spec/generate_runner_spec.rb +84 -0
  88. data/spec/initializer_spec.rb +46 -0
  89. data/spec/logged_change_spec.rb +480 -0
  90. data/spec/postgresql_replication_spec.rb +48 -0
  91. data/spec/postgresql_support_spec.rb +57 -0
  92. data/spec/progress_bar_spec.rb +77 -0
  93. data/spec/proxied_table_scan_spec.rb +151 -0
  94. data/spec/proxy_block_cursor_spec.rb +197 -0
  95. data/spec/proxy_connection_spec.rb +399 -0
  96. data/spec/proxy_cursor_spec.rb +56 -0
  97. data/spec/proxy_row_cursor_spec.rb +66 -0
  98. data/spec/proxy_runner_spec.rb +70 -0
  99. data/spec/replication_difference_spec.rb +160 -0
  100. data/spec/replication_extender_interface_spec.rb +365 -0
  101. data/spec/replication_extenders_spec.rb +32 -0
  102. data/spec/replication_helper_spec.rb +121 -0
  103. data/spec/replication_initializer_spec.rb +477 -0
  104. data/spec/replication_run_spec.rb +166 -0
  105. data/spec/replication_runner_spec.rb +213 -0
  106. data/spec/replicators_spec.rb +31 -0
  107. data/spec/rubyrep_spec.rb +8 -0
  108. data/spec/scan_detail_reporter_spec.rb +119 -0
  109. data/spec/scan_progress_printers_spec.rb +68 -0
  110. data/spec/scan_report_printers_spec.rb +67 -0
  111. data/spec/scan_runner_spec.rb +50 -0
  112. data/spec/scan_summary_reporter_spec.rb +61 -0
  113. data/spec/session_spec.rb +212 -0
  114. data/spec/spec.opts +1 -0
  115. data/spec/spec_helper.rb +295 -0
  116. data/spec/sync_helper_spec.rb +157 -0
  117. data/spec/sync_runner_spec.rb +78 -0
  118. data/spec/syncers_spec.rb +171 -0
  119. data/spec/table_scan_helper_spec.rb +29 -0
  120. data/spec/table_scan_spec.rb +49 -0
  121. data/spec/table_sorter_spec.rb +31 -0
  122. data/spec/table_spec_resolver_spec.rb +102 -0
  123. data/spec/table_sync_spec.rb +84 -0
  124. data/spec/trigger_mode_switcher_spec.rb +83 -0
  125. data/spec/two_way_replicator_spec.rb +551 -0
  126. data/spec/two_way_syncer_spec.rb +256 -0
  127. data/spec/type_casting_cursor_spec.rb +50 -0
  128. data/spec/uninstall_runner_spec.rb +86 -0
  129. data/tasks/database.rake +439 -0
  130. data/tasks/deployment.rake +29 -0
  131. data/tasks/environment.rake +9 -0
  132. data/tasks/java.rake +37 -0
  133. data/tasks/redmine_test.rake +47 -0
  134. data/tasks/rspec.rake +68 -0
  135. data/tasks/rubyrep.tailor +18 -0
  136. data/tasks/stats.rake +19 -0
  137. data/tasks/task_helper.rb +20 -0
  138. data.tar.gz.sig +0 -0
  139. metadata +243 -0
  140. 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