Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Initial commit

  • Loading branch information...
commit 6b0c3697b7a5213909243b4733d8cbb82701148d 0 parents
@lifo authored
1  .rvmrc
@@ -0,0 +1 @@
+rvm 1.9.3
7 Gemfile
@@ -0,0 +1,7 @@
+source 'http://rubygems.org'
+
+gemspec
+
+group :test do
+ gem 'mysql2', '~> 0.2.11'
+end
31 Gemfile.lock
@@ -0,0 +1,31 @@
+PATH
+ remote: .
+ specs:
+ sidekick (3.0.1)
+ activerecord (~> 3.0.9)
+
+GEM
+ remote: http://rubygems.org/
+ specs:
+ activemodel (3.0.10)
+ activesupport (= 3.0.10)
+ builder (~> 2.1.2)
+ i18n (~> 0.5.0)
+ activerecord (3.0.10)
+ activemodel (= 3.0.10)
+ activesupport (= 3.0.10)
+ arel (~> 2.0.10)
+ tzinfo (~> 0.3.23)
+ activesupport (3.0.10)
+ arel (2.0.10)
+ builder (2.1.2)
+ i18n (0.5.0)
+ mysql2 (0.2.13)
+ tzinfo (0.3.29)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ mysql2 (~> 0.2.11)
+ sidekick!
1  README
@@ -0,0 +1 @@
+Hello
11 Rakefile
@@ -0,0 +1,11 @@
+require 'rake'
+require 'rake/testtask'
+
+task :default => :test
+
+Rake::TestTask.new(:test) do |t|
+ t.libs << "test"
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = true
+end
+Rake::Task['test'].comment = "Run tests"
2  lib/sidekick.rb
@@ -0,0 +1,2 @@
+module Sidekick
+end
69 lib/sidekick/active_record.rb
@@ -0,0 +1,69 @@
+require 'sidekick/target'
+require 'sidekick/reload'
+
+class ActiveRecord::Base
+ attr_accessor :_parent_record_set
+
+ def reload_with_kick(*)
+ self._parent_record_set = nil
+ reload_without_kick
+ end
+
+ alias_method_chain :reload, :kick
+
+ module Destructor
+ def destroy(*)
+ self._parent_record_set = nil
+ super
+ end
+ end
+ include Destructor
+
+ [:clone, :dup].each do |method_name|
+ define_method(:"#{method_name}_with_kick") do
+ obj = send("#{method_name}_without_kick")
+ obj._parent_record_set = nil
+ obj
+ end
+
+ alias_method_chain method_name, :kick
+ end
+
+end
+
+class ActiveRecord::Relation
+ def to_a_with_kick
+ records = to_a_without_kick
+ records.each {|r| r._parent_record_set = records }
+ records
+ end
+
+ alias_method_chain :to_a, :kick
+end
+
+[
+ ActiveRecord::Associations::BelongsToAssociation,
+ ActiveRecord::Associations::HasManyAssociation,
+ ActiveRecord::Associations::BelongsToPolymorphicAssociation,
+ ActiveRecord::Associations::HasOneAssociation,
+ ActiveRecord::Associations::HasOneThroughAssociation,
+ ActiveRecord::Associations::HasManyThroughAssociation
+].each do |association_klass|
+ association_klass.send :include, Sidekick::Target
+end
+
+[
+ ActiveRecord::Associations::HasManyAssociation,
+ ActiveRecord::Associations::HasManyThroughAssociation
+].each do |association_klass|
+ association_klass.send :include, Sidekick::Reload
+end
+
+class ActiveRecord::Associations::HasManyThroughAssociation
+ module KickPreloadCheck
+ def skip_kick_preload?
+ super || !target_reflection_has_associated_record?
+ end
+ end
+ include KickPreloadCheck
+end
15 lib/sidekick/reload.rb
@@ -0,0 +1,15 @@
+module Sidekick
+ module Reload
+ def self.included(base)
+ base.send :alias_method_chain, :reload, :kick
+ end
+
+ def reload_with_kick
+ @owner._parent_record_set = nil
+
+ reset
+ load_target
+ self unless @target.nil?
+ end
+ end
+end
38 lib/sidekick/target.rb
@@ -0,0 +1,38 @@
+module Sidekick
+ module Target
+ def self.included(base)
+ base.send :alias_method_chain, :find_target, :kick
+ end
+
+ def find_target_with_kick
+ return find_target_without_kick if skip_kick_preload? || !@owner._parent_record_set
+
+ reflection_name = @reflection.name
+
+ # Fucking STI
+ working_record_set = @owner._parent_record_set.find_all {|r| r.respond_to?(reflection_name) }
+
+ @owner.class.send(:preload_associations, working_record_set, reflection_name.to_sym)
+
+ record_set = working_record_set.map do |r|
+ x = r.send(:instance_variable_get, "@#{reflection_name}")
+ x.target if x
+ end
+
+ if record_set.present?
+ record_set.flatten!
+ record_set.compact!
+ end
+
+ record_set.each {|r| r._parent_record_set = record_set }
+
+ associtaion = @owner.send(:instance_variable_get, "@#{reflection_name}")
+ associtaion.target if associtaion
+ end
+
+ def skip_kick_preload?
+ skip_keys = [:finder_sql, :order, :conditions, :uniq, :limit]
+ skip_keys.any? {|key| @reflection.options.has_key?(key) }
+ end
+ end
+end
18 sidekick.gemspec
@@ -0,0 +1,18 @@
+Gem::Specification.new do |s|
+ s.platform = Gem::Platform::RUBY
+ s.name = 'sidekick'
+ s.version = '3.0.1'
+ s.summary = 'Lazy preloading for Active Record.'
+ s.description = 'Sidekick provides lazy preloading for Active Record.'
+
+ s.author = 'Pratik Naik'
+ s.email = 'pratiknaik@gmail.com'
+ s.homepage = 'http://m.onkey.org'
+
+ s.add_dependency('activerecord', '~> 3.0.9')
+
+ s.files = Dir['README', 'MIT-LICENSE', 'lib/**/*']
+ s.has_rdoc = false
+
+ s.require_path = 'lib'
+end
39 test/assert_queries_patches.rb
@@ -0,0 +1,39 @@
+ActiveRecord::Base.connection.class.class_eval do
+ IGNORED_SQL = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /SHOW FIELDS/]
+
+ def execute_with_query_record(sql, name = nil, &block)
+ $queries_executed ||= []
+ $queries_executed << sql unless IGNORED_SQL.any? { |r| sql =~ r }
+ execute_without_query_record(sql, name, &block)
+ end
+
+ alias_method_chain :execute, :query_record
+end
+
+# Oracle specific ignored SQLs
+ActiveRecord::Base.connection.class.class_eval do
+ IGNORED_SELECT_SQL = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from ((all|user)_tab_columns|(all|user)_triggers|(all|user)_constraints)/im]
+
+ def select_with_query_record(sql, name = nil)
+ $queries_executed ||= []
+ $queries_executed << sql unless IGNORED_SELECT_SQL.any? { |r| sql =~ r }
+ select_without_query_record(sql, name)
+ end
+
+ alias_method_chain :select, :query_record
+end if ENV['ARCONN'] == 'oracle'
+
+ActiveRecord::Base.connection.class.class_eval {
+ attr_accessor :column_calls, :column_calls_by_table
+
+ def columns_with_calls(*args)
+ @column_calls ||= 0
+ @column_calls_by_table ||= Hash.new {|h,table| h[table] = 0}
+
+ @column_calls += 1
+ @column_calls_by_table[args.first.to_s] += 1
+ columns_without_calls(*args)
+ end
+
+ alias_method_chain :columns, :calls
+}
125 test/basic_test.rb
@@ -0,0 +1,125 @@
+require 'test_helper'
+
+class BasicTest < ActiveRecord::TestCase
+ def test_has_many
+ assert_queries(3) do
+ # Query 1
+ users = User.all
+
+ # Query 2 - Load all the posts
+ lifo = users.detect {|u| u.name == 'Lifo' }
+ lifo_posts = lifo.posts
+ assert_equal ["Cramp 1.0", "Say hi to Cramp", "First post"].sort, lifo_posts.map(&:title).sort
+
+ bob = users.detect {|u| u.name == 'Bob' }
+ bob_posts = bob.posts
+ assert_equal ["Hello"], bob_posts.map(&:title).sort
+
+ # Query 3 - Load all the comments
+ cramp_update_post = lifo_posts.detect {|p| p.title == 'Cramp 1.0' }
+ assert_equal ['Congrats'], cramp_update_post.comments.map(&:body)
+
+ bob_hello_post = bob_posts.first
+ assert_equal ["Welcome Bob!", "Thanks Lifo."].sort, bob_hello_post.comments.map(&:body).sort
+ end
+ end
+
+ def test_has_many_through
+ assert_queries(3) do
+ # Query 1
+ users = User.all
+
+ # Query 2 & 3 - Load all the posts and all the comments
+ lifo = users.detect {|u| u.name == 'Lifo' }
+ assert_equal 4, lifo.comments.length
+
+ bob = users.detect {|u| u.name == 'Bob' }
+ assert_equal 2, bob.comments.length
+ end
+ end
+
+ def test_has_many_through_polymorphic_source
+ assert_queries(3) do
+ # Query 1
+ users = User.all
+
+ # Query 2 & 3 - Load all the posts and all the comments
+ assert_equal ["first", "cramp", "first", "welcome"].sort, users.map(&:tags).flatten.map(&:name).sort
+ end
+ end
+
+ def test_belongs_to
+ assert_queries(2) do
+ # Query 1
+ posts = Post.all
+
+ # Query 2 - Load all the authors
+ assert_equal ["Lifo", "Bob"], posts.map(&:user).map(&:name).uniq
+ end
+ end
+
+ def test_belongs_reload
+ # Need to fix AR 3.0.x to not call .reload for loading the association the first time
+ flunk
+
+ # Create a post with missing user_id
+ orphan_post = Post.new(:title => 'Whatever', :content => 'Fake')
+ orphan_post.user_id = User.maximum(:id) + 10
+ orphan_post.save!
+
+ assert_queries(3) do
+ # Query 1
+ posts = Post.all
+ orphan_post = posts.detect {|p| p.id == orphan_post.id }
+
+ # Query 2 - Load all the authors
+ assert_equal ["Lifo", "Bob"], posts.map(&:user).compact.map(&:name).uniq
+ assert_nil orphan_post.user
+
+ assert_nil orphan_post.user(:reload)
+ end
+ end
+
+ def test_has_one
+ assert_queries(2) do
+ # Query 1
+ users = User.all
+
+ # Query 2 - Load all the last posts
+ assert_equal ["Cramp 1.0", "Hello"].sort, users.map(&:last_post).map(&:title).sort
+ end
+ end
+
+ def test_has_one_through
+ assert_queries(3) do
+ # Query 1
+ users = User.all
+
+ # Query 2 & 3 - Load all the last posts and then last comments
+ assert_equal ["Bob", "Lifo"].sort, users.map(&:last_comment).flatten.compact.map(&:author_name).sort
+ end
+ end
+
+ def test_polymorphic_has_many
+ assert_queries(2) do
+ # Query 1
+ posts = Post.all
+
+ # Query 2 - Load all the tags
+ assert_equal ["first", "cramp", "welcome"].sort, posts.map(&:tags).flatten.map(&:name).uniq.sort
+ end
+ end
+
+ def test_polymorphic_belongs_to
+ assert_queries(2) do
+ # Query 1
+ tags = Tag.all
+
+ # Query 2 - Load all the posts
+ tags.first.taggable
+
+ assert_equal ["Say hi to Cramp", "First post", "Hello", "First post"].sort, tags.map(&:taggable).map(&:title).sort
+ end
+ end
+
+end
26 test/fixtures/comments.yml
@@ -0,0 +1,26 @@
+troll_comment:
+ post: lifo_hello
+ body: O Hai
+ author_name: Trollol
+reply_troll_comment:
+ post: lifo_hello
+ body: Stop trolling
+ author_name: Lifo
+another_comment:
+ post: lifo_hello
+ body: Stop it both of you
+ author_name: Fifo
+
+cramp_update_comment:
+ post: cramp_update
+ body: Congrats
+ author_name: Bob
+
+bob_hello_comment:
+ post: bob_hello
+ body: Welcome Bob!
+ author_name: Lifo
+reply_bob_hello_comment:
+ post: bob_hello
+ body: Thanks Lifo.
+ author_name: Bob
19 test/fixtures/posts.yml
@@ -0,0 +1,19 @@
+lifo_hello:
+ user: lifo
+ title: First post
+ content: Welcome everyone!
+
+cramp_launch:
+ user: lifo
+ title: Say hi to Cramp
+ content: Cramp is the new async framework.
+
+cramp_update:
+ user: lifo
+ title: Cramp 1.0
+ content: Happy to announce Cramp 1.0
+
+bob_hello:
+ user: bob
+ title: Hello
+ content: Well hello Bob.
15 test/fixtures/tags.yml
@@ -0,0 +1,15 @@
+first:
+ name: first
+ taggable: lifo_hello (Post)
+
+welcome:
+ name: welcome
+ taggable: lifo_hello (Post)
+
+cramp:
+ name: cramp
+ taggable: cramp_launch (Post)
+
+bob_first:
+ name: first
+ taggable: bob_hello (Post)
4 test/fixtures/users.yml
@@ -0,0 +1,4 @@
+lifo:
+ name: Lifo
+bob:
+ name: Bob
5 test/models/comment.rb
@@ -0,0 +1,5 @@
+class Comment < ActiveRecord::Base
+ belongs_to :post
+
+ validates_presence_of :body, :author_name
+end
9 test/models/post.rb
@@ -0,0 +1,9 @@
+class Post < ActiveRecord::Base
+ belongs_to :user
+ has_many :comments
+ has_many :tags, :as => :taggable
+
+ has_one :last_comment, :class_name => 'Comment'
+
+ validates_presence_of :title, :content
+end
5 test/models/tag.rb
@@ -0,0 +1,5 @@
+class Tag < ActiveRecord::Base
+ belongs_to :taggable, :polymorphic => true
+
+ validates_presence_of :name
+end
20 test/models/user.rb
@@ -0,0 +1,20 @@
+class User < ActiveRecord::Base
+ has_many :posts
+ has_many :comments, :through => :posts
+
+ # Polymorphic source
+ has_many :tags, :through => :posts
+
+ has_one :last_post, :class_name => 'Post'
+ has_one :last_comment, :through => :last_post, :source => :last_comment
+
+ # Just for skip_test.rb
+ has_many :lol_posts, :class_name => 'Post', :finder_sql => 'select * from posts limit 2'
+ has_many :recent_posts, :class_name => 'Post', :limit => 5
+ has_many :ordered_posts, :class_name => 'Post', :order => 'posts.title DESC'
+ has_many :hello_posts, :class_name => 'Post', :conditions => "posts.title = 'Hello'"
+
+ has_many :unique_tags, :through => :posts, :source => :tags, :uniq => true
+
+ validates_presence_of :name
+end
33 test/schema.rb
@@ -0,0 +1,33 @@
+ActiveRecord::Schema.define do
+ original_stdout = $stdout
+ $stdout = StringIO.new
+
+ create_table :users, :force => true do |t|
+ t.string :name
+ end
+
+ create_table :posts, :force => true do |t|
+ t.string :title
+ t.string :content
+ t.belongs_to :user
+ end
+
+ add_index :posts, :user_id
+
+ create_table :comments, :force => true do |t|
+ t.string :body
+ t.string :author_name
+ t.belongs_to :post
+ end
+
+ add_index :comments, :post_id
+
+ create_table :tags, :force => true do |t|
+ t.string :name
+ t.belongs_to :taggable, :polymorphic => true
+ end
+
+ add_index :tags, [:taggable_id, :taggable_type]
+
+ $stdout = original_stdout
+end
56 test/skip_test.rb
@@ -0,0 +1,56 @@
+require 'test_helper'
+
+class BasicTest < ActiveRecord::TestCase
+ def setup
+ @users = User.all
+
+ @lifo = @users.detect {|u| u.name == 'Lifo' }
+ @bob = @users.detect {|u| u.name == 'Bob' }
+ end
+
+ def test_skip_on_finder_sql
+ assert_queries(2) do
+ @lifo.lol_posts.to_a
+
+ assert ! @bob.lol_posts.loaded?
+ @bob.lol_posts.to_a
+ end
+ end
+
+ def test_skip_on_order
+ assert_queries(2) do
+ @lifo.ordered_posts.to_a
+
+ assert ! @bob.ordered_posts.loaded?
+ @bob.ordered_posts.to_a
+ end
+ end
+
+ def test_skip_on_limit
+ assert_queries(2) do
+ @lifo.recent_posts.to_a
+
+ assert ! @bob.recent_posts.loaded?
+ @bob.recent_posts.to_a
+ end
+ end
+
+ def test_skip_on_conditions
+ assert_queries(2) do
+ @lifo.hello_posts.to_a
+
+ assert ! @bob.hello_posts.loaded?
+ @bob.hello_posts.to_a
+ end
+ end
+
+ def test_skip_on_uniq
+ assert_queries(2) do
+ @lifo.unique_tags.to_a
+
+ assert ! @bob.unique_tags.loaded?
+ @bob.unique_tags.to_a
+ end
+ end
+
+end
31 test/test_helper.rb
@@ -0,0 +1,31 @@
+ENV['RAILS_ENV'] = 'test'
+
+require "rubygems"
+require "bundler"
+
+Bundler.setup
+Bundler.require :default, :test
+
+require 'test/unit'
+
+require 'active_record'
+require 'sidekick/active_record'
+
+ActiveRecord::Base.configurations = {'test' => {:adapter => "mysql2", :database => "sidekick_test"}}
+ActiveRecord::Base.establish_connection('test')
+require 'schema'
+
+require 'active_record/test_case'
+require 'active_record/fixtures'
+
+# This is WTF. But whatever.
+require 'assert_queries_patches'
+
+Dir.glob(File.expand_path(File.join File.dirname(__FILE__), 'models/**/*')).each {|model| require model }
+
+class ActiveSupport::TestCase
+ include ActiveRecord::TestFixtures
+ self.fixture_path = "./test/fixtures/"
+
+ fixtures :all
+end
Please sign in to comment.
Something went wrong with that request. Please try again.