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,52 @@
|
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/..'
|
|
2
|
+
|
|
3
|
+
require 'drb'
|
|
4
|
+
|
|
5
|
+
require 'rubyrep'
|
|
6
|
+
|
|
7
|
+
module RR
|
|
8
|
+
# The proxy to a remote database connection
|
|
9
|
+
class DatabaseProxy
|
|
10
|
+
|
|
11
|
+
# Ensure that the proxy object always stays on server side and only remote
|
|
12
|
+
# references are returned to the client.
|
|
13
|
+
include DRbUndumped
|
|
14
|
+
|
|
15
|
+
# Default tcp port to listen on
|
|
16
|
+
DEFAULT_PORT = 9876
|
|
17
|
+
|
|
18
|
+
# A simple Hash to hold Session object
|
|
19
|
+
# Purpose: preventing them from being garbage collected when they are only referenced through Drb
|
|
20
|
+
attr_accessor :session_register
|
|
21
|
+
|
|
22
|
+
def initialize
|
|
23
|
+
self.session_register = {}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Create a ProxyConnection according to provided configuration Hash.
|
|
27
|
+
# +config+ is a hash as described by ActiveRecord::Base#establish_connection
|
|
28
|
+
def create_session(config)
|
|
29
|
+
session = ProxyConnection.new config
|
|
30
|
+
self.session_register[session] = session
|
|
31
|
+
session
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Destroys the given session from the session register
|
|
35
|
+
def destroy_session(session)
|
|
36
|
+
session.destroy
|
|
37
|
+
session_register.delete session
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Returns 'pong'. Used to verify that a working proxy is running.
|
|
41
|
+
def ping
|
|
42
|
+
'pong'
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Terminates this proxy
|
|
46
|
+
def terminate!
|
|
47
|
+
# AL: The only way I could find to kill the main thread from a sub thread
|
|
48
|
+
Thread.main.raise SystemExit
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module RR
|
|
2
|
+
|
|
3
|
+
# Scans two tables for differences.
|
|
4
|
+
# Doesn't have any reporting functionality by itself.
|
|
5
|
+
# Instead DirectTableScan#run yields all the differences for the caller to do with as it pleases.
|
|
6
|
+
# Usage:
|
|
7
|
+
# 1. Create a new DirectTableScan object and hand it all necessary information
|
|
8
|
+
# 2. Call DirectTableScan#run to do the actual comparison
|
|
9
|
+
# 3. The block handed to DirectTableScan#run receives all differences
|
|
10
|
+
class DirectTableScan < TableScan
|
|
11
|
+
include TableScanHelper
|
|
12
|
+
|
|
13
|
+
# The TypeCastingCursor for the left table
|
|
14
|
+
attr_accessor :left_caster
|
|
15
|
+
|
|
16
|
+
# The TypeCastingCursor for the right table
|
|
17
|
+
attr_accessor :right_caster
|
|
18
|
+
|
|
19
|
+
# Creates a new DirectTableScan instance
|
|
20
|
+
# * session: a Session object representing the current database session
|
|
21
|
+
# * left_table: name of the table in the left database
|
|
22
|
+
# * right_table: name of the table in the right database. If not given, same like left_table
|
|
23
|
+
def initialize(session, left_table, right_table = nil)
|
|
24
|
+
super
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Runs the table scan.
|
|
28
|
+
# Calls the block for every found difference.
|
|
29
|
+
# Differences are yielded with 2 parameters
|
|
30
|
+
# * type: describes the difference, either :left (row only in left table), :right (row only in right table) or :conflict
|
|
31
|
+
# * row: For :left or :right cases a hash describing the row; for :conflict an array of left and right row.
|
|
32
|
+
# A row is a hash of column_name => value pairs.
|
|
33
|
+
def run(&blck)
|
|
34
|
+
left_cursor = right_cursor = nil
|
|
35
|
+
left_cursor = session.left.select_cursor(
|
|
36
|
+
:table => left_table,
|
|
37
|
+
:row_buffer_size => scan_options[:row_buffer_size],
|
|
38
|
+
:type_cast => true
|
|
39
|
+
)
|
|
40
|
+
right_cursor = session.right.select_cursor(
|
|
41
|
+
:table => right_table,
|
|
42
|
+
:row_buffer_size => scan_options[:row_buffer_size],
|
|
43
|
+
:type_cast => true
|
|
44
|
+
)
|
|
45
|
+
left_row = right_row = nil
|
|
46
|
+
update_progress 0 # ensures progress bar is printed even if there are no records
|
|
47
|
+
while left_row or right_row or left_cursor.next? or right_cursor.next?
|
|
48
|
+
# if there is no current left row, _try_ to load the next one
|
|
49
|
+
left_row ||= left_cursor.next_row if left_cursor.next?
|
|
50
|
+
# if there is no current right row, _try_ to load the next one
|
|
51
|
+
right_row ||= right_cursor.next_row if right_cursor.next?
|
|
52
|
+
rank = rank_rows left_row, right_row
|
|
53
|
+
case rank
|
|
54
|
+
when -1
|
|
55
|
+
yield :left, left_row
|
|
56
|
+
left_row = nil
|
|
57
|
+
update_progress 1
|
|
58
|
+
when 1
|
|
59
|
+
yield :right, right_row
|
|
60
|
+
right_row = nil
|
|
61
|
+
update_progress 1
|
|
62
|
+
when 0
|
|
63
|
+
update_progress 2
|
|
64
|
+
if not left_row == right_row
|
|
65
|
+
yield :conflict, [left_row, right_row]
|
|
66
|
+
end
|
|
67
|
+
left_row = right_row = nil
|
|
68
|
+
end
|
|
69
|
+
# check for corresponding right rows
|
|
70
|
+
end
|
|
71
|
+
ensure
|
|
72
|
+
[left_cursor, right_cursor].each {|cursor| cursor.clear if cursor}
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
|
|
5
|
+
module RR
|
|
6
|
+
# This class implements the functionality of the 'generate' command.
|
|
7
|
+
class GenerateRunner
|
|
8
|
+
|
|
9
|
+
CONFIG_TEMPLATE = <<EOF
|
|
10
|
+
RR::Initializer::run do |config|
|
|
11
|
+
config.left = {
|
|
12
|
+
:adapter => 'postgresql', # or 'mysql2'
|
|
13
|
+
:database => 'SCOTT',
|
|
14
|
+
:username => 'scott',
|
|
15
|
+
:password => 'tiger',
|
|
16
|
+
:host => '172.16.1.1'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
config.right = {
|
|
20
|
+
:adapter => 'postgresql',
|
|
21
|
+
:database => 'SCOTT',
|
|
22
|
+
:username => 'scott',
|
|
23
|
+
:password => 'tiger',
|
|
24
|
+
:host => '172.16.1.2'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
config.include_tables 'dept'
|
|
28
|
+
config.include_tables /^e/ # regexp matching all tables starting with e
|
|
29
|
+
# config.include_tables /./ # regexp matching all tables in the database
|
|
30
|
+
end
|
|
31
|
+
EOF
|
|
32
|
+
|
|
33
|
+
CommandRunner.register 'generate' => {
|
|
34
|
+
:command => self,
|
|
35
|
+
:description => 'Generates a configuration file template'
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# Provided options. Possible values:
|
|
39
|
+
# * +:config_file+: path to config file
|
|
40
|
+
attr_accessor :options
|
|
41
|
+
|
|
42
|
+
# Parses the given command line parameter array.
|
|
43
|
+
# Returns the status (as per UNIX conventions: 1 if parameters were invalid,
|
|
44
|
+
# 0 otherwise)
|
|
45
|
+
def process_options(args)
|
|
46
|
+
status = 0
|
|
47
|
+
self.options = {}
|
|
48
|
+
|
|
49
|
+
parser = OptionParser.new do |opts|
|
|
50
|
+
opts.banner = <<EOS
|
|
51
|
+
Usage: #{$0} generate [file_name]
|
|
52
|
+
|
|
53
|
+
Generates a configuration file template under name [file_name].
|
|
54
|
+
EOS
|
|
55
|
+
opts.separator ""
|
|
56
|
+
opts.separator " Specific options:"
|
|
57
|
+
|
|
58
|
+
opts.on_tail("--help", "Show this message") do
|
|
59
|
+
$stderr.puts opts
|
|
60
|
+
self.options = nil
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
begin
|
|
65
|
+
unprocessed_args = parser.parse!(args)
|
|
66
|
+
if options # this will be +nil+ if the --help option is specified
|
|
67
|
+
raise("Please specify the name of the configuration file") if unprocessed_args.empty?
|
|
68
|
+
options[:file_name] = unprocessed_args[0]
|
|
69
|
+
end
|
|
70
|
+
rescue Exception => e
|
|
71
|
+
$stderr.puts "Command line parsing failed: #{e}"
|
|
72
|
+
$stderr.puts parser.help
|
|
73
|
+
self.options = nil
|
|
74
|
+
status = 1
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
return status
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Generates a configuration file template.
|
|
81
|
+
def execute
|
|
82
|
+
if File.exists?(options[:file_name])
|
|
83
|
+
raise("Cowardly refuse to overwrite existing file '#{options[:file_name]}'")
|
|
84
|
+
end
|
|
85
|
+
File.open(options[:file_name], 'w') do |f|
|
|
86
|
+
f.write CONFIG_TEMPLATE
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Entry points for executing a processing run.
|
|
91
|
+
# args: the array of command line options that were provided by the user.
|
|
92
|
+
def self.run(args)
|
|
93
|
+
runner = new
|
|
94
|
+
|
|
95
|
+
status = runner.process_options(args)
|
|
96
|
+
if runner.options
|
|
97
|
+
runner.execute
|
|
98
|
+
end
|
|
99
|
+
status
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module RR
|
|
2
|
+
|
|
3
|
+
# The settings of the current deployment are passed to Rubyrep through the
|
|
4
|
+
# Initializer::run method.
|
|
5
|
+
# This method yields a Configuration object for overwriting of the default
|
|
6
|
+
# settings.
|
|
7
|
+
# Accordingly a configuration file should look something like this:
|
|
8
|
+
#
|
|
9
|
+
# Rubyrep::Initializer.run do |config|
|
|
10
|
+
# config.left = ...
|
|
11
|
+
# end
|
|
12
|
+
class Initializer
|
|
13
|
+
|
|
14
|
+
# Sets a new Configuration object
|
|
15
|
+
# Current configuration values are lost and replaced with the default
|
|
16
|
+
# settings.
|
|
17
|
+
def self.reset
|
|
18
|
+
@@configuration = Configuration.new
|
|
19
|
+
end
|
|
20
|
+
reset
|
|
21
|
+
|
|
22
|
+
# Returns the current Configuration object
|
|
23
|
+
def self.configuration
|
|
24
|
+
@@configuration
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Allows direct overwriting of the Configuration
|
|
28
|
+
def self.configuration=(configuration)
|
|
29
|
+
@@configuration = configuration
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Yields the current Configuration object to enable overwriting of
|
|
33
|
+
# configuration values.
|
|
34
|
+
# Refer to the Initializer class documentation for a usage example.
|
|
35
|
+
def self.run
|
|
36
|
+
yield configuration
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module RR
|
|
2
|
+
|
|
3
|
+
# Shared functionality for SyncHelper and LogHelper
|
|
4
|
+
module LogHelper
|
|
5
|
+
|
|
6
|
+
# Takes outcome and details and makes them fit (for available space) in the
|
|
7
|
+
# 'descrition' and 'long_description' columns of the event log.
|
|
8
|
+
# Parameters:
|
|
9
|
+
# * outcome: short description
|
|
10
|
+
# * details: long description
|
|
11
|
+
# Returns (cut off if necessary)
|
|
12
|
+
# * outcome
|
|
13
|
+
# * details (also containig the full outcome if it had to be cut off for short description)
|
|
14
|
+
def fit_description_columns(outcome, details)
|
|
15
|
+
outcome = outcome.to_s
|
|
16
|
+
if outcome.length > ReplicationInitializer::DESCRIPTION_SIZE
|
|
17
|
+
fitting_outcome = outcome[0...ReplicationInitializer::DESCRIPTION_SIZE]
|
|
18
|
+
fitting_details = outcome + "\n"
|
|
19
|
+
else
|
|
20
|
+
fitting_outcome = outcome
|
|
21
|
+
fitting_details = ""
|
|
22
|
+
end
|
|
23
|
+
fitting_details += details if details
|
|
24
|
+
fitting_details = fitting_details[0...ReplicationInitializer::LONG_DESCRIPTION_SIZE]
|
|
25
|
+
fitting_details = nil if fitting_details.empty?
|
|
26
|
+
|
|
27
|
+
return fitting_outcome, fitting_details
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
module RR
|
|
2
|
+
|
|
3
|
+
# Describes a single logged record change.
|
|
4
|
+
#
|
|
5
|
+
# Note:
|
|
6
|
+
# The change loading functionality depends on the current database session
|
|
7
|
+
# being executed in an open database transaction.
|
|
8
|
+
# Also at the end of change processing the transaction must be committed.
|
|
9
|
+
class LoggedChange
|
|
10
|
+
|
|
11
|
+
# The current LoggedChangeLoader
|
|
12
|
+
attr_accessor :loader
|
|
13
|
+
|
|
14
|
+
# The current Session
|
|
15
|
+
def session
|
|
16
|
+
@session ||= loader.session
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# The current database (either +:left+ or +:right+)
|
|
20
|
+
def database
|
|
21
|
+
@database ||= loader.database
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# The name of the changed table
|
|
25
|
+
attr_accessor :table
|
|
26
|
+
|
|
27
|
+
# When the first change to the record happened
|
|
28
|
+
attr_accessor :first_changed_at
|
|
29
|
+
|
|
30
|
+
# When the last change to the record happened
|
|
31
|
+
attr_accessor :last_changed_at
|
|
32
|
+
|
|
33
|
+
# Type of the change. Either :+insert+, :+update+ or :+delete+.
|
|
34
|
+
attr_accessor :type
|
|
35
|
+
|
|
36
|
+
# A column_name => value hash identifying the changed record
|
|
37
|
+
attr_accessor :key
|
|
38
|
+
|
|
39
|
+
# Only used for updates: a column_name => value hash of the original primary
|
|
40
|
+
# key of the updated record
|
|
41
|
+
attr_accessor :new_key
|
|
42
|
+
|
|
43
|
+
# Creates a new LoggedChange instance.
|
|
44
|
+
# * +loader+: the current LoggedChangeLoader
|
|
45
|
+
# * +database+: either :+left+ or :+right+
|
|
46
|
+
def initialize(loader)
|
|
47
|
+
self.loader = loader
|
|
48
|
+
self.type = :no_change
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# A hash describing how the change state morph based on newly found change
|
|
52
|
+
# records.
|
|
53
|
+
# * key: String consisting of 2 letters
|
|
54
|
+
# * first letter: describes current type change (nothing, insert, update, delete)
|
|
55
|
+
# * second letter: the new change type as read of the change log table
|
|
56
|
+
# * value:
|
|
57
|
+
# The resulting change type.
|
|
58
|
+
# [1]: such cases shouldn't happen. but just in case, choose the most
|
|
59
|
+
# sensible solution.
|
|
60
|
+
TYPE_CHANGES = {
|
|
61
|
+
'NI' => 'I',
|
|
62
|
+
'NU' => 'U',
|
|
63
|
+
'ND' => 'D',
|
|
64
|
+
'II' => 'I', # [1]
|
|
65
|
+
'IU' => 'I',
|
|
66
|
+
'ID' => 'N',
|
|
67
|
+
'UI' => 'U', # [1]
|
|
68
|
+
'UU' => 'U',
|
|
69
|
+
'UD' => 'D',
|
|
70
|
+
'DI' => 'U',
|
|
71
|
+
'DU' => 'U', # [1]
|
|
72
|
+
'DD' => 'D', # [1]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# A hash translating the short 1-letter types to the according symbols
|
|
76
|
+
SHORT_TYPES = {
|
|
77
|
+
'I' => :insert,
|
|
78
|
+
'U' => :update,
|
|
79
|
+
'D' => :delete,
|
|
80
|
+
'N' => :no_change
|
|
81
|
+
}
|
|
82
|
+
# A hash translating the symbold types to according 1 letter types
|
|
83
|
+
LONG_TYPES = SHORT_TYPES.invert
|
|
84
|
+
|
|
85
|
+
# Returns the configured key separator
|
|
86
|
+
def key_sep
|
|
87
|
+
@key_sep ||= session.configuration.options[:key_sep]
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Returns a column_name => value hash based on the provided +raw_key+ string
|
|
91
|
+
# (which is a string in the format as read directly from the change log table).
|
|
92
|
+
def key_to_hash(raw_key)
|
|
93
|
+
result = {}
|
|
94
|
+
#raw_key.split(key_sep).each_slice(2) {|a| result[a[0]] = a[1]}
|
|
95
|
+
raw_key.split(key_sep).each_slice(2) {|field_name, value| result[field_name] = value}
|
|
96
|
+
result
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Loads the change as per #table and #key. Works if the LoggedChange instance
|
|
100
|
+
# is totally new or was already loaded before.
|
|
101
|
+
def load
|
|
102
|
+
current_type = LONG_TYPES[type]
|
|
103
|
+
|
|
104
|
+
org_key = new_key || key
|
|
105
|
+
# change to key string as can be found in change log table
|
|
106
|
+
org_key = session.send(database).primary_key_names(table).map do |key_name|
|
|
107
|
+
"#{key_name}#{key_sep}#{org_key[key_name]}"
|
|
108
|
+
end.join(key_sep)
|
|
109
|
+
current_key = org_key
|
|
110
|
+
|
|
111
|
+
while change = loader.load(table, current_key)
|
|
112
|
+
|
|
113
|
+
new_type = change['change_type']
|
|
114
|
+
current_type = TYPE_CHANGES["#{current_type}#{new_type}"]
|
|
115
|
+
|
|
116
|
+
self.first_changed_at ||= change['change_time']
|
|
117
|
+
self.last_changed_at = change['change_time']
|
|
118
|
+
|
|
119
|
+
if change['change_type'] == 'U' and change['change_new_key'] != current_key
|
|
120
|
+
current_key = change['change_new_key']
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
self.type = SHORT_TYPES[current_type]
|
|
125
|
+
self.new_key = nil
|
|
126
|
+
if type == :update
|
|
127
|
+
self.key ||= key_to_hash(org_key)
|
|
128
|
+
self.new_key = key_to_hash(current_key)
|
|
129
|
+
else
|
|
130
|
+
self.key = key_to_hash(current_key)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Loads the change with the specified key for the named +table+.
|
|
135
|
+
# * +table+: name of the table
|
|
136
|
+
# * +key+: a column_name => value hash for all primary key columns of the table
|
|
137
|
+
def load_specified(table, key)
|
|
138
|
+
self.table = table
|
|
139
|
+
self.key = key
|
|
140
|
+
load
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Loads the oldest available change
|
|
144
|
+
def load_oldest
|
|
145
|
+
begin
|
|
146
|
+
change = loader.oldest_change
|
|
147
|
+
break unless change
|
|
148
|
+
self.key = key_to_hash(change['change_key'])
|
|
149
|
+
self.table = change['change_table']
|
|
150
|
+
load
|
|
151
|
+
end until type != :no_change
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Prevents session from going into YAML output
|
|
155
|
+
def to_yaml_properties
|
|
156
|
+
instance_variables.sort.reject {|var_name| ['@session', '@loader'].include? var_name}
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
end
|
|
160
|
+
end
|