activerecord-futures 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +34 -8
- data/lib/active_record/connection_adapters/future_enabled.rb +6 -5
- data/lib/active_record/connection_adapters/future_enabled_mysql2_adapter.rb +2 -1
- data/lib/active_record/connection_adapters/future_enabled_postgresql_adapter.rb +21 -7
- data/lib/active_record/futures.rb +11 -23
- data/lib/active_record/futures/calculation_methods.rb +34 -0
- data/lib/active_record/futures/delegation.rb +1 -1
- data/lib/active_record/futures/finder_methods.rb +59 -0
- data/lib/active_record/futures/future.rb +21 -57
- data/lib/active_record/futures/future_array.rb +23 -0
- data/lib/active_record/futures/future_registry.rb +33 -0
- data/lib/active_record/futures/future_value.rb +20 -0
- data/lib/active_record/futures/query_recording.rb +11 -4
- data/lib/activerecord-futures.rb +5 -4
- data/lib/activerecord-futures/version.rb +1 -1
- data/spec/active_record/futures/future_array_spec.rb +26 -0
- data/spec/active_record/futures/future_registry_spec.rb +86 -0
- data/spec/active_record/futures/future_spec.rb +42 -69
- data/spec/active_record/futures/future_value_spec.rb +26 -0
- data/spec/in_action/combination_of_futures_spec.rb +13 -9
- data/spec/in_action/future_all_execution_spec.rb +20 -0
- data/spec/in_action/future_count_execution_spec.rb +8 -56
- data/spec/in_action/future_exists_execution_spec.rb +22 -0
- data/spec/in_action/future_find_execution_spec.rb +68 -0
- data/spec/in_action/future_first_execution_spec.rb +20 -0
- data/spec/in_action/future_fulfillment_spec.rb +10 -7
- data/spec/in_action/future_last_execution_spec.rb +21 -0
- data/spec/in_action/future_pluck_execution_spec.rb +4 -27
- data/spec/in_action/future_relation_execution_spec.rb +8 -30
- data/spec/spec_helper.rb +2 -1
- data/spec/support/futurized_method_examples.rb +43 -0
- metadata +25 -12
- data/lib/active_record/futures/future_calculation.rb +0 -30
- data/lib/active_record/futures/future_calculation_array.rb +0 -8
- data/lib/active_record/futures/future_calculation_value.rb +0 -7
- data/lib/active_record/futures/future_relation.rb +0 -34
- data/spec/active_record/futures/future_calculation_array_spec.rb +0 -29
- data/spec/active_record/futures/future_calculation_value_spec.rb +0 -29
- data/spec/active_record/futures/future_relation_spec.rb +0 -81
data/README.md
CHANGED
@@ -68,15 +68,41 @@ both count and page queries at once.
|
|
68
68
|
|
69
69
|
### Methods
|
70
70
|
|
71
|
-
|
72
|
-
|
71
|
+
#### #future method
|
72
|
+
ActiveRecord::Relation instances get a `future` method that futurizes a normal
|
73
|
+
relation. The future gets executed whenever `#to_a` gets executed. Note that, as ActiveRecord does, enumerable methods get delegated to `#to_a` also,
|
73
74
|
so things like `#each`, `#map`, `#collect` all trigger the future.
|
74
75
|
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
76
|
+
#### Calculation methods
|
77
|
+
You also get all the calculation methods provided by the ActiveRecord::Calculations module
|
78
|
+
"futurized". More specifically you get:
|
79
|
+
* future_count
|
80
|
+
* future_average
|
81
|
+
* future_minimum
|
82
|
+
* future_maximum
|
83
|
+
* future_sum
|
84
|
+
* future_calculate
|
85
|
+
* future_pluck
|
86
|
+
|
87
|
+
All future
|
88
|
+
calculations are triggered with the `#value` method, except for the `#future_pluck` method, that returns an array, and is
|
89
|
+
triggered with a `#to_a` method (or any other method that delegates to it).
|
90
|
+
|
91
|
+
#### Finder methods
|
92
|
+
|
93
|
+
Lastly, you also get finder methods futurized, which are:
|
94
|
+
|
95
|
+
* future_find
|
96
|
+
* future_first
|
97
|
+
* future_last
|
98
|
+
* future_exists?
|
99
|
+
* future_all
|
100
|
+
|
101
|
+
As with the other future methods, those which return an array get triggered with
|
102
|
+
the `to_a` method, or the delegated ones, and those that return a value or a hash
|
103
|
+
are triggered with the `value` method. Note that the `find` method returns an
|
104
|
+
array or a value depending on the parameters provided, and so will the futurized
|
105
|
+
version of the method.
|
80
106
|
|
81
107
|
## Database support
|
82
108
|
|
@@ -89,7 +115,7 @@ it will execute the future's query whenever the future is triggered, but it will
|
|
89
115
|
|
90
116
|
Multi statement queries are supported by the mysql2 gem since version 0.3.12b1, so you'll need to use that one or a newer
|
91
117
|
one.
|
92
|
-
Currently the adapter provided inherits the built-in one in Rails, and it also sets the MULTI_STATEMENTS flag to allow
|
118
|
+
Currently the adapter provided inherits the built-in one in Rails, and it also sets the MULTI_STATEMENTS flag to allow
|
93
119
|
multiple queries in a single command.
|
94
120
|
If you have an older version of the gem, ActiveRecord::Futures will fall back to normal query execution.
|
95
121
|
|
@@ -6,21 +6,22 @@ module ActiveRecord
|
|
6
6
|
end
|
7
7
|
|
8
8
|
def exec_query(sql, name = 'SQL', binds = [])
|
9
|
-
my_future = Futures::
|
9
|
+
my_future = Futures::FutureRegistry.current
|
10
10
|
|
11
11
|
# default behavior if not a current future or not executing
|
12
12
|
# the current future's sql (some adapters like PostgreSQL
|
13
13
|
# may execute some attribute queries during a relation evaluation)
|
14
|
-
return super
|
14
|
+
return super if !my_future || to_sql(my_future.query, my_future.binds.try(:dup)) != sql
|
15
15
|
|
16
16
|
# return fulfilled result, if exists, to load the relation
|
17
17
|
return my_future.result if my_future.fulfilled?
|
18
18
|
|
19
|
-
futures = Futures::
|
20
|
-
|
19
|
+
futures = Futures::FutureRegistry.all
|
20
|
+
future_arels = futures.map(&:query)
|
21
|
+
future_binds = futures.map(&:binds)
|
21
22
|
name = "#{name} (fetching Futures)"
|
22
23
|
|
23
|
-
result = future_execute(
|
24
|
+
result = future_execute(future_arels, future_binds, name)
|
24
25
|
|
25
26
|
futures.each do |future|
|
26
27
|
future.fulfill(build_active_record_result(result))
|
@@ -36,7 +36,8 @@ module ActiveRecord
|
|
36
36
|
@connection.respond_to?(:store_result)
|
37
37
|
end
|
38
38
|
|
39
|
-
def future_execute(
|
39
|
+
def future_execute(arels, binds, name)
|
40
|
+
sql = arels.zip(binds).map { |arel, bind| to_sql(arel, bind.try(:dup)) }.join(';')
|
40
41
|
execute(sql, name)
|
41
42
|
end
|
42
43
|
|
@@ -27,13 +27,27 @@ module ActiveRecord
|
|
27
27
|
class FutureEnabledPostgreSQLAdapter < PostgreSQLAdapter
|
28
28
|
include FutureEnabled
|
29
29
|
|
30
|
-
def future_execute(
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
30
|
+
def future_execute(arels, binds, name)
|
31
|
+
sql = arels.zip(binds).map { |arel, bind| to_sql(arel, bind.try(:dup)) }.join(';')
|
32
|
+
binds = binds.flatten(1).compact
|
33
|
+
|
34
|
+
log(sql, name, binds) do
|
35
|
+
|
36
|
+
if binds.empty?
|
37
|
+
# Clear the queue
|
38
|
+
@connection.get_last_result
|
39
|
+
@connection.send_query(sql)
|
40
|
+
@connection.block
|
41
|
+
@connection.get_result
|
42
|
+
else
|
43
|
+
# Clear the queue
|
44
|
+
@connection.get_last_result
|
45
|
+
@connection.send_query(sql, binds.map { |col, val|
|
46
|
+
type_cast(val, col)
|
47
|
+
})
|
48
|
+
@connection.block
|
49
|
+
@connection.get_result
|
50
|
+
end
|
37
51
|
end
|
38
52
|
end
|
39
53
|
|
@@ -1,35 +1,23 @@
|
|
1
1
|
module ActiveRecord
|
2
2
|
module Futures
|
3
|
-
include QueryRecording
|
4
3
|
|
5
|
-
def self.
|
6
|
-
|
4
|
+
def self.futurize(method)
|
5
|
+
"future_#{method}"
|
7
6
|
end
|
8
7
|
|
9
|
-
|
10
|
-
|
11
|
-
|
8
|
+
include QueryRecording
|
9
|
+
include FinderMethods
|
10
|
+
include CalculationMethods
|
12
11
|
|
13
12
|
def future
|
14
|
-
|
13
|
+
FutureArray.new(record_future(:to_a))
|
15
14
|
end
|
16
15
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
method_table = Hash[future_calculation_methods.zip(original_calculation_methods)]
|
24
|
-
|
25
|
-
# define a "future_" method for each calculation method
|
26
|
-
#
|
27
|
-
method_table.each do |future_method, method|
|
28
|
-
define_method(future_method) do |*args, &block|
|
29
|
-
exec = lambda { send(method, *args, &block) }
|
30
|
-
query = record_query(&exec)
|
31
|
-
FutureCalculationValue.new(self, query, exec)
|
32
|
-
end
|
16
|
+
private
|
17
|
+
def record_future(method, *args, &block)
|
18
|
+
exec = -> { send(method, *args, &block) }
|
19
|
+
query, binds = record_query(&exec)
|
20
|
+
Future.new(self, query, binds, exec)
|
33
21
|
end
|
34
22
|
end
|
35
23
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Futures
|
3
|
+
module CalculationMethods
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def future_pluck(*args, &block)
|
7
|
+
FutureArray.new(record_future(:pluck, *args, &block))
|
8
|
+
end
|
9
|
+
|
10
|
+
included do
|
11
|
+
methods = original_calculation_methods - [:pluck]
|
12
|
+
|
13
|
+
# define a "future_" method for each calculation method
|
14
|
+
#
|
15
|
+
methods.each do |method|
|
16
|
+
define_method(futurize(method)) do |*args, &block|
|
17
|
+
FutureValue.new(record_future(method, *args, &block))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
module ClassMethods
|
23
|
+
|
24
|
+
def original_calculation_methods
|
25
|
+
[:count, :average, :minimum, :maximum, :sum, :calculate, :pluck]
|
26
|
+
end
|
27
|
+
|
28
|
+
def future_calculation_methods
|
29
|
+
original_calculation_methods.map { |method| futurize(method) }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Futures
|
3
|
+
module FinderMethods
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
def future_find(*args, &block)
|
7
|
+
exec = -> { find(*args, &block) }
|
8
|
+
query, binds = record_query do
|
9
|
+
begin
|
10
|
+
exec.call
|
11
|
+
rescue RecordNotFound
|
12
|
+
nil
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
args = args.dup
|
17
|
+
args.extract_options!
|
18
|
+
|
19
|
+
# Only expect an array if a block is given, finding :all,
|
20
|
+
# finding by id passing an array or passing multiple ids
|
21
|
+
expects_array = block_given? || args.first == :all ||
|
22
|
+
args.first.kind_of?(Array) || args.size > 1
|
23
|
+
|
24
|
+
future = Future.new(self, query, binds, exec)
|
25
|
+
if expects_array
|
26
|
+
FutureArray.new(future)
|
27
|
+
else
|
28
|
+
FutureValue.new(future)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def future_all(*args, &block)
|
33
|
+
FutureArray.new(record_future(:all, *args, &block))
|
34
|
+
end
|
35
|
+
|
36
|
+
included do
|
37
|
+
methods = original_finder_methods - [:find, :all]
|
38
|
+
|
39
|
+
# define a "future_" method for each finder method
|
40
|
+
#
|
41
|
+
methods.each do |method|
|
42
|
+
define_method(futurize(method)) do |*args, &block|
|
43
|
+
FutureValue.new(record_future(method, *args, &block))
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
module ClassMethods
|
49
|
+
def original_finder_methods
|
50
|
+
[:find, :first, :last, :exists?, :all]
|
51
|
+
end
|
52
|
+
|
53
|
+
def future_finder_methods
|
54
|
+
original_finder_methods.map { |method| futurize(method) }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -1,54 +1,15 @@
|
|
1
1
|
module ActiveRecord
|
2
2
|
module Futures
|
3
3
|
class Future
|
4
|
-
|
5
|
-
|
6
|
-
Thread.current["#{self.name}_futures"] ||= []
|
7
|
-
end
|
8
|
-
alias_method :all, :futures
|
9
|
-
|
10
|
-
def current
|
11
|
-
Thread.current["#{self.name}_current"]
|
12
|
-
end
|
13
|
-
|
14
|
-
def current=(future)
|
15
|
-
Thread.current["#{self.name}_current"] = future
|
16
|
-
end
|
17
|
-
|
18
|
-
def clear
|
19
|
-
all.clear
|
20
|
-
end
|
21
|
-
|
22
|
-
def register(future)
|
23
|
-
self.futures << future
|
24
|
-
end
|
25
|
-
|
26
|
-
def flush
|
27
|
-
self.futures.each(&:load)
|
28
|
-
clear
|
29
|
-
end
|
30
|
-
|
31
|
-
private
|
32
|
-
def fetch_with(method)
|
33
|
-
define_method(method) do
|
34
|
-
# Flush all the futures upon first attempt to exec a future
|
35
|
-
Future.flush unless executed?
|
36
|
-
execute
|
37
|
-
end
|
4
|
+
attr_reader :result, :relation, :query, :binds, :execution
|
5
|
+
private :relation, :execution
|
38
6
|
|
39
|
-
|
40
|
-
send(method).inspect
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
|
46
|
-
attr_reader :result, :relation
|
47
|
-
private :relation
|
48
|
-
|
49
|
-
def initialize(relation)
|
7
|
+
def initialize(relation, query, binds, execution)
|
50
8
|
@relation = relation
|
51
|
-
|
9
|
+
@query = query
|
10
|
+
@binds = binds
|
11
|
+
@execution = execution
|
12
|
+
FutureRegistry.register(self)
|
52
13
|
end
|
53
14
|
|
54
15
|
def fulfill(result)
|
@@ -64,29 +25,32 @@ module ActiveRecord
|
|
64
25
|
# This allows to fallback to normal query execution in futures
|
65
26
|
# when the adapter does not support futures.
|
66
27
|
return unless connection_supports_futures?
|
67
|
-
|
68
|
-
execute
|
69
|
-
|
28
|
+
FutureRegistry.current = self
|
29
|
+
execute(false)
|
30
|
+
FutureRegistry.current = nil
|
70
31
|
end
|
71
32
|
|
72
|
-
def
|
73
|
-
|
74
|
-
|
33
|
+
def execute(flush = true)
|
34
|
+
# Flush all the futures upon first attempt to exec a future
|
35
|
+
FutureRegistry.flush if flush && !executed?
|
75
36
|
|
76
|
-
|
77
|
-
|
37
|
+
unless executed?
|
38
|
+
@value = execution.call
|
39
|
+
@executed = true
|
40
|
+
end
|
41
|
+
|
42
|
+
@value
|
78
43
|
end
|
79
|
-
undef_method :execute
|
80
44
|
|
81
45
|
def executed?
|
46
|
+
@executed
|
82
47
|
end
|
83
|
-
undef_method :executed?
|
84
48
|
|
49
|
+
private
|
85
50
|
def connection_supports_futures?
|
86
51
|
conn = relation.connection
|
87
52
|
conn.respond_to?(:supports_futures?) && conn.supports_futures?
|
88
53
|
end
|
89
|
-
|
90
54
|
end
|
91
55
|
end
|
92
56
|
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Futures
|
3
|
+
class FutureArray
|
4
|
+
attr_reader :future_execution
|
5
|
+
private :future_execution
|
6
|
+
|
7
|
+
delegate :to_xml, :to_yaml, :length, :collect, :map, :each,
|
8
|
+
:all?, :include?, :to_ary, to: :to_a
|
9
|
+
|
10
|
+
def initialize(future_execution)
|
11
|
+
@future_execution = future_execution
|
12
|
+
end
|
13
|
+
|
14
|
+
def to_a
|
15
|
+
future_execution.execute
|
16
|
+
end
|
17
|
+
|
18
|
+
def inspect
|
19
|
+
to_a
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Futures
|
3
|
+
module FutureRegistry
|
4
|
+
def futures
|
5
|
+
Thread.current["#{self.name}_futures"] ||= []
|
6
|
+
end
|
7
|
+
alias_method :all, :futures
|
8
|
+
|
9
|
+
def current
|
10
|
+
Thread.current["#{self.name}_current"]
|
11
|
+
end
|
12
|
+
|
13
|
+
def current=(future)
|
14
|
+
Thread.current["#{self.name}_current"] = future
|
15
|
+
end
|
16
|
+
|
17
|
+
def clear
|
18
|
+
all.clear
|
19
|
+
end
|
20
|
+
|
21
|
+
def register(future)
|
22
|
+
self.futures << future
|
23
|
+
end
|
24
|
+
|
25
|
+
def flush
|
26
|
+
self.futures.each(&:load)
|
27
|
+
clear
|
28
|
+
end
|
29
|
+
|
30
|
+
extend self
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|