activerecord-futures 0.0.1
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/.gitignore +17 -0
- data/.rspec +2 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +93 -0
- data/Rakefile +1 -0
- data/activerecord-futures.gemspec +23 -0
- data/lib/active_record/connection_adapters/future_enabled_mysql2_adapter.rb +51 -0
- data/lib/active_record/futures/delegation.rb +8 -0
- data/lib/active_record/futures/future.rb +72 -0
- data/lib/active_record/futures/future_calculation.rb +38 -0
- data/lib/active_record/futures/future_relation.rb +33 -0
- data/lib/active_record/futures/query_recording.rb +69 -0
- data/lib/active_record/futures.rb +34 -0
- data/lib/activerecord-futures/version.rb +5 -0
- data/lib/activerecord-futures.rb +21 -0
- data/spec/active_record/futures/future_relation_spec.rb +56 -0
- data/spec/active_record/futures/future_spec.rb +91 -0
- data/spec/db/schema.rb +16 -0
- data/spec/in_action/future_fulfillment_spec.rb +73 -0
- data/spec/models/country.rb +2 -0
- data/spec/models/user.rb +2 -0
- data/spec/spec_helper.rb +33 -0
- metadata +124 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Leonardo Andres Garcia Crespo
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
# ActiveRecord::Futures
|
2
|
+
|
3
|
+
Define future queries in ActiveRecord that will get executed in a single round trip to the database.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'activerecord-futures'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install activerecord-futures
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
Currently, the only database supported is MySQL, and with a special adapter, provided by the gem.
|
22
|
+
|
23
|
+
Set your config/database.yml file to use the given adapter:
|
24
|
+
|
25
|
+
```yml
|
26
|
+
development: &development
|
27
|
+
adapter: future_enabled_mysql2 # set this adapter for futures to work!
|
28
|
+
username: your_username
|
29
|
+
password: your_password
|
30
|
+
database: your_database
|
31
|
+
host: your_host
|
32
|
+
```
|
33
|
+
|
34
|
+
Now let's see what this does, consider a model `User`, with a `:name` attribute:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
# Build the queries and mark them as futures
|
38
|
+
users = User.where("name like 'John%'").future # becomes a future relation, does not execute the query.
|
39
|
+
count = User.where("name like 'John%'").future_count # becomes a future calculation, does not execute the query.
|
40
|
+
|
41
|
+
# Execute any of the futures
|
42
|
+
count = count.value # trigger the future execution, both queries will get executed in one round trip!
|
43
|
+
#=> User Load (fetching Futures) (0.6ms) SELECT `users`.* FROM `users` WHERE (name like 'John%');SELECT COUNT(*) FROM `users` WHERE (name like 'John%')
|
44
|
+
|
45
|
+
# Access the other results
|
46
|
+
users = users.to_a # does not execute the query, results from previous query get loaded
|
47
|
+
```
|
48
|
+
|
49
|
+
Any amount of futures can be prepared, and the will get executed as soon as one of them needs to be evaluated.
|
50
|
+
|
51
|
+
### Methods
|
52
|
+
|
53
|
+
ActiveRecord::Relation instances get a `future` method for all queries where multiple results are returned. The future gets
|
54
|
+
executed whenever `#to_a` gets executed. Note that, as ActiveRecord does, enumerable methods get delegated to `#to_a` also,
|
55
|
+
so things like `#each`, `#map`, `#collect` all trigger the future.
|
56
|
+
|
57
|
+
Also, ActiveRecord::Relation instances get all the calculation methods provided by the ActiveRecord::Calculations module
|
58
|
+
"futurized", that means, for `#count` you get `#future_count`, for `#sum` you get `#future_sum` and so on. These future
|
59
|
+
calculations are triggered by executing the `#value` method, which also return the actual result of the calculation.
|
60
|
+
|
61
|
+
## Database support
|
62
|
+
|
63
|
+
### SQlite
|
64
|
+
|
65
|
+
SQlite doesn't support multiple statement queries. Currently this gem doesn't fall back to the normal behavior if the
|
66
|
+
adapter does not support futures, but this is in the road map :)
|
67
|
+
|
68
|
+
### MySQL
|
69
|
+
|
70
|
+
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
|
71
|
+
one.
|
72
|
+
Currently the adapter provided is the same as the built-in in Rails, but it also sets the MULTI_STATEMENTS flag to allow
|
73
|
+
multiple queries in a single command. It also has a special way to
|
74
|
+
execute the queries in order to fetch the results correctly. You
|
75
|
+
can check the code if you're curious!
|
76
|
+
|
77
|
+
### Postgres
|
78
|
+
|
79
|
+
Coming soon!
|
80
|
+
|
81
|
+
## Contributing
|
82
|
+
|
83
|
+
1. Fork it
|
84
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
85
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
86
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
87
|
+
5. Create new Pull Request
|
88
|
+
|
89
|
+
## Roadmap
|
90
|
+
|
91
|
+
1. Support for postgres
|
92
|
+
2. Fallback to normal queries when adapter does not support futures
|
93
|
+
3. Think of a way to use the normal adapters
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'activerecord-futures/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = "activerecord-futures"
|
8
|
+
gem.version = Activerecord::Futures::VERSION
|
9
|
+
gem.authors = ["Leonardo Andres Garcia Crespo"]
|
10
|
+
gem.email = ["leoasis@gmail.com"]
|
11
|
+
gem.description = %q{Save unnecessary round trips to the database}
|
12
|
+
gem.summary = %q{Fetch all queries at once from the database and save round trips. }
|
13
|
+
gem.homepage = "https://github.com/leoasis/activerecord-futures"
|
14
|
+
|
15
|
+
gem.files = `git ls-files`.split($/)
|
16
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
17
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
|
+
gem.require_paths = ["lib"]
|
19
|
+
|
20
|
+
gem.add_dependency 'activerecord', '>= 3.2.13'
|
21
|
+
gem.add_development_dependency 'rspec'
|
22
|
+
gem.add_development_dependency 'rspec-spies'
|
23
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
require "active_record/connection_adapters/mysql2_adapter"
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
class Base
|
5
|
+
def self.future_enabled_mysql2_connection(config)
|
6
|
+
config = config.symbolize_keys
|
7
|
+
|
8
|
+
config[:username] = 'root' if config[:username].nil?
|
9
|
+
|
10
|
+
if Mysql2::Client.const_defined? :FOUND_ROWS
|
11
|
+
config[:flags] = Mysql2::Client::FOUND_ROWS | Mysql2::Client::MULTI_STATEMENTS
|
12
|
+
end
|
13
|
+
client = Mysql2::Client.new(config)
|
14
|
+
options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0]
|
15
|
+
ConnectionAdapters::FutureEnabledMysql2Adapter.new(client, logger, options, config)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
module ConnectionAdapters
|
20
|
+
class FutureEnabledMysql2Adapter < Mysql2Adapter
|
21
|
+
|
22
|
+
def supports_futures?
|
23
|
+
true
|
24
|
+
end
|
25
|
+
|
26
|
+
def exec_query(sql, name = 'SQL', binds = [])
|
27
|
+
my_future = Futures::Future.current
|
28
|
+
|
29
|
+
# default behavior if not a current future
|
30
|
+
return super unless my_future
|
31
|
+
|
32
|
+
# return fulfilled result, if exists, to load the relation
|
33
|
+
return my_future.result if my_future.fulfilled?
|
34
|
+
|
35
|
+
futures = Futures::Future.all
|
36
|
+
|
37
|
+
futures_sql = futures.map(&:to_sql).join(';')
|
38
|
+
name = "#{name} (fetching Futures)"
|
39
|
+
|
40
|
+
result = execute(futures_sql, name)
|
41
|
+
|
42
|
+
futures.each do |future|
|
43
|
+
future.fulfill(ActiveRecord::Result.new(result.fields, result.to_a))
|
44
|
+
result = @connection.store_result if @connection.next_result
|
45
|
+
end
|
46
|
+
|
47
|
+
my_future.result
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Futures
|
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
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
attr_reader :result
|
34
|
+
|
35
|
+
def initialize
|
36
|
+
Future.register(self)
|
37
|
+
end
|
38
|
+
|
39
|
+
def fulfill(result)
|
40
|
+
@result = result
|
41
|
+
end
|
42
|
+
|
43
|
+
def fulfilled?
|
44
|
+
!result.nil?
|
45
|
+
end
|
46
|
+
|
47
|
+
def load
|
48
|
+
Future.current = self
|
49
|
+
execute
|
50
|
+
Future.current = nil
|
51
|
+
end
|
52
|
+
|
53
|
+
def inspect
|
54
|
+
to_a.inspect
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_sql
|
58
|
+
end
|
59
|
+
undef_method :to_sql
|
60
|
+
|
61
|
+
private
|
62
|
+
def execute
|
63
|
+
end
|
64
|
+
undef_method :execute
|
65
|
+
|
66
|
+
def executed?
|
67
|
+
end
|
68
|
+
undef_method :executed?
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Futures
|
3
|
+
class FutureCalculation < Future
|
4
|
+
attr_reader :query, :execution
|
5
|
+
private :query, :execution
|
6
|
+
|
7
|
+
def initialize(query, execution)
|
8
|
+
super()
|
9
|
+
@query = query
|
10
|
+
@execution = execution
|
11
|
+
end
|
12
|
+
|
13
|
+
def value
|
14
|
+
Future.flush unless executed?
|
15
|
+
@value
|
16
|
+
end
|
17
|
+
|
18
|
+
def inspect
|
19
|
+
value.inspect
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_sql
|
23
|
+
query
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def execute
|
29
|
+
@value = execution.call
|
30
|
+
@executed = true
|
31
|
+
end
|
32
|
+
|
33
|
+
def executed?
|
34
|
+
@executed
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Futures
|
3
|
+
class FutureRelation < Future
|
4
|
+
include Delegation
|
5
|
+
delegate :to_sql, to: :relation
|
6
|
+
|
7
|
+
attr_reader :relation
|
8
|
+
private :relation
|
9
|
+
|
10
|
+
def initialize(relation)
|
11
|
+
super()
|
12
|
+
@relation = relation
|
13
|
+
@klass = relation.klass
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_a
|
17
|
+
# Flush all the futures upon first attempt to exec a future
|
18
|
+
Future.flush unless executed?
|
19
|
+
execute
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def execute
|
25
|
+
relation.to_a
|
26
|
+
end
|
27
|
+
|
28
|
+
def executed?
|
29
|
+
relation.loaded?
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Futures
|
3
|
+
module QueryRecording
|
4
|
+
|
5
|
+
private
|
6
|
+
def record_query
|
7
|
+
orig_klass = @klass
|
8
|
+
connection = ConnectionProxy.new(@klass.connection)
|
9
|
+
@klass = KlassProxy.new(@klass, connection)
|
10
|
+
yield
|
11
|
+
connection.recorded_query
|
12
|
+
ensure
|
13
|
+
@klass = orig_klass
|
14
|
+
end
|
15
|
+
|
16
|
+
class KlassProxy
|
17
|
+
attr_reader :klass, :connection
|
18
|
+
|
19
|
+
def initialize(klass, connection)
|
20
|
+
@klass = klass
|
21
|
+
@connection = connection
|
22
|
+
end
|
23
|
+
|
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)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
class ConnectionProxy
|
38
|
+
attr_reader :connection
|
39
|
+
attr_accessor :recorded_query
|
40
|
+
|
41
|
+
def initialize(connection)
|
42
|
+
@connection = connection
|
43
|
+
end
|
44
|
+
|
45
|
+
def select_value(arel, name = nil)
|
46
|
+
self.recorded_query = arel.to_sql
|
47
|
+
nil
|
48
|
+
end
|
49
|
+
|
50
|
+
def select_all(arel, name = nil, binds = [])
|
51
|
+
self.recorded_query = arel.to_sql
|
52
|
+
[]
|
53
|
+
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
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module Futures
|
3
|
+
include QueryRecording
|
4
|
+
|
5
|
+
def self.original_calculation_methods
|
6
|
+
ActiveRecord::Calculations.public_instance_methods
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.future_calculation_methods
|
10
|
+
original_calculation_methods.map { |name| "future_#{name}" }
|
11
|
+
end
|
12
|
+
|
13
|
+
def future
|
14
|
+
supports_futures = connection.respond_to?(:supports_futures?) &&
|
15
|
+
connection.supports_futures?
|
16
|
+
|
17
|
+
# simply pass through if the connection adapter does not support
|
18
|
+
# futures
|
19
|
+
supports_futures ? FutureRelation.new(self) : self
|
20
|
+
end
|
21
|
+
|
22
|
+
method_table = Hash[future_calculation_methods.zip(original_calculation_methods)]
|
23
|
+
|
24
|
+
# define a "future_" method for each calculation method
|
25
|
+
#
|
26
|
+
method_table.each do |future_method, method|
|
27
|
+
define_method(future_method) do |*args, &block|
|
28
|
+
exec = lambda { send(method, *args, &block) }
|
29
|
+
query = record_query(&exec)
|
30
|
+
FutureCalculation.new(query, exec)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'active_support/core_ext/module/delegation'
|
3
|
+
require "activerecord-futures/version"
|
4
|
+
|
5
|
+
require "active_record/futures/future"
|
6
|
+
require "active_record/futures/future_relation"
|
7
|
+
require "active_record/futures/future_calculation"
|
8
|
+
|
9
|
+
require "active_record/futures/query_recording"
|
10
|
+
require "active_record/futures"
|
11
|
+
require "active_record/futures/delegation"
|
12
|
+
|
13
|
+
module ActiveRecord
|
14
|
+
class Relation
|
15
|
+
include Futures
|
16
|
+
end
|
17
|
+
|
18
|
+
class Base
|
19
|
+
extend Futures::Delegation
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module ActiveRecord::Futures
|
4
|
+
describe FutureRelation do
|
5
|
+
let(:relation) { double(ActiveRecord::Relation, klass: Class.new, to_a: nil) }
|
6
|
+
subject { FutureRelation.new(relation) }
|
7
|
+
|
8
|
+
describe ".new" do
|
9
|
+
before do
|
10
|
+
subject
|
11
|
+
end
|
12
|
+
|
13
|
+
it "gets registered" do
|
14
|
+
Future.all.should have(1).future
|
15
|
+
Future.all.first.should == subject
|
16
|
+
end
|
17
|
+
|
18
|
+
its(:relation) { should eq relation }
|
19
|
+
|
20
|
+
it { should_not be_fulfilled }
|
21
|
+
end
|
22
|
+
|
23
|
+
describe "#fulfill" do
|
24
|
+
let(:result) { "Some cool result" }
|
25
|
+
|
26
|
+
before do
|
27
|
+
subject.fulfill(result)
|
28
|
+
end
|
29
|
+
|
30
|
+
it { should be_fulfilled }
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "#load" do
|
34
|
+
before do
|
35
|
+
relation.stub(:to_a) do
|
36
|
+
@current_future = Future.current
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
|
40
|
+
subject.load
|
41
|
+
end
|
42
|
+
|
43
|
+
it "calls #to_a in the relation" do
|
44
|
+
relation.should have_received(:to_a)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "sets the current future to itself while #to_a was being called in the relation" do
|
48
|
+
@current_future.should == subject
|
49
|
+
end
|
50
|
+
|
51
|
+
it "sets to nil the current future afterwards" do
|
52
|
+
Future.current.should == nil
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module ActiveRecord::Futures
|
4
|
+
describe Future do
|
5
|
+
context "class methods" do
|
6
|
+
describe ".futures" do
|
7
|
+
context "with futures in two threads" do
|
8
|
+
let(:futures_key) { "#{Future.name}_futures" }
|
9
|
+
|
10
|
+
let(:a_thread) do
|
11
|
+
thread = double("Thread 1")
|
12
|
+
thread.stub(:[]).with(futures_key).and_return([])
|
13
|
+
thread
|
14
|
+
end
|
15
|
+
|
16
|
+
let(:another_thread) do
|
17
|
+
thread = double("Thread 2")
|
18
|
+
thread.stub(:[]).with(futures_key).and_return([])
|
19
|
+
thread
|
20
|
+
end
|
21
|
+
|
22
|
+
before do
|
23
|
+
Thread.stub(:current).and_return(a_thread)
|
24
|
+
|
25
|
+
Future.futures << "Future 1"
|
26
|
+
Future.futures << "Future 2"
|
27
|
+
|
28
|
+
Thread.stub(:current).and_return(another_thread)
|
29
|
+
|
30
|
+
Future.futures << "Future 3"
|
31
|
+
Future.futures << "Future 4"
|
32
|
+
end
|
33
|
+
|
34
|
+
context "the futures in thread 1" do
|
35
|
+
let(:futures) { a_thread[futures_key] }
|
36
|
+
|
37
|
+
specify { futures.should include("Future 1") }
|
38
|
+
specify { futures.should include("Future 2") }
|
39
|
+
specify { futures.should_not include("Future 3") }
|
40
|
+
specify { futures.should_not include("Future 4") }
|
41
|
+
end
|
42
|
+
|
43
|
+
context "the futures in thread 2" do
|
44
|
+
let(:futures) { another_thread[futures_key] }
|
45
|
+
|
46
|
+
specify { futures.should_not include("Future 1") }
|
47
|
+
specify { futures.should_not include("Future 2") }
|
48
|
+
specify { futures.should include("Future 3") }
|
49
|
+
specify { futures.should include("Future 4") }
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
describe ".current" do
|
55
|
+
context "with currents in two threads" do
|
56
|
+
let(:current_key) { "#{Future.name}_current" }
|
57
|
+
|
58
|
+
let(:a_thread) do
|
59
|
+
thread = Hash.new
|
60
|
+
end
|
61
|
+
|
62
|
+
let(:another_thread) do
|
63
|
+
thread = Hash.new
|
64
|
+
end
|
65
|
+
|
66
|
+
before do
|
67
|
+
Thread.stub(:current).and_return(a_thread)
|
68
|
+
|
69
|
+
Future.current = "Future 1"
|
70
|
+
|
71
|
+
Thread.stub(:current).and_return(another_thread)
|
72
|
+
|
73
|
+
Future.current = "Future 2"
|
74
|
+
end
|
75
|
+
|
76
|
+
context "the current in thread 1" do
|
77
|
+
let(:current) { a_thread[current_key] }
|
78
|
+
|
79
|
+
specify { current.should eq "Future 1" }
|
80
|
+
end
|
81
|
+
|
82
|
+
context "the current in thread 2" do
|
83
|
+
let(:current) { another_thread[current_key] }
|
84
|
+
|
85
|
+
specify { current.should eq "Future 2" }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/spec/db/schema.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
ActiveRecord::Schema.define(:version => 1) do
|
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
|
+
create_table "users", :force => true do |t|
|
10
|
+
t.string "name"
|
11
|
+
t.string "email"
|
12
|
+
t.datetime "created_at", :null => false
|
13
|
+
t.datetime "updated_at", :null => false
|
14
|
+
end
|
15
|
+
|
16
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module ActiveRecord::Futures
|
4
|
+
describe "Future fulfillment" do
|
5
|
+
subject { Future }
|
6
|
+
|
7
|
+
context "when futurizing a relation" do
|
8
|
+
let!(:future) { User.where(name: "").future }
|
9
|
+
|
10
|
+
its(:all) { should have(1).future }
|
11
|
+
|
12
|
+
context "the relation future" do
|
13
|
+
specify { future.should_not be_fulfilled }
|
14
|
+
end
|
15
|
+
|
16
|
+
context "and executing it" do
|
17
|
+
before { future.to_a }
|
18
|
+
|
19
|
+
its(:all) { should have(0).futures }
|
20
|
+
|
21
|
+
context "the future" do
|
22
|
+
subject { future }
|
23
|
+
|
24
|
+
it { should be_fulfilled }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context "when futurizing two relations" do
|
30
|
+
let!(:future) { User.where(name: "").future }
|
31
|
+
let!(:another_future) { Country.where(name: "").future }
|
32
|
+
|
33
|
+
its(:all) { should have(2).futures }
|
34
|
+
|
35
|
+
context "the first relation future" do
|
36
|
+
specify { future.should_not be_fulfilled }
|
37
|
+
end
|
38
|
+
|
39
|
+
context "the other relation future" do
|
40
|
+
specify { another_future.should_not be_fulfilled }
|
41
|
+
end
|
42
|
+
|
43
|
+
context "and executing one of them" do
|
44
|
+
before { future.to_a }
|
45
|
+
|
46
|
+
its(:all) { should have(0).futures }
|
47
|
+
|
48
|
+
context "the first relation future" do
|
49
|
+
specify { future.should be_fulfilled }
|
50
|
+
end
|
51
|
+
|
52
|
+
context "the other relation future" do
|
53
|
+
specify { another_future.should be_fulfilled }
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
context "and executing another non futurized relation" do
|
58
|
+
let!(:normal_relation) { User.where(name: "") }
|
59
|
+
before { normal_relation.to_a }
|
60
|
+
|
61
|
+
its(:all) { should have(2).futures }
|
62
|
+
|
63
|
+
context "the first relation future" do
|
64
|
+
specify { future.should_not be_fulfilled }
|
65
|
+
end
|
66
|
+
|
67
|
+
context "the other relation future" do
|
68
|
+
specify { another_future.should_not be_fulfilled }
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
data/spec/models/user.rb
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'activerecord-futures'
|
2
|
+
|
3
|
+
config = {
|
4
|
+
adapter: "future_enabled_mysql2",
|
5
|
+
database: "test",
|
6
|
+
username: "root",
|
7
|
+
password: "root",
|
8
|
+
database: "activerecord_futures_test",
|
9
|
+
host: "localhost"
|
10
|
+
}
|
11
|
+
|
12
|
+
ActiveRecord::Base.establish_connection(config)
|
13
|
+
require 'db/schema'
|
14
|
+
|
15
|
+
Dir[File.join(File.dirname(__FILE__), 'models/**/*')].each { |f| require f }
|
16
|
+
|
17
|
+
require 'rspec-spies'
|
18
|
+
|
19
|
+
RSpec.configure do |config|
|
20
|
+
config.treat_symbols_as_metadata_keys_with_true_values = true
|
21
|
+
config.run_all_when_everything_filtered = true
|
22
|
+
config.filter_run :focus
|
23
|
+
|
24
|
+
# Run specs in random order to surface order dependencies. If you find an
|
25
|
+
# order dependency and want to debug it, you can fix the order by providing
|
26
|
+
# the seed, which is printed after each run.
|
27
|
+
# --seed 1234
|
28
|
+
config.order = 'random'
|
29
|
+
|
30
|
+
config.after do
|
31
|
+
ActiveRecord::Futures::Future.clear
|
32
|
+
end
|
33
|
+
end
|
metadata
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activerecord-futures
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Leonardo Andres Garcia Crespo
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-04-12 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activerecord
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.2.13
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 3.2.13
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rspec
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec-spies
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
description: Save unnecessary round trips to the database
|
63
|
+
email:
|
64
|
+
- leoasis@gmail.com
|
65
|
+
executables: []
|
66
|
+
extensions: []
|
67
|
+
extra_rdoc_files: []
|
68
|
+
files:
|
69
|
+
- .gitignore
|
70
|
+
- .rspec
|
71
|
+
- Gemfile
|
72
|
+
- LICENSE.txt
|
73
|
+
- README.md
|
74
|
+
- Rakefile
|
75
|
+
- activerecord-futures.gemspec
|
76
|
+
- lib/active_record/connection_adapters/future_enabled_mysql2_adapter.rb
|
77
|
+
- lib/active_record/futures.rb
|
78
|
+
- lib/active_record/futures/delegation.rb
|
79
|
+
- lib/active_record/futures/future.rb
|
80
|
+
- lib/active_record/futures/future_calculation.rb
|
81
|
+
- lib/active_record/futures/future_relation.rb
|
82
|
+
- lib/active_record/futures/query_recording.rb
|
83
|
+
- lib/activerecord-futures.rb
|
84
|
+
- lib/activerecord-futures/version.rb
|
85
|
+
- spec/active_record/futures/future_relation_spec.rb
|
86
|
+
- spec/active_record/futures/future_spec.rb
|
87
|
+
- spec/db/schema.rb
|
88
|
+
- spec/in_action/future_fulfillment_spec.rb
|
89
|
+
- spec/models/country.rb
|
90
|
+
- spec/models/user.rb
|
91
|
+
- spec/spec_helper.rb
|
92
|
+
homepage: https://github.com/leoasis/activerecord-futures
|
93
|
+
licenses: []
|
94
|
+
post_install_message:
|
95
|
+
rdoc_options: []
|
96
|
+
require_paths:
|
97
|
+
- lib
|
98
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
99
|
+
none: false
|
100
|
+
requirements:
|
101
|
+
- - ! '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
105
|
+
none: false
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
requirements: []
|
111
|
+
rubyforge_project:
|
112
|
+
rubygems_version: 1.8.24
|
113
|
+
signing_key:
|
114
|
+
specification_version: 3
|
115
|
+
summary: Fetch all queries at once from the database and save round trips.
|
116
|
+
test_files:
|
117
|
+
- spec/active_record/futures/future_relation_spec.rb
|
118
|
+
- spec/active_record/futures/future_spec.rb
|
119
|
+
- spec/db/schema.rb
|
120
|
+
- spec/in_action/future_fulfillment_spec.rb
|
121
|
+
- spec/models/country.rb
|
122
|
+
- spec/models/user.rb
|
123
|
+
- spec/spec_helper.rb
|
124
|
+
has_rdoc:
|