andyjeffries-rubyrep 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +83 -0
- data/License.txt +20 -0
- data/Manifest.txt +151 -0
- data/README.txt +37 -0
- data/bin/rubyrep +8 -0
- data/lib/rubyrep.rb +72 -0
- data/lib/rubyrep/base_runner.rb +195 -0
- data/lib/rubyrep/command_runner.rb +144 -0
- data/lib/rubyrep/committers/buffered_committer.rb +151 -0
- data/lib/rubyrep/committers/committers.rb +152 -0
- data/lib/rubyrep/configuration.rb +275 -0
- data/lib/rubyrep/connection_extenders/connection_extenders.rb +165 -0
- data/lib/rubyrep/connection_extenders/jdbc_extender.rb +65 -0
- data/lib/rubyrep/connection_extenders/mysql_extender.rb +59 -0
- data/lib/rubyrep/connection_extenders/postgresql_extender.rb +277 -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/log_helper.rb +30 -0
- data/lib/rubyrep/logged_change.rb +160 -0
- data/lib/rubyrep/logged_change_loader.rb +197 -0
- data/lib/rubyrep/noisy_connection.rb +80 -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 +431 -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 +100 -0
- data/lib/rubyrep/replication_extenders/mysql_replication.rb +271 -0
- data/lib/rubyrep/replication_extenders/postgresql_replication.rb +236 -0
- data/lib/rubyrep/replication_extenders/replication_extenders.rb +26 -0
- data/lib/rubyrep/replication_helper.rb +142 -0
- data/lib/rubyrep/replication_initializer.rb +327 -0
- data/lib/rubyrep/replication_run.rb +142 -0
- data/lib/rubyrep/replication_runner.rb +166 -0
- data/lib/rubyrep/replicators/replicators.rb +42 -0
- data/lib/rubyrep/replicators/two_way_replicator.rb +361 -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 +230 -0
- data/lib/rubyrep/sync_helper.rb +121 -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 +46 -0
- data/lib/rubyrep/table_sorter.rb +70 -0
- data/lib/rubyrep/table_spec_resolver.rb +142 -0
- data/lib/rubyrep/table_sync.rb +90 -0
- data/lib/rubyrep/task_sweeper.rb +77 -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 +93 -0
- data/lib/rubyrep/version.rb +9 -0
- data/rubyrep +8 -0
- data/rubyrep.bat +4 -0
- data/setup.rb +1585 -0
- data/spec/base_runner_spec.rb +218 -0
- data/spec/buffered_committer_spec.rb +274 -0
- data/spec/command_runner_spec.rb +145 -0
- data/spec/committers_spec.rb +178 -0
- data/spec/configuration_spec.rb +203 -0
- data/spec/connection_extender_interface_spec.rb +141 -0
- data/spec/connection_extenders_registration_spec.rb +164 -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/dolphins.jpg +0 -0
- data/spec/generate_runner_spec.rb +84 -0
- data/spec/initializer_spec.rb +46 -0
- data/spec/log_helper_spec.rb +39 -0
- data/spec/logged_change_loader_spec.rb +68 -0
- data/spec/logged_change_spec.rb +470 -0
- data/spec/noisy_connection_spec.rb +78 -0
- data/spec/postgresql_replication_spec.rb +48 -0
- data/spec/postgresql_schema_support_spec.rb +212 -0
- data/spec/postgresql_support_spec.rb +63 -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 +423 -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 +161 -0
- data/spec/replication_extender_interface_spec.rb +367 -0
- data/spec/replication_extenders_spec.rb +32 -0
- data/spec/replication_helper_spec.rb +178 -0
- data/spec/replication_initializer_spec.rb +509 -0
- data/spec/replication_run_spec.rb +443 -0
- data/spec/replication_runner_spec.rb +254 -0
- data/spec/replicators_spec.rb +36 -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 +253 -0
- data/spec/spec.opts +1 -0
- data/spec/spec_helper.rb +305 -0
- data/spec/strange_name_support_spec.rb +135 -0
- data/spec/sync_helper_spec.rb +169 -0
- data/spec/sync_runner_spec.rb +78 -0
- data/spec/syncers_spec.rb +171 -0
- data/spec/table_scan_helper_spec.rb +36 -0
- data/spec/table_scan_spec.rb +49 -0
- data/spec/table_sorter_spec.rb +30 -0
- data/spec/table_spec_resolver_spec.rb +111 -0
- data/spec/table_sync_spec.rb +140 -0
- data/spec/task_sweeper_spec.rb +47 -0
- data/spec/trigger_mode_switcher_spec.rb +83 -0
- data/spec/two_way_replicator_spec.rb +721 -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 +93 -0
- metadata +190 -0
|
@@ -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,100 @@
|
|
|
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
|
+
def session
|
|
9
|
+
@session ||= loaders.session
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# The current LoggedChangeLoaders instance
|
|
13
|
+
attr_accessor :loaders
|
|
14
|
+
|
|
15
|
+
# The type of the difference. Either
|
|
16
|
+
# * :+left+: change in left database
|
|
17
|
+
# * :+right+: change in right database
|
|
18
|
+
# * :+conflict+: change in both databases
|
|
19
|
+
# * :+no_diff+: changes in both databases constitute no difference
|
|
20
|
+
attr_accessor :type
|
|
21
|
+
|
|
22
|
+
# Is set to +true+ if first replication attempt failed but it should be tried again later
|
|
23
|
+
attr_accessor :second_chance
|
|
24
|
+
alias_method :second_chance?, :second_chance
|
|
25
|
+
|
|
26
|
+
# A hash with keys :+left+ and / or :+right+.
|
|
27
|
+
# Hash values are LoggedChange instances.
|
|
28
|
+
def changes
|
|
29
|
+
@changes ||= {}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Creates a new ReplicationDifference instance.
|
|
33
|
+
# +loaders+ is teh current LoggedChangeLoaders instance
|
|
34
|
+
def initialize(loaders)
|
|
35
|
+
self.loaders = loaders
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Should be set to +true+ if this ReplicationDifference instance was
|
|
39
|
+
# successfully loaded.
|
|
40
|
+
attr_writer :loaded
|
|
41
|
+
|
|
42
|
+
# Returns +true+ if a replication difference was loaded
|
|
43
|
+
def loaded?
|
|
44
|
+
@loaded
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Shortcut to calculate the "other" database.
|
|
48
|
+
OTHER_SIDE = {
|
|
49
|
+
:left => :right,
|
|
50
|
+
:right => :left
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
# Resulting diff type based on types of left changes (outer hash) and right
|
|
54
|
+
# changes (inner hash)
|
|
55
|
+
DIFF_TYPES = {
|
|
56
|
+
:insert => {:insert => :conflict, :update => :conflict, :delete => :conflict, :no_change => :left},
|
|
57
|
+
:update => {:insert => :conflict, :update => :conflict, :delete => :conflict, :no_change => :left},
|
|
58
|
+
:delete => {:insert => :conflict, :update => :conflict, :delete => :no_change, :no_change => :left},
|
|
59
|
+
:no_change => {:insert => :right, :update => :right, :delete => :right, :no_change => :no_change}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
# Amends a difference according to new entries in the change log table
|
|
63
|
+
def amend
|
|
64
|
+
loaders.update
|
|
65
|
+
changes[:left].load
|
|
66
|
+
changes[:right].load
|
|
67
|
+
self.type = DIFF_TYPES[changes[:left].type][changes[:right].type]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Loads a difference
|
|
71
|
+
def load
|
|
72
|
+
change_times = {}
|
|
73
|
+
[:left, :right].each do |database|
|
|
74
|
+
changes[database] = LoggedChange.new loaders[database]
|
|
75
|
+
change_times[database] = loaders[database].oldest_change_time
|
|
76
|
+
end
|
|
77
|
+
return if change_times[:left] == nil and change_times[:right] == nil
|
|
78
|
+
|
|
79
|
+
oldest = nil
|
|
80
|
+
[:left, :right].each do |database|
|
|
81
|
+
oldest = OTHER_SIDE[database] if change_times[database] == nil
|
|
82
|
+
end
|
|
83
|
+
oldest ||= change_times[:left] <= change_times[:right] ? :left : :right
|
|
84
|
+
changes[oldest].load_oldest
|
|
85
|
+
|
|
86
|
+
changes[OTHER_SIDE[oldest]].load_specified(
|
|
87
|
+
session.corresponding_table(oldest, changes[oldest].table),
|
|
88
|
+
changes[oldest].key)
|
|
89
|
+
|
|
90
|
+
self.type = DIFF_TYPES[changes[:left].type][changes[:right].type]
|
|
91
|
+
self.loaded = true
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Prevents session and change loaders from going into YAML output
|
|
95
|
+
def to_yaml_properties
|
|
96
|
+
instance_variables.sort.reject {|var_name| ['@session', '@loaders'].include? var_name}
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
module RR
|
|
2
|
+
module ReplicationExtenders
|
|
3
|
+
|
|
4
|
+
# Provides Mysql specific functionality for database replication
|
|
5
|
+
module MysqlReplication
|
|
6
|
+
RR::ReplicationExtenders.register :mysql2 => self
|
|
7
|
+
|
|
8
|
+
# Creates or replaces the replication trigger function.
|
|
9
|
+
# See #create_replication_trigger for a descriptions of the +params+ hash.
|
|
10
|
+
def create_or_replace_replication_trigger_function(params)
|
|
11
|
+
execute(<<-end_sql)
|
|
12
|
+
DROP PROCEDURE IF EXISTS `#{params[:trigger_name]}`;
|
|
13
|
+
end_sql
|
|
14
|
+
|
|
15
|
+
activity_check = ""
|
|
16
|
+
if params[:exclude_rr_activity] then
|
|
17
|
+
activity_check = <<-end_sql
|
|
18
|
+
DECLARE active INT;
|
|
19
|
+
SELECT count(*) INTO active FROM #{params[:activity_table]};
|
|
20
|
+
IF active <> 0 THEN
|
|
21
|
+
LEAVE p;
|
|
22
|
+
END IF;
|
|
23
|
+
end_sql
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
execute(<<-end_sql)
|
|
27
|
+
CREATE PROCEDURE `#{params[:trigger_name]}`(change_key varchar(2000), change_new_key varchar(2000), change_type varchar(1))
|
|
28
|
+
p: BEGIN
|
|
29
|
+
#{activity_check}
|
|
30
|
+
INSERT INTO #{params[:log_table]}(change_table, change_key, change_new_key, change_type, change_time)
|
|
31
|
+
VALUES('#{params[:table]}', change_key, change_new_key, change_type, now());
|
|
32
|
+
END;
|
|
33
|
+
end_sql
|
|
34
|
+
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Returns the key clause that is used in the trigger function.
|
|
38
|
+
# * +trigger_var+: should be either 'NEW' or 'OLD'
|
|
39
|
+
# * +params+: the parameter hash as described in #create_rep_trigger
|
|
40
|
+
def key_clause(trigger_var, params)
|
|
41
|
+
"concat_ws('#{params[:key_sep]}', " +
|
|
42
|
+
params[:keys].map { |key| "'#{key}', #{trigger_var}.#{quote_column_name(key)}"}.join(", ") +
|
|
43
|
+
")"
|
|
44
|
+
end
|
|
45
|
+
private :key_clause
|
|
46
|
+
|
|
47
|
+
# Creates a trigger to log all changes for the given table.
|
|
48
|
+
# +params+ is a hash with all necessary information:
|
|
49
|
+
# * :+trigger_name+: name of the trigger
|
|
50
|
+
# * :+table+: name of the table that should be monitored
|
|
51
|
+
# * :+keys+: array of names of the key columns of the monitored table
|
|
52
|
+
# * :+log_table+: name of the table receiving all change notifications
|
|
53
|
+
# * :+activity_table+: name of the table receiving the rubyrep activity information
|
|
54
|
+
# * :+key_sep+: column seperator to be used in the key column of the log table
|
|
55
|
+
# * :+exclude_rr_activity+:
|
|
56
|
+
# if true, the trigger will check and filter out changes initiated by RubyRep
|
|
57
|
+
def create_replication_trigger(params)
|
|
58
|
+
create_or_replace_replication_trigger_function params
|
|
59
|
+
|
|
60
|
+
%w(insert update delete).each do |action|
|
|
61
|
+
execute(<<-end_sql)
|
|
62
|
+
DROP TRIGGER IF EXISTS `#{params[:trigger_name]}_#{action}`;
|
|
63
|
+
end_sql
|
|
64
|
+
|
|
65
|
+
# The created triggers can handle the case where the trigger procedure
|
|
66
|
+
# is updated (that is: temporarily deleted and recreated) while the
|
|
67
|
+
# trigger is running.
|
|
68
|
+
# For that an MySQL internal exception is raised if the trigger
|
|
69
|
+
# procedure cannot be found. The exception is caught by an trigger
|
|
70
|
+
# internal handler.
|
|
71
|
+
# The handler causes the trigger to retry calling the
|
|
72
|
+
# trigger procedure several times with short breaks in between.
|
|
73
|
+
|
|
74
|
+
trigger_var = action == 'delete' ? 'OLD' : 'NEW'
|
|
75
|
+
if action == 'update'
|
|
76
|
+
call_statement = "CALL `#{params[:trigger_name]}`(#{key_clause('OLD', params)}, #{key_clause('NEW', params)}, '#{action[0,1].upcase}');"
|
|
77
|
+
else
|
|
78
|
+
call_statement = "CALL `#{params[:trigger_name]}`(#{key_clause(trigger_var, params)}, null, '#{action[0,1].upcase}');"
|
|
79
|
+
end
|
|
80
|
+
execute(<<-end_sql)
|
|
81
|
+
CREATE TRIGGER `#{params[:trigger_name]}_#{action}`
|
|
82
|
+
AFTER #{action} ON `#{params[:table]}` FOR EACH ROW BEGIN
|
|
83
|
+
DECLARE number_attempts INT DEFAULT 0;
|
|
84
|
+
DECLARE failed INT;
|
|
85
|
+
DECLARE CONTINUE HANDLER FOR 1305 BEGIN
|
|
86
|
+
DO SLEEP(0.05);
|
|
87
|
+
SET failed = 1;
|
|
88
|
+
SET number_attempts = number_attempts + 1;
|
|
89
|
+
END;
|
|
90
|
+
REPEAT
|
|
91
|
+
SET failed = 0;
|
|
92
|
+
#{call_statement}
|
|
93
|
+
UNTIL failed = 0 OR number_attempts >= 40 END REPEAT;
|
|
94
|
+
END;
|
|
95
|
+
end_sql
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Removes a trigger and related trigger procedure.
|
|
101
|
+
# * +trigger_name+: name of the trigger
|
|
102
|
+
# * +table_name+: name of the table for which the trigger exists
|
|
103
|
+
def drop_replication_trigger(trigger_name, table_name)
|
|
104
|
+
%w(insert update delete).each do |action|
|
|
105
|
+
execute "DROP TRIGGER `#{trigger_name}_#{action}`;"
|
|
106
|
+
end
|
|
107
|
+
execute "DROP PROCEDURE `#{trigger_name}`;"
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Returns +true+ if the named trigger exists for the named table.
|
|
111
|
+
# * +trigger_name+: name of the trigger
|
|
112
|
+
# * +table_name+: name of the table
|
|
113
|
+
def replication_trigger_exists?(trigger_name, table_name)
|
|
114
|
+
!select_all("select 1 from information_schema.triggers where trigger_schema = database() and trigger_name = '#{trigger_name}_insert' and event_object_table = '#{table_name}'").empty?
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Returns all unadjusted sequences of the given table.
|
|
118
|
+
# Parameters:
|
|
119
|
+
# * +rep_prefix+:
|
|
120
|
+
# The prefix put in front of all replication related database objects as
|
|
121
|
+
# specified via Configuration#options.
|
|
122
|
+
# Is used to create the sequences table.
|
|
123
|
+
# * +table_name+: name of the table
|
|
124
|
+
# Return value: a hash with
|
|
125
|
+
# * key: sequence name
|
|
126
|
+
# * value: a hash with
|
|
127
|
+
# * :+increment+: current sequence increment
|
|
128
|
+
# * :+value+: current value
|
|
129
|
+
def sequence_values(rep_prefix, table_name)
|
|
130
|
+
# check if the table has an auto_increment column, return if not
|
|
131
|
+
sequence_row = select_one(<<-end_sql)
|
|
132
|
+
show columns from `#{table_name}` where extra = 'auto_increment'
|
|
133
|
+
end_sql
|
|
134
|
+
return {} unless sequence_row
|
|
135
|
+
column_name = sequence_row['Field']
|
|
136
|
+
|
|
137
|
+
# check if the sequences table exists, create if necessary
|
|
138
|
+
sequence_table_name = "#{rep_prefix}_sequences"
|
|
139
|
+
unless tables.include?(sequence_table_name)
|
|
140
|
+
create_table "#{sequence_table_name}".to_sym,
|
|
141
|
+
:id => false, :options => 'ENGINE=MyISAM' do |t|
|
|
142
|
+
t.column :name, :string
|
|
143
|
+
t.column :current_value, :integer
|
|
144
|
+
t.column :increment, :integer
|
|
145
|
+
t.column :offset, :integer
|
|
146
|
+
end
|
|
147
|
+
ActiveRecord::Base.connection.execute(<<-end_sql) rescue nil
|
|
148
|
+
ALTER TABLE "#{sequence_table_name}"
|
|
149
|
+
ADD CONSTRAINT #{sequence_table_name}_pkey
|
|
150
|
+
PRIMARY KEY (name)
|
|
151
|
+
end_sql
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
sequence_row = select_one("select current_value, increment, offset from #{sequence_table_name} where name = '#{table_name}'")
|
|
155
|
+
if sequence_row == nil
|
|
156
|
+
current_max = select_one(<<-end_sql)['current_max'].to_i
|
|
157
|
+
select max(`#{column_name}`) as current_max from `#{table_name}`
|
|
158
|
+
end_sql
|
|
159
|
+
return {column_name => {
|
|
160
|
+
:increment => 1,
|
|
161
|
+
:value => current_max
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
else
|
|
165
|
+
return {column_name => {
|
|
166
|
+
:increment => sequence_row['increment'].to_i,
|
|
167
|
+
:value => sequence_row['offset'].to_i
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Ensures that the sequences of the named table (normally the primary key
|
|
174
|
+
# column) are generated with the correct increment and offset.
|
|
175
|
+
# * +rep_prefix+: not used (necessary) for the Postgres
|
|
176
|
+
# * +table_name+: name of the table (not used for Postgres)
|
|
177
|
+
# * +increment+: increment of the sequence
|
|
178
|
+
# * +offset+: offset
|
|
179
|
+
# * +left_sequence_values+:
|
|
180
|
+
# hash as returned by #sequence_values for the left database
|
|
181
|
+
# * +right_sequence_values+:
|
|
182
|
+
# hash as returned by #sequence_values for the right database
|
|
183
|
+
# * +adjustment_buffer+:
|
|
184
|
+
# the "gap" that is created during sequence update to avoid concurrency problems
|
|
185
|
+
# E. g. an increment of 2 and offset of 1 will lead to generation of odd
|
|
186
|
+
# numbers.
|
|
187
|
+
def update_sequences(
|
|
188
|
+
rep_prefix, table_name, increment, offset,
|
|
189
|
+
left_sequence_values, right_sequence_values, adjustment_buffer)
|
|
190
|
+
return if left_sequence_values.empty?
|
|
191
|
+
column_name = left_sequence_values.keys[0]
|
|
192
|
+
|
|
193
|
+
# check if the sequences table exists, create if necessary
|
|
194
|
+
sequence_table_name = "#{rep_prefix}_sequences"
|
|
195
|
+
current_max =
|
|
196
|
+
[left_sequence_values[column_name][:value], right_sequence_values[column_name][:value]].max +
|
|
197
|
+
adjustment_buffer
|
|
198
|
+
new_start = current_max - (current_max % increment) + increment + offset
|
|
199
|
+
|
|
200
|
+
sequence_row = select_one("select current_value, increment, offset from #{sequence_table_name} where name = '#{table_name}'")
|
|
201
|
+
if sequence_row == nil
|
|
202
|
+
# no sequence exists yet for the table, create it and the according
|
|
203
|
+
# sequence trigger
|
|
204
|
+
execute(<<-end_sql)
|
|
205
|
+
insert into #{sequence_table_name}(name, current_value, increment, offset)
|
|
206
|
+
values('#{table_name}', #{new_start}, #{increment}, #{offset})
|
|
207
|
+
end_sql
|
|
208
|
+
trigger_name = "#{rep_prefix}_#{table_name}_sequence"
|
|
209
|
+
execute(<<-end_sql)
|
|
210
|
+
DROP TRIGGER IF EXISTS `#{trigger_name}`;
|
|
211
|
+
end_sql
|
|
212
|
+
|
|
213
|
+
execute(<<-end_sql)
|
|
214
|
+
CREATE TRIGGER `#{trigger_name}`
|
|
215
|
+
BEFORE INSERT ON `#{table_name}` FOR EACH ROW BEGIN
|
|
216
|
+
IF NEW.`#{column_name}` = 0 THEN
|
|
217
|
+
UPDATE #{sequence_table_name}
|
|
218
|
+
SET current_value = LAST_INSERT_ID(current_value + increment)
|
|
219
|
+
WHERE name = '#{table_name}';
|
|
220
|
+
SET NEW.`#{column_name}` = LAST_INSERT_ID();
|
|
221
|
+
END IF;
|
|
222
|
+
END;
|
|
223
|
+
end_sql
|
|
224
|
+
elsif sequence_row['increment'].to_i != increment or sequence_row['offset'].to_i != offset
|
|
225
|
+
# sequence exists but with incorrect values; update it
|
|
226
|
+
execute(<<-end_sql)
|
|
227
|
+
update #{sequence_table_name}
|
|
228
|
+
set current_value = #{new_start},
|
|
229
|
+
increment = #{increment}, offset = #{offset}
|
|
230
|
+
where name = '#{table_name}'
|
|
231
|
+
end_sql
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Adds a big (8 byte value), auto-incrementing primary key column to the
|
|
236
|
+
# specified table.
|
|
237
|
+
# * table_name: name of the target table
|
|
238
|
+
# * key_name: name of the primary key column
|
|
239
|
+
def add_big_primary_key(table_name, key_name)
|
|
240
|
+
execute(<<-end_sql)
|
|
241
|
+
alter table #{table_name} add column #{key_name} bigint not null auto_increment primary key
|
|
242
|
+
end_sql
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Removes the custom sequence setup for the specified table.
|
|
246
|
+
# If no more rubyrep sequences are left, removes the sequence table.
|
|
247
|
+
# * +rep_prefix+: not used (necessary) for the Postgres
|
|
248
|
+
# * +table_name+: name of the table
|
|
249
|
+
def clear_sequence_setup(rep_prefix, table_name)
|
|
250
|
+
sequence_table_name = "#{rep_prefix}_sequences"
|
|
251
|
+
if tables.include?(sequence_table_name)
|
|
252
|
+
trigger_name = "#{rep_prefix}_#{table_name}_sequence"
|
|
253
|
+
trigger_row = select_one(<<-end_sql)
|
|
254
|
+
select * from information_schema.triggers
|
|
255
|
+
where trigger_schema = database()
|
|
256
|
+
and trigger_name = '#{trigger_name}'
|
|
257
|
+
end_sql
|
|
258
|
+
if trigger_row
|
|
259
|
+
execute "DROP TRIGGER `#{trigger_name}`"
|
|
260
|
+
execute "delete from #{sequence_table_name} where name = '#{table_name}'"
|
|
261
|
+
unless select_one("select * from #{sequence_table_name}")
|
|
262
|
+
# no more sequences left --> delete sequence table
|
|
263
|
+
drop_table sequence_table_name.to_sym
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|