andyjeffries-rubyrep 1.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (125) hide show
  1. data/History.txt +83 -0
  2. data/License.txt +20 -0
  3. data/Manifest.txt +151 -0
  4. data/README.txt +37 -0
  5. data/bin/rubyrep +8 -0
  6. data/lib/rubyrep.rb +72 -0
  7. data/lib/rubyrep/base_runner.rb +195 -0
  8. data/lib/rubyrep/command_runner.rb +144 -0
  9. data/lib/rubyrep/committers/buffered_committer.rb +151 -0
  10. data/lib/rubyrep/committers/committers.rb +152 -0
  11. data/lib/rubyrep/configuration.rb +275 -0
  12. data/lib/rubyrep/connection_extenders/connection_extenders.rb +165 -0
  13. data/lib/rubyrep/connection_extenders/jdbc_extender.rb +65 -0
  14. data/lib/rubyrep/connection_extenders/mysql_extender.rb +59 -0
  15. data/lib/rubyrep/connection_extenders/postgresql_extender.rb +277 -0
  16. data/lib/rubyrep/database_proxy.rb +52 -0
  17. data/lib/rubyrep/direct_table_scan.rb +75 -0
  18. data/lib/rubyrep/generate_runner.rb +105 -0
  19. data/lib/rubyrep/initializer.rb +39 -0
  20. data/lib/rubyrep/log_helper.rb +30 -0
  21. data/lib/rubyrep/logged_change.rb +160 -0
  22. data/lib/rubyrep/logged_change_loader.rb +197 -0
  23. data/lib/rubyrep/noisy_connection.rb +80 -0
  24. data/lib/rubyrep/proxied_table_scan.rb +171 -0
  25. data/lib/rubyrep/proxy_block_cursor.rb +145 -0
  26. data/lib/rubyrep/proxy_connection.rb +431 -0
  27. data/lib/rubyrep/proxy_cursor.rb +44 -0
  28. data/lib/rubyrep/proxy_row_cursor.rb +43 -0
  29. data/lib/rubyrep/proxy_runner.rb +89 -0
  30. data/lib/rubyrep/replication_difference.rb +100 -0
  31. data/lib/rubyrep/replication_extenders/mysql_replication.rb +271 -0
  32. data/lib/rubyrep/replication_extenders/postgresql_replication.rb +236 -0
  33. data/lib/rubyrep/replication_extenders/replication_extenders.rb +26 -0
  34. data/lib/rubyrep/replication_helper.rb +142 -0
  35. data/lib/rubyrep/replication_initializer.rb +327 -0
  36. data/lib/rubyrep/replication_run.rb +142 -0
  37. data/lib/rubyrep/replication_runner.rb +166 -0
  38. data/lib/rubyrep/replicators/replicators.rb +42 -0
  39. data/lib/rubyrep/replicators/two_way_replicator.rb +361 -0
  40. data/lib/rubyrep/scan_progress_printers/progress_bar.rb +65 -0
  41. data/lib/rubyrep/scan_progress_printers/scan_progress_printers.rb +65 -0
  42. data/lib/rubyrep/scan_report_printers/scan_detail_reporter.rb +111 -0
  43. data/lib/rubyrep/scan_report_printers/scan_report_printers.rb +67 -0
  44. data/lib/rubyrep/scan_report_printers/scan_summary_reporter.rb +75 -0
  45. data/lib/rubyrep/scan_runner.rb +25 -0
  46. data/lib/rubyrep/session.rb +230 -0
  47. data/lib/rubyrep/sync_helper.rb +121 -0
  48. data/lib/rubyrep/sync_runner.rb +31 -0
  49. data/lib/rubyrep/syncers/syncers.rb +112 -0
  50. data/lib/rubyrep/syncers/two_way_syncer.rb +174 -0
  51. data/lib/rubyrep/table_scan.rb +54 -0
  52. data/lib/rubyrep/table_scan_helper.rb +46 -0
  53. data/lib/rubyrep/table_sorter.rb +70 -0
  54. data/lib/rubyrep/table_spec_resolver.rb +142 -0
  55. data/lib/rubyrep/table_sync.rb +90 -0
  56. data/lib/rubyrep/task_sweeper.rb +77 -0
  57. data/lib/rubyrep/trigger_mode_switcher.rb +63 -0
  58. data/lib/rubyrep/type_casting_cursor.rb +31 -0
  59. data/lib/rubyrep/uninstall_runner.rb +93 -0
  60. data/lib/rubyrep/version.rb +9 -0
  61. data/rubyrep +8 -0
  62. data/rubyrep.bat +4 -0
  63. data/setup.rb +1585 -0
  64. data/spec/base_runner_spec.rb +218 -0
  65. data/spec/buffered_committer_spec.rb +274 -0
  66. data/spec/command_runner_spec.rb +145 -0
  67. data/spec/committers_spec.rb +178 -0
  68. data/spec/configuration_spec.rb +203 -0
  69. data/spec/connection_extender_interface_spec.rb +141 -0
  70. data/spec/connection_extenders_registration_spec.rb +164 -0
  71. data/spec/database_proxy_spec.rb +48 -0
  72. data/spec/database_rake_spec.rb +40 -0
  73. data/spec/db_specific_connection_extenders_spec.rb +34 -0
  74. data/spec/db_specific_replication_extenders_spec.rb +38 -0
  75. data/spec/direct_table_scan_spec.rb +61 -0
  76. data/spec/dolphins.jpg +0 -0
  77. data/spec/generate_runner_spec.rb +84 -0
  78. data/spec/initializer_spec.rb +46 -0
  79. data/spec/log_helper_spec.rb +39 -0
  80. data/spec/logged_change_loader_spec.rb +68 -0
  81. data/spec/logged_change_spec.rb +470 -0
  82. data/spec/noisy_connection_spec.rb +78 -0
  83. data/spec/postgresql_replication_spec.rb +48 -0
  84. data/spec/postgresql_schema_support_spec.rb +212 -0
  85. data/spec/postgresql_support_spec.rb +63 -0
  86. data/spec/progress_bar_spec.rb +77 -0
  87. data/spec/proxied_table_scan_spec.rb +151 -0
  88. data/spec/proxy_block_cursor_spec.rb +197 -0
  89. data/spec/proxy_connection_spec.rb +423 -0
  90. data/spec/proxy_cursor_spec.rb +56 -0
  91. data/spec/proxy_row_cursor_spec.rb +66 -0
  92. data/spec/proxy_runner_spec.rb +70 -0
  93. data/spec/replication_difference_spec.rb +161 -0
  94. data/spec/replication_extender_interface_spec.rb +367 -0
  95. data/spec/replication_extenders_spec.rb +32 -0
  96. data/spec/replication_helper_spec.rb +178 -0
  97. data/spec/replication_initializer_spec.rb +509 -0
  98. data/spec/replication_run_spec.rb +443 -0
  99. data/spec/replication_runner_spec.rb +254 -0
  100. data/spec/replicators_spec.rb +36 -0
  101. data/spec/rubyrep_spec.rb +8 -0
  102. data/spec/scan_detail_reporter_spec.rb +119 -0
  103. data/spec/scan_progress_printers_spec.rb +68 -0
  104. data/spec/scan_report_printers_spec.rb +67 -0
  105. data/spec/scan_runner_spec.rb +50 -0
  106. data/spec/scan_summary_reporter_spec.rb +61 -0
  107. data/spec/session_spec.rb +253 -0
  108. data/spec/spec.opts +1 -0
  109. data/spec/spec_helper.rb +305 -0
  110. data/spec/strange_name_support_spec.rb +135 -0
  111. data/spec/sync_helper_spec.rb +169 -0
  112. data/spec/sync_runner_spec.rb +78 -0
  113. data/spec/syncers_spec.rb +171 -0
  114. data/spec/table_scan_helper_spec.rb +36 -0
  115. data/spec/table_scan_spec.rb +49 -0
  116. data/spec/table_sorter_spec.rb +30 -0
  117. data/spec/table_spec_resolver_spec.rb +111 -0
  118. data/spec/table_sync_spec.rb +140 -0
  119. data/spec/task_sweeper_spec.rb +47 -0
  120. data/spec/trigger_mode_switcher_spec.rb +83 -0
  121. data/spec/two_way_replicator_spec.rb +721 -0
  122. data/spec/two_way_syncer_spec.rb +256 -0
  123. data/spec/type_casting_cursor_spec.rb +50 -0
  124. data/spec/uninstall_runner_spec.rb +93 -0
  125. 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
+