andyjeffries-rubyrep 1.2.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|