dynashard 0.3.1 → 0.4.0

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/README.md CHANGED
@@ -39,39 +39,34 @@ models may shard using different contexts.
39
39
  end
40
40
  end
41
41
 
42
- Associated models may be configured to use different shards determined by the
43
- association's owner.
42
+ Sharded models are returned as objects of a shard-specific subclass.
44
43
 
45
- class Company < ActiveRecord::Base
46
- shard :associated, :using => :shard
47
-
48
- has_many :customers
49
-
50
- def shard
51
- # logic to find the company's shard
52
- end
53
- end
54
-
55
- class Customer < ActiveRecord::Base
56
- belongs_to :company
57
- shard :by => :company
58
- end
59
-
60
- > c = Company.find(:first)
61
- => #<Company id:1>
44
+ > new_widget = Dynashard.with_context(:user => 'shard1') {Widget.new(:name => 'New widget')}
45
+ => <#Dynashard::Shard0::Widget id: nil, name: "New widget">
62
46
 
63
- Company is loaded using the default ActiveRecord connection.
47
+ > created_widget = Dynashard.with_context(:user => 'shard2') {Widget.create(:name => 'Created widget')}
48
+ => <#Dynashard::Shard1::Widget id: 1, name: "Created widget">
64
49
 
65
- > c.customers
66
- => [#<Dynashard::Shard0::Customer id: 1>, #<Dynashard::Shard0::Customer id: 2>]
50
+ > found_widget = Dynashard.with_context(:user => 'shard3') {Widget.find(:first)}
51
+ => <#Dynashard::Shard2::Widget id: 4, name: "Found widget">
67
52
 
68
- Customers are loaded using the connection for the Company's shard. Associated models
69
- are returned as shard-specific subclasses of the association class.
53
+ > found_widgets = Dynashard.with_context(:user => 'shard3') {Widget.find(:all)}
54
+ => [<#Dynashard::Shard2::Widget id: 4, name: "Found widget">, <#Dynashard::Shard2::Widget id: 5, name: "Other found widget">]
70
55
 
71
- > c.customers.create(:name => 'Always right')
72
- => #<Dynashard::Shard0::Customer id: 3>
56
+ New objects are saved on the shard with the context that was active
57
+ when the object was initialized.
58
+
59
+ > new_widget.save
60
+ => <#Dynashard::Shard0::Widget id: 1, name: "New widget"> # saved on 'shard1'
61
+
62
+ Created and found objects are updated on the shard with the context
63
+ that was active when they were created or found.
73
64
 
74
- New associations are saved on the Company's shard.
65
+ > created_widget.update_attribute(:name, 'New name')
66
+ => true # updated on 'shard2'
67
+
68
+ > found_widget.update_attributes(:name => 'Updated name')
69
+ => true # updated on 'shard3'
75
70
 
76
71
  Shard context values may be a valid argument to establish_connection()
77
72
  such as a string reference to a configuration from config/database.yml
@@ -96,13 +91,13 @@ establish_connection().
96
91
  <<: *defaults
97
92
 
98
93
  > @widgets = Dynashard.with_context(:user => 'shard1') { Widget.find(:all) }
99
- => [#<Widget id:1>, #<Widget id:2>]
94
+ => [#<Dynashard::Shard0::Widget id:1>, #<Dynashard::Shard0::Widget id:2>]
100
95
 
101
96
  Load widgets from a shard using a hash of connection params
102
97
 
103
98
  > conn = {:adapter => 'sqlite3', :database => 'db/shard3.sqlite3'}
104
99
  > @widgets = Dynashard.with_context(:user => conn) { Widget.find(:all) }
105
- => [#<Widget id:1>, #<Widget id:2>]
100
+ => [#<Dynashard::Shard2::Widget id:1>, #<Dynashard::Shard2::Widget id:2>]
106
101
 
107
102
  Create a widget using a method to determine the shard
108
103
 
@@ -120,7 +115,7 @@ establish_connection().
120
115
  > new_widget = Dynashard.with_context(:user => widget_shard) do
121
116
  Widget.create(:name => 'The newest of the widgets')
122
117
  end
123
- => <#Widget id:3>
118
+ => <#Dynashard::Shard4::Widget id:3>
124
119
 
125
120
  Use a Rails initializer for one-time configuration of shard context
126
121
 
@@ -132,20 +127,59 @@ establish_connection().
132
127
  end
133
128
 
134
129
  > new_widget = Widget.create(:name => 'Put this on the smallest shard')
135
- => <#Widget id:4>
130
+ => <#Dynashard::Shard5::Widget id:4>
136
131
 
137
132
  Use with_context to override an earlier context setting
138
133
 
139
134
  > Dynashard.shard_context[:user] = 'shard1'
140
135
  > new_widget = Widget.create(:name => 'Put this on shard1')
141
- => <#Widget id:5>
136
+ => <#Dynashard::Shard0::Widget id:5>
142
137
  > new_widget = Dynashard.with_context(:user => 'shard2') do
143
138
  Widget.create(:name => 'Put this on shard2')
144
139
  do
145
- > <#Widget id:6>
140
+ > <#Dynashard::Shard1::Widget id:6>
141
+
142
+ Associated models may be configured to use different shards determined by the
143
+ association's owner.
144
+
145
+ class Company < ActiveRecord::Base
146
+ shard :associated, :using => :shard
147
+
148
+ has_many :customers
149
+
150
+ def shard
151
+ # logic to find the company's shard
152
+ end
153
+ end
154
+
155
+ class Customer < ActiveRecord::Base
156
+ belongs_to :company
157
+ shard :by => :company
158
+ end
159
+
160
+ Load a Company using the default ActiveRecord connection.
161
+
162
+ > c = Company.find(:first)
163
+ => #<Company id:1>
164
+
165
+ Load Customers using the connection for the Company's shard.
166
+ Associated models are returns as shard-specific subclasses of the
167
+ association class.
168
+
169
+ > c.customers
170
+ => [#<Dynashard::Shard0::Customer id: 1>, #<Dynashard::Shard0::Customer id: 2>]
171
+
172
+ Save new associations on the Company's shard.
173
+
174
+ > c.customers.create(:name => 'Always right')
175
+ => #<Dynashard::Shard0::Customer id: 3>
146
176
 
147
177
  ## TODO: add gotcha section, eg:
148
178
 
179
+ - many-to-many associations can only be used across shards in one
180
+ direction, where the association target and the join table exist
181
+ on the same database connection (else joins don't work.)
149
182
  - uniqueness validations should be scoped by whatever is sharding
150
183
  - ways to shoot yourself in the foot with non-sharding association
151
184
  owners of sharded models
185
+ - investigate proxy extend for association proxy
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.1
1
+ 0.4.0
data/dynashard.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{dynashard}
8
- s.version = "0.3.1"
8
+ s.version = "0.4.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Nick Hengeveld"]
12
- s.date = %q{2011-02-01}
12
+ s.date = %q{2011-02-14}
13
13
  s.description = %q{Dynashard allows you to shard your ActiveRecord models. Models can be configured to shard based on context that can be defined dynamically.}
14
14
  s.email = %q{nickh@verticalresponse.com}
15
15
  s.extra_rdoc_files = [
@@ -4,15 +4,30 @@ module Dynashard
4
4
  base.alias_method_chain :initialize, :dynashard
5
5
  end
6
6
 
7
- # Initialize an association proxy. If the proxy owner class is configured to
8
- # shard its associations and the reflection klass is sharded, use a custom
9
- # reflection with a sharded class.
7
+ # Initialize an association proxy. If the proxy target needs to be sharded,
8
+ # swap in a reflection with a sharded "klass" to ensure that the shard
9
+ # connection is always used to manage records in the target model.
10
10
  def initialize_with_dynashard(owner, reflection)
11
- if owner.class.shards_associated? && reflection.klass.sharding_enabled?
11
+ if needs_sharded_reflection?(owner, reflection)
12
12
  reflection = Dynashard.reflection_for(owner, reflection)
13
+ if reflection.through_reflection != false && reflection.through_reflection.klass.sharding_enabled?
14
+ reflection.instance_variable_set('@through_reflection', Dynashard.reflection_for(owner, reflection.through_reflection))
15
+ end
13
16
  end
14
17
  initialize_without_dynashard(owner, reflection)
15
18
  end
19
+
20
+ private
21
+
22
+ # The reflection needs to use a sharded model class in these situations:
23
+ # - the proxy owner shards associations, and the proxy target has sharding enabled
24
+ # - the proxy owner is a dynashard-generated model class,
25
+ # the reflection class is configured to shard,
26
+ # the proxy owner superclass and reflection klass use the same shard context
27
+ def needs_sharded_reflection?(owner, reflection)
28
+ (owner.class.shards_associated? && reflection.klass.sharding_enabled?) ||
29
+ (owner.class.dynashard_model? && reflection.klass.sharding_enabled? && owner.class.superclass.dynashard_context == reflection.klass.dynashard_context)
30
+ end
16
31
  end
17
32
  end
18
33
 
@@ -31,7 +31,7 @@ module Dynashard
31
31
  retrieve_connection_pool_without_dynashard(klass.dynashard_klass)
32
32
  elsif klass.sharding_enabled?
33
33
  spec = Dynashard.shard_context[klass.dynashard_context]
34
- raise "Missing #{klass.dynashard_context} shard context" if spec.nil?
34
+ raise "Missing #{klass.dynashard_context} shard context for #{klass.name}" if spec.nil?
35
35
  spec = spec.call if spec.respond_to?(:call)
36
36
  shard_klass = Dynashard.class_for(spec)
37
37
  retrieve_connection_pool_without_dynashard(shard_klass)
@@ -3,18 +3,11 @@ module Dynashard
3
3
  def self.extended(base)
4
4
  base.extend(ClassMethods)
5
5
 
6
- # Change ActiveRecord::Base.arel_engine to create an engine for sharded models
7
- # rather than using the ActiveRecord::Base class.
8
- base.module_eval do
9
- def self.arel_engine
10
- if sharding_enabled?
11
- Arel::Sql::Engine.new(self)
12
- elsif self == ActiveRecord::Base
13
- Arel::Table.engine
14
- else
15
- connection_handler.connection_pools[name] ? Arel::Sql::Engine.new(self) : superclass.arel_engine
16
- end
17
- end
6
+ class << base
7
+ alias_method_chain :new, :dynashard
8
+ alias_method_chain :instantiate, :dynashard
9
+ alias_method_chain :create, :dynashard
10
+ alias_method_chain :arel_engine, :dynashard
18
11
  end
19
12
  end
20
13
 
@@ -85,6 +78,52 @@ module Dynashard
85
78
  def dynashard_association_using
86
79
  @dynashard_association_using
87
80
  end
81
+
82
+ # For sharded models, return a Arel::Sql::Engine for the shard class rather
83
+ # than ActiveRecord::Base.
84
+ def arel_engine_with_dynashard
85
+ if sharding_enabled?
86
+ Arel::Sql::Engine.new(self)
87
+ else
88
+ arel_engine_without_dynashard
89
+ end
90
+ end
91
+
92
+ # For sharded models, return new model objects with the sharded subclass
93
+ #
94
+ # > Dynashard.with_context(:owner => 'shard1'){ShardedModel.new(attrs)}
95
+ # => <#Dynashard::Shard0::ShardedModel id:nil>
96
+ def new_with_dynashard(*args)
97
+ if sharding_enabled?
98
+ dynashard_sharded_subclass.send(:new_without_dynashard, *args)
99
+ else
100
+ new_without_dynashard(*args)
101
+ end
102
+ end
103
+
104
+ # For sharded models, return instantiated model objects with the sharded subclass
105
+ #
106
+ # > Dynashard.with_context(:owner => 'shard1'){ShardedModel.find(:first)}
107
+ # => <#Dynashard::Shard0::ShardedModel id:1>
108
+ def instantiate_with_dynashard(record)
109
+ if sharding_enabled?
110
+ dynashard_sharded_subclass.send(:instantiate_without_dynashard, record)
111
+ else
112
+ instantiate_without_dynashard(record)
113
+ end
114
+ end
115
+
116
+ # For sharded models, return created model objects with the sharded subclass
117
+ #
118
+ # > Dynashard.with_context(:owner => 'shard1'){ShardedModel.create(attrs)}
119
+ # => <#Dynashard::Shard0::ShardedModel id:2>
120
+ def create_with_dynashard(attributes = nil, &block)
121
+ if sharding_enabled?
122
+ dynashard_sharded_subclass.send(:create_without_dynashard, attributes, &block)
123
+ else
124
+ create_without_dynashard(attributes, &block)
125
+ end
126
+ end
88
127
  end
89
128
  end
90
129
  end
data/lib/dynashard.rb CHANGED
@@ -112,7 +112,11 @@ module Dynashard
112
112
  # Return a reflection with a sharded class
113
113
  def self.reflection_for(owner, reflection)
114
114
  reflection_copy = reflection.dup
115
- shard_klass = Dynashard.class_for(owner.send(owner.class.dynashard_association_using))
115
+ shard_klass = if owner.class.respond_to?(:dynashard_klass)
116
+ owner.class.dynashard_klass
117
+ else
118
+ Dynashard.class_for(owner.send(owner.class.dynashard_association_using))
119
+ end
116
120
  klass = sharded_model_class(shard_klass, reflection.klass)
117
121
  reflection_copy.instance_variable_set('@klass', klass)
118
122
  reflection_copy.instance_variable_set('@class_name', klass.name)
@@ -139,6 +143,10 @@ module Dynashard
139
143
  def self.connection
140
144
  dynashard_klass.connection
141
145
  end
146
+
147
+ def self.dynashard_context
148
+ superclass.dynashard_context
149
+ end
142
150
  end
143
151
  EOE
144
152
  klass = class_name.constantize
@@ -1,5 +1,15 @@
1
1
  require 'spec_helper'
2
2
 
3
+ class SqlCounter
4
+ def initialize(connection, counter_sql)
5
+ @connection, @counter_sql = connection, counter_sql
6
+ end
7
+
8
+ def count
9
+ @connection.execute(@counter_sql).first[0]
10
+ end
11
+ end
12
+
3
13
  describe 'Dynashard::ProxyExtensions' do
4
14
  before(:each) do
5
15
  Dynashard.enable
@@ -9,6 +19,7 @@ describe 'Dynashard::ProxyExtensions' do
9
19
  before(:each) do
10
20
  @owner = Factory(:sharding_owner)
11
21
  @shard = @owner.shard_dsn
22
+ @shard_klass = Dynashard.class_for(@shard)
12
23
  end
13
24
 
14
25
  context 'and a sharding proxy target' do
@@ -102,42 +113,115 @@ describe 'Dynashard::ProxyExtensions' do
102
113
 
103
114
  context 'using a :has_many_through reflection' do
104
115
  before(:each) do
105
- # Ensure that all sharded model connections happen on the owner shard
106
- Dynashard.shard_context[:owner] = @shard
116
+ @sharded_has_many_through_klass = Dynashard.sharded_model_class(@shard_klass, ShardedHasManyThrough)
117
+ @sharded_join_klass = Dynashard.sharded_model_class(@shard_klass, ShardedJoin)
118
+ @shard_klass.connection.execute("INSERT INTO sharded_has_many_throughs (name) VALUES ('#{Factory.next :name}')")
119
+ one_of_many_id = @shard_klass.connection.execute("SELECT last_insert_rowid()").first[0]
120
+ @shard_klass.connection.execute("INSERT INTO sharded_joins (sharding_owner_id, sharded_has_many_through_id) VALUES (#{@owner.id}, #{one_of_many_id})")
121
+ join_id = @shard_klass.connection.execute("SELECT last_insert_rowid()").first[0]
122
+ @one_of_many = @sharded_has_many_through_klass.find(one_of_many_id)
123
+ end
107
124
 
108
- @one_of_many = Factory(:sharded_has_many_through, :name => "Owned by #{@owner.name}")
109
- join = Factory(:sharded_join, :sharding_owner => @owner, :sharded_has_many_through => @one_of_many)
125
+ it 'reads from the shard' do
126
+ @owner.sharded_has_many_throughs.should include(@one_of_many)
110
127
  end
111
128
 
112
- after(:each) do
113
- Dynashard.shard_context.clear
129
+ it 'destroys the model from the shard' do
130
+ lambda {@one_of_many.destroy}.should change(@sharded_has_many_through_klass, :count).by(-1)
114
131
  end
115
132
 
116
- # TODO: figure out what to do about the classes being different
117
- it 'reads from the shard' do
118
- @owner.sharded_has_many_throughs.detect{|m| m.attributes == @one_of_many.attributes}.should_not be_nil
133
+ # This class does not have :dependent => :destroy on the join
134
+ it 'leaves the join on the shard' do
135
+ lambda {@one_of_many.destroy}.should_not change(@sharded_join_klass, :count)
119
136
  end
120
137
 
121
- it 'destroys from the shard' do
138
+ it 'creates_other on the shard' do
122
139
  lambda do
123
- one_of_many = @owner.sharded_has_many_throughs.detect{|m| m.attributes == @one_of_many.attributes}
124
- one_of_many.destroy
125
- end.should change(ShardedHasManyThrough, :count).by(-1)
140
+ @owner.sharded_has_many_throughs.create(:name => Factory.next(:name))
141
+ end.should change(@sharded_has_many_through_klass, :count).by(1)
126
142
  end
127
143
 
128
- it 'creates_other on the shard' do
129
- new_owner = Factory(:sharding_owner, :shard => @owner.shard)
144
+ it 'creates joins on the shard' do
130
145
  lambda do
131
- new_owner.sharded_has_many_throughs.create(:name => "Owned by #{new_owner.name}")
132
- end.should change(ShardedHasManyThrough, :count).by(1)
146
+ @owner.sharded_has_many_throughs.create(:name => Factory.next(:name))
147
+ end.should change(@sharded_join_klass, :count).by(1)
133
148
  end
134
149
 
135
150
  it 'saves built associations on the shard' do
136
- new_owner = Factory(:sharding_owner, :shard => @owner.shard)
137
- new_one_of_many = new_owner.sharded_has_many_throughs.build(:name => "Owned by #{new_owner.name}")
151
+ new_one_of_many = @owner.sharded_has_many_throughs.build(:name => Factory.next(:name))
152
+ lambda {@owner.save}.should change(@sharded_has_many_through_klass, :count).by(1)
153
+ end
154
+
155
+ it 'saves built association joins on the shard' do
156
+ new_one_of_many = @owner.sharded_has_many_throughs.build(:name => Factory.next(:name))
157
+ lambda {@owner.save}.should change(@sharded_join_klass, :count).by(1)
158
+ end
159
+
160
+ context 'with dependent => destroy' do
161
+ before(:each) do
162
+ @sharded_dependent_has_many_through_klass = Dynashard.sharded_model_class(@shard_klass, ShardedDependentHasManyThrough)
163
+ @sharded_dependent_join_klass = Dynashard.sharded_model_class(@shard_klass, ShardedDependentJoin)
164
+ @shard_klass.connection.execute("INSERT INTO sharded_dependent_has_many_throughs (name) VALUES ('#{Factory.next :name}')")
165
+ one_of_many_id = @shard_klass.connection.execute("SELECT last_insert_rowid()").first[0]
166
+ @shard_klass.connection.execute("INSERT INTO sharded_dependent_joins (sharding_owner_id, sharded_dependent_has_many_through_id) VALUES (#{@owner.id}, #{one_of_many_id})")
167
+ join_id = @shard_klass.connection.execute("SELECT last_insert_rowid()").first[0]
168
+ @dependent_one_of_many = @sharded_dependent_has_many_through_klass.find(one_of_many_id)
169
+ end
170
+
171
+ it 'destroys the join on the shard' do
172
+ lambda {@dependent_one_of_many.destroy}.should change(@sharded_dependent_join_klass, :count).by(-1)
173
+ end
174
+ end
175
+ end
176
+
177
+ context 'using a :has_and_belongs_to_many reflection' do
178
+ before(:each) do
179
+ @sharded_habtm_klass = Dynashard.sharded_model_class(@shard_klass, ShardedHabtm)
180
+ @shard_klass.connection.execute("INSERT INTO sharded_habtms (name) VALUES ('#{Factory.next :name}')")
181
+ habtm_id = @shard_klass.connection.execute("SELECT last_insert_rowid()").first[0]
182
+ @shard_klass.connection.execute("INSERT INTO sharded_habtms_sharding_owners (sharding_owner_id, sharded_habtm_id) VALUES (#{@owner.id}, #{habtm_id})")
183
+ @habtm = @sharded_habtm_klass.find(habtm_id)
184
+ @join_counter = SqlCounter.new(@shard_klass.connection, 'SELECT COUNT(*) FROM sharded_habtms_sharding_owners')
185
+ end
186
+
187
+ it 'reads from the shard' do
188
+ @owner.sharded_habtms.should include(@habtm)
189
+ end
190
+
191
+ it 'destroys the model from the shard' do
192
+ lambda {@habtm.destroy}.should change(@sharded_habtm_klass, :count).by(-1)
193
+ end
194
+
195
+ # Rails seems to always use the ActiveRecord::Base connection
196
+ # for habtm join tables
197
+ xit 'destroys the join on the shard' do
198
+ lambda {@habtm.destroy}.should change(@join_counter, :count).by(-1)
199
+ end
200
+
201
+ it 'creates_other on the shard' do
138
202
  lambda do
139
- new_one_of_many.save
140
- end.should change(ShardedHasManyThrough, :count).by(1)
203
+ @owner.sharded_habtms.create(:name => Factory.next(:name))
204
+ end.should change(@sharded_habtm_klass, :count).by(1)
205
+ end
206
+
207
+ # Rails seems to always use the ActiveRecord::Base connection
208
+ # for habtm join tables
209
+ xit 'creates joins on the shard' do
210
+ lambda do
211
+ @owner.sharded_habtms.create(:name => Factory.next(:name))
212
+ end.should change(@join_counter, :count).by(1)
213
+ end
214
+
215
+ it 'saves built associations on the shard' do
216
+ new_habtm = @owner.sharded_habtms.build(:name => Factory.next(:name))
217
+ lambda {@owner.save}.should change(@sharded_habtm_klass, :count).by(1)
218
+ end
219
+
220
+ # Rails seems to always use the ActiveRecord::Base connection
221
+ # for habtm join tables
222
+ xit 'saves built association joins on the shard' do
223
+ new_habtm = @owner.sharded_habtms.build(:name => Factory.next(:name))
224
+ lambda {@owner.save}.should change(@join_counter, :count).by(1)
141
225
  end
142
226
  end
143
227
  end
data/spec/db/schema.rb CHANGED
@@ -15,23 +15,27 @@ ActiveRecord::Schema.define(:version => 1) do
15
15
  create_table "non_sharding_owners", :force => true do |t|
16
16
  t.string "name"
17
17
  end
18
+ add_index :non_sharding_owners, :name, :unique => true
18
19
 
19
20
  create_table "sharding_owners", :force => true do |t|
20
21
  t.string "name"
21
22
  t.integer "shard_id"
22
23
  end
24
+ add_index :sharding_owners, :name, :unique => true
23
25
 
24
26
  create_table "non_sharded_has_ones", :force => true do |t|
25
27
  t.integer "sharding_owner_id"
26
28
  t.integer "non_sharding_owner_id"
27
29
  t.string "name"
28
30
  end
31
+ add_index :non_sharded_has_ones, :name, :unique => true
29
32
 
30
33
  create_table "non_sharded_has_manies", :force => true do |t|
31
34
  t.integer "sharding_owner_id"
32
35
  t.integer "non_sharding_owner_id"
33
36
  t.string "name"
34
37
  end
38
+ add_index :non_sharded_has_manies, :name, :unique => true
35
39
 
36
40
  create_table "non_sharded_joins", :force => true do |t|
37
41
  t.integer "sharding_owner_id"
@@ -42,18 +46,21 @@ ActiveRecord::Schema.define(:version => 1) do
42
46
  create_table "non_sharded_has_many_throughs", :force => true do |t|
43
47
  t.string "name"
44
48
  end
49
+ add_index :non_sharded_has_many_throughs, :name, :unique => true
45
50
 
46
51
  create_table "sharded_has_ones", :force => true do |t|
47
52
  t.integer "sharding_owner_id"
48
53
  t.integer "non_sharding_owner_id"
49
54
  t.string "name"
50
55
  end
56
+ add_index :sharded_has_ones, :name, :unique => true
51
57
 
52
58
  create_table "sharded_has_manies", :force => true do |t|
53
59
  t.integer "sharding_owner_id"
54
60
  t.integer "non_sharding_owner_id"
55
61
  t.string "name"
56
62
  end
63
+ add_index :sharded_has_manies, :name, :unique => true
57
64
 
58
65
  create_table "sharded_joins", :force => true do |t|
59
66
  t.integer "sharding_owner_id"
@@ -64,4 +71,24 @@ ActiveRecord::Schema.define(:version => 1) do
64
71
  create_table "sharded_has_many_throughs", :force => true do |t|
65
72
  t.string "name"
66
73
  end
74
+ add_index :sharded_has_many_throughs, :name, :unique => true
75
+
76
+ create_table "sharded_dependent_joins", :force => true do |t|
77
+ t.integer "sharding_owner_id"
78
+ t.integer "non_sharding_owner_id"
79
+ t.integer "sharded_dependent_has_many_through_id"
80
+ end
81
+
82
+ create_table "sharded_dependent_has_many_throughs", :force => true do |t|
83
+ t.string "name"
84
+ end
85
+ add_index :sharded_dependent_has_many_throughs, :name, :unique => true
86
+
87
+ # create_table "sharded_habtms_sharding_owners", :id => false # doesn't seem to create a schema without an ID...
88
+ connection.execute('CREATE TABLE "sharded_habtms_sharding_owners" ("sharded_habtm_id" integer, "sharding_owner_id" integer)')
89
+
90
+ create_table "sharded_habtms", :force => true do |t|
91
+ t.string "name"
92
+ end
93
+ add_index :sharded_habtms, :name, :unique => true
67
94
  end
data/spec/model_spec.rb CHANGED
@@ -88,15 +88,53 @@ describe 'ActiveRecord Models' do
88
88
  end
89
89
 
90
90
  context 'and the shard context defined' do
91
- it 'uses the sharded connection' do
92
- test_shard = 'shard1'
93
- Dynashard.with_context(:owner => test_shard) do
94
- shard_class = Dynashard.class_for(test_shard)
95
- shard_config = shard_class.connection.instance_variable_get('@config')
96
- ar_config = ActiveRecord::Base.connection.instance_variable_get('@config')
97
- model_config = ShardedHasOne.connection.instance_variable_get('@config')
98
- model_config.should_not == ar_config
99
- model_config.should == shard_config
91
+ context 'when managing sharded models' do
92
+ before(:each) do
93
+ @shard1_klass = Dynashard.class_for('shard1')
94
+ @shard2_klass = Dynashard.class_for('shard2')
95
+ @shard1_subclass = Dynashard.sharded_model_class(@shard1_klass, ShardedHasOne)
96
+ @shard2_subclass = Dynashard.sharded_model_class(@shard2_klass, ShardedHasOne)
97
+ end
98
+
99
+ it 'uses the sharded connection for the model class' do
100
+ test_shard = 'shard1'
101
+ Dynashard.with_context(:owner => test_shard) do
102
+ shard_class = Dynashard.class_for(test_shard)
103
+ shard_config = shard_class.connection.instance_variable_get('@config')
104
+ ar_config = ActiveRecord::Base.connection.instance_variable_get('@config')
105
+ model_config = ShardedHasOne.connection.instance_variable_get('@config')
106
+ model_config.should_not == ar_config
107
+ model_config.should == shard_config
108
+ end
109
+ end
110
+
111
+ it 'associates new models with the correct shard' do
112
+ new1 = Dynashard.with_context(:owner => 'shard1') {ShardedHasOne.new(:name => Factory.next(:name))}
113
+ new2 = Dynashard.with_context(:owner => 'shard2') {ShardedHasOne.new(:name => Factory.next(:name))}
114
+ new1.should be_a(@shard1_subclass)
115
+ new2.should be_a(@shard2_subclass)
116
+ new1.connection.should == @shard1_klass.connection
117
+ new2.connection.should == @shard2_klass.connection
118
+ end
119
+
120
+ it 'associates created models with the correct shard' do
121
+ created1 = Dynashard.with_context(:owner => 'shard1') {ShardedHasOne.create(:name => Factory.next(:name))}
122
+ created2 = Dynashard.with_context(:owner => 'shard2') {ShardedHasOne.create(:name => Factory.next(:name))}
123
+ created1.should be_a(@shard1_subclass)
124
+ created2.should be_a(@shard2_subclass)
125
+ created1.connection.should == @shard1_klass.connection
126
+ created2.connection.should == @shard2_klass.connection
127
+ end
128
+
129
+ it 'associates loaded models with the correct shard' do
130
+ model1 = Dynashard.with_context(:owner => 'shard1') {Factory(:sharded_has_one)}
131
+ model2 = Dynashard.with_context(:owner => 'shard2') {Factory(:sharded_has_one)}
132
+ found1 = Dynashard.with_context(:owner => 'shard1') {ShardedHasOne.find(model1.id)}
133
+ found2 = Dynashard.with_context(:owner => 'shard2') {ShardedHasOne.find(model2.id)}
134
+ found1.should be_a(@shard1_subclass)
135
+ found2.should be_a(@shard2_subclass)
136
+ found1.connection.should == @shard1_klass.connection
137
+ found2.connection.should == @shard2_klass.connection
100
138
  end
101
139
  end
102
140
  end
data/spec/spec_helper.rb CHANGED
@@ -19,27 +19,31 @@ ActiveRecord::Base.configurations = YAML::load(ERB.new(IO.read(plugin_test_dir +
19
19
  ActiveRecord::Base.establish_connection("test")
20
20
  ActiveRecord::Migration.verbose = false
21
21
 
22
- test_database = ActiveRecord::Base.configurations['test']['database']
23
- File.unlink(test_database) if File.exists?(test_database)
24
- load(File.join(plugin_test_dir, "db", "schema.rb"))
25
-
26
- # create shards and databases that they point to
27
- base_config = ActiveRecord::Base.configurations['test']
28
- %w{shard1 shard2 shard3}.each do |shard|
29
- database = "#{plugin_test_dir}/db/#{shard}.sqlite3"
30
- File.unlink(database) if File.exists?(database)
31
- shard_config = base_config.merge('database' => database)
32
- ActiveRecord::Base.configurations['test'] = shard_config
33
- ActiveRecord::Base.establish_connection("test")
22
+ unless __FILE__ == "(irb)"
23
+ test_database = ActiveRecord::Base.configurations['test']['database']
24
+ File.unlink(test_database) if File.exists?(test_database)
34
25
  load(File.join(plugin_test_dir, "db", "schema.rb"))
26
+
27
+ # create shards and databases that they point to
28
+ base_config = ActiveRecord::Base.configurations['test']
29
+ %w{shard1 shard2 shard3}.each do |shard|
30
+ database = "#{plugin_test_dir}/db/#{shard}.sqlite3"
31
+ File.unlink(database) if File.exists?(database)
32
+ shard_config = base_config.merge('database' => database)
33
+ ActiveRecord::Base.configurations['test'] = shard_config
34
+ ActiveRecord::Base.establish_connection("test")
35
+ load(File.join(plugin_test_dir, "db", "schema.rb"))
36
+ end
37
+ ActiveRecord::Base.configurations['test'] = base_config
38
+ ActiveRecord::Base.establish_connection("test")
35
39
  end
36
- ActiveRecord::Base.configurations['test'] = base_config
37
- ActiveRecord::Base.establish_connection("test")
38
40
 
39
41
  require 'support/models'
40
42
 
41
- %w{shard1 shard2 shard3}.each do |shard|
42
- Shard.create(:adapter => 'sqlite3', :database => "#{plugin_test_dir}/db/#{shard}.sqlite3")
43
+ unless __FILE__ == "(irb)"
44
+ %w{shard1 shard2 shard3}.each do |shard|
45
+ Shard.create(:adapter => 'sqlite3', :database => "#{plugin_test_dir}/db/#{shard}.sqlite3")
46
+ end
43
47
  end
44
48
 
45
49
  # This has to happen after the models have been defined and the shards have been created
@@ -1,6 +1,6 @@
1
1
  # Sequence for generating unique names
2
2
  Factory.sequence :name do |n|
3
- "Test Owner #{n}"
3
+ "Test Name #{n}"
4
4
  end
5
5
 
6
6
  Factory.define(:sharding_owner) do |owner|
@@ -30,3 +30,7 @@ end
30
30
  Factory.define(:sharded_has_many_through) do |one_of_many|
31
31
  one_of_many.name {Factory.next :name}
32
32
  end
33
+
34
+ Factory.define(:sharded_habtm) do |habtm|
35
+ habtm.name {Factory.next :name}
36
+ end
@@ -39,6 +39,8 @@ class ShardingOwner < ActiveRecord::Base
39
39
  has_many :sharded_has_manys
40
40
  has_many :sharded_joins
41
41
  has_many :sharded_has_many_throughs, :through => :sharded_joins
42
+ has_many :sharded_dependent_has_many_throughs, :through => :sharded_dependent_joins
43
+ has_and_belongs_to_many :sharded_habtms
42
44
  end
43
45
 
44
46
  # Non-sharded has_one association class
@@ -97,3 +99,25 @@ class ShardedHasManyThrough < ActiveRecord::Base
97
99
 
98
100
  has_many :sharded_joins
99
101
  end
102
+
103
+ # Join table for has_many :through with dependent => destroy
104
+ class ShardedDependentJoin < ActiveRecord::Base
105
+ shard :by => :owner
106
+
107
+ belongs_to :sharding_owner
108
+ belongs_to :sharded_dependent_has_many_through
109
+ end
110
+
111
+ # Sharded has_many :through association class with dependent => destroy
112
+ class ShardedDependentHasManyThrough < ActiveRecord::Base
113
+ shard :by => :owner
114
+
115
+ has_many :sharded_dependent_joins, :dependent => :destroy
116
+ end
117
+
118
+ # Sharded habtm association class
119
+ class ShardedHabtm < ActiveRecord::Base
120
+ shard :by => :owner
121
+
122
+ has_and_belongs_to_many :sharding_owners
123
+ end
@@ -79,9 +79,8 @@ describe 'Dynashard::ValidationExtensions' do
79
79
  context 'with a conflicting record on the shard' do
80
80
  it 'returns invalid' do
81
81
  conflicting_record = Dynashard.with_context(:owner => @owner.shard_dsn){Factory(:sharded_has_one)}
82
- new_record = ShardedHasOne.new(:name => conflicting_record.name)
83
82
  Dynashard.with_context(:owner => @owner.shard_dsn) do
84
- new_record.should_not be_valid
83
+ ShardedHasOne.new(:name => conflicting_record.name).should_not be_valid
85
84
  end
86
85
  end
87
86
  end
@@ -90,9 +89,8 @@ describe 'Dynashard::ValidationExtensions' do
90
89
  it 'returns valid' do
91
90
  other_shard = Shard.find(:all).detect{|shard| shard != @owner.shard}
92
91
  non_conflicting_record = Dynashard.with_context(:owner => other_shard.dsn){Factory(:sharded_has_one)}
93
- new_record = ShardedHasOne.new(:name => non_conflicting_record.name)
94
92
  Dynashard.with_context(:owner => @owner.shard_dsn) do
95
- new_record.should be_valid
93
+ ShardedHasOne.new(:name => non_conflicting_record.name).should be_valid
96
94
  end
97
95
  end
98
96
  end
metadata CHANGED
@@ -1,12 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dynashard
3
3
  version: !ruby/object:Gem::Version
4
+ hash: 15
4
5
  prerelease: false
5
6
  segments:
6
7
  - 0
7
- - 3
8
- - 1
9
- version: 0.3.1
8
+ - 4
9
+ - 0
10
+ version: 0.4.0
10
11
  platform: ruby
11
12
  authors:
12
13
  - Nick Hengeveld
@@ -14,93 +15,99 @@ autorequire:
14
15
  bindir: bin
15
16
  cert_chain: []
16
17
 
17
- date: 2011-02-01 00:00:00 -08:00
18
+ date: 2011-02-14 00:00:00 -08:00
18
19
  default_executable:
19
20
  dependencies:
20
21
  - !ruby/object:Gem::Dependency
22
+ type: :runtime
23
+ prerelease: false
21
24
  name: activerecord
22
- requirement: &id001 !ruby/object:Gem::Requirement
25
+ version_requirements: &id001 !ruby/object:Gem::Requirement
23
26
  none: false
24
27
  requirements:
25
28
  - - ">="
26
29
  - !ruby/object:Gem::Version
30
+ hash: 7
27
31
  segments:
28
32
  - 3
29
33
  - 0
30
34
  version: "3.0"
31
- type: :runtime
32
- prerelease: false
33
- version_requirements: *id001
35
+ requirement: *id001
34
36
  - !ruby/object:Gem::Dependency
37
+ type: :development
38
+ prerelease: false
35
39
  name: shoulda
36
- requirement: &id002 !ruby/object:Gem::Requirement
40
+ version_requirements: &id002 !ruby/object:Gem::Requirement
37
41
  none: false
38
42
  requirements:
39
43
  - - ">="
40
44
  - !ruby/object:Gem::Version
45
+ hash: 3
41
46
  segments:
42
47
  - 0
43
48
  version: "0"
49
+ requirement: *id002
50
+ - !ruby/object:Gem::Dependency
44
51
  type: :development
45
52
  prerelease: false
46
- version_requirements: *id002
47
- - !ruby/object:Gem::Dependency
48
53
  name: bundler
49
- requirement: &id003 !ruby/object:Gem::Requirement
54
+ version_requirements: &id003 !ruby/object:Gem::Requirement
50
55
  none: false
51
56
  requirements:
52
57
  - - ~>
53
58
  - !ruby/object:Gem::Version
59
+ hash: 23
54
60
  segments:
55
61
  - 1
56
62
  - 0
57
63
  - 0
58
64
  version: 1.0.0
65
+ requirement: *id003
66
+ - !ruby/object:Gem::Dependency
59
67
  type: :development
60
68
  prerelease: false
61
- version_requirements: *id003
62
- - !ruby/object:Gem::Dependency
63
69
  name: jeweler
64
- requirement: &id004 !ruby/object:Gem::Requirement
70
+ version_requirements: &id004 !ruby/object:Gem::Requirement
65
71
  none: false
66
72
  requirements:
67
73
  - - ~>
68
74
  - !ruby/object:Gem::Version
75
+ hash: 7
69
76
  segments:
70
77
  - 1
71
78
  - 5
72
79
  - 2
73
80
  version: 1.5.2
81
+ requirement: *id004
82
+ - !ruby/object:Gem::Dependency
74
83
  type: :development
75
84
  prerelease: false
76
- version_requirements: *id004
77
- - !ruby/object:Gem::Dependency
78
85
  name: rcov
79
- requirement: &id005 !ruby/object:Gem::Requirement
86
+ version_requirements: &id005 !ruby/object:Gem::Requirement
80
87
  none: false
81
88
  requirements:
82
89
  - - ">="
83
90
  - !ruby/object:Gem::Version
91
+ hash: 3
84
92
  segments:
85
93
  - 0
86
94
  version: "0"
87
- type: :development
88
- prerelease: false
89
- version_requirements: *id005
95
+ requirement: *id005
90
96
  - !ruby/object:Gem::Dependency
97
+ type: :runtime
98
+ prerelease: false
91
99
  name: activerecord
92
- requirement: &id006 !ruby/object:Gem::Requirement
100
+ version_requirements: &id006 !ruby/object:Gem::Requirement
93
101
  none: false
94
102
  requirements:
95
103
  - - ">="
96
104
  - !ruby/object:Gem::Version
105
+ hash: 7
97
106
  segments:
98
107
  - 3
99
108
  - 0
100
109
  version: "3.0"
101
- type: :runtime
102
- prerelease: false
103
- version_requirements: *id006
110
+ requirement: *id006
104
111
  description: Dynashard allows you to shard your ActiveRecord models. Models can be configured to shard based on context that can be defined dynamically.
105
112
  email: nickh@verticalresponse.com
106
113
  executables: []
@@ -148,7 +155,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
148
155
  requirements:
149
156
  - - ">="
150
157
  - !ruby/object:Gem::Version
151
- hash: 1766305880459043419
158
+ hash: 3
152
159
  segments:
153
160
  - 0
154
161
  version: "0"
@@ -157,6 +164,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
157
164
  requirements:
158
165
  - - ">="
159
166
  - !ruby/object:Gem::Version
167
+ hash: 3
160
168
  segments:
161
169
  - 0
162
170
  version: "0"