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