active_record_shards 2.0.0.beta1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +33 -0
- data/lib/active_record_shards.rb +13 -0
- data/lib/active_record_shards/association_collection_connection_selection.rb +25 -0
- data/lib/active_record_shards/configuration_parser.rb +46 -0
- data/lib/active_record_shards/connection_pool.rb +29 -0
- data/lib/active_record_shards/connection_switcher.rb +104 -0
- data/lib/active_record_shards/model.rb +20 -0
- data/lib/active_record_shards/shard_selection.rb +41 -0
- data/test/configuration_parser_test.rb +88 -0
- data/test/connection_switching_test.rb +167 -0
- data/test/database.yml +54 -0
- data/test/database_parse_test.yml +21 -0
- data/test/helper.rb +57 -0
- data/test/models.rb +11 -0
- data/test/schema.rb +16 -0
- data/test/test.log +64898 -0
- metadata +181 -0
data/README.rdoc
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
= active_record_shards
|
2
|
+
|
3
|
+
== Example
|
4
|
+
|
5
|
+
Ticket.on_slave do
|
6
|
+
Ticket.first(50)
|
7
|
+
end
|
8
|
+
|
9
|
+
== Install
|
10
|
+
|
11
|
+
gem install active_record_shards
|
12
|
+
|
13
|
+
Add the slave database to config/database.yml:
|
14
|
+
development_slave:
|
15
|
+
adapter: mysql
|
16
|
+
...
|
17
|
+
|
18
|
+
== Note on Patches/Pull Requests
|
19
|
+
|
20
|
+
* Fork the project.
|
21
|
+
* Make your feature addition or bug fix.
|
22
|
+
* Add tests for it. This is important so I don't break it in a
|
23
|
+
future version unintentionally.
|
24
|
+
* Commit, do not mess with rakefile, version, or history.
|
25
|
+
(if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull)
|
26
|
+
* Send me a pull request. Bonus points for topic branches.
|
27
|
+
|
28
|
+
== Copyright
|
29
|
+
|
30
|
+
Copyright (c) 2009 Zendesk. See LICENSE for details.
|
31
|
+
|
32
|
+
== Authors
|
33
|
+
Mick Staugaard
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'active_record/base'
|
3
|
+
require 'active_record_shards/configuration_parser'
|
4
|
+
require 'active_record_shards/model'
|
5
|
+
require 'active_record_shards/shard_selection'
|
6
|
+
require 'active_record_shards/connection_switcher'
|
7
|
+
require 'active_record_shards/association_collection_connection_selection'
|
8
|
+
require 'active_record_shards/connection_pool'
|
9
|
+
|
10
|
+
ActiveRecord::Base.extend(ActiveRecordShards::ConfigurationParser)
|
11
|
+
ActiveRecord::Base.extend(ActiveRecordShards::Model)
|
12
|
+
ActiveRecord::Base.extend(ActiveRecordShards::ConnectionSwitcher)
|
13
|
+
ActiveRecord::Associations::AssociationCollection.send(:include, ActiveRecordShards::AssociationCollectionConnectionSelection)
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module ActiveRecordShards
|
2
|
+
module AssociationCollectionConnectionSelection
|
3
|
+
def on_slave_if(condition)
|
4
|
+
condition ? on_slave : self
|
5
|
+
end
|
6
|
+
|
7
|
+
def on_slave_unless(condition)
|
8
|
+
on_slave_if(!condition)
|
9
|
+
end
|
10
|
+
|
11
|
+
def on_slave
|
12
|
+
SlaveProxy.new(self)
|
13
|
+
end
|
14
|
+
|
15
|
+
class SlaveProxy
|
16
|
+
def initialize(association_collection)
|
17
|
+
@association_collection = association_collection
|
18
|
+
end
|
19
|
+
|
20
|
+
def method_missing(method, *args, &block)
|
21
|
+
@association_collection.proxy_reflection.klass.on_slave_block { @association_collection.send(method, *args, &block) }
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module ActiveRecordShards
|
2
|
+
module ConfigurationParser
|
3
|
+
module_function
|
4
|
+
|
5
|
+
def explode(conf)
|
6
|
+
conf.keys.each do |env_name|
|
7
|
+
env_config = conf[env_name]
|
8
|
+
if shards = env_config.delete('shards')
|
9
|
+
shards.each do |shard_name, shard_conf|
|
10
|
+
expand_child!(env_config, shard_conf)
|
11
|
+
conf["#{env_name}_shard_#{shard_name}"] = shard_conf
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
conf.keys.each do |env_name|
|
17
|
+
env_config = conf[env_name]
|
18
|
+
if slave_conf = env_config.delete('slave')
|
19
|
+
expand_child!(env_config, slave_conf)
|
20
|
+
conf["#{env_name}_slave"] = slave_conf
|
21
|
+
end
|
22
|
+
end
|
23
|
+
conf
|
24
|
+
end
|
25
|
+
|
26
|
+
def expand_child!(parent, child)
|
27
|
+
parent.each do |key, value|
|
28
|
+
unless ['slave', 'shards'].include?(key) || value.is_a?(Hash)
|
29
|
+
child[key] ||= value
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def configurations_with_shard_explosion=(conf)
|
35
|
+
self.configurations_without_shard_explosion = explode(conf)
|
36
|
+
end
|
37
|
+
|
38
|
+
def ConfigurationParser.extended(klass)
|
39
|
+
klass.singleton_class.alias_method_chain :configurations=, :shard_explosion
|
40
|
+
|
41
|
+
if !klass.configurations.nil? && !klass.configurations.empty?
|
42
|
+
klass.configurations = klass.configurations
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
ActiveRecord::ConnectionAdapters::ConnectionHandler.class_eval do
|
2
|
+
# The only difference here is that we use klass.connection_pool_name
|
3
|
+
# instead of klass.name as the pool key
|
4
|
+
def retrieve_connection_pool(klass)
|
5
|
+
pool = @connection_pools[klass.connection_pool_name]
|
6
|
+
return pool if pool
|
7
|
+
return nil if ActiveRecord::Base == klass
|
8
|
+
retrieve_connection_pool klass.superclass
|
9
|
+
end
|
10
|
+
|
11
|
+
def remove_connection(klass)
|
12
|
+
pool = @connection_pools[klass.connection_pool_name]
|
13
|
+
@connection_pools.delete_if { |key, value| value == pool }
|
14
|
+
pool.disconnect! if pool
|
15
|
+
pool.spec.config if pool
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
ActiveRecord::Base.singleton_class.class_eval do
|
20
|
+
def establish_connection_with_connection_pool_name(spec = nil)
|
21
|
+
case spec
|
22
|
+
when ActiveRecord::Base::ConnectionSpecification
|
23
|
+
connection_handler.establish_connection(connection_pool_name, spec)
|
24
|
+
else
|
25
|
+
establish_connection_without_connection_pool_name(spec)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
alias_method_chain :establish_connection, :connection_pool_name
|
29
|
+
end
|
@@ -0,0 +1,104 @@
|
|
1
|
+
module ActiveRecordShards
|
2
|
+
module ConnectionSwitcher
|
3
|
+
|
4
|
+
def on_shard(shard, &block)
|
5
|
+
old_shard = current_shard_selection.shard
|
6
|
+
switch_connection(:shard => shard)
|
7
|
+
yield
|
8
|
+
ensure
|
9
|
+
switch_connection(:shard => old_shard)
|
10
|
+
end
|
11
|
+
|
12
|
+
def on_slave_if(condition, &block)
|
13
|
+
condition ? on_slave(&block) : yield
|
14
|
+
end
|
15
|
+
|
16
|
+
def on_slave_unless(condition, &block)
|
17
|
+
on_slave_if(!condition, &block)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Executes queries using the slave database. Fails over to master if no slave is found.
|
21
|
+
# if you want to execute a block of code on the slave you can go:
|
22
|
+
# Account.on_slave do
|
23
|
+
# Account.first
|
24
|
+
# end
|
25
|
+
# the first account will be found on the slave DB
|
26
|
+
#
|
27
|
+
# For one-liners you can simply do
|
28
|
+
# Account.on_slave.first
|
29
|
+
def on_slave(&block)
|
30
|
+
if block_given?
|
31
|
+
on_slave_block(&block)
|
32
|
+
else
|
33
|
+
SlaveProxy.new(self)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def on_slave_block(&block)
|
38
|
+
old_slave = current_shard_selection.on_slave?
|
39
|
+
begin
|
40
|
+
switch_connection(:slave => true)
|
41
|
+
rescue ActiveRecord::AdapterNotSpecified => e
|
42
|
+
switch_connection(:slave => old_slave)
|
43
|
+
logger.warn("Failed to establish shard connection: #{e.message} - defaulting to master")
|
44
|
+
end
|
45
|
+
with_scope({:find => {:readonly => current_shard_selection.on_slave?}}, :merge, &block)
|
46
|
+
ensure
|
47
|
+
switch_connection(:slave => old_slave)
|
48
|
+
end
|
49
|
+
|
50
|
+
# Name of the connection pool. Used by ConnectionHandler to retrieve the current connection pool.
|
51
|
+
def connection_pool_name # :nodoc:
|
52
|
+
current_shard_selection.shard_name(self)
|
53
|
+
# if current_shard_selection.any?
|
54
|
+
# current_shard_selection.shard_name(name, self)
|
55
|
+
# elsif self == ActiveRecord::Base
|
56
|
+
# name
|
57
|
+
# else
|
58
|
+
# superclass.connection_pool_name
|
59
|
+
# end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def current_shard_selection
|
65
|
+
Thread.current[:shard_selection] ||= ShardSelection.new
|
66
|
+
end
|
67
|
+
|
68
|
+
def switch_connection(options)
|
69
|
+
if options.any?
|
70
|
+
if options.has_key?(:slave)
|
71
|
+
current_shard_selection.on_slave = options[:slave]
|
72
|
+
end
|
73
|
+
|
74
|
+
if options.has_key?(:shard)
|
75
|
+
current_shard_selection.shard = options[:shard]
|
76
|
+
end
|
77
|
+
|
78
|
+
establish_shard_connection unless connected_to_shard?
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def establish_shard_connection
|
83
|
+
name = current_shard_selection.shard_name(self)
|
84
|
+
spec = configurations[name]
|
85
|
+
raise ActiveRecord::AdapterNotSpecified.new("No database defined by #{name} in database.yml") if spec.nil?
|
86
|
+
|
87
|
+
connection_handler.establish_connection(connection_pool_name, ActiveRecord::Base::ConnectionSpecification.new(spec, "#{spec['adapter']}_connection"))
|
88
|
+
end
|
89
|
+
|
90
|
+
def connected_to_shard?
|
91
|
+
connection_handler.connection_pools.has_key?(connection_pool_name)
|
92
|
+
end
|
93
|
+
|
94
|
+
class SlaveProxy
|
95
|
+
def initialize(target)
|
96
|
+
@target = target
|
97
|
+
end
|
98
|
+
|
99
|
+
def method_missing(method, *args, &block)
|
100
|
+
@target.on_slave_block { @target.send(method, *args, &block) }
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module ActiveRecordShards
|
2
|
+
module Model
|
3
|
+
def not_sharded
|
4
|
+
if self == ActiveRecord::Base || self != base_class
|
5
|
+
raise "You should only call not_sharded on direct descendants of ActiveRecord::Base"
|
6
|
+
end
|
7
|
+
@sharded = false
|
8
|
+
end
|
9
|
+
|
10
|
+
def is_sharded?
|
11
|
+
if self == ActiveRecord::Base
|
12
|
+
true
|
13
|
+
elsif self == base_class
|
14
|
+
@sharded != false
|
15
|
+
else
|
16
|
+
base_class.is_sharded?
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module ActiveRecordShards
|
2
|
+
class ShardSelection
|
3
|
+
def initialize
|
4
|
+
@on_slave = false
|
5
|
+
end
|
6
|
+
|
7
|
+
def shard
|
8
|
+
@shard
|
9
|
+
end
|
10
|
+
|
11
|
+
def shard=(new_shard)
|
12
|
+
@shard = new_shard
|
13
|
+
end
|
14
|
+
|
15
|
+
def on_slave?
|
16
|
+
@on_slave
|
17
|
+
end
|
18
|
+
|
19
|
+
def on_slave=(new_slave)
|
20
|
+
@on_slave = (new_slave == true)
|
21
|
+
end
|
22
|
+
|
23
|
+
def connection_configuration_name
|
24
|
+
shard_name#(RAILS_ENV)
|
25
|
+
end
|
26
|
+
|
27
|
+
def shard_name(klass = nil)
|
28
|
+
s = "#{RAILS_ENV}"
|
29
|
+
if @shard && (klass.nil? || klass.is_sharded?)
|
30
|
+
s << '_shard_'
|
31
|
+
s << @shard.to_s
|
32
|
+
end
|
33
|
+
s << "_slave" if @on_slave
|
34
|
+
s
|
35
|
+
end
|
36
|
+
|
37
|
+
def any?
|
38
|
+
@on_slave || @shard.present?
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
require File.expand_path('helper', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
class ConfigurationParserTest < ActiveSupport::TestCase
|
4
|
+
context "exploding the database.yml" do
|
5
|
+
setup do
|
6
|
+
@exploded_conf = ActiveRecordShards::ConfigurationParser.explode(YAML::load(IO.read(File.dirname(__FILE__) + '/database_parse_test.yml')))
|
7
|
+
end
|
8
|
+
|
9
|
+
context "main slave" do
|
10
|
+
setup { @conf = @exploded_conf['test_slave'] }
|
11
|
+
should "be exploded" do
|
12
|
+
assert_equal({
|
13
|
+
"adapter" => "mysql",
|
14
|
+
"encoding" => "utf8",
|
15
|
+
"database" => "ars_test",
|
16
|
+
"port" => 123,
|
17
|
+
"username" => "root",
|
18
|
+
"password" => nil,
|
19
|
+
"host" => "main_slave_host"
|
20
|
+
}, @conf)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
context "shard a" do
|
25
|
+
context "master" do
|
26
|
+
setup { @conf = @exploded_conf['test_shard_a'] }
|
27
|
+
should "be exploded" do
|
28
|
+
assert_equal({
|
29
|
+
"adapter" => "mysql",
|
30
|
+
"encoding" => "utf8",
|
31
|
+
"database" => "ars_test_shard_a",
|
32
|
+
"port" => 123,
|
33
|
+
"username" => "root",
|
34
|
+
"password" => nil,
|
35
|
+
"host" => "shard_a_host"
|
36
|
+
}, @conf)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
context "slave" do
|
41
|
+
setup { @conf = @exploded_conf['test_shard_a_slave'] }
|
42
|
+
should "be exploded" do
|
43
|
+
assert_equal({
|
44
|
+
"adapter" => "mysql",
|
45
|
+
"encoding" => "utf8",
|
46
|
+
"database" => "ars_test_shard_a",
|
47
|
+
"port" => 123,
|
48
|
+
"username" => "root",
|
49
|
+
"password" => nil,
|
50
|
+
"host" => "shard_a_slave_host"
|
51
|
+
}, @conf)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
context "shard b" do
|
57
|
+
context "master" do
|
58
|
+
setup { @conf = @exploded_conf['test_shard_b'] }
|
59
|
+
should "be exploded" do
|
60
|
+
assert_equal({
|
61
|
+
"adapter" => "mysql",
|
62
|
+
"encoding" => "utf8",
|
63
|
+
"database" => "ars_test_shard_b",
|
64
|
+
"port" => 123,
|
65
|
+
"username" => "root",
|
66
|
+
"password" => nil,
|
67
|
+
"host" => "shard_b_host"
|
68
|
+
}, @conf)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
context "slave" do
|
73
|
+
setup { @conf = @exploded_conf['test_shard_b_slave'] }
|
74
|
+
should "be exploded" do
|
75
|
+
assert_equal({
|
76
|
+
"adapter" => "mysql",
|
77
|
+
"encoding" => "utf8",
|
78
|
+
"database" => "ars_test_shard_b_slave",
|
79
|
+
"port" => 123,
|
80
|
+
"username" => "root",
|
81
|
+
"password" => nil,
|
82
|
+
"host" => "shard_b_host"
|
83
|
+
}, @conf)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
require File.expand_path('helper', File.dirname(__FILE__))
|
2
|
+
|
3
|
+
class ConnectionSwitchenTest < ActiveSupport::TestCase
|
4
|
+
context "shard switching" do
|
5
|
+
should "only switch connection on sharded models" do
|
6
|
+
assert_using_database('ars_test', Ticket)
|
7
|
+
assert_using_database('ars_test', Account)
|
8
|
+
|
9
|
+
ActiveRecord::Base.on_shard(0) do
|
10
|
+
assert_using_database('ars_test_shard0', Ticket)
|
11
|
+
assert_using_database('ars_test', Account)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
should "switch to shard and back" do
|
16
|
+
assert_using_database('ars_test')
|
17
|
+
ActiveRecord::Base.on_slave { assert_using_database('ars_test_slave') }
|
18
|
+
|
19
|
+
ActiveRecord::Base.on_shard(0) do
|
20
|
+
assert_using_database('ars_test_shard0')
|
21
|
+
ActiveRecord::Base.on_slave { assert_using_database('ars_test_shard0_slave') }
|
22
|
+
|
23
|
+
ActiveRecord::Base.on_shard(nil) do
|
24
|
+
assert_using_database('ars_test')
|
25
|
+
ActiveRecord::Base.on_slave { assert_using_database('ars_test_slave') }
|
26
|
+
end
|
27
|
+
|
28
|
+
assert_using_database('ars_test_shard0')
|
29
|
+
ActiveRecord::Base.on_slave { assert_using_database('ars_test_shard0_slave') }
|
30
|
+
end
|
31
|
+
|
32
|
+
assert_using_database('ars_test')
|
33
|
+
ActiveRecord::Base.on_slave { assert_using_database('ars_test_slave') }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context "slave driving" do
|
38
|
+
context "without slave configuration" do
|
39
|
+
|
40
|
+
setup do
|
41
|
+
ActiveRecord::Base.configurations.delete('test_slave')
|
42
|
+
ActiveRecord::Base.connection_handler.connection_pools.clear
|
43
|
+
ActiveRecord::Base.establish_connection('test')
|
44
|
+
end
|
45
|
+
|
46
|
+
should "default to the master database" do
|
47
|
+
Account.create!
|
48
|
+
|
49
|
+
ActiveRecord::Base.on_slave { assert_using_master_db }
|
50
|
+
Account.on_slave { assert_using_master_db }
|
51
|
+
Ticket.on_slave { assert_using_master_db }
|
52
|
+
end
|
53
|
+
|
54
|
+
should "successfully execute queries" do
|
55
|
+
Account.create!
|
56
|
+
assert_using_master_db
|
57
|
+
|
58
|
+
assert_equal Account.count, ActiveRecord::Base.on_slave { Account.count }
|
59
|
+
assert_equal Account.count, Account.on_slave { Account.count }
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
context "with slave configuration" do
|
65
|
+
|
66
|
+
should "successfully execute queries" do
|
67
|
+
assert_using_master_db
|
68
|
+
Account.create!
|
69
|
+
|
70
|
+
assert_equal(1, Account.count)
|
71
|
+
assert_equal(0, ActiveRecord::Base.on_slave { Account.count })
|
72
|
+
end
|
73
|
+
|
74
|
+
should "support global on_slave blocks" do
|
75
|
+
assert_using_master_db
|
76
|
+
assert_using_master_db
|
77
|
+
|
78
|
+
ActiveRecord::Base.on_slave do
|
79
|
+
assert_using_slave_db
|
80
|
+
assert_using_slave_db
|
81
|
+
end
|
82
|
+
|
83
|
+
assert_using_master_db
|
84
|
+
assert_using_master_db
|
85
|
+
end
|
86
|
+
|
87
|
+
should "support conditional methods" do
|
88
|
+
assert_using_master_db
|
89
|
+
|
90
|
+
Account.on_slave_if(true) do
|
91
|
+
assert_using_slave_db
|
92
|
+
end
|
93
|
+
|
94
|
+
assert_using_master_db
|
95
|
+
|
96
|
+
Account.on_slave_if(false) do
|
97
|
+
assert_using_master_db
|
98
|
+
end
|
99
|
+
|
100
|
+
Account.on_slave_unless(true) do
|
101
|
+
assert_using_master_db
|
102
|
+
end
|
103
|
+
|
104
|
+
Account.on_slave_unless(false) do
|
105
|
+
assert_using_slave_db
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
109
|
+
|
110
|
+
context "a model loaded with the slave" do
|
111
|
+
setup do
|
112
|
+
Account.connection.execute("INSERT INTO accounts (id, name, created_at, updated_at) VALUES(1000, 'master_name', '2009-12-04 20:18:48', '2009-12-04 20:18:48')")
|
113
|
+
assert(Account.find(1000))
|
114
|
+
assert_equal('master_name', Account.find(1000).name)
|
115
|
+
|
116
|
+
Account.on_slave.connection.execute("INSERT INTO accounts (id, name, created_at, updated_at) VALUES(1000, 'slave_name', '2009-12-04 20:18:48', '2009-12-04 20:18:48')")
|
117
|
+
|
118
|
+
@model = Account.on_slave.find(1000)
|
119
|
+
assert(@model)
|
120
|
+
assert_equal('slave_name', @model.name)
|
121
|
+
end
|
122
|
+
|
123
|
+
should "read from master on reload" do
|
124
|
+
@model.reload
|
125
|
+
assert_equal('master_name', @model.name)
|
126
|
+
end
|
127
|
+
|
128
|
+
should "be marked as read only" do
|
129
|
+
assert(@model.readonly?)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
context "a model loaded with the master" do
|
134
|
+
setup do
|
135
|
+
Account.connection.execute("INSERT INTO accounts (id, name, created_at, updated_at) VALUES(1000, 'master_name', '2009-12-04 20:18:48', '2009-12-04 20:18:48')")
|
136
|
+
@model = Account.first
|
137
|
+
assert(@model)
|
138
|
+
assert_equal('master_name', @model.name)
|
139
|
+
end
|
140
|
+
|
141
|
+
should "not be marked as read only" do
|
142
|
+
assert(!@model.readonly?)
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
context "slave proxy" do
|
148
|
+
should "successfully execute queries" do
|
149
|
+
assert_using_master_db
|
150
|
+
Account.create!
|
151
|
+
|
152
|
+
assert_not_equal Account.count, Account.on_slave.count
|
153
|
+
end
|
154
|
+
|
155
|
+
should "work on association collections" do
|
156
|
+
assert_using_master_db
|
157
|
+
account = Account.create!
|
158
|
+
|
159
|
+
Ticket.connection.expects(:select_all).with("SELECT * FROM `tickets` WHERE (`tickets`.account_id = #{account.id}) LIMIT 1", anything).returns([])
|
160
|
+
Ticket.on_slave.connection.expects(:select_all).with("SELECT * FROM `tickets` WHERE (`tickets`.account_id = #{account.id}) LIMIT 1", anything).returns([])
|
161
|
+
|
162
|
+
account.tickets.first
|
163
|
+
account.tickets.on_slave.first
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|