activerecord-futures 0.0.1 → 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.
Files changed (35) hide show
  1. data/.travis.yml +20 -0
  2. data/Gemfile +11 -1
  3. data/README.md +43 -23
  4. data/Rakefile +17 -0
  5. data/activerecord-futures.gemspec +3 -1
  6. data/lib/active_record/connection_adapters/future_enabled.rb +34 -0
  7. data/lib/active_record/connection_adapters/future_enabled_mysql2_adapter.rb +23 -22
  8. data/lib/active_record/connection_adapters/future_enabled_postgresql_adapter.rb +52 -0
  9. data/lib/active_record/futures.rb +10 -9
  10. data/lib/active_record/futures/delegation.rb +1 -0
  11. data/lib/active_record/futures/future.rb +22 -6
  12. data/lib/active_record/futures/future_calculation.rb +4 -12
  13. data/lib/active_record/futures/future_calculation_array.rb +8 -0
  14. data/lib/active_record/futures/future_calculation_value.rb +11 -0
  15. data/lib/active_record/futures/future_relation.rb +12 -11
  16. data/lib/active_record/futures/proxy.rb +37 -0
  17. data/lib/active_record/futures/query_recording.rb +12 -26
  18. data/lib/activerecord-futures.rb +3 -0
  19. data/lib/activerecord-futures/version.rb +1 -1
  20. data/spec/active_record/futures/future_relation_spec.rb +9 -1
  21. data/spec/active_record/futures/proxy_spec.rb +51 -0
  22. data/spec/db/schema.rb +15 -6
  23. data/spec/in_action/combination_of_futures_spec.rb +153 -0
  24. data/spec/in_action/future_count_execution_spec.rb +98 -0
  25. data/spec/in_action/future_fulfillment_spec.rb +10 -7
  26. data/spec/in_action/future_pluck_execution_spec.rb +44 -0
  27. data/spec/in_action/future_relation_execution_spec.rb +52 -0
  28. data/spec/models/comment.rb +4 -0
  29. data/spec/models/post.rb +3 -0
  30. data/spec/models/user.rb +1 -0
  31. data/spec/spec_helper.rb +45 -9
  32. data/spec/support/matchers/exec.rb +66 -0
  33. data/spec/support/matchers/exec_query.rb +23 -0
  34. metadata +60 -6
  35. data/spec/models/country.rb +0 -2
@@ -4,21 +4,12 @@ module ActiveRecord
4
4
  attr_reader :query, :execution
5
5
  private :query, :execution
6
6
 
7
- def initialize(query, execution)
8
- super()
7
+ def initialize(relation, query, execution)
8
+ super(relation)
9
9
  @query = query
10
10
  @execution = execution
11
11
  end
12
12
 
13
- def value
14
- Future.flush unless executed?
15
- @value
16
- end
17
-
18
- def inspect
19
- value.inspect
20
- end
21
-
22
13
  def to_sql
23
14
  query
24
15
  end
@@ -26,8 +17,9 @@ module ActiveRecord
26
17
  private
27
18
 
28
19
  def execute
29
- @value = execution.call
20
+ @value = execution.call unless executed?
30
21
  @executed = true
22
+ @value
31
23
  end
32
24
 
33
25
  def executed?
@@ -0,0 +1,8 @@
1
+ module ActiveRecord
2
+ module Futures
3
+ class FutureCalculationArray < FutureCalculation
4
+ include ActiveRecord::Delegation
5
+ fetch_with(:to_a)
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ module ActiveRecord
2
+ module Futures
3
+ class FutureCalculationValue < FutureCalculation
4
+ fetch_with(:value)
5
+
6
+ def inspect
7
+ value.inspect
8
+ end
9
+ end
10
+ end
11
+ end
@@ -1,22 +1,23 @@
1
1
  module ActiveRecord
2
2
  module Futures
3
3
  class FutureRelation < Future
4
- include Delegation
5
- delegate :to_sql, to: :relation
6
-
7
- attr_reader :relation
8
- private :relation
4
+ include ActiveRecord::Delegation
9
5
 
10
6
  def initialize(relation)
11
- super()
12
- @relation = relation
7
+ super
13
8
  @klass = relation.klass
9
+
10
+ # Eagerly get sql from relation, since PostgreSQL adapter may use the
11
+ # same method `exec_query` to retrieve the columns when executing
12
+ # `to_sql`, and that will cause an infinite loop if a current future
13
+ # exists
14
+ @query = relation.to_sql
14
15
  end
15
16
 
16
- def to_a
17
- # Flush all the futures upon first attempt to exec a future
18
- Future.flush unless executed?
19
- execute
17
+ fetch_with(:to_a)
18
+
19
+ def to_sql
20
+ @query
20
21
  end
21
22
 
22
23
  private
@@ -0,0 +1,37 @@
1
+ module ActiveRecord
2
+ module Futures
3
+ class Proxy
4
+ attr_reader :proxied
5
+
6
+ def initialize(obj)
7
+ @proxied = obj
8
+ end
9
+
10
+ def proxy?
11
+ true
12
+ end
13
+
14
+ def ==(other)
15
+ other = other.proxied if other.is_a? self.class
16
+ @proxied == other
17
+ end
18
+
19
+ def !=(other)
20
+ other = other.proxied if other.is_a? self.class
21
+ @proxied != other
22
+ end
23
+
24
+ def method_missing(method, *args, &block)
25
+ if @proxied.respond_to?(method)
26
+ @proxied.send(method, *args, &block)
27
+ else
28
+ super
29
+ end
30
+ end
31
+
32
+ def respond_to?(method, include_all = false)
33
+ method.to_sym == :proxy? || super || @proxied.respond_to?(method, include_all)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -8,61 +8,47 @@ module ActiveRecord
8
8
  connection = ConnectionProxy.new(@klass.connection)
9
9
  @klass = KlassProxy.new(@klass, connection)
10
10
  yield
11
- connection.recorded_query
11
+ [connection.recorded_query, connection.query_type]
12
12
  ensure
13
13
  @klass = orig_klass
14
14
  end
15
15
 
16
- class KlassProxy
16
+ class KlassProxy < Proxy
17
17
  attr_reader :klass, :connection
18
18
 
19
19
  def initialize(klass, connection)
20
+ super(klass)
20
21
  @klass = klass
21
22
  @connection = connection
22
23
  end
23
24
 
24
- def method_missing(method, *args, &block)
25
- if klass.respond_to?(method)
26
- klass.send(method, *args, &block)
27
- else
28
- super
29
- end
30
- end
31
-
32
- def respond_to?(method, include_all = false)
33
- super || klass.respond_to?(method, include_all)
25
+ def build_default_scope
26
+ scope = @klass.send(:build_default_scope)
27
+ scope.instance_variable_set(:@klass, self)
28
+ scope
34
29
  end
35
30
  end
36
31
 
37
- class ConnectionProxy
32
+ class ConnectionProxy < Proxy
38
33
  attr_reader :connection
39
- attr_accessor :recorded_query
34
+ attr_accessor :recorded_query, :query_type
40
35
 
41
36
  def initialize(connection)
37
+ super(connection)
42
38
  @connection = connection
43
39
  end
44
40
 
45
41
  def select_value(arel, name = nil)
42
+ self.query_type = :value
46
43
  self.recorded_query = arel.to_sql
47
44
  nil
48
45
  end
49
46
 
50
47
  def select_all(arel, name = nil, binds = [])
48
+ self.query_type = :all
51
49
  self.recorded_query = arel.to_sql
52
50
  []
53
51
  end
54
-
55
- def method_missing(method, *args, &block)
56
- if connection.respond_to?(method)
57
- connection.send(method, *args, &block)
58
- else
59
- super
60
- end
61
- end
62
-
63
- def respond_to?(method, include_all = false)
64
- super || connection.respond_to?(method, include_all)
65
- end
66
52
  end
67
53
  end
68
54
  end
@@ -5,7 +5,10 @@ require "activerecord-futures/version"
5
5
  require "active_record/futures/future"
6
6
  require "active_record/futures/future_relation"
7
7
  require "active_record/futures/future_calculation"
8
+ require "active_record/futures/future_calculation_value"
9
+ require "active_record/futures/future_calculation_array"
8
10
 
11
+ require "active_record/futures/proxy"
9
12
  require "active_record/futures/query_recording"
10
13
  require "active_record/futures"
11
14
  require "active_record/futures/delegation"
@@ -1,5 +1,5 @@
1
1
  module Activerecord
2
2
  module Futures
3
- VERSION = "0.0.1"
3
+ VERSION = "0.1.0"
4
4
  end
5
5
  end
@@ -2,7 +2,15 @@ require "spec_helper"
2
2
 
3
3
  module ActiveRecord::Futures
4
4
  describe FutureRelation do
5
- let(:relation) { double(ActiveRecord::Relation, klass: Class.new, to_a: nil) }
5
+ let(:relation) do
6
+ double(ActiveRecord::Relation, {
7
+ klass: Class.new,
8
+ to_a: nil,
9
+ to_sql: "select 1",
10
+ connection: double("connection", supports_futures?: true)
11
+ })
12
+ end
13
+
6
14
  subject { FutureRelation.new(relation) }
7
15
 
8
16
  describe ".new" do
@@ -0,0 +1,51 @@
1
+ require "spec_helper"
2
+
3
+ module ActiveRecord::Futures
4
+ describe Proxy do
5
+
6
+ let(:obj) { Object.new }
7
+ subject { Proxy.new(obj) }
8
+
9
+ it "delegates method calls to the proxied object" do
10
+ obj.should_receive(:inspect)
11
+
12
+ subject.inspect
13
+ end
14
+
15
+ describe "#==" do
16
+ it "is equal to the proxied object" do
17
+ subject.should == obj
18
+ end
19
+
20
+ it "is equal to a proxy of the same object" do
21
+ subject.should == Proxy.new(obj)
22
+ end
23
+
24
+ it "is not equal to another object" do
25
+ subject.should_not == Object.new
26
+ end
27
+
28
+ it "is not equal to another object of another type" do
29
+ subject.should_not == "A string object"
30
+ end
31
+ end
32
+
33
+ describe "#!=" do
34
+ it "is equal to the proxied object" do
35
+ (subject != obj).should be_false
36
+ end
37
+
38
+ it "is equal to a proxy of the same object" do
39
+ (subject != Proxy.new(obj)).should be_false
40
+ end
41
+
42
+ it "is not equal to another object" do
43
+ (subject != Object.new).should be_true
44
+ end
45
+
46
+ it "is not equal to another object of another type" do
47
+ (subject != "A string object").should be_true
48
+ end
49
+ end
50
+ end
51
+ end
data/spec/db/schema.rb CHANGED
@@ -1,11 +1,5 @@
1
1
  ActiveRecord::Schema.define(:version => 1) do
2
2
 
3
- create_table "countries", :force => true do |t|
4
- t.string "name"
5
- t.datetime "created_at", :null => false
6
- t.datetime "updated_at", :null => false
7
- end
8
-
9
3
  create_table "users", :force => true do |t|
10
4
  t.string "name"
11
5
  t.string "email"
@@ -13,4 +7,19 @@ ActiveRecord::Schema.define(:version => 1) do
13
7
  t.datetime "updated_at", :null => false
14
8
  end
15
9
 
10
+ create_table "posts", :force => true do |t|
11
+ t.string "title"
12
+ t.text "body"
13
+ t.datetime "published_at"
14
+ t.datetime "created_at", :null => false
15
+ t.datetime "updated_at", :null => false
16
+ end
17
+
18
+ create_table "comments", :force => true do |t|
19
+ t.string "body"
20
+ t.references "user"
21
+ t.references "post"
22
+ t.datetime "created_at", :null => false
23
+ t.datetime "updated_at", :null => false
24
+ end
16
25
  end
@@ -0,0 +1,153 @@
1
+ require "spec_helper"
2
+
3
+ describe "Combination of futures" do
4
+ before do
5
+ User.create(name: "Lenny")
6
+ User.create(name: "John")
7
+ User.create(name: "Julie")
8
+
9
+ Post.create(title: "Post title 1", published_at: Time.new(2013, 3, 14))
10
+ Post.create(title: "Post title 2", published_at: Time.new(2012, 11, 9))
11
+ Post.create(title: "Post title 3")
12
+ end
13
+
14
+ let(:user_relation) { User.where(name: "Lenny") }
15
+ let(:user_relation_sql) { user_relation.to_sql }
16
+ let!(:user_future_relation) { user_relation.future }
17
+
18
+ let(:other_user_relation) { User.where("name like 'J%'") }
19
+ let(:other_user_relation_count_sql) { count(other_user_relation).to_sql }
20
+ let!(:user_future_value) { other_user_relation.future_count }
21
+
22
+ let(:post_relation) { Post.where(title: "Post title 2") }
23
+ let(:post_relation_sql) { post_relation.to_sql }
24
+ let!(:post_future_relation) { post_relation.future }
25
+
26
+ let(:post_count_sql) { count(Post.scoped).to_sql }
27
+ let!(:post_future_value) { Post.future_count }
28
+
29
+ context "the execution of any future" do
30
+ subject { -> { post_future_relation.to_a } }
31
+
32
+ context "execs only once with all queries", :supporting_adapter do
33
+ let(:futures_sql) do
34
+ [
35
+ user_relation_sql,
36
+ other_user_relation_count_sql,
37
+ post_relation_sql,
38
+ post_count_sql
39
+ ].join(';')
40
+ end
41
+
42
+ it { should exec(1).query }
43
+ it { should exec_query(futures_sql) }
44
+ end
45
+
46
+ context "execs just the executed future's query", :not_supporting_adapter do
47
+ it { should exec(1).query }
48
+ it { should exec_query(post_future_relation.to_sql) }
49
+ end
50
+ end
51
+
52
+ context "having executed the post future" do
53
+ before do
54
+ post_future_relation.to_a
55
+ end
56
+
57
+ context "the user future relation" do
58
+ subject { user_future_relation }
59
+
60
+ it(nil, :supporting_adapter) { should be_fulfilled }
61
+ it(nil, :not_supporting_adapter) { should_not be_fulfilled }
62
+
63
+ describe "#to_a" do
64
+ let(:calling_to_a) { ->{ subject.to_a } }
65
+
66
+ its(:to_a) { should eq user_relation.to_a }
67
+
68
+ context "when adapter supports futures", :supporting_adapter do
69
+ specify { calling_to_a.should exec(0).queries }
70
+ end
71
+
72
+ context "when adapter does not support futures", :not_supporting_adapter do
73
+ specify { calling_to_a.should exec(1).query }
74
+ specify { calling_to_a.should exec_query(user_relation_sql) }
75
+ end
76
+ end
77
+ end
78
+
79
+ context "the user future value" do
80
+ subject { user_future_value }
81
+
82
+ it(nil, :supporting_adapter) { should be_fulfilled }
83
+ it(nil, :not_supporting_adapter) { should_not be_fulfilled }
84
+
85
+ describe "#value" do
86
+ let(:calling_value) { ->{ subject.value } }
87
+
88
+ its(:value) { should eq other_user_relation.count }
89
+
90
+ context "when adapter supports futures", :supporting_adapter do
91
+ specify { calling_value.should exec(0).queries }
92
+ end
93
+
94
+ context "when adapter does not support futures", :not_supporting_adapter do
95
+ specify { calling_value.should exec(1).query }
96
+ specify { calling_value.should exec_query(other_user_relation_count_sql) }
97
+ end
98
+ end
99
+ end
100
+
101
+ context "the post future relation" do
102
+ subject { post_future_relation }
103
+
104
+ it(nil, :supporting_adapter) { should be_fulfilled }
105
+ it(nil, :not_supporting_adapter) { should_not be_fulfilled }
106
+
107
+ describe "#to_a" do
108
+ let(:calling_to_a) { ->{ subject.to_a } }
109
+
110
+ its(:to_a) { should eq post_relation.to_a }
111
+
112
+ context "when adapter supports futures", :supporting_adapter do
113
+ specify { calling_to_a.should exec(0).queries }
114
+ end
115
+
116
+ context "when adapter does not support futures", :not_supporting_adapter do
117
+ specify { calling_to_a.should exec(0).query }
118
+ # No queries should be executed, since this is the future we executed
119
+ # before
120
+ end
121
+ end
122
+ end
123
+
124
+ context "the post future value" do
125
+ subject { post_future_value }
126
+
127
+ it(nil, :supporting_adapter) { should be_fulfilled }
128
+ it(nil, :not_supporting_adapter) { should_not be_fulfilled }
129
+
130
+ describe "#value" do
131
+ let(:calling_value) { ->{ subject.value } }
132
+
133
+ its(:value) { should eq Post.count }
134
+
135
+ context "when adapter supports futures", :supporting_adapter do
136
+ specify { calling_value.should exec(0).queries }
137
+ end
138
+
139
+ context "when adapter does not support futures", :not_supporting_adapter do
140
+ specify { calling_value.should exec(1).query }
141
+ specify { calling_value.should exec_query(post_count_sql) }
142
+ end
143
+ end
144
+ end
145
+ end
146
+
147
+ def count(relation)
148
+ arel = relation.arel
149
+ arel.projections = []
150
+ arel.project("COUNT(*)")
151
+ arel
152
+ end
153
+ end