rubyrep 1.0.4 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/History.txt 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