rubyrep 1.0.1 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt CHANGED
@@ -1,3 +1,13 @@
1
+ == 1.0.2 2009-06-13
2
+
3
+ * 3 minor enhancements:
4
+ * Simplified low-level functions to read records from the database
5
+ (easier integration of additional databases)
6
+ * Better replication efficiency with large replication backlogs
7
+ * Reduced costs of an empty replication run
8
+ * Ignore tables that only exist in one database
9
+ (tables created for testing / backup don't block scan / sync / replication)
10
+
1
11
  == 1.0.1 2009-03-25
2
12
 
3
13
  * 3 minor enhancements:
data/README.txt CHANGED
@@ -9,7 +9,7 @@ Development of an open-source solution for asynchronous, master-master replicati
9
9
 
10
10
  == MORE INFORMATION:
11
11
 
12
- Refer to the project website at http://rubyrep.github.com
12
+ Refer to the project website at http://www.rubyrep.org
13
13
 
14
14
  == LICENSE:
15
15
 
@@ -71,9 +71,8 @@ module RR
71
71
  else
72
72
  raise "No ConnectionExtender available for :#{config[:adapter]}"
73
73
  end
74
- connection_module = ConnectionExtenders.extenders[extender]
75
- connection.extend connection_module
76
-
74
+ connection.extend ConnectionExtenders.extenders[extender]
75
+
77
76
  # Hack to get Postgres schema support under JRuby to par with the standard
78
77
  # ruby version
79
78
  if RUBY_PLATFORM =~ /java/ and config[:adapter].to_sym == :postgresql
@@ -7,106 +7,6 @@ module RR
7
7
  module JdbcSQLExtender
8
8
  RR::ConnectionExtenders.register :jdbc => self
9
9
 
10
- # A cursor to iterate over the records returned by select_cursor.
11
- # Only one row is kept in memory at a time.
12
- module JdbcResultSet
13
- # Returns true if there are more rows to read.
14
- def next?
15
- if @next_status == nil
16
- @next_status = self.next
17
- end
18
- @next_status
19
- end
20
-
21
- # Returns the row as a column => value hash and moves the cursor to the next row.
22
- def next_row
23
- raise("no more rows available") unless next?
24
- @next_status = nil
25
-
26
- unless @columns
27
- meta_data = self.getMetaData
28
- stores_upper = self.getStatement.getConnection.getMetaData.storesUpperCaseIdentifiers
29
- column_count = meta_data.getColumnCount
30
- @columns = Array.new(column_count)
31
- @columns.each_index do |i|
32
- column_name = meta_data.getColumnName(i+1)
33
- if stores_upper and not column_name =~ /[a-z]/
34
- column_name.downcase!
35
- end
36
- @columns[i] = {
37
- :index => i+1,
38
- :name => column_name,
39
- :type => meta_data.getColumnType(i+1)
40
- #:scale => meta_data.getScale(i+1)
41
- }
42
- end
43
- end
44
-
45
- row = {}
46
- @columns.each_index do |i|
47
- row[@columns[i][:name]] = jdbc_to_ruby(@columns[i])
48
- end
49
-
50
- row
51
- end
52
-
53
- # Releases the databases resources hold by this cursor
54
- def clear
55
- @columns = nil
56
- self.close
57
- end
58
-
59
- Types = java.sql.Types unless const_defined?(:Types)
60
-
61
- # Converts the specified column of the current row to the proper ruby string
62
- # column is a hash with the following elements:
63
- # * :index: field number (starting with 1) of the result set field
64
- # * :type: the java.sql.Type constant specifying the type of the result set field
65
- def jdbc_to_ruby(column)
66
- case column[:type]
67
- when Types::BINARY, Types::BLOB, Types::LONGVARBINARY, Types::VARBINARY
68
- is = self.getBinaryStream(column[:index])
69
- if is == nil or self.wasNull
70
- return nil
71
- end
72
- byte_list = org.jruby.util.ByteList.new(2048)
73
- buffer = Java::byte[2048].new
74
- while (n = is.read(buffer)) != -1
75
- byte_list.append(buffer, 0, n)
76
- end
77
- is.close
78
- return byte_list.toString
79
- when Types::LONGVARCHAR, Types::CLOB
80
- rss = self.getCharacterStream(column[:index])
81
- if rss == nil or self.wasNull
82
- return nil
83
- end
84
- str = java.lang.StringBuffer.new(2048)
85
- cuf = Java::char[2048].new
86
- while (n = rss.read(cuf)) != -1
87
- str.append(cuf, 0, n)
88
- end
89
- rss.close
90
- return str.toString
91
- when Types::TIMESTAMP
92
- time = self.getTimestamp(column[:index]);
93
- if time == nil or self.wasNull
94
- return nil
95
- end
96
- time_string = time.toString()
97
- time_string = time_string.gsub(/ 00:00:00.0$/, '')
98
- return time_string
99
- else
100
- value = self.getString(column[:index])
101
- if value == nil or self.wasNull
102
- return nil
103
- end
104
- return value
105
- end
106
- end
107
- private :jdbc_to_ruby
108
- end
109
-
110
10
  # Monkey patch for activerecord-jdbc-adapter-0.7.2 as it doesn't set the
111
11
  # +@active+ flag to false, thus ActiveRecord#active? incorrectly confirms
112
12
  # the connection to still be active.
@@ -115,20 +15,6 @@ module RR
115
15
  @active = false
116
16
  end
117
17
 
118
- # Executes the given sql query with the otional name written in the
119
- # ActiveRecord log file.
120
- # * +row_buffer_size+: not used.
121
- # Returns the results as a Cursor object supporting
122
- # * next? - returns true if there are more rows to read
123
- # * next_row - returns the row as a column => value hash and moves the cursor to the next row
124
- # * clear - clearing the cursor (making allocated memory available for GC)
125
- def select_cursor(sql, row_buffer_size = 1000)
126
- statement = @connection.connection.createStatement
127
- statement.setFetchSize row_buffer_size
128
- result_set = statement.executeQuery(sql)
129
- result_set.send :extend, JdbcResultSet
130
- end
131
-
132
18
  # Returns an ordered list of primary key column names of the given table
133
19
  def primary_key_names(table)
134
20
  if not tables.include? table
@@ -169,46 +55,12 @@ module RR
169
55
  end
170
56
  end
171
57
 
172
- require 'connection_extenders/postgresql_extender'
173
-
174
- # Adds the correct query executioner functionality to the base class
175
- class JdbcPostgreSQLFetcher < PostgreSQLFetcher
176
- # Executes the given statements and returns the result set.
177
- def execute(sql)
178
- statement = connection.instance_variable_get(:@connection).connection.createStatement
179
- execute_method = sql =~ /close/i ? :execute : :executeQuery
180
- result_set = statement.send(execute_method, sql)
181
- result_set.send :extend, RR::ConnectionExtenders::JdbcSQLExtender::JdbcResultSet
182
- end
183
- end
184
-
185
58
  # PostgreSQL specific functionality not provided by the standard JDBC
186
59
  # connection extender:
187
- # * Integration of memory efficient select_cursor.
188
60
  # * Hack to get schema support for Postgres under JRuby on par with the
189
61
  # standard ruby version.
190
62
  module JdbcPostgreSQLExtender
191
63
 
192
- # Executes the given sql query with the otional name written in the
193
- # ActiveRecord log file.
194
- #
195
- # :+row_buffer_size+ controls how many records are ready into memory at a
196
- # time. Implemented using the PostgeSQL "DECLARE CURSOR" and "FETCH" constructs.
197
- # This is necessary as the postgresql driver always reads the
198
- # complete resultset into memory.
199
- #
200
- # Returns the results as a Cursor object supporting
201
- # * next? - returns true if there are more rows to read
202
- # * next_row - returns the row as a column => value hash and moves the cursor to the next row
203
- # * clear - clearing the cursor (making allocated memory available for GC)
204
- def select_cursor(sql, row_buffer_size = 1000)
205
- cursor_name = "RR_#{Time.now.to_i}#{rand(1_000_000)}"
206
-
207
- statement = @connection.connection.createStatement
208
- statement.execute("DECLARE #{cursor_name} NO SCROLL CURSOR WITH HOLD FOR " + sql)
209
- JdbcPostgreSQLFetcher.new(self, cursor_name, row_buffer_size)
210
- end
211
-
212
64
  # Returns the list of a table's column names, data types, and default values.
213
65
  #
214
66
  # The underlying query is roughly:
@@ -242,11 +94,16 @@ module RR
242
94
  end
243
95
  end
244
96
 
97
+ require 'jdbc_adapter/jdbc_postgre'
98
+ class JdbcPostgreSQLColumn < ActiveRecord::ConnectionAdapters::Column
99
+ include ::JdbcSpec::PostgreSQL::Column
100
+ end
101
+
245
102
  # Returns the list of all column definitions for a table.
246
103
  def columns(table_name, name = nil)
247
104
  # Limit, precision, and scale are all handled by the superclass.
248
105
  column_definitions(table_name).collect do |name, type, default, notnull|
249
- ActiveRecord::ConnectionAdapters::PostgreSQLColumn.new(name, default, type, notnull == 'f')
106
+ JdbcPostgreSQLColumn.new(name, default, type, notnull == 'f')
250
107
  end
251
108
  end
252
109
 
@@ -281,4 +138,5 @@ module RR
281
138
  end
282
139
  end
283
140
  end
284
- end
141
+ end
142
+
@@ -1,120 +1,11 @@
1
- # A cursor to iterate over the records returned by select_cursor.
2
- # Only one row is kept in memory at a time.
3
-
4
- module MysqlResultExtender
5
- # Returns true if there are more rows to read.
6
- def next?
7
- @current_row_num ||= 0
8
- @num_rows ||= self.num_rows()
9
- @current_row_num < @num_rows
10
- end
11
-
12
- # Returns the row as a column => value hash and moves the cursor to the next row.
13
- def next_row
14
- raise("no more rows available") unless next?
15
- row = fetch_hash()
16
- @current_row_num += 1
17
- row
18
- end
19
-
20
- # Releases the database resources hold by this cursor
21
- def clear
22
- free
23
- end
24
- end
25
-
26
1
  module RR
27
2
 
28
- # Overwrites #select_cursor to allow fetching of MySQL results in chunks
29
- class ProxyConnection
30
-
31
- # Allow selecting of MySQL results in chunks.
32
- # For full documentation of method interface refer to ProxyConnection#select_cursor.
33
- def select_cursor_with_mysql_chunks(options)
34
- if config[:adapter] != 'mysql' or !options.include?(:row_buffer_size) or options.include?(:query)
35
- select_cursor_without_mysql_chunks options
36
- else
37
- ConnectionExtenders::MysqlFetcher.new(self, options)
38
- end
39
- end
40
- alias_method_chain :select_cursor, :mysql_chunks unless method_defined?(:select_cursor_without_mysql_chunks)
41
-
42
- end
43
-
44
3
  module ConnectionExtenders
45
4
 
46
- # Fetches MySQL results in chunks
47
- class MysqlFetcher
48
-
49
- # The current database ProxyConnection
50
- attr_accessor :connection
51
-
52
- # hash of select options
53
- attr_accessor :options
54
-
55
- # column_name => value hash of the last returned row
56
- attr_accessor :last_row
57
-
58
- # Creates a new fetcher.
59
- # * +connection+: the current database connection
60
- # * +cursor_name+: name of the cursor from which to fetch
61
- # * +row_buffer_size+: number of records to read at once
62
- def initialize(connection, options)
63
- self.connection = connection
64
- self.options = options.clone
65
- end
66
-
67
- # Returns +true+ if there are more rows to read.
68
- def next?
69
- unless @current_result
70
- if last_row
71
- options.merge! :from => last_row, :exclude_starting_row => true
72
- end
73
- options[:query] =
74
- connection.table_select_query(options[:table], options) +
75
- " limit #{options[:row_buffer_size]}"
76
- @current_result = connection.select_cursor_without_mysql_chunks(options)
77
- end
78
- @current_result.next?
79
- end
80
-
81
- # Returns the row as a column => value hash and moves the cursor to the next row.
82
- def next_row
83
- raise("no more rows available") unless next?
84
- self.last_row = @current_result.next_row
85
- unless @current_result.next?
86
- @current_result.clear
87
- @current_result = nil
88
- end
89
- self.last_row
90
- end
91
-
92
- # Closes the cursor and frees up all ressources
93
- def clear
94
- if @current_result
95
- @current_result.clear
96
- @current_result = nil
97
- end
98
- end
99
- end
100
-
101
5
  # Provides various MySQL specific functionality required by Rubyrep.
102
6
  module MysqlExtender
103
7
  RR::ConnectionExtenders.register :mysql => self
104
8
 
105
- # Executes the given sql query with the optional name written in the
106
- # ActiveRecord log file.
107
- # :+row_buffer_size+ is not currently used.
108
- # Returns the results as a Cursor object supporting
109
- # * next? - returns true if there are more rows to read
110
- # * next_row - returns the row as a column => value hash and moves the cursor to the next row
111
- # * clear - clearing the cursor (making allocated memory available for GC)
112
- def select_cursor(sql, row_buffer_size = 1000)
113
- result = execute sql
114
- result.send :extend, MysqlResultExtender
115
- result
116
- end
117
-
118
9
  # Returns an ordered list of primary key column names of the given table
119
10
  def primary_key_names(table)
120
11
  row = self.select_one(<<-end_sql)
@@ -1,34 +1,5 @@
1
1
  require 'time'
2
2
 
3
- # A cursor to iterate over the records returned by select_cursor.
4
- # Only one row is kept in memory at a time.
5
- class PGresult
6
- # Returns true if there are more rows to read.
7
- def next?
8
- @current_row_num ||= 0
9
- @num_rows ||= self.ntuples()
10
- @current_row_num < @num_rows
11
- end
12
-
13
- # Returns the row as a column => value hash and moves the cursor to the next row.
14
- def next_row
15
- raise("no more rows available") unless next?
16
- row = {}
17
- @fields ||= self.fields
18
- @fields.each_with_index do |field, field_index|
19
- if self.getisnull(@current_row_num, field_index)
20
- value = nil
21
- else
22
- value = self.getvalue @current_row_num, field_index
23
- end
24
-
25
- row[@fields[field_index]] = value
26
- end
27
- @current_row_num += 1
28
- row
29
- end
30
- end
31
-
32
3
  # Hack:
33
4
  # For some reasons these methods were removed in Rails 2.2.2, thus breaking
34
5
  # the binary and multi-lingual data loading.
@@ -110,83 +81,10 @@ end
110
81
  module RR
111
82
  module ConnectionExtenders
112
83
 
113
- # Fetches results from a PostgreSQL cursor object.
114
- class PostgreSQLFetcher
115
-
116
- # The current database connection
117
- attr_accessor :connection
118
-
119
- # Name of the cursor from which to fetch
120
- attr_accessor :cursor_name
121
-
122
- # Number of rows to be read at once
123
- attr_accessor :row_buffer_size
124
-
125
- # Creates a new fetcher.
126
- # * +connection+: the current database connection
127
- # * +cursor_name+: name of the cursor from which to fetch
128
- # * +row_buffer_size+: number of records to read at once
129
- def initialize(connection, cursor_name, row_buffer_size)
130
- self.connection = connection
131
- self.cursor_name = cursor_name
132
- self.row_buffer_size = row_buffer_size
133
- end
134
-
135
- # Executes the specified SQL staements, returning the result
136
- def execute(sql)
137
- connection.execute sql
138
- end
139
-
140
- # Returns true if there are more rows to read.
141
- def next?
142
- @current_result ||= execute("FETCH FORWARD #{row_buffer_size} FROM #{cursor_name}")
143
- @current_result.next?
144
- end
145
-
146
- # Returns the row as a column => value hash and moves the cursor to the next row.
147
- def next_row
148
- raise("no more rows available") unless next?
149
- row = @current_result.next_row
150
- unless @current_result.next?
151
- @current_result.clear
152
- @current_result = nil
153
- end
154
- row
155
- end
156
-
157
- # Closes the cursor and frees up all ressources
158
- def clear
159
- if @current_result
160
- @current_result.clear
161
- @current_result = nil
162
- end
163
- result = execute("CLOSE #{cursor_name}")
164
- result.clear if result
165
- end
166
- end
167
-
168
84
  # Provides various PostgreSQL specific functionality required by Rubyrep.
169
85
  module PostgreSQLExtender
170
86
  RR::ConnectionExtenders.register :postgresql => self
171
87
 
172
- # Executes the given sql query with the otional name written in the
173
- # ActiveRecord log file.
174
- #
175
- # :+row_buffer_size+ controls how many records are ready into memory at a
176
- # time. Implemented using the PostgeSQL "DECLARE CURSOR" and "FETCH" constructs.
177
- # This is necessary as the postgresql driver always reads the
178
- # complete resultset into memory.
179
- #
180
- # Returns the results as a Cursor object supporting
181
- # * next? - returns true if there are more rows to read
182
- # * next_row - returns the row as a column => value hash and moves the cursor to the next row
183
- # * clear - clearing the cursor (making allocated memory available for GC)
184
- def select_cursor(sql, row_buffer_size = 1000)
185
- cursor_name = "RR_#{Time.now.to_i}#{rand(1_000_000)}"
186
- execute("DECLARE #{cursor_name} NO SCROLL CURSOR WITH HOLD FOR " + sql)
187
- PostgreSQLFetcher.new(self, cursor_name, row_buffer_size)
188
- end
189
-
190
88
  # Returns an ordered list of primary key column names of the given table
191
89
  def primary_key_names(table)
192
90
  row = self.select_one(<<-end_sql)
@@ -46,7 +46,7 @@ module RR
46
46
  # 2nd level tree:
47
47
  # * key: the change_key value of the according change log records.
48
48
  # * value:
49
- # The according change log record (column_name => value hash).
49
+ # An array of according change log records (column_name => value hash).
50
50
  # Additional entry of each change log hash:
51
51
  # * key: 'array_index'
52
52
  # * value: index to the change log record in +change_array+
@@ -84,6 +84,19 @@ module RR
84
84
 
85
85
  self.last_updated = Time.now
86
86
 
87
+ # First, let's use a LIMIT clause (via :row_buffer_size option) to verify
88
+ # if there are any pending changes.
89
+ # (If there are many pending changes, this is (at least with PostgreSQL)
90
+ # much faster.)
91
+ cursor = connection.select_cursor(
92
+ :table => change_log_table,
93
+ :from => {'id' => current_id},
94
+ :exclude_starting_row => true,
95
+ :row_buffer_size => 1
96
+ )
97
+ return unless cursor.next?
98
+
99
+ # Something is here. Let's actually load it.
87
100
  cursor = connection.select_cursor(
88
101
  :table => change_log_table,
89
102
  :from => {'id' => current_id},
@@ -9,6 +9,87 @@ require 'active_record/connection_adapters/abstract_adapter'
9
9
 
10
10
  module RR
11
11
 
12
+ # Enables the fetching of (potential large) result sets in chunks.
13
+ class ResultFetcher
14
+
15
+ # The current database ProxyConnection
16
+ attr_accessor :connection
17
+
18
+ # hash of select options as described under ProxyConnection#select_cursor
19
+ attr_accessor :options
20
+
21
+ # column_name => value hash of the last returned row
22
+ attr_accessor :last_row
23
+
24
+ # The current row set: an array of column_name => value hashes
25
+ attr_accessor :rows
26
+
27
+ # Index to the current row in rows
28
+ attr_accessor :current_row_index
29
+
30
+ # Creates a new fetcher.
31
+ # * +connection+: the current ProxyConnection
32
+ # * +options+: hash of select options as described under ProxyConnection#select_cursor
33
+ def initialize(connection, options)
34
+ self.connection = connection
35
+ self.options = options.clone
36
+ end
37
+
38
+ # Returns +true+ if there are more rows to read.
39
+ def next?
40
+ unless self.rows
41
+ # Try to load some records
42
+
43
+ if options[:query] and last_row != nil
44
+ # A query was directly specified and all it's rows were returned
45
+ # ==> Finished.
46
+ return false
47
+ end
48
+
49
+ if options[:query]
50
+ # If a query has been directly specified, just directly execute it
51
+ query = options[:query]
52
+ else
53
+ # Otherwise build the query
54
+ if last_row
55
+ # There was a previous batch.
56
+ # Next batch will start after the last returned row
57
+ options.merge! :from => last_row, :exclude_starting_row => true
58
+ end
59
+
60
+ query = connection.table_select_query(options[:table], options)
61
+
62
+ if options[:row_buffer_size]
63
+ # Set the batch size
64
+ query += " limit #{options[:row_buffer_size]}"
65
+ end
66
+ end
67
+
68
+ self.rows = connection.select_all query
69
+ self.current_row_index = 0
70
+ end
71
+ self.current_row_index < self.rows.size
72
+ end
73
+
74
+ # Returns the row as a column => value hash and moves the cursor to the next row.
75
+ def next_row
76
+ raise("no more rows available") unless next?
77
+ self.last_row = self.rows[self.current_row_index]
78
+ self.current_row_index += 1
79
+
80
+ if self.current_row_index == self.rows.size
81
+ self.rows = nil
82
+ end
83
+
84
+ self.last_row
85
+ end
86
+
87
+ # Frees up all ressources
88
+ def clear
89
+ self.rows = nil
90
+ end
91
+ end
92
+
12
93
  # This class represents a remote activerecord database connection.
13
94
  # Normally created by DatabaseProxy
14
95
  class ProxyConnection
@@ -102,9 +183,7 @@ module RR
102
183
  # * :+row_buffer_size+:
103
184
  # Integer controlling how many rows a read into memory at one time.
104
185
  def select_cursor(options)
105
- row_buffer_size = options[:row_buffer_size] || DEFAULT_ROW_BUFFER_SIZE
106
- query = options[:query] || table_select_query(options[:table], options)
107
- cursor = connection.select_cursor query, row_buffer_size
186
+ cursor = ResultFetcher.new(self, options)
108
187
  if options[:type_cast]
109
188
  cursor = TypeCastingCursor.new(self, options[:table], cursor)
110
189
  end
@@ -19,24 +19,31 @@ module RR
19
19
 
20
20
  # Executes the replication run.
21
21
  def run
22
- success = false
23
- replicator # ensure that replicator is created and has chance to validate settings
22
+ return unless [:left, :right].any? do |database|
23
+ session.send(database).select_one(
24
+ "select id from #{session.configuration.options[:rep_prefix]}_pending_changes"
25
+ ) != nil
26
+ end
27
+ begin
28
+ success = false
29
+ replicator # ensure that replicator is created and has chance to validate settings
24
30
 
25
- loop do
26
- begin
27
- session.reload_changes # ensure the cache of change log records is up-to-date
28
- diff = ReplicationDifference.new session
29
- diff.load
30
- break unless diff.loaded?
31
- replicator.replicate_difference diff if diff.type != :no_diff
32
- rescue Exception => e
33
- helper.log_replication_outcome diff, e.message,
34
- e.class.to_s + "\n" + e.backtrace.join("\n")
31
+ loop do
32
+ begin
33
+ session.reload_changes # ensure the cache of change log records is up-to-date
34
+ diff = ReplicationDifference.new session
35
+ diff.load
36
+ break unless diff.loaded?
37
+ replicator.replicate_difference diff if diff.type != :no_diff
38
+ rescue Exception => e
39
+ helper.log_replication_outcome diff, e.message,
40
+ e.class.to_s + "\n" + e.backtrace.join("\n")
41
+ end
35
42
  end
43
+ success = true # considered to be successful if we get till here
44
+ ensure
45
+ helper.finalize success
36
46
  end
37
- success = true # considered to be successful if we get till here
38
- ensure
39
- helper.finalize success
40
47
  end
41
48
 
42
49
  # Creates a new ReplicationRun instance.
@@ -55,7 +55,7 @@ module RR
55
55
  #
56
56
  # Takes care that a table is only returned once.
57
57
  def resolve(included_table_specs, excluded_table_specs = [], verify = true)
58
- table_pairs = expand_table_specs(included_table_specs)
58
+ table_pairs = expand_table_specs(included_table_specs, verify)
59
59
  table_pairs = table_pairs_without_duplicates(table_pairs)
60
60
  table_pairs = table_pairs_without_excluded(table_pairs, excluded_table_specs)
61
61
 
@@ -70,11 +70,15 @@ module RR
70
70
  end
71
71
 
72
72
  # Helper for #resolve
73
- # Takes the specified table_specifications and expands it into an array of
74
- # according table pairs.
75
- # Returns the result
76
- # Refer to #resolve for a full description of parameters and result.
77
- def expand_table_specs(table_specs)
73
+ # Expands table specifications into table pairs.
74
+ # Parameters:
75
+ # * +table_specs+:
76
+ # An array of table specifications as described under #resolve.
77
+ # * +verify+:
78
+ # If +true+, table specs in regexp format only resolve if the table exists
79
+ # in left and right database.
80
+ # Return value: refer to #resolve for a detailed description
81
+ def expand_table_specs(table_specs, verify)
78
82
  table_pairs = []
79
83
  table_specs.each do |table_spec|
80
84
 
@@ -86,7 +90,9 @@ module RR
86
90
  table_spec = table_spec.sub(/^\/(.*)\/$/,'\1') # remove leading and trailing slash
87
91
  matching_tables = tables(:left).grep(Regexp.new(table_spec, Regexp::IGNORECASE, 'U'))
88
92
  matching_tables.each do |table|
89
- table_pairs << {:left => table, :right => table}
93
+ if !verify or tables(:right).include? table
94
+ table_pairs << {:left => table, :right => table}
95
+ end
90
96
  end
91
97
  when /.+,.+/ # matches e. g. 'users,users_backup'
92
98
  pair = table_spec.match(/(.*),(.*)/)[1..2].map { |str| str.strip }
@@ -107,7 +113,7 @@ module RR
107
113
  # * :+right+: name of the corresponding right table
108
114
  # +excluded_table_specs+ is the array of table specifications to be excluded.
109
115
  def table_pairs_without_excluded(table_pairs, excluded_table_specs)
110
- excluded_tables = expand_table_specs(excluded_table_specs).map do |table_pair|
116
+ excluded_tables = expand_table_specs(excluded_table_specs, false).map do |table_pair|
111
117
  table_pair[:left]
112
118
  end
113
119
  table_pairs.select {|table_pair| not excluded_tables.include? table_pair[:left]}
@@ -2,7 +2,7 @@ module RR #:nodoc:
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 1
4
4
  MINOR = 0
5
- TINY = 1
5
+ TINY = 2
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end
@@ -1,5 +1,6 @@
1
1
  require File.dirname(__FILE__) + '/spec_helper.rb'
2
2
  require 'yaml'
3
+ require 'digest/md5'
3
4
 
4
5
  include RR
5
6
 
@@ -89,20 +90,27 @@ describe "ConnectionExtender", :shared => true do
89
90
  )
90
91
  result.next_row.should == {'first_id' => '3', 'second_id' => '1', 'name' => nil}
91
92
  end
92
-
93
+
93
94
  it "should read and write binary data correctly" do
94
95
  session = Session.new
95
96
 
96
- org_data = Marshal.dump(['bla',:dummy,1,2,3])
97
+ org_data = File.new(File.dirname(__FILE__) + '/dolphins.jpg').read
97
98
  result_data = nil
98
99
  begin
99
100
  session.left.begin_db_transaction
100
- sql = "insert into extender_type_check(id, binary_test) values(2, '#{org_data}')"
101
- session.left.execute sql
101
+ session.left.insert_record('extender_type_check', {'id' => 6, 'binary_test' => org_data})
102
+
103
+ row = session.left.select_one(
104
+ 'select md5(binary_test) as md5 from extender_type_check where id = 6'
105
+ )
106
+ row['md5'].should == Digest::MD5.hexdigest(org_data)
102
107
 
103
- org_cursor = session.left.select_cursor(:query => "select id, binary_test from extender_type_check where id = 2")
104
- cursor = TypeCastingCursor.new session.left, 'extender_type_check', org_cursor
108
+ cursor = session.left.select_cursor(
109
+ :query => "select id, binary_test from extender_type_check where id = 6"
110
+ )
111
+ cursor = TypeCastingCursor.new session.left, 'extender_type_check', cursor
105
112
  result_data = cursor.next_row['binary_test']
113
+ Digest::MD5.hexdigest(result_data).should == Digest::MD5.hexdigest(org_data)
106
114
  ensure
107
115
  session.left.rollback_db_transaction
108
116
  end
@@ -60,8 +60,8 @@ describe ConnectionExtenders, "Registration" do
60
60
 
61
61
  it "db_connect should include the connection extender into connection" do
62
62
  connection = ConnectionExtenders.db_connect Initializer.configuration.left
63
-
64
- connection.should respond_to(:select_cursor)
63
+
64
+ connection.should respond_to(:primary_key_names)
65
65
  end
66
66
 
67
67
  it "db_connect should raise an Exception if no fitting connection extender is available" do
@@ -120,28 +120,20 @@ describe ProxyConnection do
120
120
  @connection.primary_key_names('dummy_table').should == ['dummy_key']
121
121
  end
122
122
 
123
- it "select_cursor should forwarded the handed over query" do
124
- @connection.connection.should_receive(:select_cursor).with('bla', 123)
125
- @connection.select_cursor(:query => 'bla', :row_buffer_size => 123)
126
- end
127
-
128
- it "select_cursor should use the default row buffer size if no explicit value is specified" do
129
- @connection.connection.should_receive(:select_cursor) \
130
- .with('bla', ProxyConnection::DEFAULT_ROW_BUFFER_SIZE)
131
- @connection.select_cursor(:query => 'bla')
132
- end
123
+ # Note:
124
+ # Additional select_cursor tests are executed via
125
+ # 'db_specific_connection_extenders_spec.rb'
126
+ # (To verify the behaviour for all supported databases)
133
127
 
134
- it "select_cursor should construct the query and execute it" do
135
- @connection.connection.should_receive(:select_cursor) \
136
- .with(sql_to_regexp('select "id", "name" from "scanner_records" order by "id"'), ProxyConnection::DEFAULT_ROW_BUFFER_SIZE)
137
- @connection.select_cursor(:table => 'scanner_records')
128
+ it "select_cursor should return the result fetcher" do
129
+ fetcher = @connection.select_cursor(:table => 'scanner_records')
130
+ fetcher.connection.should == @connection
131
+ fetcher.options.should == {:table => 'scanner_records'}
138
132
  end
139
133
 
140
134
  it "select_cursor should return a type casting cursor if :type_cast option is specified" do
141
- @connection.select_cursor(:table => 'scanner_records').
142
- should_not be_an_instance_of(TypeCastingCursor)
143
- @connection.select_cursor(:table => 'scanner_records', :type_cast => true).
144
- should be_an_instance_of(TypeCastingCursor)
135
+ fetcher = @connection.select_cursor(:table => 'scanner_records', :type_cast => true)
136
+ fetcher.should be_an_instance_of(TypeCastingCursor)
145
137
  end
146
138
 
147
139
  it "table_select_query should handle queries without any conditions" do
@@ -64,6 +64,13 @@ describe ReplicationRun do
64
64
  end
65
65
  end
66
66
 
67
+ it "run should not create the replicator if there are no pending changes" do
68
+ session = Session.new
69
+ run = ReplicationRun.new session
70
+ run.should_not_receive(:replicator)
71
+ run.run
72
+ end
73
+
67
74
  it "run should only replicate real differences" do
68
75
  session = Session.new
69
76
  session.left.begin_db_transaction
@@ -121,8 +128,22 @@ describe ReplicationRun do
121
128
  it "run should not catch exceptions raised during replicator initialization" do
122
129
  config = deep_copy(standard_config)
123
130
  config.options[:logged_replication_events] = [:invalid_option]
124
- run = ReplicationRun.new Session.new(config)
125
- lambda {run.run}.should raise_error(ArgumentError)
131
+ session = Session.new config
132
+ session.left.begin_db_transaction
133
+ begin
134
+
135
+ session.left.insert_record 'rr_pending_changes', {
136
+ 'change_table' => 'extender_no_record',
137
+ 'change_key' => 'id|1',
138
+ 'change_type' => 'D',
139
+ 'change_time' => Time.now
140
+ }
141
+
142
+ run = ReplicationRun.new session
143
+ lambda {run.run}.should raise_error(ArgumentError)
144
+ ensure
145
+ session.left.rollback_db_transaction
146
+ end
126
147
  end
127
148
 
128
149
  it "run should process trigger created change log records" do
@@ -34,6 +34,15 @@ describe TableSpecResolver do
34
34
  it "resolve should complain about non-existing tables" do
35
35
  lambda {@resolver.resolve(['dummy, scanner_records'])}.
36
36
  should raise_error(/non-existing.*dummy/)
37
+ lambda {@resolver.resolve(['left_table, left_table'])}.
38
+ should raise_error(/non-existing.*left_table/)
39
+ lambda {@resolver.resolve(['left_table'])}.
40
+ should raise_error(/non-existing.*left_table/)
41
+ end
42
+
43
+ it "resolve should not complain about regexp specified tables not existing in right database" do
44
+ @resolver.resolve([/^scanner_records$/, /left_table/]).
45
+ should == [{:left => 'scanner_records', :right => 'scanner_records'}]
37
46
  end
38
47
 
39
48
  it "resolve should not check for non-existing tables if that is disabled" do
@@ -85,7 +94,7 @@ describe TableSpecResolver do
85
94
  @resolver.non_existing_tables(table_pairs).should == {}
86
95
  end
87
96
 
88
- it "non_existing_tables should return an empty hash if all tables exist" do
97
+ it "non_existing_tables should return a hash of non-existing tables" do
89
98
  table_pairs = [{:left => 'scanner_records', :right => 'bla'}]
90
99
  @resolver.non_existing_tables(table_pairs).should == {:right => ['bla']}
91
100
 
data/tasks/database.rake CHANGED
@@ -102,11 +102,13 @@ def drop_postgres_schema(config)
102
102
  end
103
103
 
104
104
  # Creates the sample schema in the database specified by the given
105
- # configuration hash.
106
- def create_sample_schema(config)
107
- create_postgres_schema config
105
+ # configuration.
106
+ # * :+database+: either :+left+ or +:right+
107
+ # * :+config+: the Configuration object
108
+ def create_sample_schema(database, config)
109
+ create_postgres_schema config.send(database)
108
110
 
109
- ActiveRecord::Base.establish_connection config
111
+ ActiveRecord::Base.establish_connection config.send(database)
110
112
 
111
113
  ActiveRecord::Schema.define do
112
114
  create_table :scanner_text_key, :id => false do |t|
@@ -252,11 +254,11 @@ def create_sample_schema(config)
252
254
 
253
255
  create_table :left_table do |t|
254
256
  t.column :name, :string
255
- end
257
+ end if database == :left
256
258
 
257
259
  create_table :right_table do |t|
258
260
  t.column :name, :string
259
- end
261
+ end if database == :right
260
262
  end
261
263
  end
262
264
 
@@ -410,8 +412,8 @@ namespace :db do
410
412
 
411
413
  desc "Create the sample schemas"
412
414
  task :create_schema do
413
- create_sample_schema RR::Initializer.configuration.left rescue nil
414
- create_sample_schema RR::Initializer.configuration.right rescue nil
415
+ create_sample_schema :left, RR::Initializer.configuration rescue nil
416
+ create_sample_schema :right, RR::Initializer.configuration rescue nil
415
417
  end
416
418
 
417
419
  desc "Writes the sample data"
data/tasks/java.rake CHANGED
@@ -23,7 +23,8 @@ EOS
23
23
  pkg_name = "rubyrep-#{RR::VERSION::STRING}"
24
24
 
25
25
  system "rm -rf /tmp/#{pkg_name}"
26
- system "hg archive /tmp/#{pkg_name}"
26
+ system "mkdir /tmp/#{pkg_name}"
27
+ system "git archive master |tar -x -C /tmp/#{pkg_name}"
27
28
  system "mkdir -p /tmp/#{pkg_name}/jruby"
28
29
  system "cp -r #{JRUBY_HOME}/* /tmp/#{pkg_name}/jruby/"
29
30
  system "cd /tmp/#{pkg_name}/jruby; rm -rf samples share/ri lib/ruby/gems/1.8/doc"
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.1
4
+ version: 1.0.2
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-03-25 00:00:00 +09:00
33
+ date: 2009-06-13 00:00:00 +09:00
34
34
  default_executable:
35
35
  dependencies:
36
36
  - !ruby/object:Gem::Dependency
metadata.gz.sig CHANGED
Binary file