mysql_replication_adapter 0.2.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,8 @@
1
+ == 0.4.0 2007-11-02
2
+ * 1 major enhancement
3
+ * Fixed a bug in the slave selection algorithm that was causing the last item in the list to always be omitted.
4
+ * A bit of internal restructuring to make the code simpler.
5
+
1
6
  == 0.0.1 2007-07-23
2
7
 
3
8
  * 1 major enhancement:
@@ -5,6 +5,7 @@ README.txt
5
5
  Rakefile
6
6
  lib/mysql_replication_adapter.rb
7
7
  lib/mysql_replication_adapter/version.rb
8
+ lib/mysql_replication_adapter/ar_base_ext.rb
8
9
  scripts/txt2html
9
10
  setup.rb
10
11
  test/test_helper.rb
@@ -8,172 +8,22 @@ unless defined?(RAILS_CONNECTION_ADAPTERS) && RAILS_CONNECTION_ADAPTERS.include?
8
8
  RAILS_CONNECTION_ADAPTERS << "mysql_replication"
9
9
  end
10
10
 
11
- require 'active_record/connection_adapters/abstract_adapter'
12
11
  require 'set'
13
-
14
- module MysqlCompat #:nodoc:
15
- # add all_hashes method to standard mysql-c bindings or pure ruby version
16
- def self.define_all_hashes_method!
17
- raise 'Mysql not loaded' unless defined?(::Mysql)
18
-
19
- target = defined?(Mysql::Result) ? Mysql::Result : MysqlRes
20
- return if target.instance_methods.include?('all_hashes')
21
-
22
- # Ruby driver has a version string and returns null values in each_hash
23
- # C driver >= 2.7 returns null values in each_hash
24
- if Mysql.const_defined?(:VERSION) && (Mysql::VERSION.is_a?(String) || Mysql::VERSION >= 20700)
25
- target.class_eval <<-'end_eval'
26
- def all_hashes
27
- rows = []
28
- each_hash { |row| rows << row }
29
- rows
30
- end
31
- end_eval
32
-
33
- # adapters before 2.7 don't have a version constant
34
- # and don't return null values in each_hash
35
- else
36
- target.class_eval <<-'end_eval'
37
- def all_hashes
38
- rows = []
39
- all_fields = fetch_fields.inject({}) { |fields, f| fields[f.name] = nil; fields }
40
- each_hash { |row| rows << all_fields.dup.update(row) }
41
- rows
42
- end
43
- end_eval
44
- end
45
-
46
- unless target.instance_methods.include?('all_hashes')
47
- raise "Failed to defined #{target.name}#all_hashes method. Mysql::VERSION = #{Mysql::VERSION.inspect}"
48
- end
49
- end
50
- end
12
+ require "mysql_replication_adapter/ar_base_ext"
51
13
 
52
14
  module ActiveRecord
53
- class Base
54
- def self.require_mysql
55
- # Include the MySQL driver if one hasn't already been loaded
56
- unless defined? Mysql
57
- begin
58
- require_library_or_gem 'mysql'
59
- rescue LoadError => cannot_require_mysql
60
- # Use the bundled Ruby/MySQL driver if no driver is already in place
61
- begin
62
- require 'active_record/vendor/mysql'
63
- rescue LoadError
64
- raise cannot_require_mysql
65
- end
66
- end
67
- end
68
-
69
- # Define Mysql::Result.all_hashes
70
- MysqlCompat.define_all_hashes_method!
71
- end
72
-
73
- # Establishes a connection to the database that's used by all Active Record objects.
74
- def self.mysql_replication_connection(config) # :nodoc:
75
- config = config.symbolize_keys
76
- host = config[:host]
77
- port = config[:port]
78
- socket = config[:socket]
79
- username = config[:username] ? config[:username].to_s : 'root'
80
- password = config[:password].to_s
81
-
82
- if config.has_key?(:database)
83
- database = config[:database]
84
- else
85
- raise ArgumentError, "No database specified. Missing argument: database."
86
- end
87
-
88
- require_mysql
89
- mysql = Mysql.init
90
- mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslkey]
91
-
92
- ConnectionAdapters::MysqlReplicationAdapter.new(mysql, logger, [host, username, password, database, port, socket], config)
93
- end
94
-
95
- class << self
96
- alias_method :old_find_every, :find_every
97
-
98
- VALID_FIND_OPTIONS << :use_slave
99
-
100
- # Override the standard find to check for the :use_slave option. When specified, the
101
- # resulting query will be sent to a slave machine.
102
- def find_every(options)
103
- result = if options[:use_slave] && connection.is_a?(ConnectionAdapters::MysqlReplicationAdapter)
104
- connection.load_balance_query do
105
- old_find_every(options)
106
- end
107
- else
108
- old_find_every(options)
109
- end
110
- result
111
- end
112
-
113
- alias_method :old_find_by_sql, :find_by_sql
114
- # Override find_by_sql so that you can tell it to selectively use a slave machine
115
- def find_by_sql(sql, use_slave = false)
116
- if use_slave && connection.is_a?(ConnectionAdapters::MysqlReplicationAdapter)
117
- connection.load_balance_query {old_find_by_sql sql}
118
- else
119
- old_find_by_sql sql
120
- end
121
- end
122
-
123
- alias_method :old_calculate, :calculate
124
-
125
- def calculate(operation, column_name, options ={})
126
- use_slave = options.delete(:use_slave)
127
- if use_slave && connection.is_a?(ConnectionAdapters::MysqlReplicationAdapter)
128
- connection.load_balance_query {old_calculate(operation, column_name, options)}
129
- else
130
- old_calculate(operation, column_name, options)
131
- end
132
- end
133
- end
134
-
135
- end
136
-
137
15
  module ConnectionAdapters
138
16
  class CannotWriteToSlave < Exception
139
17
  end
140
18
 
141
- #class AbstractAdapter
19
+ class AbstractAdapter
142
20
  # Adding this method allows non-mysql-replication adapter applications to function without changing
143
21
  # code. Useful in development and test.
144
- #def load_balance_query
145
- # yield
146
- #end
147
- #end
148
-
149
- class MysqlColumn < Column #:nodoc:
150
- #TYPES_ALLOWING_EMPTY_STRING_DEFAULT = Set.new([:binary, :string, :text])
151
-
152
- def initialize(name, default, sql_type = nil, null = true)
153
- @original_default = default
154
- super
155
- @default = nil if missing_default_forged_as_empty_string?
22
+ def load_balance_query
23
+ yield
156
24
  end
157
-
158
- private
159
- def simplified_type(field_type)
160
- return :boolean if MysqlAdapter.emulate_booleans && field_type.downcase.index("tinyint(1)")
161
- return :string if field_type =~ /enum/i
162
- super
163
- end
164
-
165
- # MySQL misreports NOT NULL column default when none is given.
166
- # We can't detect this for columns which may have a legitimate ''
167
- # default (string, text, binary) but we can for others (integer,
168
- # datetime, boolean, and the rest).
169
- #
170
- # Test whether the column has default '', is not null, and is not
171
- # a type allowing default ''.
172
- def missing_default_forged_as_empty_string?
173
- !null && @original_default == '' && !TYPES_ALLOWING_EMPTY_STRING_DEFAULT.include?(type)
174
- end
175
25
  end
176
-
26
+
177
27
  # The MySQL adapter will work with both Ruby/MySQL, which is a Ruby-based MySQL adapter that comes bundled with Active Record, and with
178
28
  # the faster C-based MySQL/Ruby adapter (available both as a gem and from http://www.tmtm.org/en/mysql/ruby/).
179
29
  #
@@ -196,51 +46,27 @@ module ActiveRecord
196
46
  # to your environment.rb file:
197
47
  #
198
48
  # ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false
199
- class MysqlReplicationAdapter < AbstractAdapter
200
- @@emulate_booleans = true
201
- cattr_accessor :emulate_booleans
202
- @@use_master_only = false
203
- cattr_accessor :use_master_only
204
-
205
- LOST_CONNECTION_ERROR_MESSAGES = [
206
- "Server shutdown in progress",
207
- "Broken pipe",
208
- "Lost connection to MySQL server during query",
209
- "MySQL server has gone away"
210
- ]
49
+ class MysqlReplicationAdapter < MysqlAdapter
50
+ # @@emulate_booleans = true
51
+ # cattr_accessor :emulate_booleans
52
+ #
53
+ # LOST_CONNECTION_ERROR_MESSAGES = [
54
+ # "Server shutdown in progress",
55
+ # "Broken pipe",
56
+ # "Lost connection to MySQL server during query",
57
+ # "MySQL server has gone away"
58
+ # ]
211
59
 
212
60
  def initialize(connection, logger, connection_options, config)
213
- super(connection, logger)
214
- @connection_options, @config = connection_options, config
215
-
216
- connect
61
+ @master = @clones = nil
62
+ @retries = config[:retries]
63
+ super(connection, logger, connection_options, config)
217
64
  end
218
65
 
219
66
  def adapter_name #:nodoc:
220
67
  'MySQLReplication'
221
68
  end
222
69
 
223
- def supports_migrations? #:nodoc:
224
- true
225
- end
226
-
227
- def native_database_types #:nodoc:
228
- {
229
- :primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
230
- :string => { :name => "varchar", :limit => 255 },
231
- :text => { :name => "text" },
232
- :integer => { :name => "int", :limit => 11 },
233
- :float => { :name => "float" },
234
- :decimal => { :name => "decimal" },
235
- :datetime => { :name => "datetime" },
236
- :timestamp => { :name => "datetime" },
237
- :time => { :name => "time" },
238
- :date => { :name => "date" },
239
- :binary => { :name => "blob" },
240
- :boolean => { :name => "tinyint", :limit => 1 }
241
- }
242
- end
243
-
244
70
  # the magic load_balance method
245
71
  def load_balance_query
246
72
  old_connection = @connection
@@ -253,9 +79,9 @@ module ActiveRecord
253
79
  # choose a random clone to use for the moment
254
80
  def select_clone
255
81
  # if we happen not to be connected to any clones, just use the master
256
- return @master if @clones.empty?
82
+ return @master if @clones.nil? || @clones.empty?
257
83
  # return a random clone
258
- return @clones[rand(@clones.size - 1)]
84
+ return @clones[rand(@clones.size)]
259
85
  end
260
86
 
261
87
  # This method raises an exception if the current connection is a clone. It is called inside
@@ -264,65 +90,13 @@ module ActiveRecord
264
90
  def ensure_master
265
91
  raise CannotWriteToSlave, "You attempted to perform a write operation inside a slave-balanced read block." unless @connection == @master
266
92
  end
267
-
268
- # QUOTING ==================================================
269
-
270
- def quote(value, column = nil)
271
- if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
272
- s = column.class.string_to_binary(value).unpack("H*")[0]
273
- "x'#{s}'"
274
- elsif value.kind_of?(BigDecimal)
275
- "'#{value.to_s("F")}'"
276
- else
277
- super
278
- end
279
- end
280
-
281
- def quote_column_name(name) #:nodoc:
282
- "`#{name}`"
283
- end
284
-
285
- def quote_string(string) #:nodoc:
286
- @connection.quote(string)
287
- end
288
-
289
- def quoted_true
290
- "1"
291
- end
292
-
293
- def quoted_false
294
- "0"
295
- end
296
-
297
-
298
- # CONNECTION MANAGEMENT ====================================
299
-
300
- def active?
301
- if @connection.respond_to?(:stat)
302
- @connection.stat
303
- else
304
- @connection.query 'select 1'
305
- end
306
-
307
- # mysql-ruby doesn't raise an exception when stat fails.
308
- if @connection.respond_to?(:errno)
309
- @connection.errno.zero?
310
- else
311
- true
312
- end
313
- rescue Mysql::Error
314
- false
315
- end
316
-
317
- def reconnect!
318
- disconnect!
319
- connect
320
- end
321
-
93
+
322
94
  def disconnect!
323
- @connection.close rescue nil
324
- @clones.each do |clone|
325
- clone.close rescue nil
95
+ @master.close rescue nil
96
+ if @clones
97
+ @clones.each do |clone|
98
+ clone.close rescue nil
99
+ end
326
100
  end
327
101
  end
328
102
 
@@ -330,9 +104,23 @@ module ActiveRecord
330
104
  # DATABASE STATEMENTS ======================================
331
105
 
332
106
  def execute(sql, name = nil) #:nodoc:
107
+ retries = 0
333
108
  log(sql, "#{name} against #{@connection.host_info}") do
334
109
  @connection.query(sql)
335
110
  end
111
+ rescue Mysql::Error => ex
112
+ if ex.message =~ /MySQL server has gone away/
113
+ if @retries && retries < @retries
114
+ retries += 1
115
+ disconnect!
116
+ connect
117
+ retry
118
+ else
119
+ raise
120
+ end
121
+ else
122
+ raise
123
+ end
336
124
  rescue ActiveRecord::StatementInvalid => exception
337
125
  if exception.message.split(":").first =~ /Packets out of order/
338
126
  raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings."
@@ -353,179 +141,82 @@ module ActiveRecord
353
141
  @connection.affected_rows
354
142
  end
355
143
 
356
- def begin_db_transaction #:nodoc:
357
- execute "BEGIN"
358
- rescue Exception
359
- # Transactions aren't supported
360
- end
361
-
362
- def commit_db_transaction #:nodoc:
363
- execute "COMMIT"
364
- rescue Exception
365
- # Transactions aren't supported
366
- end
367
-
368
- def rollback_db_transaction #:nodoc:
369
- execute "ROLLBACK"
370
- rescue Exception
371
- # Transactions aren't supported
372
- end
373
-
374
-
375
- def add_limit_offset!(sql, options) #:nodoc:
376
- if limit = options[:limit]
377
- unless offset = options[:offset]
378
- sql << " LIMIT #{limit}"
379
- else
380
- sql << " LIMIT #{offset}, #{limit}"
381
- end
382
- end
144
+ private
145
+ # Create the array of clone Mysql instances. Note that the instances
146
+ # actually don't correspond to the clone specs at this point. We're
147
+ # just getting Mysql object instances we can connect with later.
148
+ def init_clones
149
+ @clones = (@config[:clones] || @config[:slaves]).map{Mysql.init}
383
150
  end
384
-
385
-
386
- # SCHEMA STATEMENTS ========================================
387
-
388
- def structure_dump #:nodoc:
389
- if supports_views?
390
- sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'"
391
- else
392
- sql = "SHOW TABLES"
151
+
152
+ # Given a Mysql object and connection options, call #real_connect
153
+ # on the connection.
154
+ def setup_connection(conn, conn_opts)
155
+ # figure out if we're going to be doing any different
156
+ # encoding. if so, set it.
157
+ encoding = @config[:encoding]
158
+ if encoding
159
+ conn.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil
393
160
  end
394
161
 
395
- select_all(sql).inject("") do |structure, table|
396
- table.delete('Table_type')
397
- structure += select_one("SHOW CREATE TABLE #{table.to_a.first.last}")["Create Table"] + ";\n\n"
398
- end
399
- end
400
-
401
- def recreate_database(name) #:nodoc:
402
- drop_database(name)
403
- create_database(name)
404
- end
405
-
406
- def create_database(name) #:nodoc:
407
- execute "CREATE DATABASE `#{name}`"
408
- end
409
-
410
- def drop_database(name) #:nodoc:
411
- execute "DROP DATABASE IF EXISTS `#{name}`"
412
- end
413
-
414
- def current_database
415
- select_one("SELECT DATABASE() as db")["db"]
416
- end
417
-
418
- def tables(name = nil) #:nodoc:
419
- tables = []
420
- execute("SHOW TABLES", name).each { |field| tables << field[0] }
421
- tables
422
- end
423
-
424
- def indexes(table_name, name = nil)#:nodoc:
425
- indexes = []
426
- current_index = nil
427
- execute("SHOW KEYS FROM #{table_name}", name).each do |row|
428
- if current_index != row[2]
429
- next if row[2] == "PRIMARY" # skip the primary key
430
- current_index = row[2]
431
- indexes << IndexDefinition.new(row[0], row[2], row[1] == "0", [])
432
- end
433
-
434
- indexes.last.columns << row[4]
435
- end
436
- indexes
437
- end
438
-
439
- def columns(table_name, name = nil)#:nodoc:
440
- sql = "SHOW FIELDS FROM #{table_name}"
441
- columns = []
442
- execute(sql, name).each { |field| columns << MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") }
443
- columns
444
- end
445
-
446
- def create_table(name, options = {}) #:nodoc:
447
- super(name, {:options => "ENGINE=InnoDB"}.merge(options))
448
- end
449
-
450
- def rename_table(name, new_name)
451
- execute "RENAME TABLE #{name} TO #{new_name}"
452
- end
453
-
454
- def change_column_default(table_name, column_name, default) #:nodoc:
455
- current_type = select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Type"]
456
-
457
- execute("ALTER TABLE #{table_name} CHANGE #{column_name} #{column_name} #{current_type} DEFAULT #{quote(default)}")
458
- end
459
-
460
- def change_column(table_name, column_name, type, options = {}) #:nodoc:
461
- unless options_include_default?(options)
462
- options[:default] = select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Default"]
463
- end
464
-
465
- change_column_sql = "ALTER TABLE #{table_name} CHANGE #{column_name} #{column_name} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
466
- add_column_options!(change_column_sql, options)
467
- execute(change_column_sql)
468
- end
469
-
470
- def rename_column(table_name, column_name, new_column_name) #:nodoc:
471
- current_type = select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Type"]
472
- execute "ALTER TABLE #{table_name} CHANGE #{column_name} #{new_column_name} #{current_type}"
162
+ # set the ssl options
163
+ conn.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher]) if @config[:sslkey]
164
+
165
+ # do the actual connect
166
+ conn.real_connect(*conn_opts)
167
+
168
+ # swap the current connection for the connection we just set up
169
+ old_conn, @connection = @connection, conn
170
+
171
+ # set these options!
172
+ execute("SET NAMES '#{encoding}'") if encoding
173
+ # By default, MySQL 'where id is null' selects the last inserted id.
174
+ # Turn this off. http://dev.rubyonrails.org/ticket/6778
175
+ execute("SET SQL_AUTO_IS_NULL=0")
176
+
177
+ # swap the old current connection back into place
178
+ @connection = old_conn
473
179
  end
474
-
475
-
476
- private
477
- def connect
478
- encoding = @config[:encoding]
479
- if encoding
480
- @connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil
481
- end
482
- @connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher]) if @config[:sslkey]
483
- @connection.real_connect(*@connection_options)
484
- execute("SET NAMES '#{encoding}'") if encoding
485
-
486
- # By default, MySQL 'where id is null' selects the last inserted id.
487
- # Turn this off. http://dev.rubyonrails.org/ticket/6778
488
- execute("SET SQL_AUTO_IS_NULL=0")
489
-
490
- # save the master in a separate instance variable so we always know what it is
491
- @master = @connection
180
+
181
+ def connect
182
+ # if this is our first time in this method, then master will be
183
+ # nil, and should get set.
184
+ @master = @connection unless @master
185
+
186
+ # set up the master connection
187
+ setup_connection(@master, @connection_options)
188
+
189
+ clone_config = @config[:clones] || @config[:slaves]
190
+
191
+ # if clones are specified, then set up those connections
192
+ if clone_config
193
+ # create the clone connections if they don't already exist
194
+ init_clones unless @clones
492
195
 
493
- # connect to all the clone machines
494
- @clones = []
495
- clone_config = @config[:clones] || @config[:slaves]
196
+ # produce a pairing of connection options with an existing clone
197
+ clones_with_configs = @clones.zip(clone_config)
496
198
 
497
- if clone_config
498
- clone_config.each do |clone|
499
- conn = Mysql.init
500
- if encoding
501
- conn.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil
502
- end
503
- conn.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher]) if @config[:sslkey]
199
+ clones_with_configs.each do |clone_and_config|
200
+ clone, config = clone_and_config
504
201
 
505
- conn.real_connect(clone["host"], clone["username"], clone["password"], clone["database"], clone["port"], clone["socket"])
506
- @clones << conn
507
- end
508
- else
509
- # warning, no slaves specified.
510
- warn "Warning: MysqlReplicationAdapter in use, but no slave database connections specified."
202
+ # Cause the individual clone Mysql instances to (re)connect
203
+ # Note - the instances aren't being replaced. This is critical,
204
+ # as otherwise the current connection could end up pointed at a
205
+ # bad connection object in the case of a failure.
206
+ setup_connection(clone,
207
+ [
208
+ config["host"],
209
+ config["username"], config["password"],
210
+ config["database"], config["port"], config["socket"]
211
+ ]
212
+ )
511
213
  end
214
+ else
215
+ # warning, no slaves specified.
216
+ warn "Warning: MysqlReplicationAdapter in use, but no slave database connections specified."
512
217
  end
513
-
514
- def select(sql, name = nil)
515
- @connection.query_with_result = true
516
- result = execute(sql, name)
517
- rows = result.all_hashes
518
- result.free
519
- rows
520
- end
521
-
522
- def supports_views?
523
- version[0] >= 5
524
- end
525
-
526
- def version
527
- @version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
528
- end
218
+ end
219
+
529
220
  end
530
221
  end
531
222
  end
@@ -0,0 +1,69 @@
1
+ module ActiveRecord
2
+
3
+ class Base
4
+ class << self
5
+
6
+ # Establishes a connection to the database that's used by all Active Record objects.
7
+ def mysql_replication_connection(config) # :nodoc:
8
+ config = config.symbolize_keys
9
+ host = config[:host]
10
+ port = config[:port]
11
+ socket = config[:socket]
12
+ username = config[:username] ? config[:username].to_s : 'root'
13
+ password = config[:password].to_s
14
+
15
+ if config.has_key?(:database)
16
+ database = config[:database]
17
+ else
18
+ raise ArgumentError, "No database specified. Missing argument: database."
19
+ end
20
+
21
+ require_mysql
22
+ mysql = Mysql.init
23
+ mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslkey]
24
+
25
+ ConnectionAdapters::MysqlReplicationAdapter.new(mysql, logger, [host, username, password, database, port, socket], config)
26
+ end
27
+
28
+ alias_method :old_find_every, :find_every
29
+
30
+ VALID_FIND_OPTIONS << :use_slave
31
+
32
+ # Override the standard find to check for the :use_slave option. When specified, the
33
+ # resulting query will be sent to a slave machine.
34
+ def find_every(options)
35
+ result = if options[:use_slave] && connection.is_a?(ConnectionAdapters::MysqlReplicationAdapter)
36
+ connection.load_balance_query do
37
+ old_find_every(options)
38
+ end
39
+ else
40
+ old_find_every(options)
41
+ end
42
+ result
43
+ end
44
+
45
+ alias_method :old_find_by_sql, :find_by_sql
46
+ # Override find_by_sql so that you can tell it to selectively use a slave machine
47
+ def find_by_sql(sql, use_slave = false)
48
+ if use_slave && connection.is_a?(ConnectionAdapters::MysqlReplicationAdapter)
49
+ connection.load_balance_query {old_find_by_sql sql}
50
+ else
51
+ old_find_by_sql sql
52
+ end
53
+ end
54
+
55
+ alias_method :old_calculate, :calculate
56
+
57
+ def calculate(operation, column_name, options ={})
58
+ use_slave = options.delete(:use_slave)
59
+ if use_slave && connection.is_a?(ConnectionAdapters::MysqlReplicationAdapter)
60
+ connection.load_balance_query {old_calculate(operation, column_name, options)}
61
+ else
62
+ old_calculate(operation, column_name, options)
63
+ end
64
+ end
65
+
66
+ end
67
+
68
+ end
69
+ end
@@ -1,7 +1,7 @@
1
1
  module MysqlReplicationAdapter #:nodoc:
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 0
4
- MINOR = 2
4
+ MINOR = 4
5
5
  TINY = 0
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
@@ -1,2 +1,3 @@
1
1
  require 'test/unit'
2
+ require "rubygems"
2
3
  require File.dirname(__FILE__) + '/../lib/mysql_replication_adapter'
@@ -1,11 +1,61 @@
1
1
  require File.dirname(__FILE__) + '/test_helper.rb'
2
2
 
3
+ class Person < ActiveRecord::Base
4
+
5
+ end
6
+
3
7
  class TestMysqlReplicationAdapter < Test::Unit::TestCase
4
8
 
5
9
  def setup
10
+ @adapter = get_adapter
11
+ ActiveRecord::Base.connection = @adapter
12
+
13
+ @adapter.drop_table :people rescue nil
14
+
15
+ @adapter.create_table :people do |t|
16
+ t.column :name, :string
17
+ end
18
+
19
+ end
20
+
21
+ def teardown
22
+ @adapter.drop_table :people
23
+ end
24
+
25
+ def get_adapter
26
+ ActiveRecord::Base.mysql_replication_connection(
27
+ {"host" => "localhost",
28
+ "username" => "root",
29
+ "password" => "",
30
+ "database" => "mysql_repl_test_master",
31
+ "retries" => 2,
32
+ "slaves" => [
33
+ {"host" => "localhost",
34
+ "username" => "root",
35
+ "password" => "",
36
+ "database" => "mysql_repl_test_slave_1"
37
+ },
38
+ {"host" => "localhost",
39
+ "username" => "root",
40
+ "password" => "",
41
+ "database" => "mysql_repl_test_slave_2"
42
+ }
43
+ ]
44
+ }
45
+ )
46
+ end
47
+
48
+ def test_insert
49
+ assert_not_nil Person.create(:name => "blah")
50
+ end
51
+
52
+ def test_lookup
53
+ Person.create(:name => "blah")
54
+ assert_equal Person.find(1).id, 1
6
55
  end
7
56
 
8
- def test_truth
9
- assert true
57
+ def test_slave_lookup
58
+ Person.create(:name => "blah")
59
+ assert_nil Person.find_by_id(1, :use_slave => true)
10
60
  end
11
61
  end
@@ -33,7 +33,7 @@
33
33
  <h1>mysql_replication_adapter</h1>
34
34
  <div id="version" class="clickable" onclick='document.location = "http://rubyforge.org/projects/mysql_replication_adapter"; return false'>
35
35
  <p>Get Version</p>
36
- <a href="http://rubyforge.org/projects/mysql_replication_adapter" class="numbers">0.2.0</a>
36
+ <a href="http://rubyforge.org/projects/mysql_replication_adapter" class="numbers">0.4.0</a>
37
37
  </div>
38
38
  <h1>&#x2192; &#8216;mysql_replication_adapter&#8217;</h1>
39
39
 
metadata CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.9.4
3
3
  specification_version: 1
4
4
  name: mysql_replication_adapter
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.2.0
7
- date: 2007-08-01 00:00:00 -07:00
6
+ version: 0.4.0
7
+ date: 2007-11-02 00:00:00 -07:00
8
8
  summary: An ActiveRecord database adapter that allows you to specify a single write master and multiple read-only slaves.
9
9
  require_paths:
10
10
  - lib
@@ -36,6 +36,7 @@ files:
36
36
  - Rakefile
37
37
  - lib/mysql_replication_adapter.rb
38
38
  - lib/mysql_replication_adapter/version.rb
39
+ - lib/mysql_replication_adapter/ar_base_ext.rb
39
40
  - scripts/txt2html
40
41
  - setup.rb
41
42
  - test/test_helper.rb