octoball 0.1.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.
@@ -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