ar-octopus-master 0.9.2.master → 0.9.2.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (161) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +12 -0
  3. data/.rspec +2 -0
  4. data/.rubocop.yml +46 -0
  5. data/.rubocop_todo.yml +56 -0
  6. data/.travis.yml +25 -0
  7. data/Appraisals +20 -0
  8. data/Gemfile +4 -0
  9. data/README.mkdn +242 -0
  10. data/Rakefile +172 -0
  11. data/TODO.txt +7 -0
  12. data/ar-octopus.gemspec +39 -0
  13. data/gemfiles/rails4.gemfile +7 -0
  14. data/gemfiles/rails41.gemfile +7 -0
  15. data/gemfiles/rails42.gemfile +7 -0
  16. data/gemfiles/rails5.gemfile +7 -0
  17. data/gemfiles/rails51.gemfile +7 -0
  18. data/lib/ar-octopus.rb +1 -0
  19. data/lib/octopus.rb +205 -0
  20. data/lib/octopus/abstract_adapter.rb +33 -0
  21. data/lib/octopus/association.rb +14 -0
  22. data/lib/octopus/association_shard_tracking.rb +74 -0
  23. data/lib/octopus/collection_association.rb +17 -0
  24. data/lib/octopus/collection_proxy.rb +16 -0
  25. data/lib/octopus/exception.rb +4 -0
  26. data/lib/octopus/finder_methods.rb +8 -0
  27. data/lib/octopus/has_and_belongs_to_many_association.rb +9 -0
  28. data/lib/octopus/load_balancing.rb +4 -0
  29. data/lib/octopus/load_balancing/round_robin.rb +20 -0
  30. data/lib/octopus/log_subscriber.rb +26 -0
  31. data/lib/octopus/migration.rb +195 -0
  32. data/lib/octopus/model.rb +223 -0
  33. data/lib/octopus/persistence.rb +45 -0
  34. data/lib/octopus/proxy.rb +346 -0
  35. data/lib/octopus/proxy_config.rb +252 -0
  36. data/lib/octopus/query_cache_for_shards.rb +25 -0
  37. data/lib/octopus/railtie.rb +11 -0
  38. data/lib/octopus/relation_proxy.rb +58 -0
  39. data/lib/octopus/result_patch.rb +19 -0
  40. data/lib/octopus/scope_proxy.rb +68 -0
  41. data/lib/octopus/shard_tracking.rb +46 -0
  42. data/lib/octopus/shard_tracking/attribute.rb +22 -0
  43. data/lib/octopus/shard_tracking/dynamic.rb +11 -0
  44. data/lib/octopus/singular_association.rb +9 -0
  45. data/lib/octopus/slave_group.rb +13 -0
  46. data/lib/octopus/version.rb +3 -0
  47. data/lib/tasks/octopus.rake +16 -0
  48. data/sample_app/.gitignore +4 -0
  49. data/sample_app/.rspec +1 -0
  50. data/sample_app/Gemfile +20 -0
  51. data/sample_app/README +3 -0
  52. data/sample_app/README.rdoc +261 -0
  53. data/sample_app/Rakefile +7 -0
  54. data/sample_app/app/assets/images/rails.png +0 -0
  55. data/sample_app/app/assets/javascripts/application.js +15 -0
  56. data/sample_app/app/assets/stylesheets/application.css +13 -0
  57. data/sample_app/app/controllers/application_controller.rb +4 -0
  58. data/sample_app/app/helpers/application_helper.rb +2 -0
  59. data/sample_app/app/mailers/.gitkeep +0 -0
  60. data/sample_app/app/models/.gitkeep +0 -0
  61. data/sample_app/app/models/item.rb +3 -0
  62. data/sample_app/app/models/user.rb +3 -0
  63. data/sample_app/app/views/layouts/application.html.erb +14 -0
  64. data/sample_app/autotest/discover.rb +2 -0
  65. data/sample_app/config.ru +4 -0
  66. data/sample_app/config/application.rb +62 -0
  67. data/sample_app/config/boot.rb +6 -0
  68. data/sample_app/config/cucumber.yml +8 -0
  69. data/sample_app/config/database.yml +28 -0
  70. data/sample_app/config/environment.rb +5 -0
  71. data/sample_app/config/environments/development.rb +37 -0
  72. data/sample_app/config/environments/production.rb +67 -0
  73. data/sample_app/config/environments/test.rb +37 -0
  74. data/sample_app/config/initializers/backtrace_silencers.rb +7 -0
  75. data/sample_app/config/initializers/inflections.rb +15 -0
  76. data/sample_app/config/initializers/mime_types.rb +5 -0
  77. data/sample_app/config/initializers/secret_token.rb +7 -0
  78. data/sample_app/config/initializers/session_store.rb +8 -0
  79. data/sample_app/config/initializers/wrap_parameters.rb +14 -0
  80. data/sample_app/config/locales/en.yml +5 -0
  81. data/sample_app/config/routes.rb +58 -0
  82. data/sample_app/config/shards.yml +28 -0
  83. data/sample_app/db/migrate/20100720172715_create_users.rb +15 -0
  84. data/sample_app/db/migrate/20100720172730_create_items.rb +16 -0
  85. data/sample_app/db/migrate/20100720210335_create_sample_users.rb +11 -0
  86. data/sample_app/db/schema.rb +29 -0
  87. data/sample_app/db/seeds.rb +16 -0
  88. data/sample_app/doc/README_FOR_APP +2 -0
  89. data/sample_app/features/migrate.feature +45 -0
  90. data/sample_app/features/seed.feature +15 -0
  91. data/sample_app/features/step_definitions/seeds_steps.rb +13 -0
  92. data/sample_app/features/step_definitions/web_steps.rb +218 -0
  93. data/sample_app/features/support/database.rb +13 -0
  94. data/sample_app/features/support/env.rb +57 -0
  95. data/sample_app/features/support/paths.rb +33 -0
  96. data/sample_app/lib/assets/.gitkeep +0 -0
  97. data/sample_app/lib/tasks/.gitkeep +0 -0
  98. data/sample_app/lib/tasks/cucumber.rake +64 -0
  99. data/sample_app/log/.gitkeep +0 -0
  100. data/sample_app/public/404.html +26 -0
  101. data/sample_app/public/422.html +26 -0
  102. data/sample_app/public/500.html +26 -0
  103. data/sample_app/public/favicon.ico +0 -0
  104. data/sample_app/public/images/rails.png +0 -0
  105. data/sample_app/public/index.html +279 -0
  106. data/sample_app/public/javascripts/application.js +2 -0
  107. data/sample_app/public/javascripts/controls.js +965 -0
  108. data/sample_app/public/javascripts/dragdrop.js +974 -0
  109. data/sample_app/public/javascripts/effects.js +1123 -0
  110. data/sample_app/public/javascripts/prototype.js +4874 -0
  111. data/sample_app/public/javascripts/rails.js +118 -0
  112. data/sample_app/public/robots.txt +5 -0
  113. data/sample_app/public/stylesheets/.gitkeep +0 -0
  114. data/sample_app/script/cucumber +10 -0
  115. data/sample_app/script/rails +6 -0
  116. data/sample_app/spec/models/item_spec.rb +5 -0
  117. data/sample_app/spec/models/user_spec.rb +5 -0
  118. data/sample_app/spec/spec_helper.rb +27 -0
  119. data/sample_app/vendor/assets/javascripts/.gitkeep +0 -0
  120. data/sample_app/vendor/assets/stylesheets/.gitkeep +0 -0
  121. data/sample_app/vendor/plugins/.gitkeep +0 -0
  122. data/spec/config/shards.yml +229 -0
  123. data/spec/migrations/10_create_users_using_replication.rb +9 -0
  124. data/spec/migrations/11_add_field_in_all_slaves.rb +11 -0
  125. data/spec/migrations/12_create_users_using_block.rb +23 -0
  126. data/spec/migrations/13_create_users_using_block_and_using.rb +15 -0
  127. data/spec/migrations/14_create_users_on_shards_of_a_group_with_versions.rb +11 -0
  128. data/spec/migrations/15_create_user_on_shards_of_default_group_with_versions.rb +9 -0
  129. data/spec/migrations/1_create_users_on_master.rb +9 -0
  130. data/spec/migrations/2_create_users_on_canada.rb +11 -0
  131. data/spec/migrations/3_create_users_on_both_shards.rb +11 -0
  132. data/spec/migrations/4_create_users_on_shards_of_a_group.rb +11 -0
  133. data/spec/migrations/5_create_users_on_multiples_groups.rb +11 -0
  134. data/spec/migrations/6_raise_exception_with_invalid_shard_name.rb +11 -0
  135. data/spec/migrations/7_raise_exception_with_invalid_multiple_shard_names.rb +11 -0
  136. data/spec/migrations/8_raise_exception_with_invalid_group_name.rb +11 -0
  137. data/spec/migrations/9_raise_exception_with_multiple_invalid_group_names.rb +11 -0
  138. data/spec/octopus/association_shard_tracking_spec.rb +1036 -0
  139. data/spec/octopus/collection_proxy_spec.rb +16 -0
  140. data/spec/octopus/load_balancing/round_robin_spec.rb +15 -0
  141. data/spec/octopus/log_subscriber_spec.rb +19 -0
  142. data/spec/octopus/migration_spec.rb +134 -0
  143. data/spec/octopus/model_spec.rb +754 -0
  144. data/spec/octopus/octopus_spec.rb +123 -0
  145. data/spec/octopus/proxy_spec.rb +303 -0
  146. data/spec/octopus/query_cache_for_shards_spec.rb +17 -0
  147. data/spec/octopus/relation_proxy_spec.rb +124 -0
  148. data/spec/octopus/replicated_slave_grouped_spec.rb +91 -0
  149. data/spec/octopus/replication_spec.rb +196 -0
  150. data/spec/octopus/scope_proxy_spec.rb +97 -0
  151. data/spec/octopus/sharded_replicated_slave_grouped_spec.rb +55 -0
  152. data/spec/octopus/sharded_spec.rb +33 -0
  153. data/spec/spec_helper.rb +18 -0
  154. data/spec/support/active_record/connection_adapters/modify_config_adapter.rb +15 -0
  155. data/spec/support/database_connection.rb +4 -0
  156. data/spec/support/database_models.rb +118 -0
  157. data/spec/support/octopus_helper.rb +54 -0
  158. data/spec/support/query_count.rb +17 -0
  159. data/spec/support/shared_contexts.rb +18 -0
  160. data/spec/tasks/octopus.rake_spec.rb +32 -0
  161. metadata +203 -5
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ describe Octopus::CollectionProxy do
4
+ describe 'method dispatch' do
5
+ before :each do
6
+ @client = Client.using(:canada).create!
7
+ @client.items << Item.using(:canada).create!
8
+ end
9
+
10
+ it 'computes the size of the collection without loading it' do
11
+ expect(@client.items.size).to eq(1)
12
+
13
+ expect(@client.items.loaded?).to be false
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,15 @@
1
+ require 'spec_helper'
2
+
3
+ describe Octopus::LoadBalancing::RoundRobin do
4
+ it "raises an error when no shards are given" do
5
+ expect do
6
+ Octopus::LoadBalancing::RoundRobin.new([])
7
+ end.to raise_error Octopus::Exception
8
+ end
9
+
10
+ it "does not raise an error if slaves given" do
11
+ expect do
12
+ Octopus::LoadBalancing::RoundRobin.new([:stub])
13
+ end.to_not raise_error Octopus::Exception
14
+ end
15
+ end
@@ -0,0 +1,19 @@
1
+ require 'spec_helper'
2
+
3
+ describe Octopus::LogSubscriber, :shards => [:canada] do
4
+ before :each do
5
+ @out = StringIO.new
6
+ @log = Logger.new(@out)
7
+ ActiveRecord::Base.logger = @log
8
+ ActiveRecord::Base.logger.level = Logger::DEBUG
9
+ end
10
+
11
+ after :each do
12
+ ActiveRecord::Base.logger = Logger.new(File.open('database.log', 'a'))
13
+ end
14
+
15
+ it 'should add to the default logger the shard name the query was sent to' do
16
+ User.using(:canada).create!(:name => 'test')
17
+ expect(@out.string).to match(/Shard: canada/)
18
+ end
19
+ end
@@ -0,0 +1,134 @@
1
+ require 'spec_helper'
2
+
3
+ describe Octopus::Migration do
4
+ it 'should run just in the master shard' do
5
+ OctopusHelper.migrating_to_version 1 do
6
+ expect(User.using(:master).find_by_name('Master')).not_to be_nil
7
+ expect(User.using(:canada).find_by_name('Master')).to be_nil
8
+ end
9
+ end
10
+
11
+ it 'should run on specific shard' do
12
+ OctopusHelper.migrating_to_version 2 do
13
+ expect(User.using(:master).find_by_name('Sharding')).to be_nil
14
+ expect(User.using(:canada).find_by_name('Sharding')).not_to be_nil
15
+ end
16
+ end
17
+
18
+ it 'should run on specifieds shards' do
19
+ OctopusHelper.migrating_to_version 3 do
20
+ expect(User.using(:brazil).find_by_name('Both')).not_to be_nil
21
+ expect(User.using(:canada).find_by_name('Both')).not_to be_nil
22
+ end
23
+ end
24
+
25
+ it 'should run on specified group' do
26
+ OctopusHelper.migrating_to_version 4 do
27
+ expect(User.using(:canada).find_by_name('Group')).not_to be_nil
28
+ expect(User.using(:brazil).find_by_name('Group')).not_to be_nil
29
+ expect(User.using(:russia).find_by_name('Group')).not_to be_nil
30
+ end
31
+ end
32
+
33
+ it "should rollback correctly migrations" do
34
+ migrations_root = File.expand_path(File.join(File.dirname(__FILE__), '..', 'migrations'))
35
+
36
+ ActiveRecord::Migrator.run(:up, migrations_root, 4)
37
+
38
+ expect(User.using(:canada).find_by_name('Group')).not_to be_nil
39
+ expect(User.using(:brazil).find_by_name('Group')).not_to be_nil
40
+ expect(User.using(:russia).find_by_name('Group')).not_to be_nil
41
+
42
+
43
+ Octopus.using(:canada) do
44
+ ActiveRecord::Migrator.rollback(migrations_root, 4)
45
+ end
46
+
47
+ expect(User.using(:canada).find_by_name('Group')).to be_nil
48
+ expect(User.using(:brazil).find_by_name('Group')).to be_nil
49
+ expect(User.using(:russia).find_by_name('Group')).to be_nil
50
+ end
51
+
52
+ it 'should run once per shard' do
53
+ OctopusHelper.migrating_to_version 5 do
54
+ expect(User.using(:canada).where(:name => 'MultipleGroup').size).to eq(1)
55
+ expect(User.using(:brazil).where(:name => 'MultipleGroup').size).to eq(1)
56
+ expect(User.using(:russia).where(:name => 'MultipleGroup').size).to eq(1)
57
+ end
58
+ end
59
+
60
+ it 'should create users inside block' do
61
+ OctopusHelper.migrating_to_version 12 do
62
+ expect(User.using(:brazil).where(:name => 'UsingBlock1').size).to eq(1)
63
+ expect(User.using(:brazil).where(:name => 'UsingBlock2').size).to eq(1)
64
+ expect(User.using(:canada).where(:name => 'UsingCanada').size).to eq(1)
65
+ expect(User.using(:canada).where(:name => 'UsingCanada2').size).to eq(1)
66
+ end
67
+ end
68
+
69
+ it 'should send the query to the correct shard' do
70
+ OctopusHelper.migrating_to_version 13 do
71
+ expect(User.using(:brazil).where(:name => 'Brazil').size).to eq(1)
72
+ expect(User.using(:brazil).where(:name => 'Canada').size).to eq(0)
73
+ expect(User.using(:canada).where(:name => 'Brazil').size).to eq(0)
74
+ expect(User.using(:canada).where(:name => 'Canada').size).to eq(1)
75
+ end
76
+ end
77
+
78
+ describe 'when using replication' do
79
+ it 'should run writes on master when you use replication' do
80
+ OctopusHelper.using_environment :production_replicated do
81
+ OctopusHelper.migrating_to_version 10 do
82
+ expect(Cat.find_by_name('Replication')).to be_nil
83
+ end
84
+ end
85
+ end
86
+
87
+ it 'should run in all shards, master or another shards' do
88
+ OctopusHelper.using_environment :production_replicated do
89
+ OctopusHelper.migrating_to_version 11 do
90
+ [:slave4, :slave1, :slave2, :slave3].each do |_sym|
91
+ expect(Cat.find_by_name('Slaves')).not_to be_nil
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+
98
+ it 'should store the migration versions in each shard' do
99
+ class SchemaMigration < ActiveRecord::Base; end
100
+
101
+ OctopusHelper.migrating_to_version 14 do
102
+ expect(Octopus.using(:canada) { ActiveRecord::Migrator.get_all_versions }).to include(14)
103
+ expect(Octopus.using(:brazil) { ActiveRecord::Migrator.get_all_versions }).to include(14)
104
+ expect(Octopus.using(:russia) { ActiveRecord::Migrator.get_all_versions }).to include(14)
105
+ end
106
+ end
107
+
108
+ it 'should run the migrations on shards that are missing them' do
109
+ class SchemaMigration < ActiveRecord::Base; end
110
+
111
+ Octopus.using(:master) { SchemaMigration.create(:version => 14) }
112
+ Octopus.using(:canada) { SchemaMigration.create(:version => 14) }
113
+
114
+ OctopusHelper.migrating_to_version 14 do
115
+ expect(Octopus.using(:canada) { ActiveRecord::Migrator.get_all_versions }).to include(14)
116
+ expect(Octopus.using(:brazil) { ActiveRecord::Migrator.get_all_versions }).to include(14)
117
+ expect(Octopus.using(:russia) { ActiveRecord::Migrator.get_all_versions }).to include(14)
118
+ end
119
+ end
120
+
121
+ describe 'when using a default_migration_group' do
122
+ it 'should run migrations on all shards in the default_migration_group' do
123
+ OctopusHelper.using_environment :octopus_with_default_migration_group do
124
+ OctopusHelper.migrating_to_version 15 do
125
+ expect(Octopus.using(:master) { ActiveRecord::Migrator.get_all_versions }).not_to include(15)
126
+ expect(Octopus.using(:canada) { ActiveRecord::Migrator.get_all_versions }).to include(15)
127
+ expect(Octopus.using(:brazil) { ActiveRecord::Migrator.get_all_versions }).to include(15)
128
+ expect(Octopus.using(:russia) { ActiveRecord::Migrator.get_all_versions }).to include(15)
129
+ end
130
+ end
131
+ end
132
+ end
133
+
134
+ end
@@ -0,0 +1,754 @@
1
+ require 'spec_helper'
2
+
3
+ describe Octopus::Model do
4
+ describe '#using method' do
5
+ it 'raise when Model#using receives a block' do
6
+ expect { User.using(:master) { true } }.to raise_error(Octopus::Exception, /User\.using is not allowed to receive a block/)
7
+ end
8
+
9
+ it 'should allow to send a block to the master shard' do
10
+ Octopus.using(:master) do
11
+ User.create!(:name => 'Block test')
12
+ end
13
+
14
+ expect(User.using(:master).find_by_name('Block test')).not_to be_nil
15
+ end
16
+
17
+ it 'should allow to pass a string as the shard name to a AR subclass' do
18
+ User.using('canada').create!(:name => 'Rafael Pilha')
19
+
20
+ expect(User.using('canada').find_by_name('Rafael Pilha')).not_to be_nil
21
+ end
22
+
23
+ it 'should allow comparison of a string shard name with symbol shard name' do
24
+ u = User.using('canada').create!(:name => 'Rafael Pilha')
25
+ expect(u).to eq(User.using(:canada).find_by_name('Rafael Pilha'))
26
+ end
27
+
28
+ it 'should allow comparison of a symbol shard name with string shard name' do
29
+ u = User.using(:canada).create!(:name => 'Rafael Pilha')
30
+ expect(u).to eq(User.using('canada').find_by_name('Rafael Pilha'))
31
+ end
32
+
33
+ it 'should allow to pass a string as the shard name to a block' do
34
+ Octopus.using('canada') do
35
+ User.create!(:name => 'Rafael Pilha')
36
+ end
37
+
38
+ expect(User.using('canada').find_by_name('Rafael Pilha')).not_to be_nil
39
+ end
40
+
41
+ it 'should allow selecting the shards on scope' do
42
+ User.using(:canada).create!(:name => 'oi')
43
+ expect(User.using(:canada).count).to eq(1)
44
+ expect(User.count).to eq(0)
45
+ end
46
+
47
+ it 'should allow selecting the shard using #new' do
48
+ u = User.using(:canada).new
49
+ u.name = 'Thiago'
50
+ u.save
51
+
52
+ expect(User.using(:canada).count).to eq(1)
53
+ expect(User.using(:brazil).count).to eq(0)
54
+
55
+ u1 = User.new
56
+ u1.name = 'Joaquim'
57
+ u2 = User.using(:canada).new
58
+ u2.name = 'Manuel'
59
+ u1.save
60
+ u2.save
61
+
62
+ expect(User.using(:canada).all).to eq([u, u2])
63
+ expect(User.all).to eq([u1])
64
+ end
65
+
66
+ it "should allow the #select method to fetch the correct data when using a block" do
67
+ canadian_user = User.using(:canada).create!(:name => 'Rafael Pilha')
68
+
69
+ Octopus.using('canada') do
70
+ @all_canadian_user_ids = User.select('id').to_a
71
+ end
72
+
73
+ expect(@all_canadian_user_ids).to eq([canadian_user])
74
+ end
75
+
76
+ it "should allow objects to be fetch using different blocks - GH #306" do
77
+ canadian_user = User.using(:canada).create!(:name => 'Rafael Pilha')
78
+
79
+ Octopus.using(:canada) { @users = User.where('id is not null') }
80
+ Octopus.using(:canada) { @user = @users.first }
81
+
82
+ Octopus.using(:canada) { @user2 = User.where('id is not null').first }
83
+
84
+ expect(@user).to eq(canadian_user)
85
+ expect(@user2).to eq(canadian_user)
86
+ end
87
+
88
+ describe 'multiple calls to the same scope' do
89
+ it 'works with nil response' do
90
+ scope = User.using(:canada)
91
+ expect(scope.count).to eq(0)
92
+ expect(scope.first).to be_nil
93
+ end
94
+
95
+ it 'works with non-nil response' do
96
+ user = User.using(:canada).create!(:name => 'oi')
97
+ scope = User.using(:canada)
98
+ expect(scope.count).to eq(1)
99
+ expect(scope.first).to eq(user)
100
+ end
101
+ end
102
+
103
+ it 'should select the correct shard' do
104
+ User.using(:canada)
105
+ User.create!(:name => 'oi')
106
+ expect(User.count).to eq(1)
107
+ end
108
+
109
+ it 'should ensure that the connection will be cleaned' do
110
+ expect(ActiveRecord::Base.connection.current_shard).to eq(:master)
111
+ expect do
112
+ Octopus.using(:canada) do
113
+ fail 'Some Exception'
114
+ end
115
+ end.to raise_error(RuntimeError)
116
+
117
+ expect(ActiveRecord::Base.connection.current_shard).to eq(:master)
118
+ end
119
+
120
+ it 'should ensure that the connection will be cleaned with custom master' do
121
+ OctopusHelper.using_environment :octopus do
122
+ Octopus.config[:master_shard] = :brazil
123
+ expect(ActiveRecord::Base.connection.current_shard).to eq(:brazil)
124
+ expect do
125
+ Octopus.using(:canada) do
126
+ fail 'Some Exception'
127
+ end
128
+ end.to raise_error(RuntimeError)
129
+
130
+ expect(ActiveRecord::Base.connection.current_shard).to eq(:brazil)
131
+ Octopus.config[:master_shard] = nil
132
+ end
133
+ end
134
+
135
+ it 'should allow creating more than one user' do
136
+ User.using(:canada).create([{ :name => 'America User 1' }, { :name => 'America User 2' }])
137
+ User.create!(:name => 'Thiago')
138
+ expect(User.using(:canada).find_by_name('America User 1')).not_to be_nil
139
+ expect(User.using(:canada).find_by_name('America User 2')).not_to be_nil
140
+ expect(User.using(:master).find_by_name('Thiago')).not_to be_nil
141
+ end
142
+
143
+ it 'should work when you have a SQLite3 shard' do
144
+ u = User.using(:sqlite_shard).create!(:name => 'Sqlite3')
145
+ expect(User.using(:sqlite_shard).where(name: 'Sqlite3').first).to eq(u)
146
+ end
147
+
148
+ it 'should clean #current_shard from proxy when using execute' do
149
+ User.using(:canada).connection.execute('select * from users limit 1;')
150
+ expect(User.connection.current_shard).to eq(:master)
151
+ end
152
+
153
+ it 'should clean #current_shard from proxy when using execute' do
154
+ OctopusHelper.using_environment :octopus do
155
+ Octopus.config[:master_shard] = :brazil
156
+ User.using(:canada).connection.execute('select * from users limit 1;')
157
+ expect(User.connection.current_shard).to eq(:brazil)
158
+ Octopus.config[:master_shard] = nil
159
+ end
160
+ end
161
+
162
+ it 'should allow scoping dynamically' do
163
+ User.using(:canada).using(:master).using(:canada).create!(:name => 'oi')
164
+ expect(User.using(:canada).using(:master).count).to eq(0)
165
+ expect(User.using(:master).using(:canada).count).to eq(1)
166
+ end
167
+
168
+ it 'should allow find inside blocks' do
169
+ @user = User.using(:brazil).create!(:name => 'Thiago')
170
+
171
+ Octopus.using(:brazil) do
172
+ expect(User.first).to eq(@user)
173
+ end
174
+
175
+ expect(User.using(:brazil).find_by_name('Thiago')).to eq(@user)
176
+ end
177
+
178
+ it 'should clean the current_shard after executing the current query' do
179
+ User.using(:canada).create!(:name => 'oi')
180
+ expect(User.count).to eq(0)
181
+ end
182
+
183
+ it 'should support both groups and alone shards' do
184
+ _u = User.using(:alone_shard).create!(:name => 'Alone')
185
+ expect(User.using(:alone_shard).count).to eq(1)
186
+ expect(User.using(:canada).count).to eq(0)
187
+ expect(User.using(:brazil).count).to eq(0)
188
+ expect(User.count).to eq(0)
189
+ end
190
+
191
+ it 'should work with named scopes' do
192
+ u = User.using(:brazil).create!(:name => 'Thiago')
193
+
194
+ expect(User.thiago.using(:brazil).first).to eq(u)
195
+ expect(User.using(:brazil).thiago.first).to eq(u)
196
+
197
+ Octopus.using(:brazil) do
198
+ expect(User.thiago.first).to eq(u)
199
+ end
200
+ end
201
+
202
+ describe '#current_shard attribute' do
203
+ it 'should store the attribute when you create or find an object' do
204
+ u = User.using(:alone_shard).create!(:name => 'Alone')
205
+ expect(u.current_shard).to eq(:alone_shard)
206
+ User.using(:canada).create!(:name => 'oi')
207
+ u = User.using(:canada).find_by_name('oi')
208
+ expect(u.current_shard).to eq(:canada)
209
+ end
210
+
211
+ it 'should store the attribute when you find multiple instances' do
212
+ 5.times { User.using(:alone_shard).create!(:name => 'Alone') }
213
+
214
+ User.using(:alone_shard).all.each do |u|
215
+ expect(u.current_shard).to eq(:alone_shard)
216
+ end
217
+ end
218
+
219
+ it 'should works when you find, and after that, alter that object' do
220
+ alone_user = User.using(:alone_shard).create!(:name => 'Alone')
221
+ _mstr_user = User.using(:master).create!(:name => 'Master')
222
+ alone_user.name = 'teste'
223
+ alone_user.save
224
+ expect(User.using(:master).first.name).to eq('Master')
225
+ expect(User.using(:alone_shard).first.name).to eq('teste')
226
+ end
227
+
228
+ it 'should work for the reload method' do
229
+ User.using(:alone_shard).create!(:name => 'Alone')
230
+ u = User.using(:alone_shard).find_by_name('Alone')
231
+ u.reload
232
+ expect(u.name).to eq('Alone')
233
+ end
234
+
235
+ it 'should work passing some arguments to reload method' do
236
+ User.using(:alone_shard).create!(:name => 'Alone')
237
+ u = User.using(:alone_shard).find_by_name('Alone')
238
+ u.reload(:lock => true)
239
+ expect(u.name).to eq('Alone')
240
+ end
241
+ end
242
+
243
+ describe 'passing a block' do
244
+ it 'should allow queries be executed inside the block, ponting to a specific shard' do
245
+ Octopus.using(:canada) do
246
+ User.create(:name => 'oi')
247
+ end
248
+
249
+ expect(User.using(:canada).count).to eq(1)
250
+ expect(User.using(:master).count).to eq(0)
251
+ expect(User.count).to eq(0)
252
+ end
253
+
254
+ it 'should allow execute queries inside a model' do
255
+ u = User.new
256
+ u.awesome_queries
257
+ expect(User.using(:canada).count).to eq(1)
258
+ expect(User.count).to eq(0)
259
+ end
260
+ end
261
+
262
+ describe 'raising errors' do
263
+ it "should raise a error when you specify a shard that doesn't exist" do
264
+ expect { User.using(:crazy_shard).create!(:name => 'Thiago') }.to raise_error('Nonexistent Shard Name: crazy_shard')
265
+ end
266
+ end
267
+
268
+ describe 'equality' do
269
+ let(:canada1) do
270
+ u = User.new
271
+ u.id = 1
272
+ u.current_shard = :canada
273
+ u
274
+ end
275
+
276
+ let(:canada1_dup) do
277
+ u = User.new
278
+ u.id = 1
279
+ u.current_shard = :canada
280
+ u
281
+ end
282
+
283
+ let(:brazil1) do
284
+ u = User.new
285
+ u.id = 1
286
+ u.current_shard = :brazil
287
+ u
288
+ end
289
+
290
+ it 'should work with persisted objects' do
291
+ u = User.using(:brazil).create(:name => 'Mike')
292
+ expect(User.using(:brazil).find_by_name('Mike')).to eq(u)
293
+ end
294
+
295
+ it 'should check current_shard when determining equality' do
296
+ expect(canada1).not_to eq(brazil1)
297
+ expect(canada1).to eq(canada1_dup)
298
+ end
299
+
300
+ it 'delegates equality check on scopes' do
301
+ u = User.using(:brazil).create!(:name => 'Mike')
302
+ expect(User.using(:brazil).where(:name => 'Mike')).to eq([u])
303
+ end
304
+ end
305
+ end
306
+
307
+ describe 'using a postgresql shard' do
308
+ it 'should update the Arel Engine' do
309
+ expect(User.using(:postgresql_shard).arel_engine.connection.adapter_name).to eq('PostgreSQL')
310
+ expect(User.using(:alone_shard).arel_engine.connection.adapter_name).to eq('Mysql2')
311
+ end
312
+
313
+ it 'should works with writes and reads' do
314
+ u = User.using(:postgresql_shard).create!(:name => 'PostgreSQL User')
315
+ expect(User.using(:postgresql_shard).all).to eq([u])
316
+ expect(User.using(:alone_shard).all).to eq([])
317
+ end
318
+ end
319
+
320
+ describe 'AR basic methods' do
321
+ it 'establish_connection' do
322
+ expect(CustomConnection.connection.current_database).to eq('octopus_shard_2')
323
+ end
324
+
325
+ it 'reuses parent model connection' do
326
+ klass = Class.new(CustomConnection)
327
+
328
+ expect(klass.connection).to be klass.connection
329
+ end
330
+
331
+ it 'should not mess with custom connection table names' do
332
+ expect(Advert.connection.current_database).to eq('octopus_shard_1')
333
+ Advert.create!(:name => 'Teste')
334
+ end
335
+
336
+ it 'increment' do
337
+ _ = User.using(:brazil).create!(:name => 'Teste', :number => 10)
338
+ u = User.using(:brazil).find_by_number(10)
339
+ u.increment(:number)
340
+ u.save
341
+ expect(User.using(:brazil).find_by_number(11)).not_to be_nil
342
+ end
343
+
344
+ it 'increment!' do
345
+ _ = User.using(:brazil).create!(:name => 'Teste', :number => 10)
346
+ u = User.using(:brazil).find_by_number(10)
347
+ u.increment!(:number)
348
+ expect(User.using(:brazil).find_by_number(11)).not_to be_nil
349
+ end
350
+
351
+ it 'decrement' do
352
+ _ = User.using(:brazil).create!(:name => 'Teste', :number => 10)
353
+ u = User.using(:brazil).find_by_number(10)
354
+ u.decrement(:number)
355
+ u.save
356
+ expect(User.using(:brazil).find_by_number(9)).not_to be_nil
357
+ end
358
+
359
+ it 'decrement!' do
360
+ _ = User.using(:brazil).create!(:name => 'Teste', :number => 10)
361
+ u = User.using(:brazil).find_by_number(10)
362
+ u.decrement!(:number)
363
+ expect(User.using(:brazil).find_by_number(9)).not_to be_nil
364
+ end
365
+
366
+ it 'toggle' do
367
+ _ = User.using(:brazil).create!(:name => 'Teste', :admin => false)
368
+ u = User.using(:brazil).find_by_name('Teste')
369
+ u.toggle(:admin)
370
+ u.save
371
+ expect(User.using(:brazil).find_by_name('Teste').admin).to be true
372
+ end
373
+
374
+ it 'toggle!' do
375
+ _ = User.using(:brazil).create!(:name => 'Teste', :admin => false)
376
+ u = User.using(:brazil).find_by_name('Teste')
377
+ u.toggle!(:admin)
378
+ expect(User.using(:brazil).find_by_name('Teste').admin).to be true
379
+ end
380
+
381
+ it 'count' do
382
+ _u = User.using(:brazil).create!(:name => 'User1')
383
+ _v = User.using(:brazil).create!(:name => 'User2')
384
+ _w = User.using(:brazil).create!(:name => 'User3')
385
+ expect(User.using(:brazil).where(:name => 'User2').all.count).to eq(1)
386
+ end
387
+
388
+ it 'maximum' do
389
+ _u = User.using(:brazil).create!(:name => 'Teste', :number => 11)
390
+ _v = User.using(:master).create!(:name => 'Teste', :number => 12)
391
+
392
+ expect(User.using(:brazil).maximum(:number)).to eq(11)
393
+ expect(User.using(:master).maximum(:number)).to eq(12)
394
+ end
395
+
396
+ describe 'any?' do
397
+ before { User.using(:brazil).create!(:name => 'User1') }
398
+
399
+ it 'works when true' do
400
+ scope = User.using(:brazil).where(:name => 'User1')
401
+ expect(scope.any?).to be true
402
+ end
403
+
404
+ it 'works when false' do
405
+ scope = User.using(:brazil).where(:name => 'User2')
406
+ expect(scope.any?).to be false
407
+ end
408
+ end
409
+
410
+ it 'exists?' do
411
+ @user = User.using(:brazil).create!(:name => 'User1')
412
+
413
+ expect(User.using(:brazil).where(:name => 'User1').exists?).to be true
414
+ expect(User.using(:brazil).where(:name => 'User2').exists?).to be false
415
+ end
416
+
417
+ describe 'touch' do
418
+ it 'updates updated_at by default' do
419
+ @user = User.using(:brazil).create!(:name => 'User1')
420
+ User.using(:brazil).where(:id => @user.id).update_all(:updated_at => Time.now - 3.months)
421
+ @user.touch
422
+ expect(@user.reload.updated_at.in_time_zone('GMT').to_date).to eq(Time.now.in_time_zone('GMT').to_date)
423
+ end
424
+
425
+ it 'updates passed in attribute name' do
426
+ @user = User.using(:brazil).create!(:name => 'User1')
427
+ User.using(:brazil).where(:id => @user.id).update_all(:created_at => Time.now - 3.months)
428
+ @user.touch(:created_at)
429
+ expect(@user.reload.created_at.in_time_zone('GMT').to_date).to eq(Time.now.in_time_zone('GMT').to_date)
430
+ end
431
+ end
432
+
433
+ describe '#pluck' do
434
+ before { User.using(:brazil).create!(:name => 'User1') }
435
+
436
+ it 'should works from scope proxy' do
437
+ names = User.using(:brazil).pluck(:name)
438
+ expect(names).to eq(['User1'])
439
+ expect(User.using(:master).pluck(:name)).to eq([])
440
+ end
441
+ end
442
+
443
+ it 'update_column' do
444
+ @user = User.using(:brazil).create!(:name => 'User1')
445
+ @user2 = User.using(:brazil).find(@user.id)
446
+ @user2.update_column(:name, 'Joaquim Shard Brazil')
447
+ expect(User.using(:brazil).find_by_name('Joaquim Shard Brazil')).not_to be_nil
448
+ end
449
+
450
+ it 'update_attributes' do
451
+ @user = User.using(:brazil).create!(:name => 'User1')
452
+ @user2 = User.using(:brazil).find(@user.id)
453
+ @user2.update_attributes(:name => 'Joaquim')
454
+ expect(User.using(:brazil).find_by_name('Joaquim')).not_to be_nil
455
+ end
456
+
457
+ it 'using update_attributes inside a block' do
458
+ Octopus.using(:brazil) do
459
+ @user = User.create!(:name => 'User1')
460
+ @user2 = User.find(@user.id)
461
+ @user2.update_attributes(:name => 'Joaquim')
462
+ end
463
+
464
+ expect(User.find_by_name('Joaquim')).to be_nil
465
+ expect(User.using(:brazil).find_by_name('Joaquim')).not_to be_nil
466
+ end
467
+
468
+ it 'update_attribute' do
469
+ @user = User.using(:brazil).create!(:name => 'User1')
470
+ @user2 = User.using(:brazil).find(@user.id)
471
+ @user2.update_attribute(:name, 'Joaquim')
472
+ expect(User.using(:brazil).find_by_name('Joaquim')).not_to be_nil
473
+ end
474
+
475
+ it 'as_json' do
476
+ ActiveRecord::Base.include_root_in_json = false
477
+
478
+ Octopus.using(:brazil) do
479
+ User.create!(:name => 'User1')
480
+ end
481
+
482
+ user = User.using(:brazil).where(:name => 'User1').first
483
+ expect(user.as_json(:except => [:created_at, :updated_at, :id])).to eq('admin' => nil, 'name' => 'User1', 'number' => nil)
484
+ end
485
+
486
+ it 'transaction' do
487
+ _u = User.create!(:name => 'Thiago')
488
+
489
+ expect(User.using(:brazil).count).to eq(0)
490
+ expect(User.using(:master).count).to eq(1)
491
+
492
+ User.using(:brazil).transaction do
493
+ expect(User.find_by_name('Thiago')).to be_nil
494
+ User.create!(:name => 'Brazil')
495
+ end
496
+
497
+ expect(User.using(:brazil).count).to eq(1)
498
+ expect(User.using(:master).count).to eq(1)
499
+ end
500
+
501
+ describe "#finder methods" do
502
+ before(:each) do
503
+ @user1 = User.using(:brazil).create!(:name => 'User1')
504
+ @user2 = User.using(:brazil).create!(:name => 'User2')
505
+ @user3 = User.using(:brazil).create!(:name => 'User3')
506
+ end
507
+
508
+ it "#find_each should work" do
509
+ result_array = []
510
+
511
+ User.using(:brazil).where("name is not NULL").find_each do |user|
512
+ result_array << user
513
+ end
514
+
515
+ expect(result_array).to eq([@user1, @user2, @user3])
516
+ end
517
+
518
+ it "#find_in_batches, should work" do
519
+ result_array = []
520
+
521
+ User.using(:brazil).where("name is not NULL").find_in_batches(batch_size: 1) do |user|
522
+ result_array << user
523
+ end
524
+
525
+ expect(result_array).to eq([[@user1], [@user2], [@user3]])
526
+ end
527
+ end
528
+
529
+ describe 'deleting a record' do
530
+ before(:each) do
531
+ @user = User.using(:brazil).create!(:name => 'User1')
532
+ @user2 = User.using(:brazil).find(@user.id)
533
+ end
534
+
535
+ it 'delete' do
536
+ @user2.delete
537
+ expect { User.using(:brazil).find(@user2.id) }.to raise_error(ActiveRecord::RecordNotFound)
538
+ end
539
+
540
+ it "delete within block shouldn't lose shard" do
541
+ Octopus.using(:brazil) do
542
+ @user2.delete
543
+ @user3 = User.create(:name => 'User3')
544
+
545
+ expect(User.connection.current_shard).to eq(:brazil)
546
+ expect(User.find(@user3.id)).to eq(@user3)
547
+ end
548
+ end
549
+
550
+ it 'destroy' do
551
+ @user2.destroy
552
+ expect { User.using(:brazil).find(@user2.id) }.to raise_error(ActiveRecord::RecordNotFound)
553
+ end
554
+
555
+ it "destroy within block shouldn't lose shard" do
556
+ Octopus.using(:brazil) do
557
+ @user2.destroy
558
+ @user3 = User.create(:name => 'User3')
559
+
560
+ expect(User.connection.current_shard).to eq(:brazil)
561
+ expect(User.find(@user3.id)).to eq(@user3)
562
+ end
563
+ end
564
+ end
565
+ end
566
+
567
+ describe 'custom connection' do
568
+ context 'by default' do
569
+ it 'with plain call should use custom connection' do
570
+ expect(CustomConnection.connection.current_database).to eq('octopus_shard_2')
571
+ end
572
+
573
+ it 'should ignore using called on relation' do
574
+ expect(CustomConnection.using(:postgresql_shard).connection.current_database).to eq('octopus_shard_2')
575
+ end
576
+
577
+ it 'should ignore Octopus.using block' do
578
+ Octopus.using(:postgresql_shard) do
579
+ expect(CustomConnection.connection.current_database).to eq('octopus_shard_2')
580
+ end
581
+ end
582
+
583
+ it 'should save to correct shard' do
584
+ expect { CustomConnection.create(:value => 'custom value') }.to change {
585
+ CustomConnection
586
+ .connection
587
+ .execute("select count(*) as ct from custom where value = 'custom value'")
588
+ .to_a.first.first
589
+ }.by 1
590
+ end
591
+ end
592
+
593
+ context 'with allowed_shards configured' do
594
+ before do
595
+ CustomConnection.allow_shard :postgresql_shard
596
+ end
597
+
598
+ it 'with plain call should use custom connection' do
599
+ expect(CustomConnection.connection.current_database).to eq('octopus_shard_2')
600
+ end
601
+
602
+ it 'with using called on relation with allowed shard should use' do
603
+ expect(CustomConnection.using(:postgresql_shard).connection.current_database).to eq('octopus_shard_1')
604
+ end
605
+
606
+ it 'within Octopus.using block with allowed shard should use' do
607
+ Octopus.using(:postgresql_shard) do
608
+ expect(CustomConnection.connection.current_database).to eq('octopus_shard_1')
609
+ end
610
+ end
611
+
612
+ it 'with using called on relation with disallowed shard should not use' do
613
+ expect(CustomConnection.using(:brazil).connection.current_database).to eq('octopus_shard_2')
614
+ end
615
+
616
+ it 'within Octopus.using block with disallowed shard should not use' do
617
+ Octopus.using(:brazil) do
618
+ expect(CustomConnection.connection.current_database).to eq('octopus_shard_2')
619
+ end
620
+ end
621
+
622
+ it 'should save to correct shard' do
623
+ expect { CustomConnection.create(:value => 'custom value') }.to change {
624
+ CustomConnection
625
+ .connection
626
+ .execute("select count(*) as ct from custom where value = 'custom value'")
627
+ .to_a.first.first
628
+ }.by 1
629
+ end
630
+
631
+ it 'should clean up correctly' do
632
+ User.create!(:name => 'CleanUser')
633
+ CustomConnection.using(:postgresql_shard).first
634
+ expect(User.first).not_to be_nil
635
+ end
636
+
637
+ it 'should clean up correctly even inside block' do
638
+ User.create!(:name => 'CleanUser')
639
+
640
+ Octopus.using(:master) do
641
+ CustomConnection.using(:postgresql_shard).connection.execute('select count(*) from users')
642
+ expect(User.first).not_to be_nil
643
+ end
644
+ end
645
+ end
646
+
647
+ describe 'clear_active_connections!' do
648
+ it 'should not leak connection' do
649
+ CustomConnection.create(:value => 'custom value')
650
+
651
+ # This is what Rails, Sidekiq etc call--this normally handles all connection pools in the app
652
+ expect { ActiveRecord::Base.clear_active_connections! }
653
+ .to change { CustomConnection.connection_pool.active_connection? }
654
+
655
+ expect(CustomConnection.connection_pool.active_connection?).to be_falsey
656
+ end
657
+ end
658
+ end
659
+
660
+ describe 'when using set_table_name' do
661
+ it 'should work correctly' do
662
+ Bacon.using(:brazil).create!(:name => 'YUMMMYYYY')
663
+ end
664
+
665
+ it 'should work correctly with a block' do
666
+ Cheese.using(:brazil).create!(:name => 'YUMMMYYYY')
667
+ end
668
+ end
669
+
670
+ describe 'when using table_name=' do
671
+ it 'should work correctly' do
672
+ Ham.using(:brazil).create!(:name => 'YUMMMYYYY')
673
+ end
674
+ end
675
+
676
+ describe 'when using a environment with a single adapter' do
677
+ it 'should not clean the table name' do
678
+ OctopusHelper.using_environment :production_fully_replicated do
679
+ expect(Keyboard).not_to receive(:reset_table_name)
680
+ Keyboard.using(:master).create!(:name => 'Master Cat')
681
+ end
682
+ end
683
+ end
684
+
685
+ describe 'when you have joins/include' do
686
+ before(:each) do
687
+ @client1 = Client.using(:brazil).create(:name => 'Thiago')
688
+
689
+ Octopus.using(:canada) do
690
+ @client2 = Client.create(:name => 'Mike')
691
+ @client3 = Client.create(:name => 'Joao')
692
+ @item1 = Item.create(:client => @client2, :name => 'Item 1')
693
+ @item2 = Item.create(:client => @client2, :name => 'Item 2')
694
+ @item3 = Item.create(:client => @client3, :name => 'Item 3')
695
+ @part1 = Part.create(:item => @item1, :name => 'Part 1')
696
+ @part2 = Part.create(:item => @item1, :name => 'Part 2')
697
+ @part3 = Part.create(:item => @item2, :name => 'Part 3')
698
+ end
699
+
700
+ @item4 = Item.using(:brazil).create(:client => @client1, :name => 'Item 4')
701
+ end
702
+
703
+ it 'should work using the rails 3.x syntax' do
704
+ items = Item.using(:canada).joins(:client).where("clients.id = #{@client2.id}").all
705
+ expect(items).to eq([@item1, @item2])
706
+ end
707
+
708
+ it 'should work for include also, rails 3.x syntax' do
709
+ items = Item.using(:canada).includes(:client).where(:clients => { :id => @client2.id }).all
710
+ expect(items).to eq([@item1, @item2])
711
+ end
712
+ end
713
+
714
+ describe 'ActiveRecord::Base Validations' do
715
+ it 'should work correctly when using validations' do
716
+ @key = Keyboard.create!(:name => 'Key')
717
+ expect { Keyboard.using(:brazil).create!(:name => 'Key') }.not_to raise_error
718
+ expect { Keyboard.create!(:name => 'Key') }.to raise_error(ActiveRecord::RecordInvalid)
719
+ end
720
+
721
+ it 'should work correctly when using validations with using syntax' do
722
+ @key = Keyboard.using(:brazil).create!(:name => 'Key')
723
+ expect { Keyboard.create!(:name => 'Key') }.not_to raise_error
724
+ expect { Keyboard.using(:brazil).create!(:name => 'Key') }
725
+ .to raise_error(ActiveRecord::RecordInvalid)
726
+ end
727
+ end
728
+
729
+ describe '#replicated_model method' do
730
+ it 'should be replicated' do
731
+ OctopusHelper.using_environment :production_replicated do
732
+ expect(ActiveRecord::Base.connection_proxy.replicated).to be true
733
+ end
734
+ end
735
+
736
+ it 'should mark the Cat model as replicated' do
737
+ OctopusHelper.using_environment :production_replicated do
738
+ expect(User.replicated).to be_falsey
739
+ expect(Cat.replicated).to be true
740
+ end
741
+ end
742
+
743
+ it "should work on a fully replicated environment" do
744
+ OctopusHelper.using_environment :production_fully_replicated do
745
+ User.using(:slave1).create!(name: 'Thiago')
746
+ User.using(:slave2).create!(name: 'Thiago')
747
+
748
+ replicated_cat = User.find_by_name 'Thiago'
749
+
750
+ expect(replicated_cat.current_shard.to_s).to match(/master/)
751
+ end
752
+ end
753
+ end
754
+ end