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
data/.travis.yml
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
language: ruby
|
2
|
+
rvm:
|
3
|
+
- 1.9.3
|
4
|
+
env:
|
5
|
+
- ADAPTER=future_enabled_mysql2 activerecord=3.2.11
|
6
|
+
- ADAPTER=future_enabled_mysql2 activerecord=3.2.12
|
7
|
+
- ADAPTER=future_enabled_mysql2 activerecord=3.2.13
|
8
|
+
- ADAPTER=mysql2 activerecord=3.2.11
|
9
|
+
- ADAPTER=mysql2 activerecord=3.2.12
|
10
|
+
- ADAPTER=mysql2 activerecord=3.2.13
|
11
|
+
- ADAPTER=future_enabled_postgresql activerecord=3.2.11
|
12
|
+
- ADAPTER=future_enabled_postgresql activerecord=3.2.12
|
13
|
+
- ADAPTER=future_enabled_postgresql activerecord=3.2.13
|
14
|
+
- ADAPTER=postgresql activerecord=3.2.11
|
15
|
+
- ADAPTER=postgresql activerecord=3.2.12
|
16
|
+
- ADAPTER=postgresql activerecord=3.2.13
|
17
|
+
|
18
|
+
before_script:
|
19
|
+
- mysql -e 'create database activerecord_futures_test;'
|
20
|
+
- psql -c 'create database activerecord_futures_test;' -U postgres
|
data/Gemfile
CHANGED
@@ -1,4 +1,14 @@
|
|
1
|
-
source '
|
1
|
+
source 'http://rubygems.org'
|
2
2
|
|
3
3
|
# Specify your gem's dependencies in activerecord-futures.gemspec
|
4
4
|
gemspec
|
5
|
+
|
6
|
+
group :test do
|
7
|
+
gem 'rake'
|
8
|
+
end
|
9
|
+
|
10
|
+
gem 'coveralls', require: false
|
11
|
+
|
12
|
+
if ENV['activerecord']
|
13
|
+
gem "activerecord", ENV['activerecord']
|
14
|
+
end
|
data/README.md
CHANGED
@@ -1,7 +1,21 @@
|
|
1
1
|
# ActiveRecord::Futures
|
2
2
|
|
3
|
+
[](https://travis-ci.org/leoasis/activerecord-futures)
|
4
|
+
[](https://codeclimate.com/github/leoasis/activerecord-futures)
|
5
|
+
[](https://coveralls.io/r/leoasis/activerecord-futures)
|
6
|
+
|
7
|
+
|
3
8
|
Define future queries in ActiveRecord that will get executed in a single round trip to the database.
|
4
9
|
|
10
|
+
This gem allows to easily optimize an application using activerecord. All
|
11
|
+
independent queries can be marked as futures, so that when you execute any of
|
12
|
+
them at a later time, all the other ones will be executed as well, but the query
|
13
|
+
of all of them will be executed in a single round trip to the database. That way,
|
14
|
+
when you access the other results, they'll already be there, not needing to go
|
15
|
+
to the database again.
|
16
|
+
|
17
|
+
The idea is heavily inspired from [NHibernate's future queries](http://ayende.com/blog/3979/nhibernate-futures)
|
18
|
+
|
5
19
|
## Installation
|
6
20
|
|
7
21
|
Add this line to your application's Gemfile:
|
@@ -18,13 +32,11 @@ Or install it yourself as:
|
|
18
32
|
|
19
33
|
## Usage
|
20
34
|
|
21
|
-
|
22
|
-
|
23
|
-
Set your config/database.yml file to use the given adapter:
|
35
|
+
Once the gem is installed, set your config/database.yml file to use a future enabled adapter:
|
24
36
|
|
25
37
|
```yml
|
26
38
|
development: &development
|
27
|
-
adapter: future_enabled_mysql2 #
|
39
|
+
adapter: future_enabled_mysql2 # or "future_enabled_postgresql"
|
28
40
|
username: your_username
|
29
41
|
password: your_password
|
30
42
|
database: your_database
|
@@ -34,19 +46,24 @@ development: &development
|
|
34
46
|
Now let's see what this does, consider a model `User`, with a `:name` attribute:
|
35
47
|
|
36
48
|
```ruby
|
49
|
+
|
37
50
|
# Build the queries and mark them as futures
|
38
|
-
users = User.where("name like 'John%'")
|
39
|
-
|
51
|
+
users = User.where("name like 'John%'")
|
52
|
+
user_list = users.future # becomes a future relation, does not execute the query.
|
53
|
+
count = users.future_count # becomes a future calculation, does not execute the query.
|
40
54
|
|
41
55
|
# Execute any of the futures
|
42
56
|
count = count.value # trigger the future execution, both queries will get executed in one round trip!
|
43
57
|
#=> User Load (fetching Futures) (0.6ms) SELECT `users`.* FROM `users` WHERE (name like 'John%');SELECT COUNT(*) FROM `users` WHERE (name like 'John%')
|
44
58
|
|
45
59
|
# Access the other results
|
46
|
-
|
60
|
+
user_list.to_a # does not execute the query, results from previous query get loaded
|
47
61
|
```
|
48
62
|
|
49
|
-
Any amount of futures can be prepared, and
|
63
|
+
Any amount of futures can be prepared, and they will get executed as soon as one of them needs to be evaluated.
|
64
|
+
|
65
|
+
This makes this especially useful for pagination queries, since you can execute
|
66
|
+
both count and page queries at once.
|
50
67
|
|
51
68
|
### Methods
|
52
69
|
|
@@ -55,28 +72,37 @@ executed whenever `#to_a` gets executed. Note that, as ActiveRecord does, enumer
|
|
55
72
|
so things like `#each`, `#map`, `#collect` all trigger the future.
|
56
73
|
|
57
74
|
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.
|
59
|
-
|
75
|
+
"futurized", that means, for `#count` you get `#future_count`, for `#sum` you get `#future_sum` and so on. If the calculation
|
76
|
+
returns a list of values, for example with a `#future_pluck` or a grouped `#future_count`, the future will be triggered with
|
77
|
+
the `#to_a` method (or any of the methods that delegate to `#to_a`). If it returns a single value, the future will be
|
78
|
+
triggered when you execute the `#value` method.
|
60
79
|
|
61
80
|
## Database support
|
62
81
|
|
63
82
|
### SQlite
|
64
83
|
|
65
|
-
SQlite doesn't support multiple statement queries.
|
66
|
-
|
84
|
+
SQlite doesn't support multiple statement queries. ActiveRecord::Futures will fall back to normal query execution, that is,
|
85
|
+
it will execute the future's query whenever the future is triggered, but it will not execute the other futures' queries.
|
67
86
|
|
68
87
|
### MySQL
|
69
88
|
|
70
89
|
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
90
|
one.
|
72
|
-
Currently the adapter provided
|
73
|
-
multiple queries in a single command.
|
74
|
-
|
75
|
-
can check the code if you're curious!
|
91
|
+
Currently the adapter provided inherits the built-in one in Rails, and it also sets the MULTI_STATEMENTS flag to allow
|
92
|
+
multiple queries in a single command.
|
93
|
+
If you have an older version of the gem, ActiveRecord::Futures will fall back to normal query execution.
|
76
94
|
|
77
95
|
### Postgres
|
78
96
|
|
79
|
-
|
97
|
+
The pg gem supports multiple statement queries by using the `send_query` method
|
98
|
+
and retrieving the results via `get_result`.
|
99
|
+
|
100
|
+
### Other databases
|
101
|
+
|
102
|
+
In general, ActiveRecord::Futures will look for a method `#supports_futures?` in the adapter. So any adapter that returns
|
103
|
+
false when calling the method, or does not respond to it, will fall back to normal query execution.
|
104
|
+
If you want to have support for ActiveRecord::Futures with your database, feel free to create a pull request with it, or
|
105
|
+
create your own gem, or just create an issue.
|
80
106
|
|
81
107
|
## Contributing
|
82
108
|
|
@@ -85,9 +111,3 @@ Coming soon!
|
|
85
111
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
86
112
|
4. Push to the branch (`git push origin my-new-feature`)
|
87
113
|
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
CHANGED
@@ -1 +1,18 @@
|
|
1
1
|
require "bundler/gem_tasks"
|
2
|
+
require 'rspec/core/rake_task'
|
3
|
+
|
4
|
+
RSpec::Core::RakeTask.new(:spec)
|
5
|
+
|
6
|
+
task :default => :spec
|
7
|
+
|
8
|
+
ADAPTERS = %w(future_enabled_mysql2 future_enabled_postgresql postgresql mysql2)
|
9
|
+
|
10
|
+
desc "Runs the specs with all databases"
|
11
|
+
task :all do
|
12
|
+
success = true
|
13
|
+
ADAPTERS.each do |adapter|
|
14
|
+
status = system({ "ADAPTER" => adapter }, "bundle exec rspec")
|
15
|
+
success &&= status
|
16
|
+
end
|
17
|
+
abort unless success
|
18
|
+
end
|
@@ -17,7 +17,9 @@ Gem::Specification.new do |gem|
|
|
17
17
|
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
18
18
|
gem.require_paths = ["lib"]
|
19
19
|
|
20
|
-
gem.add_dependency 'activerecord', '>= 3.2.
|
20
|
+
gem.add_dependency 'activerecord', '>= 3.2.11'
|
21
21
|
gem.add_development_dependency 'rspec'
|
22
22
|
gem.add_development_dependency 'rspec-spies'
|
23
|
+
gem.add_development_dependency 'mysql2', '>= 0.3.12.b1'
|
24
|
+
gem.add_development_dependency 'pg'
|
23
25
|
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module ActiveRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
module FutureEnabled
|
4
|
+
def supports_futures?
|
5
|
+
true
|
6
|
+
end
|
7
|
+
|
8
|
+
def exec_query(sql, name = 'SQL', binds = [])
|
9
|
+
my_future = Futures::Future.current
|
10
|
+
|
11
|
+
# default behavior if not a current future or not executing
|
12
|
+
# the current future's sql (some adapters like PostgreSQL
|
13
|
+
# may execute some attribute queries during a relation evaluation)
|
14
|
+
return super unless my_future && my_future.to_sql == sql
|
15
|
+
|
16
|
+
# return fulfilled result, if exists, to load the relation
|
17
|
+
return my_future.result if my_future.fulfilled?
|
18
|
+
|
19
|
+
futures = Futures::Future.all
|
20
|
+
futures_sql = futures.map(&:to_sql).join(';')
|
21
|
+
name = "#{name} (fetching Futures)"
|
22
|
+
|
23
|
+
result = future_execute(futures_sql, name)
|
24
|
+
|
25
|
+
futures.each do |future|
|
26
|
+
future.fulfill(build_active_record_result(result))
|
27
|
+
result = next_result
|
28
|
+
end
|
29
|
+
|
30
|
+
my_future.result
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
require "active_record/connection_adapters/mysql2_adapter"
|
2
|
-
|
2
|
+
require "active_record/connection_adapters/future_enabled"
|
3
3
|
module ActiveRecord
|
4
4
|
class Base
|
5
5
|
def self.future_enabled_mysql2_connection(config)
|
@@ -18,33 +18,34 @@ module ActiveRecord
|
|
18
18
|
|
19
19
|
module ConnectionAdapters
|
20
20
|
class FutureEnabledMysql2Adapter < Mysql2Adapter
|
21
|
+
include FutureEnabled
|
22
|
+
|
23
|
+
def initialize(*args)
|
24
|
+
super
|
25
|
+
unless supports_futures?
|
26
|
+
logger.warn("ActiveRecord::Futures - You're using the mysql2 future "\
|
27
|
+
"enabled adapter with an old version of the mysql2 gem. You must "\
|
28
|
+
"use a mysql2 gem version higher than or equal to 0.3.12b1 to take "\
|
29
|
+
"advantage of futures.\nFalling back to normal query execution behavior.")
|
30
|
+
end
|
31
|
+
end
|
21
32
|
|
22
33
|
def supports_futures?
|
23
|
-
|
34
|
+
# Support only if the mysql client allows fetching multiple statements
|
35
|
+
# results
|
36
|
+
@connection.respond_to?(:store_result)
|
24
37
|
end
|
25
38
|
|
26
|
-
def
|
27
|
-
|
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)
|
39
|
+
def future_execute(sql, name)
|
40
|
+
execute(sql, name)
|
41
|
+
end
|
41
42
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
end
|
43
|
+
def build_active_record_result(raw_result)
|
44
|
+
ActiveRecord::Result.new(raw_result.fields, raw_result.to_a)
|
45
|
+
end
|
46
46
|
|
47
|
-
|
47
|
+
def next_result
|
48
|
+
@connection.store_result if @connection.next_result
|
48
49
|
end
|
49
50
|
end
|
50
51
|
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'active_record/connection_adapters/postgresql_adapter'
|
2
|
+
require "active_record/connection_adapters/future_enabled"
|
3
|
+
|
4
|
+
module ActiveRecord
|
5
|
+
class Base
|
6
|
+
# Establishes a connection to the database that's used by all Active Record objects
|
7
|
+
def self.future_enabled_postgresql_connection(config) # :nodoc:
|
8
|
+
config = config.symbolize_keys
|
9
|
+
host = config[:host]
|
10
|
+
port = config[:port] || 5432
|
11
|
+
username = config[:username].to_s if config[:username]
|
12
|
+
password = config[:password].to_s if config[:password]
|
13
|
+
|
14
|
+
if config.key?(:database)
|
15
|
+
database = config[:database]
|
16
|
+
else
|
17
|
+
raise ArgumentError, "No database specified. Missing argument: database."
|
18
|
+
end
|
19
|
+
|
20
|
+
# The postgres drivers don't allow the creation of an unconnected PGconn object,
|
21
|
+
# so just pass a nil connection object for the time being.
|
22
|
+
ConnectionAdapters::FutureEnabledPostgreSQLAdapter.new(nil, logger, [host, port, nil, nil, database, username, password], config)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module ConnectionAdapters
|
27
|
+
class FutureEnabledPostgreSQLAdapter < PostgreSQLAdapter
|
28
|
+
include FutureEnabled
|
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
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def build_active_record_result(raw_result)
|
41
|
+
return if raw_result.nil?
|
42
|
+
result = ActiveRecord::Result.new(raw_result.fields, result_as_array(raw_result))
|
43
|
+
raw_result.clear
|
44
|
+
result
|
45
|
+
end
|
46
|
+
|
47
|
+
def next_result
|
48
|
+
@connection.get_result
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -3,7 +3,7 @@ module ActiveRecord
|
|
3
3
|
include QueryRecording
|
4
4
|
|
5
5
|
def self.original_calculation_methods
|
6
|
-
|
6
|
+
[:count, :average, :minimum, :maximum, :sum, :calculate, :pluck]
|
7
7
|
end
|
8
8
|
|
9
9
|
def self.future_calculation_methods
|
@@ -11,12 +11,7 @@ module ActiveRecord
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def future
|
14
|
-
|
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
|
14
|
+
FutureRelation.new(self)
|
20
15
|
end
|
21
16
|
|
22
17
|
method_table = Hash[future_calculation_methods.zip(original_calculation_methods)]
|
@@ -26,8 +21,14 @@ module ActiveRecord
|
|
26
21
|
method_table.each do |future_method, method|
|
27
22
|
define_method(future_method) do |*args, &block|
|
28
23
|
exec = lambda { send(method, *args, &block) }
|
29
|
-
query = record_query(&exec)
|
30
|
-
|
24
|
+
query, type = record_query(&exec)
|
25
|
+
|
26
|
+
case type
|
27
|
+
when :value
|
28
|
+
FutureCalculationValue.new(self, query, exec)
|
29
|
+
when :all
|
30
|
+
FutureCalculationArray.new(self, query, exec)
|
31
|
+
end
|
31
32
|
end
|
32
33
|
end
|
33
34
|
end
|
@@ -27,12 +27,23 @@ module ActiveRecord
|
|
27
27
|
self.futures.each(&:load)
|
28
28
|
clear
|
29
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
|
38
|
+
end
|
30
39
|
end
|
31
40
|
|
32
41
|
|
33
|
-
attr_reader :result
|
42
|
+
attr_reader :result, :relation
|
43
|
+
private :relation
|
34
44
|
|
35
|
-
def initialize
|
45
|
+
def initialize(relation)
|
46
|
+
@relation = relation
|
36
47
|
Future.register(self)
|
37
48
|
end
|
38
49
|
|
@@ -45,15 +56,15 @@ module ActiveRecord
|
|
45
56
|
end
|
46
57
|
|
47
58
|
def load
|
59
|
+
# Only perform a load if the adapter supports futures.
|
60
|
+
# This allows to fallback to normal query execution in futures
|
61
|
+
# when the adapter does not support futures.
|
62
|
+
return unless connection_supports_futures?
|
48
63
|
Future.current = self
|
49
64
|
execute
|
50
65
|
Future.current = nil
|
51
66
|
end
|
52
67
|
|
53
|
-
def inspect
|
54
|
-
to_a.inspect
|
55
|
-
end
|
56
|
-
|
57
68
|
def to_sql
|
58
69
|
end
|
59
70
|
undef_method :to_sql
|
@@ -67,6 +78,11 @@ module ActiveRecord
|
|
67
78
|
end
|
68
79
|
undef_method :executed?
|
69
80
|
|
81
|
+
def connection_supports_futures?
|
82
|
+
conn = relation.connection
|
83
|
+
conn.respond_to?(:supports_futures?) && conn.supports_futures?
|
84
|
+
end
|
85
|
+
|
70
86
|
end
|
71
87
|
end
|
72
88
|
end
|