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 +6 -1
- data/Manifest.txt +1 -0
- data/lib/rubyrep/connection_extenders/jdbc_extender.rb +3 -48
- data/lib/rubyrep/connection_extenders/postgresql_extender.rb +95 -59
- data/lib/rubyrep/replication_extenders/postgresql_replication.rb +4 -4
- data/lib/rubyrep/replicators/two_way_replicator.rb +24 -7
- data/lib/rubyrep/version.rb +1 -1
- data/spec/postgresql_schema_support_spec.rb +179 -0
- data/spec/postgresql_support_spec.rb +1 -1
- data/spec/session_spec.rb +8 -7
- data/spec/table_sorter_spec.rb +7 -8
- data/spec/two_way_replicator_spec.rb +86 -0
- data/tasks/database.rake +40 -0
- data.tar.gz.sig +0 -0
- metadata +3 -2
- metadata.gz.sig +0 -0
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
|
-
|
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
@@ -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
|
-
|
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
|
-
|
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
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
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
|
-
|
111
|
-
|
112
|
-
|
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
|
-
|
115
|
-
|
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
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
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
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
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
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
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
|
-
|
151
|
-
|
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
|
-
|
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
|
-
|
267
|
-
|
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
|
-
|
282
|
-
|
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
|
data/lib/rubyrep/version.rb
CHANGED
@@ -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
|
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 =
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
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
|
-
|
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
|
data/spec/table_sorter_spec.rb
CHANGED
@@ -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
|
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
|
+
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-
|
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
|