├── .rspec ├── spec ├── models │ ├── user.rb │ ├── comment.rb │ └── post.rb ├── active_record │ └── futures │ │ ├── future_array_spec.rb │ │ ├── future_value_spec.rb │ │ ├── middleware_spec.rb │ │ ├── proxy_spec.rb │ │ ├── future_spec.rb │ │ └── future_registry_spec.rb ├── in_action │ ├── future_first_execution_spec.rb │ ├── future_all_execution_spec.rb │ ├── future_exists_execution_spec.rb │ ├── future_last_execution_spec.rb │ ├── future_pluck_execution_spec.rb │ ├── future_relation_execution_spec.rb │ ├── future_count_execution_spec.rb │ ├── future_find_execution_spec.rb │ ├── future_fulfillment_spec.rb │ └── combination_of_futures_spec.rb ├── support │ ├── matchers │ │ ├── exec_query.rb │ │ └── exec.rb │ └── futurized_method_examples.rb ├── db │ └── schema.rb └── spec_helper.rb ├── lib ├── activerecord-futures │ └── version.rb ├── active_record │ ├── futures │ │ ├── railtie.rb │ │ ├── delegation.rb │ │ ├── middleware.rb │ │ ├── future_value.rb │ │ ├── future_array.rb │ │ ├── future_registry.rb │ │ ├── proxy.rb │ │ ├── calculation_methods.rb │ │ ├── future.rb │ │ ├── query_recording.rb │ │ └── finder_methods.rb │ ├── futures.rb │ └── connection_adapters │ │ ├── future_enabled.rb │ │ ├── future_enabled_mysql2_adapter.rb │ │ └── future_enabled_postgresql_adapter.rb └── activerecord-futures.rb ├── .gitignore ├── Gemfile ├── Rakefile ├── .travis.yml ├── LICENSE.txt ├── activerecord-futures.gemspec └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format progress 3 | -------------------------------------------------------------------------------- /spec/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | has_many :comments 3 | end -------------------------------------------------------------------------------- /spec/models/comment.rb: -------------------------------------------------------------------------------- 1 | class Comment < ActiveRecord::Base 2 | belongs_to :user 3 | belongs_to :post 4 | end -------------------------------------------------------------------------------- /spec/models/post.rb: -------------------------------------------------------------------------------- 1 | class Post < ActiveRecord::Base 2 | default_scope { where("published_at is not null") } 3 | end -------------------------------------------------------------------------------- /lib/activerecord-futures/version.rb: -------------------------------------------------------------------------------- 1 | module Activerecord 2 | module Futures 3 | VERSION = "0.4.0" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/active_record/futures/railtie.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | module Futures 3 | class Railtie < ::Rails::Railtie 4 | config.app_middleware.use Middleware 5 | end 6 | end 7 | end -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | *.sqlite3 7 | Gemfile.lock 8 | InstalledFiles 9 | _yardoc 10 | coverage 11 | doc/ 12 | lib/bundler/man 13 | pkg 14 | rdoc 15 | spec/reports 16 | test/tmp 17 | test/version_tmp 18 | tmp 19 | -------------------------------------------------------------------------------- /lib/active_record/futures/delegation.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | module Futures 3 | module Delegation 4 | delegate :future, to: :scoped 5 | delegate *Futures.future_calculation_methods, to: :scoped 6 | delegate *Futures.future_finder_methods, to: :scoped 7 | end 8 | end 9 | end -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | # Specify your gem's dependencies in activerecord-futures.gemspec 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 15 | -------------------------------------------------------------------------------- /lib/active_record/futures/middleware.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | module Futures 3 | class Middleware 4 | def initialize(app) 5 | @app = app 6 | end 7 | 8 | def call(env) 9 | FutureRegistry.clear 10 | @app.call(env) 11 | end 12 | end 13 | end 14 | end -------------------------------------------------------------------------------- /lib/active_record/futures/future_value.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | module Futures 3 | class FutureValue 4 | attr_reader :future_execution 5 | private :future_execution 6 | 7 | def initialize(future_execution) 8 | @future_execution = future_execution 9 | end 10 | 11 | def value 12 | future_execution.execute 13 | end 14 | 15 | def inspect 16 | value 17 | end 18 | end 19 | end 20 | end -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 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_postgresql future_enabled_mysql2 postgresql mysql2 sqlite3) 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 19 | -------------------------------------------------------------------------------- /lib/active_record/futures.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | module Futures 3 | 4 | def self.futurize(method) 5 | "future_#{method}" 6 | end 7 | 8 | include QueryRecording 9 | include FinderMethods 10 | include CalculationMethods 11 | 12 | def future 13 | FutureArray.new(record_future(:to_a)) 14 | end 15 | 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) 21 | end 22 | end 23 | end -------------------------------------------------------------------------------- /lib/active_record/futures/future_array.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /spec/active_record/futures/future_array_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module ActiveRecord::Futures 4 | describe FutureArray do 5 | let(:future) do 6 | double(Future, execute: nil) 7 | end 8 | subject { FutureArray.new(future) } 9 | 10 | describe "#to_a" do 11 | before do 12 | subject.to_a 13 | end 14 | 15 | specify { future.should have_received(:execute) } 16 | end 17 | 18 | describe "#inspect" do 19 | before do 20 | subject.inspect 21 | end 22 | 23 | specify { future.should have_received(:execute) } 24 | end 25 | end 26 | end -------------------------------------------------------------------------------- /spec/active_record/futures/future_value_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module ActiveRecord::Futures 4 | describe FutureValue do 5 | let(:future) do 6 | double(Future, execute: nil) 7 | end 8 | subject { FutureValue.new(future) } 9 | 10 | describe "#value" do 11 | before do 12 | subject.value 13 | end 14 | 15 | specify { future.should have_received(:execute) } 16 | end 17 | 18 | describe "#inspect" do 19 | before do 20 | subject.inspect 21 | end 22 | 23 | specify { future.should have_received(:execute) } 24 | end 25 | end 26 | end -------------------------------------------------------------------------------- /spec/in_action/future_first_execution_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "future_first method" do 4 | let(:relation) { Post.where("published_at < ?", Time.new(2013, 1, 1)) } 5 | let(:future) { relation.future_first } 6 | let(:relation_result) { relation.first } 7 | let(:future_sql) do 8 | arel = relation.arel 9 | arel.limit = 1 10 | arel.to_sql 11 | end 12 | 13 | before do 14 | Post.create(published_at: Time.new(2012, 12, 10)) 15 | Post.create(published_at: Time.new(2012, 6, 23)) 16 | Post.create(published_at: Time.new(2013, 4, 5)) 17 | end 18 | 19 | it_behaves_like "a futurized method", :value 20 | end -------------------------------------------------------------------------------- /spec/support/matchers/exec_query.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :exec_query do |expected| 2 | 3 | match do |block| 4 | query(&block) == expected 5 | end 6 | 7 | failure_message_for_should do |actual| 8 | "Expected to execute #{expected}, got #{@query}" 9 | end 10 | 11 | failure_message_for_should_not do |actual| 12 | "Expected to not execute #{expected}, got #{actual}" 13 | end 14 | 15 | def query(&block) 16 | query = lambda do |name, start, finish, message_id, values| 17 | @query = values[:sql] 18 | end 19 | ActiveSupport::Notifications.subscribed(query, 'sql.active_record', &block) 20 | @query 21 | end 22 | 23 | end -------------------------------------------------------------------------------- /spec/in_action/future_all_execution_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "future_all method" do 4 | context "with no parameters" do 5 | let(:relation) { Post.where("published_at < ?", Time.new(2013, 1, 1)) } 6 | let(:future) { relation.future_all } 7 | let(:relation_result) { relation.all } 8 | let(:future_sql) do 9 | relation.to_sql 10 | end 11 | 12 | before do 13 | Post.create(published_at: Time.new(2012, 12, 10)) 14 | Post.create(published_at: Time.new(2012, 6, 23)) 15 | Post.create(published_at: Time.new(2013, 4, 5)) 16 | end 17 | 18 | it_behaves_like "a futurized method", :to_a 19 | end 20 | end -------------------------------------------------------------------------------- /spec/in_action/future_exists_execution_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "future_exists? method" do 4 | let(:relation) { Post.where("published_at < ?", Time.new(2013, 1, 1)) } 5 | let(:future) { relation.future_exists? } 6 | let(:relation_result) { relation.exists? } 7 | let(:future_sql) do 8 | arel = relation.arel 9 | arel.projections = [] 10 | arel.project("1 AS one") 11 | arel.limit = 1 12 | arel.to_sql 13 | end 14 | 15 | before do 16 | Post.create(published_at: Time.new(2012, 12, 10)) 17 | Post.create(published_at: Time.new(2012, 6, 23)) 18 | Post.create(published_at: Time.new(2013, 4, 5)) 19 | end 20 | 21 | it_behaves_like "a futurized method", :value 22 | end -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 1.9.3 4 | env: 5 | - ADAPTER=mysql2 activerecord=3.2.11 6 | - ADAPTER=mysql2 activerecord=3.2.12 7 | - ADAPTER=mysql2 activerecord=3.2.13 8 | - ADAPTER=future_enabled_mysql2 activerecord=3.2.13 9 | - ADAPTER=postgresql activerecord=3.2.11 10 | - ADAPTER=postgresql activerecord=3.2.12 11 | - ADAPTER=postgresql activerecord=3.2.13 12 | - ADAPTER=future_enabled_postgresql activerecord=3.2.13 13 | - ADAPTER=sqlite3 activerecord=3.2.11 14 | - ADAPTER=sqlite3 activerecord=3.2.12 15 | - ADAPTER=sqlite3 activerecord=3.2.13 16 | 17 | before_script: 18 | - mysql -e 'create database activerecord_futures_test;' 19 | - psql -c 'create database activerecord_futures_test;' -U postgres -------------------------------------------------------------------------------- /spec/in_action/future_last_execution_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "future_last method" do 4 | let(:relation) { Post.where("published_at < ?", Time.new(2013, 1, 1)) } 5 | let(:future) { relation.future_last } 6 | let(:relation_result) { relation.last } 7 | let(:future_sql) do 8 | arel = relation.arel 9 | arel.order("#{relation.quoted_table_name}.#{relation.quoted_primary_key} DESC") 10 | arel.limit = 1 11 | arel.to_sql 12 | end 13 | 14 | before do 15 | Post.create(published_at: Time.new(2012, 12, 10)) 16 | Post.create(published_at: Time.new(2012, 6, 23)) 17 | Post.create(published_at: Time.new(2013, 4, 5)) 18 | end 19 | 20 | it_behaves_like "a futurized method", :value 21 | end -------------------------------------------------------------------------------- /lib/active_record/futures/future_registry.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /spec/in_action/future_pluck_execution_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "future_pluck method" do 4 | let(:relation) { Post.where("published_at < ?", Time.new(2013, 1, 1)) } 5 | let(:future) { relation.future_pluck('title') } 6 | let(:relation_result) { relation.pluck('title') } 7 | let(:future_sql) do 8 | arel = relation.arel 9 | arel.projections = [] 10 | arel.project('title') 11 | arel.to_sql 12 | end 13 | 14 | before do 15 | Post.create(title: "Post 1", published_at: Time.new(2012, 12, 10)) 16 | Post.create(title: "Post 2", published_at: Time.new(2012, 6, 23)) 17 | Post.create(title: "Post 3", published_at: Time.new(2013, 4, 5)) 18 | end 19 | 20 | it_behaves_like "a futurized method", :to_a 21 | end -------------------------------------------------------------------------------- /spec/db/schema.rb: -------------------------------------------------------------------------------- 1 | ActiveRecord::Schema.define(:version => 1) do 2 | 3 | create_table "users", :force => true do |t| 4 | t.string "name" 5 | t.string "email" 6 | t.datetime "created_at", :null => false 7 | t.datetime "updated_at", :null => false 8 | end 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 25 | end -------------------------------------------------------------------------------- /spec/in_action/future_relation_execution_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe "future method" do 4 | 5 | before do 6 | Post.create(published_at: Time.new(2012, 12, 10)) 7 | Post.create(published_at: Time.new(2012, 6, 23)) 8 | Post.create(published_at: Time.new(2013, 4, 5)) 9 | end 10 | 11 | def self.test_case(description, &relation_lambda) 12 | context "with a sample relation that #{description}" do 13 | let(:relation) { relation_lambda.call } 14 | let(:relation_result) { relation.to_a } 15 | let(:future) { relation.future } 16 | let(:future_sql) { relation.to_sql } 17 | 18 | it_behaves_like "a futurized method", :to_a 19 | end 20 | end 21 | 22 | test_case "filters by published_at" do 23 | Post.where("published_at < ?", Time.new(2013, 1, 1)) 24 | end 25 | 26 | test_case "limits by 10" do 27 | Post.limit(10) 28 | end 29 | 30 | end -------------------------------------------------------------------------------- /lib/active_record/futures/proxy.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /lib/active_record/futures/calculation_methods.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 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. -------------------------------------------------------------------------------- /spec/active_record/futures/middleware_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | require 'active_record/futures/middleware' 3 | 4 | module ActiveRecord::Futures 5 | describe Middleware do 6 | let(:app) { double("Rack Application", call: nil) } 7 | let(:env) { double("App environment") } 8 | 9 | subject(:middleware) { Middleware.new(app) } 10 | 11 | before do 12 | FutureRegistry.stub(:clear) 13 | end 14 | 15 | context "normal flow" do 16 | before do 17 | middleware.call(env) 18 | end 19 | 20 | it "resets the registry" do 21 | FutureRegistry.should have_received(:clear) 22 | end 23 | 24 | it "continues calling the middleware stack" do 25 | app.should have_received(:call).with(env) 26 | end 27 | end 28 | 29 | context "when app.call raises exception" do 30 | before do 31 | app.stub(:call).and_raise("some error") 32 | begin 33 | middleware.call(env) 34 | rescue 35 | end 36 | end 37 | 38 | it "still clears the registry" do 39 | FutureRegistry.should have_received(:clear) 40 | end 41 | end 42 | end 43 | end -------------------------------------------------------------------------------- /activerecord-futures.gemspec: -------------------------------------------------------------------------------- 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.11' 21 | gem.add_development_dependency 'rspec', '2.13.0' 22 | gem.add_development_dependency 'rspec-spies' 23 | gem.add_development_dependency 'mysql2', '>= 0.3.12.b1' 24 | gem.add_development_dependency 'pg' 25 | gem.add_development_dependency 'sqlite3' 26 | end 27 | -------------------------------------------------------------------------------- /lib/active_record/connection_adapters/future_enabled.rb: -------------------------------------------------------------------------------- 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::FutureRegistry.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 if !my_future || to_sql(my_future.query, my_future.binds.try(:dup)) != 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::FutureRegistry.all 20 | future_arels = futures.map(&:query) 21 | future_binds = futures.map(&:binds) 22 | name = "#{name} (fetching Futures)" 23 | 24 | result = future_execute(future_arels, future_binds, name) 25 | 26 | futures.each do |future| 27 | future.fulfill(build_active_record_result(result)) 28 | result = next_result 29 | end 30 | 31 | my_future.result 32 | end 33 | end 34 | end 35 | end -------------------------------------------------------------------------------- /spec/active_record/futures/proxy_spec.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /spec/support/futurized_method_examples.rb: -------------------------------------------------------------------------------- 1 | shared_examples "a futurized method" do |exec_trigger| 2 | describe "##{exec_trigger}" do 3 | let(:future_execution) { future.send(:future_execution) } 4 | let(:calling_future) { -> { future.send(exec_trigger) } } 5 | 6 | specify(nil, :supporting_adapter) { future_execution.should_not be_fulfilled } 7 | 8 | specify do 9 | calling_future.should exec(1).query 10 | end 11 | 12 | specify(nil, postgresql: false, sqlite3: false) do 13 | calling_future.should exec_query(future_sql) 14 | end 15 | 16 | specify(nil, postgresql: true) do 17 | sql = respond_to?(:future_sql_postgresql) ? future_sql_postgresql : future_sql 18 | calling_future.should exec_query(sql) 19 | end 20 | 21 | specify(nil, sqlite3: true) do 22 | sql = respond_to?(:future_sql_sqlite3) ? future_sql_sqlite3 : future_sql 23 | calling_future.should exec_query(sql) 24 | end 25 | 26 | specify { future.send(exec_trigger).should eq relation_result } 27 | 28 | context "after executing the future" do 29 | before do 30 | future.send(exec_trigger) 31 | end 32 | 33 | specify(nil, :supporting_adapter) { future_execution.should be_fulfilled } 34 | end 35 | 36 | context "executing it twice" do 37 | before do 38 | future.send(exec_trigger) 39 | end 40 | 41 | specify do 42 | calling_future.should exec(0).queries 43 | end 44 | 45 | specify { future.send(exec_trigger).should eq relation_result } 46 | end 47 | end 48 | end -------------------------------------------------------------------------------- /lib/active_record/futures/future.rb: -------------------------------------------------------------------------------- 1 | module ActiveRecord 2 | module Futures 3 | class Future 4 | attr_reader :result, :relation, :query, :binds, :execution 5 | private :relation, :execution 6 | 7 | def initialize(relation, query, binds, execution) 8 | @relation = relation 9 | @query = query 10 | @binds = binds 11 | @execution = execution 12 | FutureRegistry.register(self) 13 | end 14 | 15 | def fulfill(result) 16 | @result = result 17 | end 18 | 19 | def fulfilled? 20 | !result.nil? 21 | end 22 | 23 | def load 24 | # Only perform a load if the adapter supports futures. 25 | # This allows to fallback to normal query execution in futures 26 | # when the adapter does not support futures. 27 | return unless connection_supports_futures? 28 | FutureRegistry.current = self 29 | execute(false) 30 | FutureRegistry.current = nil 31 | end 32 | 33 | def execute(flush = true) 34 | # Flush all the futures upon first attempt to exec a future 35 | FutureRegistry.flush if flush && !executed? 36 | 37 | unless executed? 38 | @value = execution.call 39 | @executed = true 40 | end 41 | 42 | @value 43 | end 44 | 45 | def executed? 46 | @executed 47 | end 48 | 49 | private 50 | def connection_supports_futures? 51 | conn = relation.connection 52 | conn.respond_to?(:supports_futures?) && conn.supports_futures? 53 | end 54 | end 55 | end 56 | end -------------------------------------------------------------------------------- /spec/in_action/future_count_execution_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "future_count method" do 4 | context "single value count" do 5 | let(:relation) { Post.where("published_at < ?", Time.new(2013, 1, 1)) } 6 | let(:future) { relation.future_count } 7 | let(:relation_result) { relation.count } 8 | let(:future_sql) do 9 | arel = relation.arel 10 | arel.projections = [] 11 | arel.project("COUNT(*)") 12 | arel.to_sql 13 | end 14 | 15 | before do 16 | Post.create(published_at: Time.new(2012, 12, 10)) 17 | Post.create(published_at: Time.new(2012, 6, 23)) 18 | Post.create(published_at: Time.new(2013, 4, 5)) 19 | end 20 | 21 | it_behaves_like "a futurized method", :value 22 | end 23 | 24 | context "grouped value count" do 25 | let(:relation) { Comment.scoped } 26 | let(:future) { relation.future_count(group: :post_id) } 27 | let(:relation_result) { relation.count(group: :post_id) } 28 | let(:future_sql) do 29 | arel = relation.arel 30 | arel.projections = [] 31 | arel.project("COUNT(*) AS count_all") 32 | arel.project("post_id AS post_id") 33 | arel.group("post_id") 34 | arel.to_sql 35 | end 36 | 37 | let(:post_1) { Post.create(published_at: Time.now) } 38 | let(:post_2) { Post.create(published_at: Time.now) } 39 | 40 | before do 41 | Comment.create(post: post_1) 42 | Comment.create(post: post_1) 43 | Comment.create(post: post_2) 44 | Comment.create(post: post_2) 45 | Comment.create(post: post_2) 46 | end 47 | 48 | it_behaves_like "a futurized method", :value 49 | end 50 | end -------------------------------------------------------------------------------- /lib/active_record/futures/query_recording.rb: -------------------------------------------------------------------------------- 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 | @loaded = false 12 | [connection.recorded_query, connection.recorded_binds] 13 | ensure 14 | @klass = orig_klass 15 | end 16 | 17 | class KlassProxy < Proxy 18 | attr_reader :klass, :connection 19 | 20 | def initialize(klass, connection) 21 | super(klass) 22 | @klass = klass 23 | @connection = connection 24 | end 25 | 26 | def build_default_scope 27 | scope = @klass.send(:build_default_scope) 28 | scope.instance_variable_set(:@klass, self) 29 | scope 30 | end 31 | 32 | def find_by_sql(sql, binds = []) 33 | connection.recorded_query = sanitize_sql(sql) 34 | connection.recorded_binds = binds 35 | [] 36 | end 37 | end 38 | 39 | class ConnectionProxy < Proxy 40 | attr_reader :connection 41 | attr_accessor :recorded_query, :recorded_binds 42 | 43 | def initialize(connection) 44 | super(connection) 45 | @connection = connection 46 | end 47 | 48 | def select_value(arel, name = nil) 49 | self.recorded_query = arel 50 | nil 51 | end 52 | 53 | def select_all(arel, name = nil, binds = []) 54 | self.recorded_query = arel 55 | [] 56 | end 57 | end 58 | end 59 | end 60 | end -------------------------------------------------------------------------------- /spec/active_record/futures/future_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module ActiveRecord::Futures 4 | describe Future do 5 | let(:relation) do 6 | double(ActiveRecord::Relation, { 7 | connection: double("connection", supports_futures?: true) 8 | }) 9 | end 10 | 11 | let(:query) { double("A query") } 12 | let(:binds) { double("Some query binds") } 13 | let(:execution) { double("The query execution", call: nil) } 14 | 15 | subject { Future.new(relation, query, binds, execution) } 16 | 17 | describe ".new" do 18 | before { FutureRegistry.stub(:register) } 19 | before { subject } 20 | 21 | it "gets registered" do 22 | FutureRegistry.should have_received(:register).with(subject) 23 | end 24 | 25 | its(:query) { should eq query } 26 | its(:binds) { should eq binds } 27 | 28 | it { should_not be_fulfilled } 29 | end 30 | 31 | describe "#fulfill" do 32 | let(:result) { "Some cool result" } 33 | 34 | before do 35 | subject.fulfill(result) 36 | end 37 | 38 | it { should be_fulfilled } 39 | end 40 | 41 | describe "#load" do 42 | before do 43 | execution.stub(:call) do 44 | @current_future = FutureRegistry.current 45 | nil 46 | end 47 | 48 | subject.load 49 | end 50 | 51 | it "calls the execution" do 52 | execution.should have_received(:call) 53 | end 54 | 55 | it "sets the current future to itself while execution was being called in the relation" do 56 | @current_future.should == subject 57 | end 58 | 59 | it "sets to nil the current future afterwards" do 60 | FutureRegistry.current.should == nil 61 | end 62 | end 63 | end 64 | end -------------------------------------------------------------------------------- /lib/active_record/futures/finder_methods.rb: -------------------------------------------------------------------------------- 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 -------------------------------------------------------------------------------- /spec/support/matchers/exec.rb: -------------------------------------------------------------------------------- 1 | RSpec::Matchers.define :exec do |expected| 2 | 3 | match do |block| 4 | query_count(&block) == expected 5 | end 6 | 7 | chain :queries do 8 | end 9 | 10 | chain :query do 11 | end 12 | 13 | description do 14 | queries = expected == 1 ? "query" : "queries" 15 | "exec #{expected} #{queries}" 16 | end 17 | 18 | failure_message_for_should do |actual| 19 | "Expected to execute #{expected} queries, executed #{@query_counter.query_count}: #{@query_counter.queries}" 20 | end 21 | 22 | failure_message_for_should_not do |actual| 23 | "Expected to not execute #{expected} queries" 24 | end 25 | 26 | def query_count(&block) 27 | @query_counter = QueryCounter.new 28 | ActiveSupport::Notifications.subscribed(@query_counter.method(:call), 'sql.active_record', &block) 29 | @query_counter.query_count 30 | end 31 | end 32 | 33 | class QueryCounter 34 | attr_accessor :query_count 35 | attr_accessor :queries 36 | 37 | IGNORED_SQL = [ 38 | /^PRAGMA (?!(table_info))/, 39 | /^SELECT currval/, 40 | /^SELECT CAST/, 41 | /^SELECT @@IDENTITY/, 42 | /^SELECT @@ROWCOUNT/, 43 | /^SAVEPOINT/, 44 | /^ROLLBACK TO SAVEPOINT/, 45 | /^RELEASE SAVEPOINT/, 46 | /^SHOW max_identifier_length/, 47 | /SELECT attr.attname\n FROM pg_attribute attr/, 48 | /SHOW/ 49 | ] 50 | 51 | def initialize 52 | self.query_count = 0 53 | self.queries = [] 54 | end 55 | 56 | def call(name, start, finish, message_id, values) 57 | # FIXME: this seems bad. we should probably have a better way to indicate 58 | # the query was cached 59 | unless 'CACHE' == values[:name] 60 | unless IGNORED_SQL.any? { |r| values[:sql] =~ r } 61 | self.query_count += 1 62 | self.queries << values[:sql] 63 | end 64 | end 65 | end 66 | end -------------------------------------------------------------------------------- /lib/activerecord-futures.rb: -------------------------------------------------------------------------------- 1 | require 'active_record' 2 | require 'active_support/core_ext/module/delegation' 3 | require "activerecord-futures/version" 4 | 5 | require "active_record/futures/future_registry" 6 | require "active_record/futures/future" 7 | require "active_record/futures/future_array" 8 | require "active_record/futures/future_value" 9 | 10 | require "active_record/futures/proxy" 11 | require "active_record/futures/query_recording" 12 | require "active_record/futures/finder_methods" 13 | require "active_record/futures/calculation_methods" 14 | require "active_record/futures" 15 | require "active_record/futures/delegation" 16 | 17 | require "active_record/futures/middleware" if defined?(Rack) 18 | require "active_record/futures/railtie" if defined?(Rails) 19 | 20 | module ActiveRecord 21 | class Relation 22 | include Futures 23 | end 24 | 25 | class Base 26 | extend Futures::Delegation 27 | end 28 | end 29 | 30 | class ActiveRecord::Base::ConnectionSpecification 31 | class Resolver 32 | def spec_with_futures 33 | spec = spec_without_futures 34 | begin 35 | config = spec.config 36 | future_adapter_name = "future_enabled_#{config[:adapter]}" 37 | 38 | # Try to load the future version of the adapter 39 | require "active_record/connection_adapters/#{future_adapter_name}_adapter" 40 | 41 | config[:adapter] = future_adapter_name 42 | adapter_method = "future_enabled_#{spec.adapter_method}" 43 | 44 | # Return the specification with the future adapter instead 45 | ActiveRecord::Base::ConnectionSpecification.new(config, adapter_method) 46 | rescue LoadError 47 | # No future version of the adapter, or the adapter was already a future 48 | # one. Keep going as usual... 49 | spec 50 | end 51 | end 52 | 53 | alias_method_chain :spec, :futures 54 | end 55 | end -------------------------------------------------------------------------------- /lib/active_record/connection_adapters/future_enabled_mysql2_adapter.rb: -------------------------------------------------------------------------------- 1 | require "active_record/connection_adapters/mysql2_adapter" 2 | require "active_record/connection_adapters/future_enabled" 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 | 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 32 | 33 | def supports_futures? 34 | # Support only if the mysql client allows fetching multiple statements 35 | # results 36 | @connection.respond_to?(:store_result) 37 | end 38 | 39 | def future_execute(arels, binds, name) 40 | sql = arels.zip(binds).map { |arel, bind| to_sql(arel, bind.try(:dup)) }.join(';') 41 | execute(sql, name) 42 | end 43 | 44 | def build_active_record_result(raw_result) 45 | ActiveRecord::Result.new(raw_result.fields, raw_result.to_a) 46 | end 47 | 48 | def next_result 49 | @connection.store_result if @connection.next_result 50 | end 51 | end 52 | end 53 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'coveralls' 2 | Coveralls.wear! 3 | 4 | require 'activerecord-futures' 5 | 6 | configs = { 7 | future_enabled_postgresql: { 8 | adapter: "postgresql", 9 | database: "activerecord_futures_test", 10 | username: "postgres" 11 | }, 12 | future_enabled_mysql2: { 13 | adapter: "mysql2", 14 | database: "activerecord_futures_test", 15 | username: "root", 16 | encoding: "utf8" 17 | }, 18 | postgresql: { 19 | adapter: "postgresql", 20 | database: "activerecord_futures_test", 21 | username: "postgres" 22 | }, 23 | mysql2: { 24 | adapter: "mysql2", 25 | database: "activerecord_futures_test", 26 | username: "root", 27 | encoding: "utf8" 28 | }, 29 | sqlite3: { 30 | adapter: "sqlite3", 31 | database: ':memory:' 32 | } 33 | } 34 | 35 | env_config = ENV['ADAPTER'].try(:to_sym) 36 | config_key = configs.keys.include?(env_config) ? env_config : :future_enabled_mysql2 37 | config = configs[config_key] 38 | puts "Using #{config_key} configuration" 39 | 40 | ActiveRecord::Base.establish_connection(config) 41 | supports_futures = 42 | ActiveRecord::Base.connection.respond_to?(:supports_futures?) && 43 | ActiveRecord::Base.connection.supports_futures? 44 | 45 | puts "Supports futures!" if supports_futures 46 | 47 | require 'db/schema' 48 | Dir['./spec/models/**/*.rb'].each { |f| require f } 49 | 50 | Dir["./spec/support/**/*.rb"].sort.each {|f| require f} 51 | 52 | require 'rspec-spies' 53 | 54 | RSpec.configure do |config| 55 | config.treat_symbols_as_metadata_keys_with_true_values = true 56 | config.run_all_when_everything_filtered = true 57 | config.filter_run :focus 58 | config.filter_run_excluding(supports_futures ? :not_supporting_adapter : :supporting_adapter) 59 | 60 | %w(postgresql mysql2 sqlite3).each do |adapter| 61 | config.filter_run_excluding(adapter.to_sym => !config_key.to_s.include?(adapter)) 62 | end 63 | # Run specs in random order to surface order dependencies. If you find an 64 | # order dependency and want to debug it, you can fix the order by providing 65 | # the seed, which is printed after each run. 66 | # --seed 1234 67 | config.order = 'random' 68 | 69 | config.around do |example| 70 | ActiveRecord::Base.transaction do 71 | example.run 72 | raise ActiveRecord::Rollback 73 | end 74 | end 75 | 76 | config.after do 77 | ActiveRecord::Futures::FutureRegistry.clear 78 | end 79 | end 80 | -------------------------------------------------------------------------------- /lib/active_record/connection_adapters/future_enabled_postgresql_adapter.rb: -------------------------------------------------------------------------------- 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(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 51 | end 52 | end 53 | 54 | def build_active_record_result(raw_result) 55 | return if raw_result.nil? 56 | result = ActiveRecord::Result.new(raw_result.fields, result_as_array(raw_result)) 57 | raw_result.clear 58 | result 59 | end 60 | 61 | def next_result 62 | @connection.get_result 63 | end 64 | end 65 | end 66 | end 67 | -------------------------------------------------------------------------------- /spec/in_action/future_find_execution_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe "future_find method" do 4 | context "finding by a single id" do 5 | let(:relation) { Post.where("published_at < ?", Time.new(2013, 1, 1)) } 6 | let(:future) { relation.future_find(@post_id) } 7 | let(:relation_result) { relation.find(@post_id) } 8 | let(:future_sql) do 9 | arel = relation.where(id: @post_id).arel 10 | arel.limit = 1 11 | arel.to_sql 12 | end 13 | 14 | let(:future_sql_postgresql) do 15 | arel = relation.arel 16 | arel.constraints.unshift(Arel.sql('"posts"."id" = $1')) 17 | arel.limit = 1 18 | arel.to_sql 19 | end 20 | 21 | let(:future_sql_sqlite3) do 22 | arel = relation.arel 23 | arel.constraints.unshift(Arel.sql('"posts"."id" = ?')) 24 | arel.limit = 1 25 | arel.to_sql 26 | end 27 | 28 | before do 29 | Post.create(published_at: Time.new(2012, 12, 10)) 30 | Post.create(published_at: Time.new(2012, 6, 23)) 31 | Post.create(published_at: Time.new(2013, 4, 5)) 32 | @post_id = relation.first.id 33 | end 34 | 35 | it_behaves_like "a futurized method", :value 36 | end 37 | 38 | context "finding by multiple ids" do 39 | let(:relation) { Post.where("published_at < ?", Time.new(2013, 1, 1)) } 40 | let(:future) { relation.future_find(*@post_ids) } 41 | let(:relation_result) { relation.find(*@post_ids) } 42 | let(:future_sql) do 43 | arel = relation.where(id: @post_ids).arel 44 | arel.to_sql 45 | end 46 | 47 | before do 48 | Post.create(published_at: Time.new(2012, 12, 10)) 49 | Post.create(published_at: Time.new(2012, 6, 23)) 50 | Post.create(published_at: Time.new(2013, 4, 5)) 51 | @post_ids = [relation.first.id, relation.last.id] 52 | end 53 | 54 | it_behaves_like "a futurized method", :to_a 55 | end 56 | 57 | context "finding by multiple ids, with single array parameter" do 58 | let(:relation) { Post.where("published_at < ?", Time.new(2013, 1, 1)) } 59 | let(:future) { relation.future_find(@post_ids) } 60 | let(:relation_result) { relation.find(@post_ids) } 61 | let(:future_sql) do 62 | arel = relation.where(id: @post_ids).arel 63 | arel.to_sql 64 | end 65 | 66 | before do 67 | Post.create(published_at: Time.new(2012, 12, 10)) 68 | Post.create(published_at: Time.new(2012, 6, 23)) 69 | Post.create(published_at: Time.new(2013, 4, 5)) 70 | @post_ids = [relation.first.id, relation.last.id] 71 | end 72 | 73 | it_behaves_like "a futurized method", :to_a 74 | end 75 | end -------------------------------------------------------------------------------- /spec/active_record/futures/future_registry_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | module ActiveRecord::Futures 4 | describe FutureRegistry do 5 | subject { FutureRegistry } 6 | 7 | describe ".futures" do 8 | context "with futures in two threads" do 9 | let(:futures_key) { "#{subject.name}_futures" } 10 | 11 | let(:a_thread) do 12 | thread = double("Thread 1") 13 | thread.stub(:[]).with(futures_key).and_return([]) 14 | thread 15 | end 16 | 17 | let(:another_thread) do 18 | thread = double("Thread 2") 19 | thread.stub(:[]).with(futures_key).and_return([]) 20 | thread 21 | end 22 | 23 | before do 24 | Thread.stub(:current).and_return(a_thread) 25 | 26 | subject.futures << "Future 1" 27 | subject.futures << "Future 2" 28 | 29 | Thread.stub(:current).and_return(another_thread) 30 | 31 | subject.futures << "Future 3" 32 | subject.futures << "Future 4" 33 | end 34 | 35 | context "the futures in thread 1" do 36 | let(:futures) { a_thread[futures_key] } 37 | 38 | specify { futures.should include("Future 1") } 39 | specify { futures.should include("Future 2") } 40 | specify { futures.should_not include("Future 3") } 41 | specify { futures.should_not include("Future 4") } 42 | end 43 | 44 | context "the futures in thread 2" do 45 | let(:futures) { another_thread[futures_key] } 46 | 47 | specify { futures.should_not include("Future 1") } 48 | specify { futures.should_not include("Future 2") } 49 | specify { futures.should include("Future 3") } 50 | specify { futures.should include("Future 4") } 51 | end 52 | end 53 | end 54 | 55 | describe ".current" do 56 | context "with currents in two threads" do 57 | let(:current_key) { "#{subject.name}_current" } 58 | 59 | let(:a_thread) { Hash.new } 60 | let(:another_thread) { Hash.new } 61 | 62 | before do 63 | Thread.stub(:current).and_return(a_thread) 64 | 65 | subject.current = "Future 1" 66 | 67 | Thread.stub(:current).and_return(another_thread) 68 | 69 | subject.current = "Future 2" 70 | end 71 | 72 | context "the current in thread 1" do 73 | let(:current) { a_thread[current_key] } 74 | 75 | specify { current.should eq "Future 1" } 76 | end 77 | 78 | context "the current in thread 2" do 79 | let(:current) { another_thread[current_key] } 80 | 81 | specify { current.should eq "Future 2" } 82 | end 83 | end 84 | end 85 | end 86 | end -------------------------------------------------------------------------------- /spec/in_action/future_fulfillment_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | module ActiveRecord::Futures 4 | describe "Future fulfillment" do 5 | subject { FutureRegistry } 6 | 7 | context "when futurizing a relation" do 8 | let!(:future_result) { Post.where(title: "Some post").future } 9 | let!(:future) { future_result.send(:future_execution) } 10 | 11 | its(:all) { should have(1).future } 12 | 13 | context "the future" do 14 | specify { future.should_not be_fulfilled } 15 | end 16 | 17 | context "and executing it" do 18 | before { future_result.to_a } 19 | 20 | its(:all) { should have(0).futures } 21 | 22 | context "the future" do 23 | subject { future } 24 | 25 | it(nil, :supporting_adapter) { should be_fulfilled } 26 | it(nil, :not_supporting_adapter) { should_not be_fulfilled } 27 | end 28 | end 29 | end 30 | 31 | context "when futurizing two relations" do 32 | let!(:future_result) { Post.where(title: "Some post").future } 33 | let!(:future) { future_result.send(:future_execution) } 34 | let!(:another_future_result) { User.where(name: "Lenny").future } 35 | let!(:another_future) { another_future_result.send(:future_execution) } 36 | 37 | its(:all) { should have(2).futures } 38 | 39 | context "the first relation future" do 40 | specify { future.should_not be_fulfilled } 41 | end 42 | 43 | context "the other relation future" do 44 | specify { another_future.should_not be_fulfilled } 45 | end 46 | 47 | context "and executing one of them" do 48 | before { future_result.to_a } 49 | 50 | its(:all) { should have(0).futures } 51 | 52 | context "the first relation future" do 53 | specify(nil, :supporting_adapter) { future.should be_fulfilled } 54 | specify(nil, :not_supporting_adapter) { future.should_not be_fulfilled } 55 | end 56 | 57 | context "the other relation future" do 58 | specify(nil, :supporting_adapter) { another_future.should be_fulfilled } 59 | specify(nil, :not_supporting_adapter) { another_future.should_not be_fulfilled } 60 | end 61 | end 62 | 63 | context "and executing another non futurized relation" do 64 | let!(:normal_relation) { User.where(name: "John") } 65 | before { normal_relation.to_a } 66 | 67 | its(:all) { should have(2).futures } 68 | 69 | context "the first relation future" do 70 | specify { future.should_not be_fulfilled } 71 | end 72 | 73 | context "the other relation future" do 74 | specify { another_future.should_not be_fulfilled } 75 | end 76 | end 77 | end 78 | end 79 | end -------------------------------------------------------------------------------- /spec/in_action/combination_of_futures_spec.rb: -------------------------------------------------------------------------------- 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_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) { execution(subject).should be_fulfilled } 61 | it(nil, :not_supporting_adapter) { execution(subject).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) { execution(subject).should be_fulfilled } 83 | it(nil, :not_supporting_adapter) { execution(subject).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) { execution(subject).should be_fulfilled } 105 | it(nil, :not_supporting_adapter) { execution(subject).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) { execution(subject).should be_fulfilled } 128 | it(nil, :not_supporting_adapter) { execution(subject).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 execution(future) 148 | future.send(:future_execution) 149 | end 150 | 151 | def count(relation) 152 | arel = relation.arel 153 | arel.projections = [] 154 | arel.project("COUNT(*)") 155 | arel 156 | end 157 | end -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ActiveRecord::Futures 2 | 3 | [![Gem Version](https://badge.fury.io/rb/activerecord-futures.png)](http://badge.fury.io/rb/activerecord-futures) 4 | [![Build Status](https://travis-ci.org/leoasis/activerecord-futures.png)](https://travis-ci.org/leoasis/activerecord-futures) 5 | [![Code Climate](https://codeclimate.com/github/leoasis/activerecord-futures.png)](https://codeclimate.com/github/leoasis/activerecord-futures) 6 | [![Coverage Status](https://coveralls.io/repos/leoasis/activerecord-futures/badge.png?branch=master)](https://coveralls.io/r/leoasis/activerecord-futures) 7 | 8 | 9 | Define future queries in ActiveRecord that will get executed in a single round trip to the database. 10 | 11 | This gem allows to easily optimize an application using activerecord. All 12 | independent queries can be marked as futures, so that when you execute any of 13 | them at a later time, all the other ones will be executed as well, but the query 14 | of all of them will be executed in a single round trip to the database. That way, 15 | when you access the other results, they'll already be there, not needing to go 16 | to the database again. 17 | 18 | The idea is heavily inspired from [NHibernate's future queries](http://ayende.com/blog/3979/nhibernate-futures) 19 | 20 | ## Installation 21 | 22 | Add this line to your application's Gemfile: 23 | 24 | gem 'activerecord-futures' 25 | 26 | And then execute: 27 | 28 | $ bundle 29 | 30 | Or install it yourself as: 31 | 32 | $ gem install activerecord-futures 33 | 34 | ## Usage 35 | 36 | If you're using postgresql or mysql2 currently, you have nothing more to do. The gem will automatically use the future enabled adapter and just work. If you are using a custom adapter, specify it in the config/database.yml file as you're used to. 37 | 38 | Check the database support (below) section for more info. 39 | 40 | Now let's see what this does, consider a model `User`, with a `:name` attribute: 41 | 42 | ```ruby 43 | 44 | # Build the queries and mark them as futures 45 | users = User.where("name like 'John%'") 46 | user_list = users.future # becomes a future relation, does not execute the query. 47 | count = users.future_count # becomes a future calculation, does not execute the query. 48 | 49 | # Execute any of the futures 50 | count = count.value # trigger the future execution, both queries will get executed in one round trip! 51 | #=> User Load (fetching Futures) (0.6ms) SELECT `users`.* FROM `users` WHERE (name like 'John%');SELECT COUNT(*) FROM `users` WHERE (name like 'John%') 52 | 53 | # Access the other results 54 | user_list.to_a # does not execute the query, results from previous query get loaded 55 | ``` 56 | 57 | Any amount of futures can be prepared, and they will get executed as soon as one of them needs to be evaluated. 58 | 59 | This makes this especially useful for pagination queries, since you can execute 60 | both count and page queries at once. 61 | 62 | #### Rails 63 | 64 | No configuration to do, things will Just Work. 65 | 66 | #### Rack based apps (not Rails) 67 | 68 | You will need to manually add the `ActiveRecord::Futures::Middleware` somewhere in the middleware stack: 69 | 70 | ```ruby 71 | use ActiveRecord::Futures::Middleware 72 | ``` 73 | 74 | This is to clear the futures that were defined and not triggered between requests. 75 | 76 | ### Methods 77 | 78 | #### #future method 79 | ActiveRecord::Relation instances get a `future` method that futurizes a normal 80 | relation. The future gets executed whenever `#to_a` gets executed. Note that, as ActiveRecord does, enumerable methods get delegated to `#to_a` also, 81 | so things like `#each`, `#map`, `#collect` all trigger the future. 82 | 83 | #### Calculation methods 84 | You also get all the calculation methods provided by the ActiveRecord::Calculations module 85 | "futurized". More specifically you get: 86 | * future_count 87 | * future_average 88 | * future_minimum 89 | * future_maximum 90 | * future_sum 91 | * future_calculate 92 | * future_pluck 93 | 94 | All future 95 | calculations are triggered with the `#value` method, except for the `#future_pluck` method, that returns an array, and is 96 | triggered with a `#to_a` method (or any other method that delegates to it). 97 | 98 | #### Finder methods 99 | 100 | Lastly, you also get finder methods futurized, which are: 101 | 102 | * future_find 103 | * future_first 104 | * future_last 105 | * future_exists? 106 | * future_all 107 | 108 | As with the other future methods, those which return an array get triggered with 109 | the `#to_a` method, or the delegated ones, and those that return a value or a hash 110 | are triggered with the `#value` method. Note that the `#find` method returns an 111 | array or a value depending on the parameters provided, and so will the futurized 112 | version of the method. 113 | 114 | ## Database support 115 | 116 | ### SQlite 117 | 118 | SQlite doesn't support multiple statement queries. ActiveRecord::Futures will fall back to normal query execution, that is, 119 | it will execute the future's query whenever the future is triggered, but it will not execute the other futures' queries. 120 | 121 | ### MySQL 122 | 123 | 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 124 | one. 125 | Currently the adapter provided inherits the built-in one in Rails, and it also sets the MULTI_STATEMENTS flag to allow 126 | multiple queries in a single command. 127 | If you have an older version of the gem, ActiveRecord::Futures will fall back to normal query execution. 128 | 129 | ### Postgres 130 | 131 | The pg gem supports multiple statement queries by using the `#send_query` method 132 | and retrieving the results via `#get_result`. 133 | 134 | ### Other databases 135 | 136 | In general, ActiveRecord::Futures will look for a method `#supports_futures?` in the adapter. So any adapter that returns 137 | false when calling the method, or does not respond to it, will fall back to normal query execution. 138 | If you want to have support for ActiveRecord::Futures with your database, feel free to create a pull request with it, or 139 | create your own gem, or just create an issue. 140 | 141 | ## Contributing 142 | 143 | 1. Fork it 144 | 2. Create your feature branch (`git checkout -b my-new-feature`) 145 | 3. Commit your changes (`git commit -am 'Add some feature'`) 146 | 4. Push to the branch (`git push origin my-new-feature`) 147 | 5. Create new Pull Request 148 | --------------------------------------------------------------------------------