octoball 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,130 @@
1
+ require 'spec_helper'
2
+
3
+ describe Octoball::RelationProxy do
4
+ describe 'shard tracking' do
5
+ before :each do
6
+ @client = Client.using(:canada).create!
7
+ @client.items << Item.using(:canada).create!
8
+ @relation = @client.items
9
+ end
10
+
11
+ it 'remembers the shard on which a relation was created' do
12
+ expect(@relation.current_shard).to eq(:canada)
13
+ end
14
+
15
+ it 'can define collection association with the same name as ancestor private method' do
16
+ @client.comments << Comment.using(:canada).create!(open: true)
17
+ expect(@client.comments.open.ar_relation).to be_a_kind_of(ActiveRecord::Relation)
18
+ end
19
+
20
+ it 'can be dumped and loaded' do
21
+ expect(Marshal.load(Marshal.dump(@relation))).to eq @relation
22
+ end
23
+
24
+ it 'is flattenable' do
25
+ expect([@relation].flatten).to eq @relation.to_a
26
+ end
27
+
28
+ it 'maintains the current shard when using where.not(...)' do
29
+ where_chain = @relation.where
30
+ expect(where_chain.current_shard).to eq(@relation.current_shard)
31
+ not_relation = where_chain.not("1=0")
32
+ expect(not_relation.current_shard).to eq(@relation.current_shard)
33
+ end
34
+
35
+ context 'when a new relation is constructed from the original relation' do
36
+ context 'and a where(...) is used' do
37
+ it 'does not tamper with the original relation' do
38
+ relation = Item.using(:canada).where(id: 1)
39
+ original_sql = relation.to_sql
40
+ new_relation = relation.where(id: 2)
41
+ expect(relation.to_sql).to eq(original_sql)
42
+ end
43
+ end
44
+
45
+ context 'and a where.not(...) is used' do
46
+ it 'does not tamper with the original relation' do
47
+ relation = Item.using(:canada).where(id: 1)
48
+ original_sql = relation.to_sql
49
+ new_relation = relation.where.not(id: 2)
50
+ expect(relation.to_sql).to eq(original_sql)
51
+ end
52
+ end
53
+ end
54
+
55
+ context 'when comparing to other Relation objects' do
56
+ before :each do
57
+ @relation.reset
58
+ end
59
+
60
+ it 'is equal to its clone' do
61
+ expect(@relation).to eq(@relation.clone)
62
+ end
63
+ end
64
+
65
+ it "can deliver methods in ActiveRecord::Batches correctly when given a block" do
66
+ expect { @relation.find_each(&:inspect) }.not_to raise_error
67
+ end
68
+
69
+ it "can deliver methods in ActiveRecord::Batches correctly as an enumerator" do
70
+ expect { @relation.find_each.each(&:inspect) }.not_to raise_error
71
+ end
72
+
73
+ it "can deliver methods in ActiveRecord::Batches correctly as a lazy enumerator" do
74
+ expect { @relation.find_each.lazy.each(&:inspect) }.not_to raise_error
75
+ end
76
+
77
+ it 'should be able to return its ActiveRecord::Relation' do
78
+ expect(@relation.ar_relation.is_a? ActiveRecord::Relation).to eq true
79
+ end
80
+
81
+ it 'is equal to an identically-defined, but different, RelationProxy' do
82
+ i = @client.items
83
+ expect(@relation).to eq(i)
84
+ expect(@relation.__id__).not_to eq(i.__id__)
85
+ end
86
+
87
+ it 'is equal to its own underlying ActiveRecord::Relation' do
88
+ expect(@relation).to eq(@relation.ar_relation)
89
+ expect(@relation.ar_relation).to eq(@relation)
90
+ end
91
+
92
+ context 'when no explicit shard context is provided' do
93
+ it 'uses the correct shard' do
94
+ expect(@relation.count).to eq(1)
95
+ end
96
+
97
+ it 'lazily evaluates on the correct shard' do
98
+ # Do something to force Client.connection_proxy.current_shard to change
99
+ _some_count = Client.using(:brazil).count
100
+ expect(@relation.select(:client_id).count).to eq(1)
101
+ end
102
+ end
103
+
104
+ context 'when an explicit, but different, shard context is provided' do
105
+ it 'uses the correct shard' do
106
+ expect(Item.using(:brazil).count).to eq(0)
107
+ _clients_on_brazil = Client.using(:brazil).all
108
+ Octoball.using(:brazil) do
109
+ expect(@relation.count).to eq(1)
110
+ end
111
+ end
112
+
113
+ it 'uses the correct shard in block when method_missing is triggered on CollectionProxy objects' do
114
+ Octoball.using(:brazil) do
115
+ @client.items.each do |item|
116
+ expect(item.current_shard).to eq(:canada)
117
+ expect(ActiveRecord::Base.connection.current_shard).to eq(:brazil)
118
+ end
119
+ end
120
+ end
121
+
122
+ it 'lazily evaluates on the correct shard' do
123
+ expect(Item.using(:brazil).count).to eq(0)
124
+ Octoball.using(:brazil) do
125
+ expect(@relation.select(:client_id).count).to eq(1)
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,97 @@
1
+ require 'spec_helper'
2
+
3
+ describe Octoball do
4
+ it 'should allow nested queries' do
5
+ @user1 = User.using(:brazil).create!(:name => 'Thiago P', :number => 3)
6
+ @user2 = User.using(:brazil).create!(:name => 'Thiago', :number => 1)
7
+ @user3 = User.using(:brazil).create!(:name => 'Thiago', :number => 2)
8
+
9
+ expect(User.using(:brazil).where(:name => 'Thiago').where(:number => 4).order(:number).all).to eq([])
10
+ expect(User.using(:brazil).where(:name => 'Thiago').using(:canada).where(:number => 2).using(:brazil).order(:number).all).to eq([@user3])
11
+ expect(User.using(:brazil).where(:name => 'Thiago').using(:canada).where(:number => 4).using(:brazil).order(:number).all).to eq([])
12
+ end
13
+
14
+ context 'When array-like-selecting an item in a group' do
15
+ before(:each) do
16
+ User.using(:brazil).create!(:name => 'Evan', :number => 1)
17
+ User.using(:brazil).create!(:name => 'Evan', :number => 2)
18
+ User.using(:brazil).create!(:name => 'Evan', :number => 3)
19
+ @evans = User.using(:brazil).where(:name => 'Evan')
20
+ end
21
+
22
+ it 'allows a block to select an item' do
23
+ expect(@evans.select { |u| u.number == 2 }.first.number).to eq(2)
24
+ end
25
+ end
26
+
27
+ context 'When selecting a field within a scope' do
28
+ before(:each) do
29
+ User.using(:brazil).create!(:name => 'Evan', :number => 4)
30
+ @evan = User.using(:brazil).where(:name => 'Evan')
31
+ end
32
+
33
+ it 'allows single field selection' do
34
+ expect(@evan.select('name').first.name).to eq('Evan')
35
+ end
36
+
37
+ it 'allows selection by array' do
38
+ expect(@evan.select(['name']).first.name).to eq('Evan')
39
+ end
40
+
41
+ it 'allows multiple selection by string' do
42
+ expect(@evan.select('id, name').first.id).to be_a(Fixnum)
43
+ end
44
+
45
+ it 'allows multiple selection by array' do
46
+ expect(@evan.select(%w(id name)).first.id).to be_a(Fixnum)
47
+ end
48
+
49
+ it 'allows multiple selection by symbol' do
50
+ expect(@evan.select(:id, :name).first.id).to be_a(Fixnum)
51
+ end
52
+
53
+ it 'allows multiple selection by string and symbol' do
54
+ expect(@evan.select(:id, 'name').first.id).to be_a(Fixnum)
55
+ end
56
+ end
57
+
58
+ it "should raise a exception when trying to send a query to a shard that don't exists" do
59
+ expect { User.using(:dont_exists).first }.to raise_exception(ActiveRecord::ConnectionNotEstablished)
60
+ end
61
+
62
+ context "dup / clone" do
63
+ before(:each) do
64
+ User.using(:brazil).create!(:name => 'Thiago', :number => 1)
65
+ end
66
+
67
+ it "should change it's object id" do
68
+ user = User.using(:brazil).where(id: 1)
69
+ dupped_object = user.dup
70
+ cloned_object = user.clone
71
+
72
+ expect(dupped_object.object_id).not_to eq(user.object_id)
73
+ expect(cloned_object.object_id).not_to eq(user.object_id)
74
+ end
75
+ end
76
+
77
+ context 'When iterated with Enumerable methods' do
78
+ before(:each) do
79
+ User.using(:brazil).create!(:name => 'Evan', :number => 1)
80
+ User.using(:brazil).create!(:name => 'Evan', :number => 2)
81
+ User.using(:brazil).create!(:name => 'Evan', :number => 3)
82
+ @evans = User.using(:brazil).where(:name => 'Evan')
83
+ end
84
+
85
+ it 'allows each method' do
86
+ expect(@evans.each.count).to eq(3)
87
+ end
88
+
89
+ it 'allows each_with_index method' do
90
+ expect(@evans.each_with_index.to_a.flatten.count).to eq(6)
91
+ end
92
+
93
+ it 'allows map method' do
94
+ expect(@evans.map(&:number)).to eq([1, 2, 3])
95
+ end
96
+ end
97
+ end
File without changes
@@ -0,0 +1,31 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'octoball'
4
+
5
+ Octoball.instance_variable_set(:@directory, File.dirname(__FILE__))
6
+
7
+ # Requires supporting files with custom matchers and macros, etc,
8
+ # in ./support/ and its subdirectories.
9
+ Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].sort.each { |f| require f }
10
+
11
+ RSpec.configure do |config|
12
+ mysql_spec = {
13
+ adapter: 'mysql2',
14
+ host: (ENV['MYSQL_HOST'] || 'localhost'),
15
+ username: (ENV['MYSQL_USER'] || 'root'),
16
+ encoding: 'utf8mb4',
17
+ }
18
+ ActiveRecord::Base.configurations = {
19
+ "test" => {
20
+ shard1: mysql_spec.merge(database: 'octoball_shard_1'),
21
+ shard2: mysql_spec.merge(database: 'octoball_shard_2'),
22
+ shard3: mysql_spec.merge(database: 'octoball_shard_3'),
23
+ shard4: mysql_spec.merge(database: 'octoball_shard_4'),
24
+ shard5: mysql_spec.merge(database: 'octoball_shard_5'),
25
+ }
26
+ }
27
+
28
+ config.before(:each) do |example|
29
+ TestHelper.clean_all_shards(example.metadata[:shards])
30
+ end
31
+ end
@@ -0,0 +1,22 @@
1
+ require 'logger'
2
+
3
+ ActiveRecord::Base.logger = Logger.new(File.open('database.log', 'a'))
4
+
5
+ mysql_spec = {
6
+ adapter: 'mysql2',
7
+ host: (ENV['MYSQL_HOST'] || 'localhost'),
8
+ username: (ENV['MYSQL_USER'] || 'root'),
9
+ encoding: 'utf8mb4',
10
+ }
11
+
12
+ ActiveRecord::Base.configurations = {
13
+ "test" => {
14
+ shard1: mysql_spec.merge(database: 'octoball_shard_1'),
15
+ shard2: mysql_spec.merge(database: 'octoball_shard_2'),
16
+ shard3: mysql_spec.merge(database: 'octoball_shard_3'),
17
+ shard4: mysql_spec.merge(database: 'octoball_shard_4'),
18
+ shard5: mysql_spec.merge(database: 'octoball_shard_5'),
19
+ }
20
+ }
21
+ ActiveRecord::Base.establish_connection(
22
+ ActiveRecord::Base.configurations.configs_for(env_name: "test").first)
@@ -0,0 +1,115 @@
1
+ require './spec/models/application_record.rb'
2
+
3
+ # The user class is just sharded, not replicated
4
+ class User < ApplicationRecord
5
+ scope :thiago, -> { where(:name => 'Thiago') }
6
+
7
+ def awesome_queries
8
+ Octoball.using(:canada) do
9
+ User.create(:name => 'teste')
10
+ end
11
+ end
12
+ end
13
+
14
+ # The client class isn't replicated
15
+ class Client < ApplicationRecord
16
+ has_many :items
17
+ has_many :comments, :as => :commentable
18
+ end
19
+
20
+ # This class sets its own connection
21
+ class CustomConnectionBase < ActiveRecord::Base
22
+ self.abstract_class = true
23
+ establish_connection(:adapter => 'mysql2', :host => (ENV['MYSQL_HOST'] || 'localhost'), :database => 'octoball_shard_2', :username => "#{ENV['MYSQL_USER'] || 'root'}", :password => '')
24
+ connects_to shards: {
25
+ custom_shard: { writing: :shard3 }
26
+ }
27
+ end
28
+
29
+ class CustomConnection < CustomConnectionBase
30
+ self.table_name = 'custom'
31
+ end
32
+
33
+ # This items belongs to a client
34
+ class Item < ApplicationRecord
35
+ belongs_to :client
36
+ has_many :parts
37
+ end
38
+
39
+ class Part < ApplicationRecord
40
+ belongs_to :item
41
+ end
42
+
43
+ class Keyboard < ApplicationRecord
44
+ validates_uniqueness_of(:name, case_sensitive: true)
45
+ belongs_to :computer
46
+ end
47
+
48
+ class Computer < ApplicationRecord
49
+ has_one :keyboard
50
+ end
51
+
52
+ class Role < ApplicationRecord
53
+ has_and_belongs_to_many :permissions
54
+ end
55
+
56
+ class Permission < ApplicationRecord
57
+ has_and_belongs_to_many :roles
58
+ end
59
+
60
+ class Assignment < ApplicationRecord
61
+ belongs_to :programmer
62
+ belongs_to :project
63
+ end
64
+
65
+ class Programmer < ApplicationRecord
66
+ has_many :assignments
67
+ has_many :projects, :through => :assignments
68
+ end
69
+
70
+ class Project < ApplicationRecord
71
+ has_many :assignments
72
+ has_many :programmers, :through => :assignments
73
+ end
74
+
75
+ class Comment < ApplicationRecord
76
+ belongs_to :commentable, :polymorphic => true
77
+ scope :open, -> { where(open: true) }
78
+ end
79
+
80
+ class Bacon < ApplicationRecord
81
+ self.table_name = 'yummy'
82
+ end
83
+
84
+ class Cheese < ApplicationRecord
85
+ self.table_name = 'yummy'
86
+ end
87
+
88
+ class Ham < ApplicationRecord
89
+ self.table_name = 'yummy'
90
+ end
91
+
92
+ # This class sets its own connection
93
+ class Advert < ApplicationRecord
94
+ end
95
+
96
+ class MmorpgPlayer < ApplicationRecord
97
+ has_many :weapons
98
+ has_many :skills
99
+ end
100
+
101
+ class Weapon < ApplicationRecord
102
+ belongs_to :mmorpg_player, :inverse_of => :weapons
103
+ validates :hand, :uniqueness => { :scope => :mmorpg_player_id }
104
+ validates_presence_of :mmorpg_player
105
+ has_many :skills
106
+ end
107
+
108
+ class Skill < ApplicationRecord
109
+ belongs_to :weapon, :inverse_of => :skills
110
+ belongs_to :mmorpg_player, :inverse_of => :skills
111
+
112
+ validates_presence_of :weapon
113
+ validates_presence_of :mmorpg_player
114
+ validates :name, :uniqueness => { :scope => :mmorpg_player_id }
115
+ end
@@ -0,0 +1,17 @@
1
+ module ActiveRecord
2
+ class QueryCounter
3
+ attr_accessor :query_count
4
+
5
+ def initialize
6
+ @query_count = 0
7
+ end
8
+
9
+ def to_proc
10
+ lambda(&method(:callback))
11
+ end
12
+
13
+ def callback(_name, _start, _finish, _message_id, values)
14
+ @query_count += 1 unless %w(CACHE SCHEMA).include?(values[:name])
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ shared_context 'with query cache enabled' do
2
+ let!(:counter) { ActiveRecord::QueryCounter.new }
3
+
4
+ before(:each) do
5
+ ActiveRecord::Base.connection.enable_query_cache!
6
+ counter.query_count = 0
7
+ end
8
+
9
+ after(:each) do
10
+ ActiveRecord::Base.connection.disable_query_cache!
11
+ end
12
+
13
+ around(:each) do |example|
14
+ active_support_subscribed(counter.to_proc, 'sql.active_record') do
15
+ example.run
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,14 @@
1
+ module TestHelper
2
+ def self.clean_all_shards(shards)
3
+ (shards || [:master, :brazil, :canada, :russia, :alone_shard]).each do |shard_symbol|
4
+ %w(users clients cats items keyboards computers permissions_roles roles permissions assignments projects programmers yummy adverts).each do |tables|
5
+ ApplicationRecord.using(shard_symbol).connection.execute("DELETE FROM #{tables}")
6
+ end
7
+ if shard_symbol == :alone_shard
8
+ %w(mmorpg_players weapons skills).each do |table|
9
+ ApplicationRecord.using(shard_symbol).connection.execute("DELETE FROM #{table}")
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end