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.
Files changed (39) hide show
  1. data/README.md +34 -8
  2. data/lib/active_record/connection_adapters/future_enabled.rb +6 -5
  3. data/lib/active_record/connection_adapters/future_enabled_mysql2_adapter.rb +2 -1
  4. data/lib/active_record/connection_adapters/future_enabled_postgresql_adapter.rb +21 -7
  5. data/lib/active_record/futures.rb +11 -23
  6. data/lib/active_record/futures/calculation_methods.rb +34 -0
  7. data/lib/active_record/futures/delegation.rb +1 -1
  8. data/lib/active_record/futures/finder_methods.rb +59 -0
  9. data/lib/active_record/futures/future.rb +21 -57
  10. data/lib/active_record/futures/future_array.rb +23 -0
  11. data/lib/active_record/futures/future_registry.rb +33 -0
  12. data/lib/active_record/futures/future_value.rb +20 -0
  13. data/lib/active_record/futures/query_recording.rb +11 -4
  14. data/lib/activerecord-futures.rb +5 -4
  15. data/lib/activerecord-futures/version.rb +1 -1
  16. data/spec/active_record/futures/future_array_spec.rb +26 -0
  17. data/spec/active_record/futures/future_registry_spec.rb +86 -0
  18. data/spec/active_record/futures/future_spec.rb +42 -69
  19. data/spec/active_record/futures/future_value_spec.rb +26 -0
  20. data/spec/in_action/combination_of_futures_spec.rb +13 -9
  21. data/spec/in_action/future_all_execution_spec.rb +20 -0
  22. data/spec/in_action/future_count_execution_spec.rb +8 -56
  23. data/spec/in_action/future_exists_execution_spec.rb +22 -0
  24. data/spec/in_action/future_find_execution_spec.rb +68 -0
  25. data/spec/in_action/future_first_execution_spec.rb +20 -0
  26. data/spec/in_action/future_fulfillment_spec.rb +10 -7
  27. data/spec/in_action/future_last_execution_spec.rb +21 -0
  28. data/spec/in_action/future_pluck_execution_spec.rb +4 -27
  29. data/spec/in_action/future_relation_execution_spec.rb +8 -30
  30. data/spec/spec_helper.rb +2 -1
  31. data/spec/support/futurized_method_examples.rb +43 -0
  32. metadata +25 -12
  33. data/lib/active_record/futures/future_calculation.rb +0 -30
  34. data/lib/active_record/futures/future_calculation_array.rb +0 -8
  35. data/lib/active_record/futures/future_calculation_value.rb +0 -7
  36. data/lib/active_record/futures/future_relation.rb +0 -34
  37. data/spec/active_record/futures/future_calculation_array_spec.rb +0 -29
  38. data/spec/active_record/futures/future_calculation_value_spec.rb +0 -29
  39. 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
- ActiveRecord::Relation instances get a `future` method for all queries where multiple results are returned. The future gets
72
- executed whenever `#to_a` gets executed. Note that, as ActiveRecord does, enumerable methods get delegated to `#to_a` also,
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
- Also, ActiveRecord::Relation instances get all the calculation methods provided by the ActiveRecord::Calculations module
76
- "futurized", that means, for `#count` you get `#future_count`, for `#sum` you get `#future_sum` and so on. If the calculation
77
- returns a list of values, for example with a `#future_pluck` or a grouped `#future_count`, the future will be triggered with
78
- the `#to_a` method (or any of the methods that delegate to `#to_a`). If it returns a single value, the future will be
79
- triggered when you execute the `#value` method.
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::Future.current
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 unless my_future && my_future.to_sql == sql
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::Future.all
20
- futures_sql = futures.map(&:to_sql).join(';')
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(futures_sql, name)
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(sql, name)
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(sql, name)
31
- log(sql, name) do
32
- # Clear the queue
33
- @connection.get_last_result
34
- @connection.send_query(sql)
35
- @connection.block
36
- @connection.get_result
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.original_calculation_methods
6
- [:count, :average, :minimum, :maximum, :sum, :calculate]
4
+ def self.futurize(method)
5
+ "future_#{method}"
7
6
  end
8
7
 
9
- def self.future_calculation_methods
10
- original_calculation_methods.map { |name| "future_#{name}" }
11
- end
8
+ include QueryRecording
9
+ include FinderMethods
10
+ include CalculationMethods
12
11
 
13
12
  def future
14
- FutureRelation.new(self)
13
+ FutureArray.new(record_future(:to_a))
15
14
  end
16
15
 
17
- def future_pluck(*args, &block)
18
- exec = lambda { pluck(*args, &block) }
19
- query = record_query(&exec)
20
- FutureCalculationArray.new(self, query, exec)
21
- end
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
@@ -2,8 +2,8 @@ module ActiveRecord
2
2
  module Futures
3
3
  module Delegation
4
4
  delegate :future, to: :scoped
5
- delegate :future_pluck, to: :scoped
6
5
  delegate *Futures.future_calculation_methods, to: :scoped
6
+ delegate *Futures.future_finder_methods, to: :scoped
7
7
  end
8
8
  end
9
9
  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
- class << self
5
- def futures
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
- define_method(:inspect) do
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
- Future.register(self)
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
- Future.current = self
68
- execute
69
- Future.current = nil
28
+ FutureRegistry.current = self
29
+ execute(false)
30
+ FutureRegistry.current = nil
70
31
  end
71
32
 
72
- def to_sql
73
- end
74
- undef_method :to_sql
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
- private
77
- def execute
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