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.
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
+