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.
- data/.travis.yml +20 -0
- data/Gemfile +11 -1
- data/README.md +43 -23
- data/Rakefile +17 -0
- data/activerecord-futures.gemspec +3 -1
- data/lib/active_record/connection_adapters/future_enabled.rb +34 -0
- data/lib/active_record/connection_adapters/future_enabled_mysql2_adapter.rb +23 -22
- data/lib/active_record/connection_adapters/future_enabled_postgresql_adapter.rb +52 -0
- data/lib/active_record/futures.rb +10 -9
- data/lib/active_record/futures/delegation.rb +1 -0
- data/lib/active_record/futures/future.rb +22 -6
- data/lib/active_record/futures/future_calculation.rb +4 -12
- data/lib/active_record/futures/future_calculation_array.rb +8 -0
- data/lib/active_record/futures/future_calculation_value.rb +11 -0
- data/lib/active_record/futures/future_relation.rb +12 -11
- data/lib/active_record/futures/proxy.rb +37 -0
- data/lib/active_record/futures/query_recording.rb +12 -26
- data/lib/activerecord-futures.rb +3 -0
- data/lib/activerecord-futures/version.rb +1 -1
- data/spec/active_record/futures/future_relation_spec.rb +9 -1
- data/spec/active_record/futures/proxy_spec.rb +51 -0
- data/spec/db/schema.rb +15 -6
- data/spec/in_action/combination_of_futures_spec.rb +153 -0
- data/spec/in_action/future_count_execution_spec.rb +98 -0
- data/spec/in_action/future_fulfillment_spec.rb +10 -7
- data/spec/in_action/future_pluck_execution_spec.rb +44 -0
- data/spec/in_action/future_relation_execution_spec.rb +52 -0
- data/spec/models/comment.rb +4 -0
- data/spec/models/post.rb +3 -0
- data/spec/models/user.rb +1 -0
- data/spec/spec_helper.rb +45 -9
- data/spec/support/matchers/exec.rb +66 -0
- data/spec/support/matchers/exec_query.rb +23 -0
- metadata +60 -6
- 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?
|
@@ -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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
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
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
data/lib/activerecord-futures.rb
CHANGED
@@ -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"
|
@@ -2,7 +2,15 @@ require "spec_helper"
|
|
2
2
|
|
3
3
|
module ActiveRecord::Futures
|
4
4
|
describe FutureRelation do
|
5
|
-
let(:relation)
|
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
|