rubyrep 1.0.4 → 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt CHANGED
@@ -1,6 +1,11 @@
1
+ == 1.0.5 2009-07-03
2
+
3
+ * Bug fix: rubyrep replication runs should survive update or delete attempts rejected by the database.
4
+ * Bug fix: support for multiple PostgreSQL schemas holding tables of the same name.
5
+
1
6
  == 1.0.4 2009-06-20
2
7
 
3
- * Bug fix: added missing file (log_helper.rb) to gem manifest
8
+ * Bug fix: added missing file (log_helper.rb) to gem manifest
4
9
 
5
10
  == 1.0.3 2009-06-20
6
11
 
data/Manifest.txt CHANGED
@@ -91,6 +91,7 @@ spec/initializer_spec.rb
91
91
  spec/log_helper_spec.rb
92
92
  spec/logged_change_spec.rb
93
93
  spec/postgresql_replication_spec.rb
94
+ spec/postgresql_schema_support_spec.rb
94
95
  spec/postgresql_support_spec.rb
95
96
  spec/progress_bar_spec.rb
96
97
  spec/proxied_table_scan_spec.rb
@@ -61,41 +61,11 @@ module RR
61
61
  # * Hack to get schema support for Postgres under JRuby on par with the
62
62
  # standard ruby version.
63
63
  module JdbcPostgreSQLExtender
64
+ require 'jdbc_adapter/jdbc_postgre'
65
+ require "#{File.dirname(__FILE__)}/postgresql_extender"
64
66
 
65
- # Returns the list of a table's column names, data types, and default values.
66
- #
67
- # The underlying query is roughly:
68
- # SELECT column.name, column.type, default.value
69
- # FROM column LEFT JOIN default
70
- # ON column.table_id = default.table_id
71
- # AND column.num = default.column_num
72
- # WHERE column.table_id = get_table_id('table_name')
73
- # AND column.num > 0
74
- # AND NOT column.is_dropped
75
- # ORDER BY column.num
76
- #
77
- # If the table name is not prefixed with a schema, the database will
78
- # take the first match from the schema search path.
79
- #
80
- # Query implementation notes:
81
- # - format_type includes the column size constraint, e.g. varchar(50)
82
- # - ::regclass is a function that gives the id for a table name
83
- def column_definitions(table_name) #:nodoc:
84
- rows = select_all(<<-end_sql)
85
- SELECT a.attname as name, format_type(a.atttypid, a.atttypmod) as type, d.adsrc as default, a.attnotnull as notnull
86
- FROM pg_attribute a LEFT JOIN pg_attrdef d
87
- ON a.attrelid = d.adrelid AND a.attnum = d.adnum
88
- WHERE a.attrelid = (SELECT oid FROM pg_class WHERE relname = '#{table_name}')
89
- AND a.attnum > 0 AND NOT a.attisdropped
90
- ORDER BY a.attnum
91
- end_sql
92
-
93
- rows.map do |row|
94
- [row['name'], row['type'], row['default'], row['notnull']]
95
- end
96
- end
67
+ include PostgreSQLExtender
97
68
 
98
- require 'jdbc_adapter/jdbc_postgre'
99
69
  class JdbcPostgreSQLColumn < ActiveRecord::ConnectionAdapters::Column
100
70
  include ::JdbcSpec::PostgreSQL::Column
101
71
  end
@@ -113,21 +83,6 @@ module RR
113
83
  execute "SET search_path TO #{config[:schema_search_path]}" if config[:schema_search_path]
114
84
  end
115
85
 
116
- # Returns the active schema search path.
117
- def schema_search_path
118
- @schema_search_path ||= select_one('SHOW search_path')['search_path']
119
- end
120
-
121
- # Returns the list of all tables in the schema search path or a specified schema.
122
- def tables(name = nil)
123
- schemas = schema_search_path.split(/,/).map { |p| quote(p) }.join(',')
124
- select_all(<<-SQL, name).map { |row| row['tablename'] }
125
- SELECT tablename
126
- FROM pg_tables
127
- WHERE schemaname IN (#{schemas})
128
- SQL
129
- end
130
-
131
86
  # Converts the given Time object into the correctly formatted string
132
87
  # representation.
133
88
  #
@@ -84,92 +84,128 @@ module RR
84
84
  # Provides various PostgreSQL specific functionality required by Rubyrep.
85
85
  module PostgreSQLExtender
86
86
  RR::ConnectionExtenders.register :postgresql => self
87
+
88
+ # Returns an array of schemas in the current search path.
89
+ def schemas
90
+ unless @schemas
91
+ search_path = select_one("show search_path")['search_path']
92
+ @schemas = search_path.split(/,/).map { |p| quote(p.strip) }.join(',')
93
+ end
94
+ @schemas
95
+ end
96
+
97
+ # *** Monkey patch***
98
+ # Returns the list of all tables in the schema search path or a specified schema.
99
+ # This overwrites the according ActiveRecord::PostgreSQLAdapter method
100
+ # to make sure that also search paths with spaces work
101
+ # (E. g. 'public, rr' instead of only 'public,rr')
102
+ def tables(name = nil)
103
+ select_all(<<-SQL, name).map { |row| row['tablename'] }
104
+ SELECT tablename
105
+ FROM pg_tables
106
+ WHERE schemaname IN (#{schemas})
107
+ SQL
108
+ end
87
109
 
88
110
  # Returns an ordered list of primary key column names of the given table
89
111
  def primary_key_names(table)
90
112
  row = self.select_one(<<-end_sql)
91
113
  SELECT relname
92
114
  FROM pg_class
93
- WHERE relname = '#{table}'
115
+ WHERE relname = '#{table}' and relnamespace IN
116
+ (SELECT oid FROM pg_namespace WHERE nspname in (#{schemas}))
94
117
  end_sql
95
- if row.nil?
96
- raise "table '#{table}' does not exist"
97
- end
118
+ raise "table '#{table}' does not exist" if row.nil?
98
119
 
99
- row = self.select_one(<<-end_sql)
120
+ row = self.select_one(<<-end_sql)
100
121
  SELECT cons.conkey
101
122
  FROM pg_class rel
102
123
  JOIN pg_constraint cons ON (rel.oid = cons.conrelid)
103
- WHERE cons.contype = 'p' AND rel.relname = '#{table}'
104
- end_sql
105
- if row.nil?
106
- return []
107
- end
108
- column_parray = row['conkey']
124
+ WHERE cons.contype = 'p' AND rel.relname = '#{table}' AND rel.relnamespace IN
125
+ (SELECT oid FROM pg_namespace WHERE nspname in (#{schemas}))
126
+ end_sql
127
+ return [] if row.nil?
128
+ column_parray = row['conkey']
109
129
 
110
- # Change a Postgres Array of attribute numbers
111
- # (returned in String form, e. g.: "{1,2}") into an array of Integers
112
- column_ids = column_parray.sub(/^\{(.*)\}$/,'\1').split(',').map {|a| a.to_i}
130
+ # Change a Postgres Array of attribute numbers
131
+ # (returned in String form, e. g.: "{1,2}") into an array of Integers
132
+ column_ids = column_parray.sub(/^\{(.*)\}$/,'\1').split(',').map {|a| a.to_i}
113
133
 
114
- columns = {}
115
- rows = self.select_all(<<-end_sql)
134
+ columns = {}
135
+ rows = self.select_all(<<-end_sql)
116
136
  SELECT attnum, attname
117
137
  FROM pg_class rel
118
138
  JOIN pg_constraint cons ON (rel.oid = cons.conrelid)
119
139
  JOIN pg_attribute attr ON (rel.oid = attr.attrelid and attr.attnum = any (cons.conkey))
120
- WHERE cons.contype = 'p' AND rel.relname = '#{table}'
121
- end_sql
122
- sorted_columns = []
123
- if not rows.nil?
124
- rows.each() {|r| columns[r['attnum'].to_i] = r['attname']}
125
- sorted_columns = column_ids.map {|column_id| columns[column_id]}
126
- end
127
- sorted_columns
128
- end
140
+ WHERE cons.contype = 'p' AND rel.relname = '#{table}' AND rel.relnamespace IN
141
+ (SELECT oid FROM pg_namespace WHERE nspname in (#{schemas}))
142
+ end_sql
143
+ sorted_columns = []
144
+ if not rows.nil?
145
+ rows.each() {|r| columns[r['attnum'].to_i] = r['attname']}
146
+ sorted_columns = column_ids.map {|column_id| columns[column_id]}
147
+ end
148
+ sorted_columns
149
+ end
129
150
 
130
- # Returns for each given table, which other tables it references via
131
- # foreign key constraints.
132
- # * tables: an array of table names
133
- # Returns: a hash with
134
- # * key: name of the referencing table
135
- # * value: an array of names of referenced tables
136
- def referenced_tables(tables)
137
- rows = self.select_all(<<-end_sql)
151
+ # Returns for each given table, which other tables it references via
152
+ # foreign key constraints.
153
+ # * tables: an array of table names
154
+ # Returns: a hash with
155
+ # * key: name of the referencing table
156
+ # * value: an array of names of referenced tables
157
+ def referenced_tables(tables)
158
+ rows = self.select_all(<<-end_sql)
138
159
  select distinct referencing.relname as referencing_table, referenced.relname as referenced_table
139
160
  from pg_class referencing
140
161
  left join pg_constraint on referencing.oid = pg_constraint.conrelid
141
162
  left join pg_class referenced on pg_constraint.confrelid = referenced.oid
142
163
  where referencing.relkind='r'
143
164
  and referencing.relname in ('#{tables.join("', '")}')
144
- end_sql
145
- result = {}
146
- rows.each do |row|
147
- unless result.include? row['referencing_table']
148
- result[row['referencing_table']] = []
165
+ and referencing.relnamespace IN
166
+ (SELECT oid FROM pg_namespace WHERE nspname in (#{schemas}))
167
+ end_sql
168
+ result = {}
169
+ rows.each do |row|
170
+ unless result.include? row['referencing_table']
171
+ result[row['referencing_table']] = []
172
+ end
173
+ if row['referenced_table'] != nil
174
+ result[row['referencing_table']] << row['referenced_table']
175
+ end
176
+ end
177
+ result
149
178
  end
150
- if row['referenced_table'] != nil
151
- result[row['referencing_table']] << row['referenced_table']
179
+
180
+ # *** Monkey patch***
181
+ # Returns the list of a table's column names, data types, and default values.
182
+ # This overwrites the according ActiveRecord::PostgreSQLAdapter method
183
+ # to
184
+ # * work with tables containing a dot (".") and
185
+ # * only look for tables in the current schema search path.
186
+ def column_definitions(table_name) #:nodoc:
187
+ rows = self.select_all <<-end_sql
188
+ SELECT
189
+ a.attname as name,
190
+ format_type(a.atttypid, a.atttypmod) as type,
191
+ d.adsrc as source,
192
+ a.attnotnull as notnull
193
+ FROM pg_attribute a LEFT JOIN pg_attrdef d
194
+ ON a.attrelid = d.adrelid AND a.attnum = d.adnum
195
+ WHERE a.attrelid = (
196
+ SELECT oid FROM pg_class
197
+ WHERE relname = '#{table_name}' AND relnamespace IN
198
+ (SELECT oid FROM pg_namespace WHERE nspname in (#{schemas}))
199
+ LIMIT 1
200
+ )
201
+ AND a.attnum > 0 AND NOT a.attisdropped
202
+ ORDER BY a.attnum
203
+ end_sql
204
+
205
+ rows.map {|row| [row['name'], row['type'], row['source'], row['notnull']]}
152
206
  end
153
- end
154
- result
155
- end
156
207
 
157
- # *** Monkey patch***
158
- # Returns the list of a table's column names, data types, and default values.
159
- # This overwrites the according ActiveRecord::PostgreSQLAdapter method
160
- # to work with tables containing a dot (".").
161
- def column_definitions(table_name) #:nodoc:
162
- query <<-end_sql
163
- SELECT a.attname, format_type(a.atttypid, a.atttypmod), d.adsrc, a.attnotnull
164
- FROM pg_attribute a LEFT JOIN pg_attrdef d
165
- ON a.attrelid = d.adrelid AND a.attnum = d.adnum
166
- WHERE a.attrelid = (SELECT oid FROM pg_class WHERE relname = '#{table_name}')
167
- AND a.attnum > 0 AND NOT a.attisdropped
168
- ORDER BY a.attnum
169
- end_sql
208
+ end
170
209
  end
171
-
172
- end
173
- end
174
210
  end
175
211
 
@@ -87,8 +87,6 @@ module RR
87
87
  # * +trigger_name+: name of the trigger
88
88
  # * +table_name+: name of the table
89
89
  def replication_trigger_exists?(trigger_name, table_name)
90
- search_path = select_one("show search_path")['search_path']
91
- schemas = search_path.split(/,/).map { |p| quote(p) }.join(',')
92
90
  !select_all(<<-end_sql).empty?
93
91
  select 1 from information_schema.triggers
94
92
  where event_object_schema in (#{schemas})
@@ -114,7 +112,8 @@ module RR
114
112
  join pg_depend as r on t.oid = r.refobjid
115
113
  join pg_class as s on r.objid = s.oid
116
114
  and s.relkind = 'S'
117
- and t.relname = '#{table_name}'
115
+ and t.relname = '#{table_name}' AND t.relnamespace IN
116
+ (SELECT oid FROM pg_namespace WHERE nspname in (#{schemas}))
118
117
  end_sql
119
118
  sequence_names.each do |sequence_name|
120
119
  row = select_one("select last_value, increment_by from \"#{sequence_name}\"")
@@ -171,7 +170,8 @@ module RR
171
170
  join pg_depend as r on t.oid = r.refobjid
172
171
  join pg_class as s on r.objid = s.oid
173
172
  and s.relkind = 'S'
174
- and t.relname = '#{table_name}'
173
+ and t.relname = '#{table_name}' and t.relnamespace IN
174
+ (SELECT oid FROM pg_namespace WHERE nspname in (#{schemas}))
175
175
  end_sql
176
176
  sequence_names.each do |sequence_name|
177
177
  execute(<<-end_sql)
@@ -169,7 +169,7 @@ module RR
169
169
  when :update
170
170
  attempt_update source_db, diff, remaining_attempts, source_key, target_key
171
171
  when :delete
172
- attempt_delete source_db, diff, target_key
172
+ attempt_delete source_db, diff, remaining_attempts, target_key
173
173
  end
174
174
  end
175
175
 
@@ -263,8 +263,16 @@ module RR
263
263
  diff.amend
264
264
  replicate_difference diff, remaining_attempts - 1, "source record for update vanished"
265
265
  else
266
- log_replication_outcome source_db, diff
267
- rep_helper.update_record target_db, target_table, values, target_key
266
+ begin
267
+ rep_helper.session.send(target_db).execute "savepoint rr_update"
268
+ log_replication_outcome source_db, diff
269
+ rep_helper.update_record target_db, target_table, values, target_key
270
+ rescue Exception => e
271
+ rep_helper.session.send(target_db).execute "rollback to savepoint rr_update"
272
+ diff.amend
273
+ replicate_difference diff, remaining_attempts - 1,
274
+ "update failed with #{e.message}"
275
+ end
268
276
  end
269
277
  end
270
278
 
@@ -273,13 +281,22 @@ module RR
273
281
  # :+right+.
274
282
  # * +source_db+: either :+left+ or :+right+ - source database of replication
275
283
  # * +diff+: the current ReplicationDifference instance
284
+ # * +remaining_attempts+: the number of remaining replication attempts for this difference
276
285
  # * +target_key+: a column_name => value hash identifying the source record
277
- def attempt_delete(source_db, diff, target_key)
286
+ def attempt_delete(source_db, diff, remaining_attempts, target_key)
278
287
  change = diff.changes[source_db]
279
288
  target_db = OTHER_SIDE[source_db]
280
289
  target_table = rep_helper.corresponding_table(source_db, change.table)
281
- log_replication_outcome source_db, diff
282
- rep_helper.delete_record target_db, target_table, target_key
290
+ begin
291
+ rep_helper.session.send(target_db).execute "savepoint rr_delete"
292
+ log_replication_outcome source_db, diff
293
+ rep_helper.delete_record target_db, target_table, target_key
294
+ rescue Exception => e
295
+ rep_helper.session.send(target_db).execute "rollback to savepoint rr_delete"
296
+ diff.amend
297
+ replicate_difference diff, remaining_attempts - 1,
298
+ "delete failed with #{e.message}"
299
+ end
283
300
  end
284
301
 
285
302
  # Called to replicate the specified difference.
@@ -306,7 +323,7 @@ module RR
306
323
  when :update
307
324
  attempt_update source_db, diff, remaining_attempts, change.new_key, change.key
308
325
  when :delete
309
- attempt_delete source_db, diff, change.key
326
+ attempt_delete source_db, diff, remaining_attempts, change.key
310
327
  end
311
328
  else # option must be a Proc
312
329
  option.call rep_helper, diff
@@ -2,7 +2,7 @@ module RR #:nodoc:
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 1
4
4
  MINOR = 0
5
- TINY = 4
5
+ TINY = 5
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
@@ -0,0 +1,179 @@
1
+ require File.dirname(__FILE__) + '/spec_helper.rb'
2
+
3
+ include RR
4
+
5
+ require File.dirname(__FILE__) + "/../config/test_config.rb"
6
+
7
+ describe "PostgreSQL schema support" do
8
+ before(:each) do
9
+ config = deep_copy(standard_config)
10
+ config.left[:schema_search_path] = 'rr'
11
+ config.right[:schema_search_path] = 'rr'
12
+ Initializer.configuration = config
13
+ end
14
+
15
+ after(:each) do
16
+ end
17
+
18
+ if Initializer.configuration.left[:adapter] == 'postgresql'
19
+ it "tables should show the tables from the schema and no others" do
20
+ session = Session.new
21
+ session.left.tables.include?('rr_simple').should be_true
22
+ session.left.tables.include?('scanner_records').should be_false
23
+ end
24
+
25
+ it "tables should not show the tables from other schemas" do
26
+ session = Session.new standard_config
27
+ session.left.tables.include?('scanner_records').should be_true
28
+ session.left.tables.include?('rr_simple').should be_false
29
+ end
30
+
31
+ it "primary_key_names should work" do
32
+ session = Session.new
33
+ session.left.primary_key_names('rr_simple').should == ['id']
34
+ end
35
+
36
+ it "primary_key_names should pick the table in the target schema" do
37
+ session = Session.new
38
+ session.left.primary_key_names('rr_duplicate').should == ['id']
39
+ end
40
+
41
+ it "column_names should work" do
42
+ session = Session.new
43
+ session.left.column_names('rr_simple').should == ['id', 'name']
44
+ end
45
+
46
+ it "column_names should pick the table in the target schema" do
47
+ session = Session.new
48
+ session.left.column_names('rr_duplicate').should == ['id', 'name']
49
+ end
50
+
51
+ it "referenced_tables should work" do
52
+ session = Session.new
53
+ session.left.referenced_tables(['rr_referencing']).should == {
54
+ 'rr_referencing' => ['rr_referenced']
55
+ }
56
+ end
57
+
58
+ it "table_select_query should work" do
59
+ session = Session.new
60
+ session.left.table_select_query('rr_simple').
61
+ should == 'select "id", "name" from "rr_simple" order by "id"'
62
+ end
63
+
64
+ it "TypeCasingCursor should work" do
65
+ session = Session.new
66
+ org_cursor = session.left.select_cursor(:query => "select id, name from rr_simple where id = 1")
67
+ cursor = TypeCastingCursor.new session.left, 'rr_simple', org_cursor
68
+
69
+ row = cursor.next_row
70
+
71
+ row.should == {
72
+ 'id' => 1,
73
+ 'name' => 'bla'
74
+ }
75
+ end
76
+
77
+ it "sequence_values should pick the table in the target schema" do
78
+ session = Session.new
79
+ session.left.sequence_values('rr', 'rr_duplicate').keys.should == ["rr_duplicate_id_seq"]
80
+ end
81
+
82
+ it "clear_sequence_setup should pick the table in the target schema" do
83
+ session = nil
84
+ begin
85
+ session = Session.new
86
+ initializer = ReplicationInitializer.new(session)
87
+ session.left.begin_db_transaction
88
+ session.right.begin_db_transaction
89
+ table_pair = {:left => 'rr_duplicate', :right => 'rr_duplicate'}
90
+ initializer.ensure_sequence_setup table_pair, 5, 2, 1
91
+ id1, id2 = get_example_sequence_values(session, 'rr_duplicate')
92
+ (id2 - id1).should == 5
93
+ (id1 % 5).should == 2
94
+
95
+ initializer.clear_sequence_setup :left, 'rr_duplicate'
96
+ id1, id2 = get_example_sequence_values(session, 'rr_duplicate')
97
+ (id2 - id1).should == 1
98
+ ensure
99
+ [:left, :right].each do |database|
100
+ initializer.clear_sequence_setup database, 'rr_duplicate' rescue nil if session
101
+ session.send(database).execute "delete from rr_duplicate" rescue nil if session
102
+ session.send(database).rollback_db_transaction rescue nil if session
103
+ end
104
+ end
105
+
106
+ end
107
+
108
+ it "sequence setup should work" do
109
+ session = nil
110
+ begin
111
+ session = Session.new
112
+ initializer = ReplicationInitializer.new(session)
113
+ session.left.begin_db_transaction
114
+ session.right.begin_db_transaction
115
+ table_pair = {:left => 'rr_sequence_test', :right => 'rr_sequence_test'}
116
+ initializer.ensure_sequence_setup table_pair, 5, 2, 1
117
+ id1, id2 = get_example_sequence_values(session, 'rr_sequence_test')
118
+ (id2 - id1).should == 5
119
+ (id1 % 5).should == 2
120
+ ensure
121
+ [:left, :right].each do |database|
122
+ initializer.clear_sequence_setup database, 'rr_sequence_test' rescue nil if session
123
+ session.send(database).execute "delete from rr_sequence_test" rescue nil if session
124
+ session.send(database).rollback_db_transaction rescue nil if session
125
+ end
126
+ end
127
+ end
128
+
129
+ it "clear_sequence_setup should work" do
130
+ session = nil
131
+ begin
132
+ session = Session.new
133
+ initializer = ReplicationInitializer.new(session)
134
+ session.left.begin_db_transaction
135
+ session.right.begin_db_transaction
136
+ table_pair = {:left => 'rr_sequence_test', :right => 'rr_sequence_test'}
137
+ initializer.ensure_sequence_setup table_pair, 5, 2, 2
138
+ initializer.clear_sequence_setup :left, 'rr_sequence_test'
139
+ id1, id2 = get_example_sequence_values(session, 'rr_sequence_test')
140
+ (id2 - id1).should == 1
141
+ ensure
142
+ [:left, :right].each do |database|
143
+ initializer.clear_sequence_setup database, 'rr_sequence_test' if session
144
+ session.send(database).execute "delete from rr_sequence_test" if session
145
+ session.send(database).rollback_db_transaction if session
146
+ end
147
+ end
148
+ end
149
+
150
+ it "create_trigger, trigger_exists? and drop_trigger should work" do
151
+ session = nil
152
+ begin
153
+ session = Session.new
154
+ initializer = ReplicationInitializer.new(session)
155
+ session.left.begin_db_transaction
156
+
157
+ initializer.create_trigger :left, 'rr_trigger_test'
158
+ initializer.trigger_exists?(:left, 'rr_trigger_test').
159
+ should be_true
160
+ initializer.drop_trigger(:left, 'rr_trigger_test')
161
+ initializer.trigger_exists?(:left, 'rr_trigger_test').
162
+ should be_false
163
+ ensure
164
+ session.left.rollback_db_transaction if session
165
+ end
166
+ end
167
+
168
+ it "should work with complex search paths" do
169
+ config = deep_copy(standard_config)
170
+ config.left[:schema_search_path] = 'public,rr'
171
+ config.right[:schema_search_path] = 'public,rr'
172
+ session = Session.new(config)
173
+
174
+ tables = session.left.tables
175
+ tables.include?('rr_simple').should be_true
176
+ tables.include?('scanner_records').should be_true
177
+ end
178
+ end
179
+ end
@@ -4,7 +4,7 @@ include RR
4
4
 
5
5
  require File.dirname(__FILE__) + "/../config/test_config.rb"
6
6
 
7
- describe "PostgreSQL schema support" do
7
+ describe "PostgreSQL support" do
8
8
  before(:each) do
9
9
  Initializer.configuration = standard_config
10
10
  end
data/spec/session_spec.rb CHANGED
@@ -186,14 +186,15 @@ describe Session do # here database connection caching is _not_ disabled
186
186
  'referenced_table',
187
187
  'scanner_text_key',
188
188
  ])
189
- sorted_table_pairs = convert_table_array_to_table_pair_array([
190
- 'scanner_records',
191
- 'referenced_table',
192
- 'referencing_table',
193
- 'scanner_text_key',
194
- ])
189
+ sorted_table_pairs = Session.new.sort_table_pairs(table_pairs)
190
+
191
+ # ensure result holds the original table pairs
192
+ p = Proc.new {|l, r| l[:left] <=> r[:left]}
193
+ sorted_table_pairs.sort(&p).should == table_pairs.sort(&p)
195
194
 
196
- Session.new.sort_table_pairs(table_pairs).should == sorted_table_pairs
195
+ # make sure the referenced table comes before the referencing table
196
+ sorted_table_pairs.map {|table_pair| table_pair[:left]}.grep(/referenc/).
197
+ should == ['referenced_table', 'referencing_table']
197
198
  end
198
199
 
199
200
  it "sort_table_pairs should not sort the tables if table_ordering is not enabled in the configuration" do
@@ -14,15 +14,14 @@ describe TableSorter do
14
14
  'scanner_text_key',
15
15
  ]
16
16
 
17
- sorted_tables = [
18
- 'scanner_records',
19
- 'referenced_table',
20
- 'referencing_table',
21
- 'scanner_text_key',
22
- ]
23
-
24
17
  sorter = TableSorter.new Session.new(standard_config), tables
25
- sorter.sort.should == sorted_tables
18
+ sorted_tables = sorter.sort
19
+
20
+ # make sure it contains the original tables
21
+ sorted_tables.sort.should == tables.sort
22
+
23
+ # make sure the referenced table comes before the referencing table
24
+ sorted_tables.grep(/referenc/).should == ['referenced_table', 'referencing_table']
26
25
 
27
26
  # verify that we are using TSort#tsort to get that result
28
27
  sorter.should_not_receive(:tsort)
@@ -496,6 +496,92 @@ describe Replicators::TwoWayReplicator do
496
496
  lambda {replicator.replicate_difference :dummy_diff, 0}.
497
497
  should raise_error(Exception, "max replication attempts exceeded")
498
498
  end
499
+
500
+ it "replicate_difference should handle updates rejected by the database" do
501
+ begin
502
+ config = deep_copy(standard_config)
503
+ config.options[:committer] = :never_commit
504
+ config.options[:replication_conflict_handling] = :left_wins
505
+
506
+ session = Session.new(config)
507
+
508
+ session.left.insert_record 'rr_pending_changes', {
509
+ 'change_table' => 'scanner_records',
510
+ 'change_key' => 'id|1',
511
+ 'change_new_key' => 'id|2',
512
+ 'change_type' => 'U',
513
+ 'change_time' => Time.now
514
+ }
515
+
516
+ rep_run = ReplicationRun.new session
517
+ helper = ReplicationHelper.new(rep_run)
518
+ replicator = Replicators::TwoWayReplicator.new(helper)
519
+
520
+ diff = ReplicationDifference.new session
521
+ diff.load
522
+
523
+ lambda {replicator.replicate_difference diff, 1}.should raise_error(/duplicate/i)
524
+
525
+ # Verify that the transaction has not become invalid
526
+ helper.log_replication_outcome diff, "bla", "blub"
527
+
528
+ row = session.left.select_one("select * from rr_logged_events")
529
+ row['change_table'].should == 'scanner_records'
530
+ row['change_key'].should == '1'
531
+ row['description'].should == 'bla'
532
+
533
+ ensure
534
+ Committers::NeverCommitter.rollback_current_session
535
+ if session
536
+ session.left.execute "delete from rr_pending_changes"
537
+ session.left.execute "delete from rr_logged_events"
538
+ end
539
+ end
540
+ end
541
+
542
+ it "replicate_difference should handle deletes rejected by the database" do
543
+ begin
544
+ config = deep_copy(standard_config)
545
+ config.options[:committer] = :never_commit
546
+ config.options[:replication_conflict_handling] = :left_wins
547
+
548
+ session = Session.new(config)
549
+
550
+ session.left.select_all("select * from rr_logged_events").should == []
551
+
552
+ session.left.insert_record 'rr_pending_changes', {
553
+ 'change_table' => 'referenced_table',
554
+ 'change_key' => 'first_id|1|second_id|2',
555
+ 'change_new_key' => nil,
556
+ 'change_type' => 'D',
557
+ 'change_time' => Time.now
558
+ }
559
+
560
+ rep_run = ReplicationRun.new session
561
+ helper = ReplicationHelper.new(rep_run)
562
+ replicator = Replicators::TwoWayReplicator.new(helper)
563
+
564
+ diff = ReplicationDifference.new session
565
+ diff.load
566
+
567
+ lambda {replicator.replicate_difference diff, 1}.should raise_error(/referencing_table_fkey/)
568
+
569
+ # Verify that the transaction has not become invalid
570
+ helper.log_replication_outcome diff, "bla", "blub"
571
+
572
+ row = session.left.select_one("select * from rr_logged_events")
573
+ row['change_table'].should == 'referenced_table'
574
+ row['change_key'].should =~ /first_id.*1.*second_id.*2/
575
+ row['description'].should == 'bla'
576
+
577
+ ensure
578
+ Committers::NeverCommitter.rollback_current_session
579
+ if session
580
+ session.left.execute "delete from rr_pending_changes"
581
+ session.left.execute "delete from rr_logged_events"
582
+ end
583
+ end
584
+ end
499
585
 
500
586
  it "replicate_difference should handle updates failing due to the source record being deleted after the original diff was loaded" do
501
587
  begin
data/tasks/database.rake CHANGED
@@ -90,6 +90,10 @@ def create_postgres_schema(config)
90
90
  create_table :rr_sequence_test do |t|
91
91
  t.column :name, :string
92
92
  end
93
+
94
+ create_table :rr_duplicate do |t|
95
+ t.column :name, :string
96
+ end
93
97
  end
94
98
  end
95
99
 
@@ -272,6 +276,33 @@ def create_sample_schema(database, config)
272
276
  create_table :right_table do |t|
273
277
  t.column :name, :string
274
278
  end if database == :right
279
+
280
+ if config.send(database)[:adapter] == 'postgresql'
281
+ create_table :rr_duplicate, :id => false do |t|
282
+ t.column :blub, :string
283
+ end rescue nil
284
+
285
+ ActiveRecord::Base.connection.execute(<<-end_sql) rescue nil
286
+ ALTER TABLE rr_duplicate ADD COLUMN key SERIAL
287
+ end_sql
288
+
289
+ ActiveRecord::Base.connection.execute(<<-end_sql) rescue nil
290
+ ALTER TABLE rr_duplicate ADD CONSTRAINT rr_duplicate_pkey
291
+ PRIMARY KEY (key)
292
+ end_sql
293
+
294
+ # duplicate that should *not* be found during PostgreSQL schema support tests
295
+ create_table :rr_referencing do |t|
296
+ t.column :first_fk, :integer
297
+ t.column :second_fk, :integer
298
+ end rescue nil
299
+
300
+ ActiveRecord::Base.connection.execute(<<-end_sql)
301
+ ALTER TABLE rr_referencing ADD CONSTRAINT rr_referencing_fkey
302
+ FOREIGN KEY (first_fk, second_fk)
303
+ REFERENCES referenced_table(first_id, second_id)
304
+ end_sql
305
+ end
275
306
  end
276
307
  end
277
308
 
@@ -283,6 +314,8 @@ def drop_sample_schema(config)
283
314
  ActiveRecord::Base.establish_connection config
284
315
 
285
316
  ActiveRecord::Schema.define do
317
+ drop_table :rr_referencing rescue nil
318
+ drop_table :rr_duplicate rescue nil
286
319
  drop_table STRANGE_TABLE rescue nil
287
320
  drop_table :extender_type_check rescue nil
288
321
  drop_table :extender_no_record rescue nil
@@ -380,6 +413,13 @@ def delete_all_and_create_shared_sample_data(config)
380
413
  {:first_id => 2, :second_id => 1, :name => 'ba'},
381
414
  {:first_id => 3, :second_id => 1}
382
415
  ].each { |row| create_row connection, 'extender_combined_key', row}
416
+
417
+ connection.execute("delete from referenced_table")
418
+ connection.execute("delete from referencing_table")
419
+ create_row connection, 'referenced_table', {
420
+ :first_id => 1, :second_id => 2, :name => 'bla'
421
+ }
422
+ create_row connection, 'referencing_table', {:first_fk => 1, :second_fk => 2}
383
423
  end
384
424
 
385
425
  # Reinitializes the sample schema with the sample data
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubyrep
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.4
4
+ version: 1.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Arndt Lehmann
@@ -30,7 +30,7 @@ cert_chain:
30
30
  NwT26VZnE2nr8g==
31
31
  -----END CERTIFICATE-----
32
32
 
33
- date: 2009-06-20 00:00:00 +09:00
33
+ date: 2009-07-03 00:00:00 +09:00
34
34
  default_executable:
35
35
  dependencies:
36
36
  - !ruby/object:Gem::Dependency
@@ -168,6 +168,7 @@ files:
168
168
  - spec/log_helper_spec.rb
169
169
  - spec/logged_change_spec.rb
170
170
  - spec/postgresql_replication_spec.rb
171
+ - spec/postgresql_schema_support_spec.rb
171
172
  - spec/postgresql_support_spec.rb
172
173
  - spec/progress_bar_spec.rb
173
174
  - spec/proxied_table_scan_spec.rb
metadata.gz.sig CHANGED
Binary file