kasket 1.0.4 → 2.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/README.rdoc +1 -1
- data/lib/kasket.rb +21 -6
- data/lib/kasket/configuration_mixin.rb +1 -5
- data/lib/kasket/query_parser.rb +15 -4
- data/lib/kasket/read_mixin.rb +28 -23
- data/lib/kasket/relation_mixin.rb +9 -0
- data/lib/kasket/reload_association_mixin.rb +3 -3
- data/lib/kasket/select_manager_mixin.rb +26 -0
- data/lib/kasket/version.rb +5 -3
- data/lib/kasket/visitor.rb +140 -0
- data/test/configuration_mixin_test.rb +2 -2
- data/test/database.yml +1 -3
- data/test/fixtures/authors.yml +5 -0
- data/test/fixtures/comments.yml +5 -3
- data/test/fixtures/posts.yml +9 -1
- data/test/helper.rb +15 -4
- data/test/parser_test.rb +64 -35
- data/test/read_mixin_test.rb +14 -7
- data/test/reload_test.rb +85 -0
- data/test/schema.rb +30 -0
- data/test/test_models.rb +20 -24
- data/test/visitor_test.rb +17 -0
- metadata +41 -22
- data/lib/kasket/active_record_patches.rb +0 -57
    
        data/README.rdoc
    CHANGED
    
    | @@ -1,6 +1,6 @@ | |
| 1 1 | 
             
            = Kasket {<img src="https://secure.travis-ci.org/staugaard/kasket.png" />}[http://travis-ci.org/staugaard/kasket]
         | 
| 2 2 | 
             
            === Puts a cap on your queries
         | 
| 3 | 
            -
            A caching layer for ActiveRecord
         | 
| 3 | 
            +
            A caching layer for ActiveRecord (2.3.x and 3.1.x)
         | 
| 4 4 |  | 
| 5 5 | 
             
            Developed and used on http://zendesk.com.
         | 
| 6 6 |  | 
    
        data/lib/kasket.rb
    CHANGED
    
    | @@ -2,26 +2,42 @@ | |
| 2 2 | 
             
            require 'active_record'
         | 
| 3 3 | 
             
            require 'active_support'
         | 
| 4 4 |  | 
| 5 | 
            -
            require 'kasket/active_record_patches'
         | 
| 6 5 | 
             
            require 'kasket/version'
         | 
| 7 6 |  | 
| 8 7 | 
             
            module Kasket
         | 
| 9 | 
            -
              autoload : | 
| 8 | 
            +
              autoload :ReadMixin,              'kasket/read_mixin'
         | 
| 9 | 
            +
              autoload :WriteMixin,             'kasket/write_mixin'
         | 
| 10 | 
            +
              autoload :DirtyMixin,             'kasket/dirty_mixin'
         | 
| 11 | 
            +
              autoload :QueryParser,            'kasket/query_parser'
         | 
| 12 | 
            +
              autoload :ConfigurationMixin,     'kasket/configuration_mixin'
         | 
| 10 13 | 
             
              autoload :ReloadAssociationMixin, 'kasket/reload_association_mixin'
         | 
| 11 | 
            -
              autoload :Query, | 
| 14 | 
            +
              autoload :Query,                  'kasket/query'
         | 
| 15 | 
            +
              autoload :Visitor,                'kasket/visitor'
         | 
| 16 | 
            +
              autoload :SelectManagerMixin,     'kasket/select_manager_mixin'
         | 
| 17 | 
            +
              autoload :RelationMixin,          'kasket/relation_mixin'
         | 
| 18 | 
            +
             | 
| 19 | 
            +
              AR30 = (ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR == 0)
         | 
| 12 20 |  | 
| 13 21 | 
             
              CONFIGURATION = {:max_collection_size => 100}
         | 
| 14 22 |  | 
| 15 23 | 
             
              module_function
         | 
| 16 24 |  | 
| 17 25 | 
             
              def setup(options = {})
         | 
| 18 | 
            -
                return if ActiveRecord::Base. | 
| 26 | 
            +
                return if ActiveRecord::Base.respond_to?(:has_kasket)
         | 
| 19 27 |  | 
| 20 28 | 
             
                CONFIGURATION[:max_collection_size] = options[:max_collection_size] if options[:max_collection_size]
         | 
| 21 29 |  | 
| 22 30 | 
             
                ActiveRecord::Base.extend(Kasket::ConfigurationMixin)
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                if defined?(ActiveRecord::Relation)
         | 
| 33 | 
            +
                  ActiveRecord::Relation.send(:include, Kasket::RelationMixin)
         | 
| 34 | 
            +
                  Arel::SelectManager.send(:include, Kasket::SelectManagerMixin)
         | 
| 35 | 
            +
                end
         | 
| 36 | 
            +
             | 
| 23 37 | 
             
                ActiveRecord::Associations::BelongsToAssociation.send(:include, Kasket::ReloadAssociationMixin)
         | 
| 24 | 
            -
                ActiveRecord:: | 
| 38 | 
            +
                if ActiveRecord::VERSION::MAJOR == 2 || AR30
         | 
| 39 | 
            +
                  ActiveRecord::Associations::BelongsToPolymorphicAssociation.send(:include, Kasket::ReloadAssociationMixin)
         | 
| 40 | 
            +
                end
         | 
| 25 41 | 
             
                ActiveRecord::Associations::HasOneThroughAssociation.send(:include, Kasket::ReloadAssociationMixin)
         | 
| 26 42 | 
             
              end
         | 
| 27 43 |  | 
| @@ -39,4 +55,3 @@ module Kasket | |
| 39 55 | 
             
                end
         | 
| 40 56 | 
             
              end
         | 
| 41 57 | 
             
            end
         | 
| 42 | 
            -
             | 
| @@ -3,10 +3,6 @@ require 'active_support' | |
| 3 3 | 
             
            require "digest/md5"
         | 
| 4 4 |  | 
| 5 5 | 
             
            module Kasket
         | 
| 6 | 
            -
              autoload :ReadMixin, 'kasket/read_mixin'
         | 
| 7 | 
            -
              autoload :WriteMixin, 'kasket/write_mixin'
         | 
| 8 | 
            -
              autoload :DirtyMixin, 'kasket/dirty_mixin'
         | 
| 9 | 
            -
              autoload :QueryParser, 'kasket/query_parser'
         | 
| 10 6 |  | 
| 11 7 | 
             
              module ConfigurationMixin
         | 
| 12 8 |  | 
| @@ -27,7 +23,7 @@ module Kasket | |
| 27 23 | 
             
                end
         | 
| 28 24 |  | 
| 29 25 | 
             
                def kasket_key_prefix
         | 
| 30 | 
            -
                  @kasket_key_prefix ||= "kasket-#{Kasket::Version:: | 
| 26 | 
            +
                  @kasket_key_prefix ||= "kasket-#{Kasket::Version::PROTOCOL}/#{table_name}/version=#{column_names.join.sum}/"
         | 
| 31 27 | 
             
                end
         | 
| 32 28 |  | 
| 33 29 | 
             
                def kasket_key_for(attribute_value_pairs)
         | 
    
        data/lib/kasket/query_parser.rb
    CHANGED
    
    | @@ -11,15 +11,26 @@ module Kasket | |
| 11 11 |  | 
| 12 12 | 
             
                def initialize(model_class)
         | 
| 13 13 | 
             
                  @model_class = model_class
         | 
| 14 | 
            -
                  @supported_query_pattern =  | 
| 15 | 
            -
             | 
| 14 | 
            +
                  @supported_query_pattern = if AR30
         | 
| 15 | 
            +
                    /^select\s+(?:`#{@model_class.table_name}`.)?\* from (?:`|")#{@model_class.table_name}(?:`|") where (.*?)\s*$/i
         | 
| 16 | 
            +
                  else
         | 
| 17 | 
            +
                    /^select \* from (?:`|")#{@model_class.table_name}(?:`|") where \((.*)\)(|\s+limit 1)\s*$/i
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
                  @table_and_column_pattern = /(?:(?:`|")?#{@model_class.table_name}(?:`|")?\.)?(?:`|")?([a-zA-Z]\w*)(?:`|")?/ # Matches: `users`.id, `users`.`id`, users.id, id
         | 
| 16 20 | 
             
                  @key_eq_value_pattern = /^[\(\s]*#{@table_and_column_pattern}\s+(=|IN)\s+#{VALUE}[\)\s]*$/ # Matches: KEY = VALUE, (KEY = VALUE), ()(KEY = VALUE))
         | 
| 17 21 | 
             
                end
         | 
| 18 22 |  | 
| 19 23 | 
             
                def parse(sql)
         | 
| 20 24 | 
             
                  if match = @supported_query_pattern.match(sql)
         | 
| 25 | 
            +
                    where, limit = match[1], match[2]
         | 
| 26 | 
            +
                    if AR30 && where =~ /limit \d+\s*$/i
         | 
| 27 | 
            +
                      # limit is harder to find in rails 3.0 since where does not use surrounding braces
         | 
| 28 | 
            +
                      return unless where =~ /(.*?)(\s+limit 1)\s*$/i
         | 
| 29 | 
            +
                      where, limit = $1, $2
         | 
| 30 | 
            +
                    end
         | 
| 31 | 
            +
             | 
| 21 32 | 
             
                    query = Hash.new
         | 
| 22 | 
            -
                    query[:attributes] = sorted_attribute_value_pairs( | 
| 33 | 
            +
                    query[:attributes] = sorted_attribute_value_pairs(where)
         | 
| 23 34 | 
             
                    return nil if query[:attributes].nil?
         | 
| 24 35 |  | 
| 25 36 | 
             
                    if query[:attributes].size > 1 && query[:attributes].map(&:last).any? {|a| a.is_a?(Array)}
         | 
| @@ -28,7 +39,7 @@ module Kasket | |
| 28 39 | 
             
                    end
         | 
| 29 40 |  | 
| 30 41 | 
             
                    query[:index] = query[:attributes].map(&:first)
         | 
| 31 | 
            -
                    query[:limit] =  | 
| 42 | 
            +
                    query[:limit] = limit.blank? ? nil : 1
         | 
| 32 43 | 
             
                    query[:key] = @model_class.kasket_key_for(query[:attributes])
         | 
| 33 44 | 
             
                    query[:key] << '/first' if query[:limit] == 1 && !query[:index].include?(:id)
         | 
| 34 45 | 
             
                    query
         | 
    
        data/lib/kasket/read_mixin.rb
    CHANGED
    
    | @@ -8,8 +8,17 @@ module Kasket | |
| 8 8 | 
             
                  end
         | 
| 9 9 | 
             
                end
         | 
| 10 10 |  | 
| 11 | 
            -
                def find_by_sql_with_kasket( | 
| 12 | 
            -
                   | 
| 11 | 
            +
                def find_by_sql_with_kasket(*args)
         | 
| 12 | 
            +
                  sql = args[0]
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                  if use_kasket?
         | 
| 15 | 
            +
                    if sql.respond_to?(:to_kasket_query)
         | 
| 16 | 
            +
                      query = sql.to_kasket_query(self, args[1])
         | 
| 17 | 
            +
                    else
         | 
| 18 | 
            +
                      query = kasket_parser.parse(sanitize_sql(sql))
         | 
| 19 | 
            +
                    end
         | 
| 20 | 
            +
                  end
         | 
| 21 | 
            +
             | 
| 13 22 | 
             
                  if query && has_kasket_index_on?(query[:index])
         | 
| 14 23 | 
             
                    if query[:key].is_a?(Array)
         | 
| 15 24 | 
             
                      find_by_sql_with_kasket_on_id_array(query[:key])
         | 
| @@ -21,40 +30,36 @@ module Kasket | |
| 21 30 | 
             
                          Array.wrap(value).collect { |record| instantiate(record.dup) }
         | 
| 22 31 | 
             
                        end
         | 
| 23 32 | 
             
                      else
         | 
| 24 | 
            -
                        store_in_kasket(query[:key], find_by_sql_without_kasket( | 
| 33 | 
            +
                        store_in_kasket(query[:key], find_by_sql_without_kasket(*args))
         | 
| 25 34 | 
             
                      end
         | 
| 26 35 | 
             
                    end
         | 
| 27 36 | 
             
                  else
         | 
| 28 | 
            -
                    find_by_sql_without_kasket( | 
| 37 | 
            +
                    find_by_sql_without_kasket(*args)
         | 
| 29 38 | 
             
                  end
         | 
| 30 39 | 
             
                end
         | 
| 31 40 |  | 
| 32 41 | 
             
                def find_by_sql_with_kasket_on_id_array(keys)
         | 
| 33 | 
            -
                   | 
| 34 | 
            -
                  missing_ids = []
         | 
| 35 | 
            -
             | 
| 36 | 
            -
                  keys.each do |key|
         | 
| 37 | 
            -
                    if value = key_value_map[key]
         | 
| 38 | 
            -
                      key_value_map[key] = instantiate(value.dup)
         | 
| 39 | 
            -
                    else
         | 
| 40 | 
            -
                      missing_ids << key.split('=').last.to_i
         | 
| 41 | 
            -
                    end
         | 
| 42 | 
            -
                  end
         | 
| 42 | 
            +
                  key_attributes_map = Kasket.cache.read_multi(*keys)
         | 
| 43 43 |  | 
| 44 | 
            -
                   | 
| 45 | 
            -
             | 
| 46 | 
            -
             | 
| 47 | 
            -
                        instance.store_in_kasket
         | 
| 48 | 
            -
                        key_value_map[instance.kasket_key] = instance
         | 
| 49 | 
            -
                      end
         | 
| 50 | 
            -
                    end
         | 
| 51 | 
            -
                  end
         | 
| 44 | 
            +
                  found_keys, missing_keys = keys.partition{|k| key_attributes_map[k] }
         | 
| 45 | 
            +
                  found_keys.each{|k| key_attributes_map[k] = instantiate(key_attributes_map[k].dup) }
         | 
| 46 | 
            +
                  key_attributes_map.merge!(missing_records_from_db(missing_keys))
         | 
| 52 47 |  | 
| 53 | 
            -
                   | 
| 48 | 
            +
                  key_attributes_map.values.compact
         | 
| 54 49 | 
             
                end
         | 
| 55 50 |  | 
| 56 51 | 
             
                protected
         | 
| 57 52 |  | 
| 53 | 
            +
                  def missing_records_from_db(missing_keys)
         | 
| 54 | 
            +
                    return {} if missing_keys.empty?
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                    id_key_map = Hash[missing_keys.map{|key| [key.split('=').last.to_i, key] }]
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                    found = without_kasket { find_all_by_id(id_key_map.keys) }
         | 
| 59 | 
            +
                    found.each(&:store_in_kasket)
         | 
| 60 | 
            +
                    Hash[found.map{|record| [id_key_map[record.id], record] }]
         | 
| 61 | 
            +
                  end
         | 
| 62 | 
            +
             | 
| 58 63 | 
             
                  def store_in_kasket(key, records)
         | 
| 59 64 | 
             
                    if records.size == 1
         | 
| 60 65 | 
             
                      if records.first.kasket_cacheable?
         | 
| @@ -1,13 +1,13 @@ | |
| 1 1 | 
             
            # -*- encoding: utf-8 -*-
         | 
| 2 2 | 
             
            module Kasket
         | 
| 3 3 | 
             
              module ReloadAssociationMixin
         | 
| 4 | 
            -
                # TODO write tests for this
         | 
| 5 4 | 
             
                def reload_with_kasket_clearing(*args)
         | 
| 6 5 | 
             
                  if loaded?
         | 
| 7 6 | 
             
                    Kasket.clear_local if target.class.include?(WriteMixin)
         | 
| 8 7 | 
             
                  else
         | 
| 9 | 
            -
                     | 
| 10 | 
            -
                     | 
| 8 | 
            +
                    refl = respond_to?(:reflection) ? reflection : proxy_reflection
         | 
| 9 | 
            +
                    target_class = (refl.options[:polymorphic] ? (respond_to?(:klass) ? klass : association_class) : refl.klass)
         | 
| 10 | 
            +
                    Kasket.clear_local if target_class && target_class.include?(WriteMixin)
         | 
| 11 11 | 
             
                  end
         | 
| 12 12 |  | 
| 13 13 | 
             
                  reload_without_kasket_clearing(*args)
         | 
| @@ -0,0 +1,26 @@ | |
| 1 | 
            +
            module Kasket
         | 
| 2 | 
            +
              module SelectManagerMixin
         | 
| 3 | 
            +
                def to_kasket_query(klass, binds = [])
         | 
| 4 | 
            +
                  query = Kasket::Visitor.new(klass, binds).accept(ast)
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  return nil if query.nil? || query == :unsupported
         | 
| 7 | 
            +
                  return nil if query[:attributes].blank?
         | 
| 8 | 
            +
             | 
| 9 | 
            +
                  query[:index] = query[:attributes].map(&:first)
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                  if query[:limit]
         | 
| 12 | 
            +
                    return nil if query[:limit] > 1
         | 
| 13 | 
            +
                    # return nil if !query[:index].include?(:id)
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                  if query[:index].size > 1 && query[:attributes].any? { |attribute, value| value.is_a?(Array) }
         | 
| 17 | 
            +
                    return nil
         | 
| 18 | 
            +
                  end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                  query[:key] = klass.kasket_key_for(query[:attributes])
         | 
| 21 | 
            +
                  query[:key] << '/first' if query[:limit] == 1 && query[:index] != [:id]
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  query
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
              end
         | 
| 26 | 
            +
            end
         | 
    
        data/lib/kasket/version.rb
    CHANGED
    
    
| @@ -0,0 +1,140 @@ | |
| 1 | 
            +
            require 'arel'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Kasket
         | 
| 4 | 
            +
              class Visitor < Arel::Visitors::Visitor
         | 
| 5 | 
            +
                def initialize(model_class, binds)
         | 
| 6 | 
            +
                  @model_class = model_class
         | 
| 7 | 
            +
                  @binds       = binds.dup
         | 
| 8 | 
            +
                end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                def accept(node)
         | 
| 11 | 
            +
                  self.last_column = nil
         | 
| 12 | 
            +
                  super
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                def last_column=(col)
         | 
| 16 | 
            +
                  Thread.current[:arel_visitors_to_sql_last_column] = col
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                def last_column
         | 
| 20 | 
            +
                  Thread.current[:arel_visitors_to_sql_last_column]
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                def column_for(name)
         | 
| 24 | 
            +
                  @model_class.columns_hash[name.to_s]
         | 
| 25 | 
            +
                end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                def visit_Arel_Nodes_SelectStatement(node)
         | 
| 28 | 
            +
                  return :unsupported if !AR30 && node.with
         | 
| 29 | 
            +
                  return :unsupported if node.offset
         | 
| 30 | 
            +
                  return :unsupported if node.lock
         | 
| 31 | 
            +
                  return :unsupported if node.orders.any?
         | 
| 32 | 
            +
                  return :unsupported if node.cores.size != 1
         | 
| 33 | 
            +
             | 
| 34 | 
            +
                  query = visit_Arel_Nodes_SelectCore(node.cores[0])
         | 
| 35 | 
            +
                  return query if query == :unsupported
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  query = query.inject({}) do |memo, item|
         | 
| 38 | 
            +
                    memo.merge(item)
         | 
| 39 | 
            +
                  end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
                  query.merge!(visit(node.limit)) if node.limit
         | 
| 42 | 
            +
                  query
         | 
| 43 | 
            +
                end
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                def visit_Arel_Nodes_SelectCore(node)
         | 
| 46 | 
            +
                  return :unsupported if node.groups.any?
         | 
| 47 | 
            +
                  return :unsupported if node.having
         | 
| 48 | 
            +
                  return :unsupported if !AR30 && node.set_quantifier
         | 
| 49 | 
            +
                  return :unsupported if !AR30 && (!node.source || node.source.empty?)
         | 
| 50 | 
            +
                  return :unsupported if node.projections.size != 1
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                  select = node.projections[0]
         | 
| 53 | 
            +
                  select = select.name if select.respond_to?(:name)
         | 
| 54 | 
            +
                  return :unsupported if select != '*'
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                  parts = [visit(node.source)]
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                  parts += node.wheres.map {|where| visit(where) }
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                  parts.include?(:unsupported) ? :unsupported : parts
         | 
| 61 | 
            +
                end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
                def visit_Arel_Nodes_Limit(node)
         | 
| 64 | 
            +
                  {:limit => node.value.to_i}
         | 
| 65 | 
            +
                end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                def visit_Arel_Nodes_JoinSource(node)
         | 
| 68 | 
            +
                  return :unsupported if !node.left || node.right.any?
         | 
| 69 | 
            +
                  return :unsupported if !node.left.is_a?(Arel::Table)
         | 
| 70 | 
            +
                  visit(node.left)
         | 
| 71 | 
            +
                end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
                def visit_Arel_Table(node)
         | 
| 74 | 
            +
                  {:from => node.name}
         | 
| 75 | 
            +
                end
         | 
| 76 | 
            +
             | 
| 77 | 
            +
                def visit_Arel_Nodes_And(node)
         | 
| 78 | 
            +
                  attributes = node.children.map { |child| visit(child) }
         | 
| 79 | 
            +
                  return :unsupported if attributes.include?(:unsupported)
         | 
| 80 | 
            +
                  attributes.sort! { |pair1, pair2| pair1[0].to_s <=> pair2[0].to_s }
         | 
| 81 | 
            +
                  { :attributes => attributes }
         | 
| 82 | 
            +
                end
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                def visit_Arel_Nodes_In(node)
         | 
| 85 | 
            +
                  left = visit(node.left)
         | 
| 86 | 
            +
                  return :unsupported if left != :id
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                  [left, visit(node.right)]
         | 
| 89 | 
            +
                end
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                def visit_Arel_Nodes_Equality(node)
         | 
| 92 | 
            +
                  right = node.right
         | 
| 93 | 
            +
                  [visit(node.left), right ? visit(right) : nil]
         | 
| 94 | 
            +
                end
         | 
| 95 | 
            +
             | 
| 96 | 
            +
                def visit_Arel_Attributes_Attribute(node)
         | 
| 97 | 
            +
                  self.last_column = column_for(node.name)
         | 
| 98 | 
            +
                  node.name.to_sym
         | 
| 99 | 
            +
                end
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                def literal(node)
         | 
| 102 | 
            +
                  if node == '?'
         | 
| 103 | 
            +
                    column, value = @binds.shift
         | 
| 104 | 
            +
                    value.to_s
         | 
| 105 | 
            +
                  else
         | 
| 106 | 
            +
                    node.to_s
         | 
| 107 | 
            +
                  end
         | 
| 108 | 
            +
                end
         | 
| 109 | 
            +
             | 
| 110 | 
            +
                # only gets used on 1.8.7
         | 
| 111 | 
            +
                def visit_Arel_Nodes_BindParam(x)
         | 
| 112 | 
            +
                  @binds.shift[1]
         | 
| 113 | 
            +
                end
         | 
| 114 | 
            +
             | 
| 115 | 
            +
                def visit_Array(node)
         | 
| 116 | 
            +
                  node.map {|value| quoted(value) }
         | 
| 117 | 
            +
                end
         | 
| 118 | 
            +
             | 
| 119 | 
            +
                #TODO: We are actually not using this?
         | 
| 120 | 
            +
                def quoted(node)
         | 
| 121 | 
            +
                  @model_class.connection.quote(node, self.last_column)
         | 
| 122 | 
            +
                end
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                alias :visit_String                :literal
         | 
| 125 | 
            +
                alias :visit_Fixnum                :literal
         | 
| 126 | 
            +
                alias :visit_TrueClass             :literal
         | 
| 127 | 
            +
                alias :visit_FalseClass            :literal
         | 
| 128 | 
            +
                alias :visit_Arel_Nodes_SqlLiteral :literal
         | 
| 129 | 
            +
             | 
| 130 | 
            +
                def method_missing(name, *args, &block)
         | 
| 131 | 
            +
                  return :unsupported if name.to_s.start_with?('visit_')
         | 
| 132 | 
            +
                  super
         | 
| 133 | 
            +
                end
         | 
| 134 | 
            +
             | 
| 135 | 
            +
                def respond_to?(name, include_private = false)
         | 
| 136 | 
            +
                  return super || name.to_s.start_with?('visit_')
         | 
| 137 | 
            +
                end
         | 
| 138 | 
            +
             | 
| 139 | 
            +
              end
         | 
| 140 | 
            +
            end
         | 
| @@ -6,7 +6,7 @@ class ConfigurationMixinTest < ActiveSupport::TestCase | |
| 6 6 | 
             
              context "Generating cache keys" do
         | 
| 7 7 |  | 
| 8 8 | 
             
                should "not choke on empty numeric attributes" do
         | 
| 9 | 
            -
                  expected_cache_key = "kasket-#{Kasket::Version:: | 
| 9 | 
            +
                  expected_cache_key = "kasket-#{Kasket::Version::PROTOCOL}/posts/version=#{POST_VERSION}/blog_id=null"
         | 
| 10 10 | 
             
                  query_attributes   = [ [:blog_id, ''] ]
         | 
| 11 11 |  | 
| 12 12 | 
             
                  assert_equal expected_cache_key, Post.kasket_key_for(query_attributes)
         | 
| @@ -27,7 +27,7 @@ class ConfigurationMixinTest < ActiveSupport::TestCase | |
| 27 27 |  | 
| 28 28 | 
             
                should "downcase string attributes" do
         | 
| 29 29 | 
             
                  query_attributes = [ [:title, 'ThIs'] ]
         | 
| 30 | 
            -
                  expected_cache_key = "kasket-#{Kasket::Version:: | 
| 30 | 
            +
                  expected_cache_key = "kasket-#{Kasket::Version::PROTOCOL}/posts/version=#{POST_VERSION}/title='this'"
         | 
| 31 31 |  | 
| 32 32 | 
             
                  assert_equal expected_cache_key, Post.kasket_key_for(query_attributes)
         | 
| 33 33 | 
             
                end
         | 
    
        data/test/database.yml
    CHANGED
    
    
    
        data/test/fixtures/comments.yml
    CHANGED
    
    | @@ -1,12 +1,14 @@ | |
| 1 1 | 
             
            few_comments_1:
         | 
| 2 | 
            -
               | 
| 2 | 
            +
              id: 1
         | 
| 3 | 
            +
              post_id: 2
         | 
| 3 4 | 
             
              body: what ever body 1
         | 
| 4 5 |  | 
| 5 6 | 
             
            few_comments_2:
         | 
| 6 | 
            -
               | 
| 7 | 
            +
              id: 2
         | 
| 8 | 
            +
              post_id: 2
         | 
| 7 9 | 
             
              body: what ever body 2
         | 
| 8 10 |  | 
| 9 | 
            -
            <% (1.. | 
| 11 | 
            +
            <% (1..10).each do |i| %>
         | 
| 10 12 | 
             
            many_comments_<%= i %>:
         | 
| 11 13 | 
             
              post: has_many_comments
         | 
| 12 14 | 
             
              body: what ever body <%= i %>
         | 
    
        data/test/fixtures/posts.yml
    CHANGED
    
    | @@ -1,15 +1,23 @@ | |
| 1 1 | 
             
            no_comments:
         | 
| 2 | 
            +
              id: 1
         | 
| 2 3 | 
             
              blog: a_blog
         | 
| 3 4 | 
             
              title: no_comments
         | 
| 5 | 
            +
              author: mick
         | 
| 4 6 |  | 
| 5 7 | 
             
            has_two_comments:
         | 
| 8 | 
            +
              id: 2
         | 
| 6 9 | 
             
              blog: a_blog
         | 
| 7 10 | 
             
              title: few_comments
         | 
| 11 | 
            +
              author: mick
         | 
| 8 12 |  | 
| 9 13 | 
             
            on_other_blog:
         | 
| 14 | 
            +
              id: 3
         | 
| 10 15 | 
             
              blog: other_blog
         | 
| 11 16 | 
             
              title: no_comments
         | 
| 17 | 
            +
              author: eric
         | 
| 12 18 |  | 
| 13 19 | 
             
            has_many_comments:
         | 
| 20 | 
            +
              id: 4
         | 
| 14 21 | 
             
              blog: a_blog
         | 
| 15 | 
            -
              title: many_comments
         | 
| 22 | 
            +
              title: many_comments
         | 
| 23 | 
            +
              author: eric
         | 
    
        data/test/helper.rb
    CHANGED
    
    | @@ -10,6 +10,7 @@ if defined?(Debugger) | |
| 10 10 | 
             
            end
         | 
| 11 11 |  | 
| 12 12 | 
             
            require 'test/unit'
         | 
| 13 | 
            +
            require 'active_record'
         | 
| 13 14 | 
             
            require 'active_record/fixtures'
         | 
| 14 15 |  | 
| 15 16 | 
             
            $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
         | 
| @@ -18,13 +19,9 @@ require 'kasket' | |
| 18 19 |  | 
| 19 20 | 
             
            Kasket.setup
         | 
| 20 21 |  | 
| 21 | 
            -
            require 'shoulda'
         | 
| 22 | 
            -
             | 
| 23 22 | 
             
            class ActiveSupport::TestCase
         | 
| 24 23 | 
             
              include ActiveRecord::TestFixtures
         | 
| 25 24 |  | 
| 26 | 
            -
              fixtures :all
         | 
| 27 | 
            -
             | 
| 28 25 | 
             
              def create_fixtures(*table_names)
         | 
| 29 26 | 
             
                if block_given?
         | 
| 30 27 | 
             
                  Fixtures.create_fixtures(Test::Unit::TestCase.fixture_path, table_names) { yield }
         | 
| @@ -41,11 +38,23 @@ class ActiveSupport::TestCase | |
| 41 38 | 
             
              def clear_cache
         | 
| 42 39 | 
             
                Kasket.cache.clear
         | 
| 43 40 | 
             
              end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
              def arel?
         | 
| 43 | 
            +
                self.class.arel?
         | 
| 44 | 
            +
              end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
              def self.arel?
         | 
| 47 | 
            +
                ActiveRecord::VERSION::MAJOR >= 3 && ActiveRecord::VERSION::MINOR >= 1
         | 
| 48 | 
            +
              end
         | 
| 44 49 | 
             
            end
         | 
| 45 50 |  | 
| 46 51 | 
             
            ActiveSupport::TestCase.fixture_path = File.dirname(__FILE__) + "/fixtures/"
         | 
| 47 52 | 
             
            $LOAD_PATH.unshift(ActiveSupport::TestCase.fixture_path)
         | 
| 48 53 |  | 
| 54 | 
            +
            class ActiveSupport::TestCase
         | 
| 55 | 
            +
              fixtures :all
         | 
| 56 | 
            +
            end
         | 
| 57 | 
            +
             | 
| 49 58 | 
             
            module Rails
         | 
| 50 59 | 
             
              module_function
         | 
| 51 60 | 
             
              CACHE = ActiveSupport::Cache::MemoryStore.new
         | 
| @@ -61,3 +70,5 @@ module Rails | |
| 61 70 | 
             
            end
         | 
| 62 71 |  | 
| 63 72 | 
             
            require 'test_models'
         | 
| 73 | 
            +
            POST_VERSION = Post.column_names.join.sum
         | 
| 74 | 
            +
            COMMENT_VERSION = Comment.column_names.join.sum
         | 
    
        data/test/parser_test.rb
    CHANGED
    
    | @@ -2,6 +2,24 @@ require File.expand_path("helper", File.dirname(__FILE__)) | |
| 2 2 | 
             
            require 'kasket/query_parser'
         | 
| 3 3 |  | 
| 4 4 | 
             
            class ParserTest < ActiveSupport::TestCase
         | 
| 5 | 
            +
              def parse(options)
         | 
| 6 | 
            +
                scope = Post
         | 
| 7 | 
            +
                if arel?
         | 
| 8 | 
            +
                  options.each do |k,v|
         | 
| 9 | 
            +
                    scope = case k
         | 
| 10 | 
            +
                    when :conditions then scope.where(v)
         | 
| 11 | 
            +
                    else
         | 
| 12 | 
            +
                      scope.send(k, v)
         | 
| 13 | 
            +
                    end
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
                  scope.to_kasket_query
         | 
| 16 | 
            +
                elsif ActiveRecord::VERSION::MAJOR == 3 && ActiveRecord::VERSION::MINOR == 0
         | 
| 17 | 
            +
                  @parser.parse(scope.scoped(options).to_sql)
         | 
| 18 | 
            +
                else
         | 
| 19 | 
            +
                  sql = scope.send(:construct_finder_sql, options)
         | 
| 20 | 
            +
                  @parser.parse(sql)
         | 
| 21 | 
            +
                end
         | 
| 22 | 
            +
              end
         | 
| 5 23 |  | 
| 6 24 | 
             
              context "Parsing" do
         | 
| 7 25 | 
             
                setup do
         | 
| @@ -9,40 +27,50 @@ class ParserTest < ActiveSupport::TestCase | |
| 9 27 | 
             
                end
         | 
| 10 28 |  | 
| 11 29 | 
             
                should "not support conditions with number as column (e.g. 0 = 1)" do
         | 
| 12 | 
            -
                   | 
| 13 | 
            -
             | 
| 30 | 
            +
                  assert !parse(:conditions => "0 = 1")
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                should "not support conditions with number as column and parans (e.g. 0 = 1)" do
         | 
| 34 | 
            +
                  assert !parse(:conditions => "(0 = 1)")
         | 
| 14 35 | 
             
                end
         | 
| 15 36 |  | 
| 16 37 | 
             
                should 'not support IN queries in combination with other conditions' do
         | 
| 17 | 
            -
                   | 
| 18 | 
            -
                  assert(!parsed_query)
         | 
| 38 | 
            +
                  assert !parse(:conditions => {:id => [1,2,3], :is_active => true})
         | 
| 19 39 | 
             
                end
         | 
| 20 40 |  | 
| 21 41 | 
             
                should "extract conditions" do
         | 
| 22 | 
            -
                   | 
| 42 | 
            +
                  kasket_query = parse(:conditions => {:title => 'red', :blog_id => 1})
         | 
| 43 | 
            +
                  assert_equal [[:blog_id, "1"], [:title, "red"]], kasket_query[:attributes]
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
                should "extract conditions with parans that do not surround" do
         | 
| 47 | 
            +
                  kasket_query = parse(:conditions => "(title = 'red') AND (blog_id = 1)")
         | 
| 48 | 
            +
                  if ActiveRecord::VERSION::STRING > "3.1.0"
         | 
| 49 | 
            +
                    assert !kasket_query
         | 
| 50 | 
            +
                  else
         | 
| 51 | 
            +
                    assert_equal [[:blog_id, "1"], [:title, "red"]], kasket_query[:attributes]
         | 
| 52 | 
            +
                  end
         | 
| 23 53 | 
             
                end
         | 
| 24 54 |  | 
| 25 55 | 
             
                should "extract required index" do
         | 
| 26 | 
            -
                  assert_equal [:blog_id, :title],  | 
| 56 | 
            +
                  assert_equal [:blog_id, :title], parse(:conditions => {:title => 'red', :blog_id => 1})[:index]
         | 
| 27 57 | 
             
                end
         | 
| 28 58 |  | 
| 29 59 | 
             
                should "only support queries against its model's table" do
         | 
| 30 | 
            -
                  assert  | 
| 60 | 
            +
                  assert !parse(:conditions => {'users.id' => 2}, :from => 'apples')
         | 
| 31 61 | 
             
                end
         | 
| 32 62 |  | 
| 33 63 | 
             
                should "support cachable queries" do
         | 
| 34 | 
            -
                  assert  | 
| 35 | 
            -
                  assert  | 
| 64 | 
            +
                  assert parse(:conditions => {:id => 1})
         | 
| 65 | 
            +
                  assert parse(:conditions => {:id => 1}, :limit => 1)
         | 
| 36 66 | 
             
                end
         | 
| 37 67 |  | 
| 38 68 | 
             
                should "support IN queries on id" do
         | 
| 39 | 
            -
                   | 
| 40 | 
            -
                  assert(parsed_query)
         | 
| 41 | 
            -
                  assert_equal([[:id, ['1', '2', '3']]], parsed_query[:attributes])
         | 
| 69 | 
            +
                  assert_equal [[:id, ['1', '2', '3']]], parse(:conditions => {:id => [1,2,3]})[:attributes]
         | 
| 42 70 | 
             
                end
         | 
| 43 71 |  | 
| 44 72 | 
             
                should "not support IN queries on other attributes" do
         | 
| 45 | 
            -
                  assert | 
| 73 | 
            +
                  assert !parse(:conditions => {:hest => [1,2,3]})
         | 
| 46 74 | 
             
                end
         | 
| 47 75 |  | 
| 48 76 | 
             
                should "support vaguely formatted queries" do
         | 
| @@ -50,57 +78,58 @@ class ParserTest < ActiveSupport::TestCase | |
| 50 78 | 
             
                end
         | 
| 51 79 |  | 
| 52 80 | 
             
                context "extract options" do
         | 
| 53 | 
            -
             | 
| 54 81 | 
             
                  should "provide the limit" do
         | 
| 55 | 
            -
                     | 
| 56 | 
            -
                    assert_equal  | 
| 57 | 
            -
             | 
| 58 | 
            -
                    sql << ' LIMIT 1'
         | 
| 59 | 
            -
                    assert_equal 1, @parser.parse(sql)[:limit]
         | 
| 82 | 
            +
                    assert_equal nil, parse(:conditions => {:id => 2})[:limit]
         | 
| 83 | 
            +
                    assert_equal 1, parse(:conditions => {:id => 2}, :limit => 1)[:limit]
         | 
| 60 84 | 
             
                  end
         | 
| 61 | 
            -
             | 
| 62 85 | 
             
                end
         | 
| 63 86 |  | 
| 64 87 | 
             
                context "unsupported queries" do
         | 
| 65 | 
            -
             | 
| 66 88 | 
             
                  should "include advanced limits" do
         | 
| 67 | 
            -
                    assert  | 
| 89 | 
            +
                    assert !parse(:conditions => {:title => 'red', :blog_id => 1}, :limit => 2)
         | 
| 68 90 | 
             
                  end
         | 
| 69 91 |  | 
| 70 92 | 
             
                  should "include joins" do
         | 
| 71 | 
            -
                    assert  | 
| 93 | 
            +
                    assert !parse(:conditions => {:title => 'test', 'apple.tree_id' => 'posts.id'}, :from => ['posts', 'apple'])
         | 
| 94 | 
            +
                    assert !parse(:conditions => {:title => 'test'}, :joins => :comments)
         | 
| 72 95 | 
             
                  end
         | 
| 73 96 |  | 
| 74 97 | 
             
                  should "include specific selects" do
         | 
| 75 | 
            -
                    assert  | 
| 98 | 
            +
                    assert !parse(:conditions => {:title => 'red'}, :select => :id)
         | 
| 76 99 | 
             
                  end
         | 
| 77 100 |  | 
| 78 101 | 
             
                  should "include offset" do
         | 
| 79 | 
            -
                    assert  | 
| 102 | 
            +
                    assert !parse(:conditions => {:title => 'red'}, :limit => 1, :offset => 2)
         | 
| 80 103 | 
             
                  end
         | 
| 81 104 |  | 
| 82 105 | 
             
                  should "include order" do
         | 
| 83 | 
            -
                    assert  | 
| 106 | 
            +
                    assert !parse(:conditions => {:title => 'red'}, :order => :title)
         | 
| 84 107 | 
             
                  end
         | 
| 85 108 |  | 
| 86 109 | 
             
                  should "include the OR operator" do
         | 
| 87 | 
            -
                    assert  | 
| 110 | 
            +
                    assert !parse(:conditions => "title = 'red' OR blog_id = 1")
         | 
| 88 111 | 
             
                  end
         | 
| 89 112 | 
             
                end
         | 
| 90 113 |  | 
| 91 114 | 
             
                context "key generation" do
         | 
| 92 115 | 
             
                  should "include the table name and version" do
         | 
| 93 | 
            -
                     | 
| 116 | 
            +
                    kasket_query = parse(:conditions => {:id => 1})
         | 
| 117 | 
            +
                    assert_match(/^kasket-#{Kasket::Version::PROTOCOL}\/posts\/version=#{POST_VERSION}\//, kasket_query[:key])
         | 
| 94 118 | 
             
                  end
         | 
| 95 119 |  | 
| 96 120 | 
             
                  should "include all indexed attributes" do
         | 
| 97 | 
            -
                     | 
| 98 | 
            -
                    assert_match(/ | 
| 99 | 
            -
             | 
| 121 | 
            +
                    kasket_query = parse(:conditions => {:id => 1})
         | 
| 122 | 
            +
                    assert_match(/id=1$/, kasket_query[:key])
         | 
| 123 | 
            +
             | 
| 124 | 
            +
                    kasket_query = parse(:conditions => {:id => 1, :blog_id => 2})
         | 
| 125 | 
            +
                    assert_match(/blog_id=2\/id=1$/, kasket_query[:key])
         | 
| 126 | 
            +
             | 
| 127 | 
            +
                    kasket_query = parse(:conditions => {:id => 1, :title => 'title'})
         | 
| 128 | 
            +
                    assert_match(/id=1\/title='title'$/, kasket_query[:key])
         | 
| 100 129 | 
             
                  end
         | 
| 101 130 |  | 
| 102 131 | 
             
                  should "generate multiple keys on IN queries" do
         | 
| 103 | 
            -
                    keys =  | 
| 132 | 
            +
                    keys = parse(:conditions => {:id => [1,2]})[:key]
         | 
| 104 133 | 
             
                    assert_instance_of(Array, keys)
         | 
| 105 134 | 
             
                    assert_match(/id=1$/, keys[0])
         | 
| 106 135 | 
             
                    assert_match(/id=2$/, keys[1])
         | 
| @@ -108,13 +137,13 @@ class ParserTest < ActiveSupport::TestCase | |
| 108 137 |  | 
| 109 138 | 
             
                  context "when limit 1" do
         | 
| 110 139 | 
             
                    should "add /first to the key if the index does not include id" do
         | 
| 111 | 
            -
                      assert_match(/title='a'\/first$/,  | 
| 140 | 
            +
                      assert_match(/title='a'\/first$/, parse(:conditions => {:title => 'a'}, :limit => 1)[:key])
         | 
| 112 141 | 
             
                    end
         | 
| 142 | 
            +
             | 
| 113 143 | 
             
                    should "not add /first to the key when the index includes id" do
         | 
| 114 | 
            -
                      assert_match(/id=1$/,  | 
| 144 | 
            +
                      assert_match(/id=1$/, parse(:conditions => {:id => 1}, :limit => 1)[:key])
         | 
| 115 145 | 
             
                    end
         | 
| 116 146 | 
             
                  end
         | 
| 117 147 | 
             
                end
         | 
| 118 148 | 
             
              end
         | 
| 119 | 
            -
             | 
| 120 149 | 
             
            end
         | 
    
        data/test/read_mixin_test.rb
    CHANGED
    
    | @@ -20,20 +20,25 @@ class ReadMixinTest < ActiveSupport::TestCase | |
| 20 20 | 
             
                end
         | 
| 21 21 |  | 
| 22 22 | 
             
                should "read results" do
         | 
| 23 | 
            -
                  Kasket.cache.write("kasket-#{Kasket::Version:: | 
| 23 | 
            +
                  Kasket.cache.write("kasket-#{Kasket::Version::PROTOCOL}/posts/version=#{POST_VERSION}/id=1", @post_database_result)
         | 
| 24 24 | 
             
                  assert_equal @post_records, Post.find_by_sql('SELECT * FROM `posts` WHERE (id = 1)')
         | 
| 25 25 | 
             
                end
         | 
| 26 26 |  | 
| 27 | 
            +
                should "support sql with ?" do
         | 
| 28 | 
            +
                  Kasket.cache.write("kasket-#{Kasket::Version::PROTOCOL}/posts/version=#{POST_VERSION}/id=1", @post_database_result)
         | 
| 29 | 
            +
                  assert_equal @post_records, Post.find_by_sql(['SELECT * FROM `posts` WHERE (id = ?)', 1])
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 27 32 | 
             
                should "store results in kasket" do
         | 
| 28 33 | 
             
                  Post.find_by_sql('SELECT * FROM `posts` WHERE (id = 1)')
         | 
| 29 34 |  | 
| 30 | 
            -
                  assert_equal @post_database_result, Kasket.cache.read("kasket-#{Kasket::Version:: | 
| 35 | 
            +
                  assert_equal @post_database_result, Kasket.cache.read("kasket-#{Kasket::Version::PROTOCOL}/posts/version=#{POST_VERSION}/id=1")
         | 
| 31 36 | 
             
                end
         | 
| 32 37 |  | 
| 33 38 | 
             
                should "store multiple records in cache" do
         | 
| 34 39 | 
             
                  Comment.find_by_sql('SELECT * FROM `comments` WHERE (post_id = 1)')
         | 
| 35 | 
            -
                  stored_value = Kasket.cache.read("kasket-#{Kasket::Version:: | 
| 36 | 
            -
                  assert_equal(["kasket-#{Kasket::Version:: | 
| 40 | 
            +
                  stored_value = Kasket.cache.read("kasket-#{Kasket::Version::PROTOCOL}/comments/version=#{COMMENT_VERSION}/post_id=1")
         | 
| 41 | 
            +
                  assert_equal(["kasket-#{Kasket::Version::PROTOCOL}/comments/version=#{COMMENT_VERSION}/id=1", "kasket-#{Kasket::Version::PROTOCOL}/comments/version=#{COMMENT_VERSION}/id=2"], stored_value)
         | 
| 37 42 | 
             
                  assert_equal(@comment_database_result, stored_value.map {|key| Kasket.cache.read(key)})
         | 
| 38 43 |  | 
| 39 44 | 
             
                  Comment.expects(:find_by_sql_without_kasket).never
         | 
| @@ -43,13 +48,15 @@ class ReadMixinTest < ActiveSupport::TestCase | |
| 43 48 |  | 
| 44 49 | 
             
                context "modifying results" do
         | 
| 45 50 | 
             
                  setup do
         | 
| 46 | 
            -
                    Kasket.cache.write("kasket-#{Kasket::Version:: | 
| 47 | 
            -
                    @ | 
| 51 | 
            +
                    Kasket.cache.write("kasket-#{Kasket::Version::PROTOCOL}/posts/version=#{POST_VERSION}/id=1", {'id' => 1, 'title' => "asd"})
         | 
| 52 | 
            +
                    @sql = 'SELECT * FROM `posts` WHERE (id = 1)'
         | 
| 53 | 
            +
                    @record = Post.find_by_sql(@sql).first
         | 
| 54 | 
            +
                    assert_equal "asd", @record.title # read from cache ?
         | 
| 48 55 | 
             
                    @record.instance_variable_get(:@attributes)['id'] = 3
         | 
| 49 56 | 
             
                  end
         | 
| 50 57 |  | 
| 51 58 | 
             
                  should "not impact other queries" do
         | 
| 52 | 
            -
                    same_record = Post.find_by_sql( | 
| 59 | 
            +
                    same_record = Post.find_by_sql(@sql).first
         | 
| 53 60 |  | 
| 54 61 | 
             
                    assert_not_equal @record, same_record
         | 
| 55 62 | 
             
                  end
         | 
    
        data/test/reload_test.rb
    ADDED
    
    | @@ -0,0 +1,85 @@ | |
| 1 | 
            +
            require File.expand_path("helper", File.dirname(__FILE__))
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            class ReloadTest < ActiveSupport::TestCase
         | 
| 4 | 
            +
              context "Loading a polymorphic belongs_to" do
         | 
| 5 | 
            +
                should "not clear cache when loading nil" do
         | 
| 6 | 
            +
                  @post = Post.first
         | 
| 7 | 
            +
                  @post.poly = nil
         | 
| 8 | 
            +
                  @post.save!
         | 
| 9 | 
            +
                  Kasket.expects(:clear_local).never
         | 
| 10 | 
            +
                  assert_nil @post.poly
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                context "that is uncached" do
         | 
| 14 | 
            +
                  setup do
         | 
| 15 | 
            +
                    @post = Post.first
         | 
| 16 | 
            +
                    @post.poly = Blog.first
         | 
| 17 | 
            +
                    @post.save!
         | 
| 18 | 
            +
                    assert @post.poly
         | 
| 19 | 
            +
                  end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                  should "not clear local when it is unloaded" do
         | 
| 22 | 
            +
                    Kasket.expects(:clear_local).never
         | 
| 23 | 
            +
                    assert Post.first.poly
         | 
| 24 | 
            +
                  end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  should "not clear local when it is loaded" do
         | 
| 27 | 
            +
                    Kasket.expects(:clear_local).never
         | 
| 28 | 
            +
                    assert @post.poly.reload
         | 
| 29 | 
            +
                  end
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                context "that is cached" do
         | 
| 33 | 
            +
                  setup do
         | 
| 34 | 
            +
                    @post = Post.first
         | 
| 35 | 
            +
                    @post.poly = Comment.first
         | 
| 36 | 
            +
                    @post.save!
         | 
| 37 | 
            +
                    assert @post.poly
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                  should "clear local when it is loaded" do
         | 
| 41 | 
            +
                    Kasket.expects(:clear_local)
         | 
| 42 | 
            +
                    @post.poly.reload
         | 
| 43 | 
            +
                  end
         | 
| 44 | 
            +
                end
         | 
| 45 | 
            +
              end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
              context "Reloading a model" do
         | 
| 48 | 
            +
                setup do
         | 
| 49 | 
            +
                  @post = Post.first
         | 
| 50 | 
            +
                  assert @post
         | 
| 51 | 
            +
                  assert @post.title
         | 
| 52 | 
            +
                end
         | 
| 53 | 
            +
             | 
| 54 | 
            +
                should "clear local cache" do
         | 
| 55 | 
            +
                  Kasket.expects(:clear_local)
         | 
| 56 | 
            +
                  @post.reload
         | 
| 57 | 
            +
                end
         | 
| 58 | 
            +
              end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
              context "Reloading a belongs_to association" do
         | 
| 61 | 
            +
                setup do
         | 
| 62 | 
            +
                  @post = Comment.first.post
         | 
| 63 | 
            +
                  assert @post
         | 
| 64 | 
            +
                  assert @post.title
         | 
| 65 | 
            +
                end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                should "clear local cache" do
         | 
| 68 | 
            +
                  Kasket.expects(:clear_local)
         | 
| 69 | 
            +
                  @post.reload
         | 
| 70 | 
            +
                end
         | 
| 71 | 
            +
              end
         | 
| 72 | 
            +
             | 
| 73 | 
            +
              context "Reloading a has_one_through association" do
         | 
| 74 | 
            +
                setup do
         | 
| 75 | 
            +
                  @author = Comment.first.author
         | 
| 76 | 
            +
                  assert @author
         | 
| 77 | 
            +
                  assert @author.name
         | 
| 78 | 
            +
                end
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                should "clear local cache" do
         | 
| 81 | 
            +
                  Kasket.expects(:clear_local)
         | 
| 82 | 
            +
                  @author.reload
         | 
| 83 | 
            +
                end
         | 
| 84 | 
            +
              end
         | 
| 85 | 
            +
            end
         | 
    
        data/test/schema.rb
    ADDED
    
    | @@ -0,0 +1,30 @@ | |
| 1 | 
            +
            ActiveRecord::Schema.define(:version => 1) do
         | 
| 2 | 
            +
              suppress_messages do
         | 
| 3 | 
            +
                create_table 'comments', :force => true do |t|
         | 
| 4 | 
            +
                  t.text     'body'
         | 
| 5 | 
            +
                  t.integer  'post_id'
         | 
| 6 | 
            +
                  t.datetime 'created_at'
         | 
| 7 | 
            +
                  t.datetime 'updated_at'
         | 
| 8 | 
            +
                end
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                create_table 'authors', :force => true do |t|
         | 
| 11 | 
            +
                  t.string 'name'
         | 
| 12 | 
            +
                end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                create_table 'posts', :force => true do |t|
         | 
| 15 | 
            +
                  t.string   'title'
         | 
| 16 | 
            +
                  t.integer  'author_id'
         | 
| 17 | 
            +
                  t.integer  'blog_id'
         | 
| 18 | 
            +
                  t.integer  'poly_id'
         | 
| 19 | 
            +
                  t.string   'poly_type'
         | 
| 20 | 
            +
                  t.datetime 'created_at'
         | 
| 21 | 
            +
                  t.datetime 'updated_at'
         | 
| 22 | 
            +
                end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                create_table 'blogs', :force => true do |t|
         | 
| 25 | 
            +
                  t.string   'name'
         | 
| 26 | 
            +
                  t.datetime 'created_at'
         | 
| 27 | 
            +
                  t.datetime 'updated_at'
         | 
| 28 | 
            +
                end
         | 
| 29 | 
            +
              end
         | 
| 30 | 
            +
            end
         | 
    
        data/test/test_models.rb
    CHANGED
    
    | @@ -1,29 +1,29 @@ | |
| 1 | 
            -
             | 
| 2 | 
            -
             | 
| 3 | 
            -
             | 
| 4 | 
            -
             | 
| 5 | 
            -
             | 
| 6 | 
            -
             | 
| 7 | 
            -
             | 
| 8 | 
            -
                t.datetime "created_at"
         | 
| 9 | 
            -
                t.datetime "updated_at"
         | 
| 10 | 
            -
              end
         | 
| 1 | 
            +
            ActiveRecord::Base.configurations = YAML::load(IO.read(File.expand_path("database.yml", File.dirname(__FILE__))))
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            conf = ActiveRecord::Base.configurations['test']
         | 
| 4 | 
            +
            `echo "drop DATABASE if exists #{conf['database']}" | mysql --user=#{conf['username']}`
         | 
| 5 | 
            +
            `echo "create DATABASE #{conf['database']}" | mysql --user=#{conf['username']}`
         | 
| 6 | 
            +
            ActiveRecord::Base.establish_connection('test')
         | 
| 7 | 
            +
            load(File.dirname(__FILE__) + "/schema.rb")
         | 
| 11 8 |  | 
| 9 | 
            +
            class Comment < ActiveRecord::Base
         | 
| 12 10 | 
             
              belongs_to :post
         | 
| 11 | 
            +
              has_one :author, :through => :post
         | 
| 13 12 |  | 
| 14 13 | 
             
              has_kasket_on :post_id
         | 
| 15 14 | 
             
            end
         | 
| 16 15 |  | 
| 17 | 
            -
             | 
| 18 | 
            -
               | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 21 | 
            -
             | 
| 22 | 
            -
                t.datetime "updated_at"
         | 
| 23 | 
            -
              end
         | 
| 16 | 
            +
            class Author < ActiveRecord::Base
         | 
| 17 | 
            +
              has_many :posts
         | 
| 18 | 
            +
             | 
| 19 | 
            +
              has_kasket
         | 
| 20 | 
            +
            end
         | 
| 24 21 |  | 
| 22 | 
            +
            class Post < ActiveRecord::Base
         | 
| 25 23 | 
             
              belongs_to :blog
         | 
| 24 | 
            +
              belongs_to :author
         | 
| 26 25 | 
             
              has_many :comments
         | 
| 26 | 
            +
              belongs_to :poly, :polymorphic => true
         | 
| 27 27 |  | 
| 28 28 | 
             
              has_kasket
         | 
| 29 29 | 
             
              has_kasket_on :title
         | 
| @@ -33,15 +33,11 @@ create_model :post do | |
| 33 33 | 
             
                self.updated_at = Time.now
         | 
| 34 34 | 
             
                self.connection.execute("UPDATE posts SET updated_at = '#{updated_at.utc.to_s(:db)}' WHERE id = #{id}")
         | 
| 35 35 | 
             
              end
         | 
| 36 | 
            +
             | 
| 36 37 | 
             
              kasket_dirty_methods :make_dirty!
         | 
| 37 38 | 
             
            end
         | 
| 38 39 |  | 
| 39 | 
            -
             | 
| 40 | 
            -
              with_columns do |t|
         | 
| 41 | 
            -
                t.string   "name"
         | 
| 42 | 
            -
                t.datetime "created_at"
         | 
| 43 | 
            -
                t.datetime "updated_at"
         | 
| 44 | 
            -
              end
         | 
| 45 | 
            -
             | 
| 40 | 
            +
            class Blog < ActiveRecord::Base
         | 
| 46 41 | 
             
              has_many :posts
         | 
| 42 | 
            +
              has_many :comments, :through => :posts
         | 
| 47 43 | 
             
            end
         | 
| @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            require File.expand_path("helper", File.dirname(__FILE__))
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            class VisitorTest < ActiveSupport::TestCase
         | 
| 4 | 
            +
              if arel?
         | 
| 5 | 
            +
                context Kasket::Visitor do
         | 
| 6 | 
            +
                  should "build select id" do
         | 
| 7 | 
            +
                    expected = {
         | 
| 8 | 
            +
                      :attributes=>[[:id, "1"]],
         | 
| 9 | 
            +
                      :from=>"posts",
         | 
| 10 | 
            +
                      :index=>[:id],
         | 
| 11 | 
            +
                      :key=>"kasket-#{Kasket::Version::PROTOCOL}/posts/version=#{POST_VERSION}/id=1"
         | 
| 12 | 
            +
                    }
         | 
| 13 | 
            +
                    assert_equal expected, Post.where(:id => 1).to_kasket_query
         | 
| 14 | 
            +
                  end
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
            end
         | 
    
        metadata
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: kasket
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 1.0 | 
| 4 | 
            +
              version: 2.1.0
         | 
| 5 5 | 
             
              prerelease: 
         | 
| 6 6 | 
             
            platform: ruby
         | 
| 7 7 | 
             
            authors:
         | 
| @@ -10,24 +10,30 @@ authors: | |
| 10 10 | 
             
            autorequire: 
         | 
| 11 11 | 
             
            bindir: bin
         | 
| 12 12 | 
             
            cert_chain: []
         | 
| 13 | 
            -
            date: 2012-11- | 
| 13 | 
            +
            date: 2012-11-20 00:00:00.000000000 Z
         | 
| 14 14 | 
             
            dependencies:
         | 
| 15 15 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 16 16 | 
             
              name: activerecord
         | 
| 17 17 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| 18 18 | 
             
                none: false
         | 
| 19 19 | 
             
                requirements:
         | 
| 20 | 
            -
                - -  | 
| 20 | 
            +
                - - ! '>='
         | 
| 21 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 22 | 
            +
                    version: 2.3.6
         | 
| 23 | 
            +
                - - <
         | 
| 21 24 | 
             
                  - !ruby/object:Gem::Version
         | 
| 22 | 
            -
                    version:  | 
| 25 | 
            +
                    version: '3.3'
         | 
| 23 26 | 
             
              type: :runtime
         | 
| 24 27 | 
             
              prerelease: false
         | 
| 25 28 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 26 29 | 
             
                none: false
         | 
| 27 30 | 
             
                requirements:
         | 
| 28 | 
            -
                - -  | 
| 31 | 
            +
                - - ! '>='
         | 
| 32 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 33 | 
            +
                    version: 2.3.6
         | 
| 34 | 
            +
                - - <
         | 
| 29 35 | 
             
                  - !ruby/object:Gem::Version
         | 
| 30 | 
            -
                    version:  | 
| 36 | 
            +
                    version: '3.3'
         | 
| 31 37 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 32 38 | 
             
              name: rake
         | 
| 33 39 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| @@ -61,23 +67,23 @@ dependencies: | |
| 61 67 | 
             
                  - !ruby/object:Gem::Version
         | 
| 62 68 | 
             
                    version: '0'
         | 
| 63 69 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 64 | 
            -
              name:  | 
| 70 | 
            +
              name: appraisal
         | 
| 65 71 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| 66 72 | 
             
                none: false
         | 
| 67 73 | 
             
                requirements:
         | 
| 68 | 
            -
                - -  | 
| 74 | 
            +
                - - ~>
         | 
| 69 75 | 
             
                  - !ruby/object:Gem::Version
         | 
| 70 | 
            -
                    version: '0'
         | 
| 76 | 
            +
                    version: '0.5'
         | 
| 71 77 | 
             
              type: :development
         | 
| 72 78 | 
             
              prerelease: false
         | 
| 73 79 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 74 80 | 
             
                none: false
         | 
| 75 81 | 
             
                requirements:
         | 
| 76 | 
            -
                - -  | 
| 82 | 
            +
                - - ~>
         | 
| 77 83 | 
             
                  - !ruby/object:Gem::Version
         | 
| 78 | 
            -
                    version: '0'
         | 
| 84 | 
            +
                    version: '0.5'
         | 
| 79 85 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 80 | 
            -
              name:  | 
| 86 | 
            +
              name: shoulda
         | 
| 81 87 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| 82 88 | 
             
                none: false
         | 
| 83 89 | 
             
                requirements:
         | 
| @@ -93,13 +99,13 @@ dependencies: | |
| 93 99 | 
             
                  - !ruby/object:Gem::Version
         | 
| 94 100 | 
             
                    version: '0'
         | 
| 95 101 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 96 | 
            -
              name:  | 
| 102 | 
            +
              name: mocha
         | 
| 97 103 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| 98 104 | 
             
                none: false
         | 
| 99 105 | 
             
                requirements:
         | 
| 100 106 | 
             
                - - ~>
         | 
| 101 107 | 
             
                  - !ruby/object:Gem::Version
         | 
| 102 | 
            -
                    version:  | 
| 108 | 
            +
                    version: 0.10.5
         | 
| 103 109 | 
             
              type: :development
         | 
| 104 110 | 
             
              prerelease: false
         | 
| 105 111 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| @@ -107,23 +113,23 @@ dependencies: | |
| 107 113 | 
             
                requirements:
         | 
| 108 114 | 
             
                - - ~>
         | 
| 109 115 | 
             
                  - !ruby/object:Gem::Version
         | 
| 110 | 
            -
                    version:  | 
| 116 | 
            +
                    version: 0.10.5
         | 
| 111 117 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 112 | 
            -
              name:  | 
| 118 | 
            +
              name: test-unit
         | 
| 113 119 | 
             
              requirement: !ruby/object:Gem::Requirement
         | 
| 114 120 | 
             
                none: false
         | 
| 115 121 | 
             
                requirements:
         | 
| 116 | 
            -
                - -  | 
| 122 | 
            +
                - - ~>
         | 
| 117 123 | 
             
                  - !ruby/object:Gem::Version
         | 
| 118 | 
            -
                    version:  | 
| 124 | 
            +
                    version: 2.5.1
         | 
| 119 125 | 
             
              type: :development
         | 
| 120 126 | 
             
              prerelease: false
         | 
| 121 127 | 
             
              version_requirements: !ruby/object:Gem::Requirement
         | 
| 122 128 | 
             
                none: false
         | 
| 123 129 | 
             
                requirements:
         | 
| 124 | 
            -
                - -  | 
| 130 | 
            +
                - - ~>
         | 
| 125 131 | 
             
                  - !ruby/object:Gem::Version
         | 
| 126 | 
            -
                    version:  | 
| 132 | 
            +
                    version: 2.5.1
         | 
| 127 133 | 
             
            description: puts a cap on your queries
         | 
| 128 134 | 
             
            email:
         | 
| 129 135 | 
             
            - mick@zendesk.com
         | 
| @@ -132,13 +138,15 @@ executables: [] | |
| 132 138 | 
             
            extensions: []
         | 
| 133 139 | 
             
            extra_rdoc_files: []
         | 
| 134 140 | 
             
            files:
         | 
| 135 | 
            -
            - lib/kasket/active_record_patches.rb
         | 
| 136 141 | 
             
            - lib/kasket/configuration_mixin.rb
         | 
| 137 142 | 
             
            - lib/kasket/dirty_mixin.rb
         | 
| 138 143 | 
             
            - lib/kasket/query_parser.rb
         | 
| 139 144 | 
             
            - lib/kasket/read_mixin.rb
         | 
| 145 | 
            +
            - lib/kasket/relation_mixin.rb
         | 
| 140 146 | 
             
            - lib/kasket/reload_association_mixin.rb
         | 
| 147 | 
            +
            - lib/kasket/select_manager_mixin.rb
         | 
| 141 148 | 
             
            - lib/kasket/version.rb
         | 
| 149 | 
            +
            - lib/kasket/visitor.rb
         | 
| 142 150 | 
             
            - lib/kasket/write_mixin.rb
         | 
| 143 151 | 
             
            - lib/kasket.rb
         | 
| 144 152 | 
             
            - README.rdoc
         | 
| @@ -149,14 +157,18 @@ files: | |
| 149 157 | 
             
            - test/dirty_test.rb
         | 
| 150 158 | 
             
            - test/find_one_test.rb
         | 
| 151 159 | 
             
            - test/find_some_test.rb
         | 
| 160 | 
            +
            - test/fixtures/authors.yml
         | 
| 152 161 | 
             
            - test/fixtures/blogs.yml
         | 
| 153 162 | 
             
            - test/fixtures/comments.yml
         | 
| 154 163 | 
             
            - test/fixtures/posts.yml
         | 
| 155 164 | 
             
            - test/helper.rb
         | 
| 156 165 | 
             
            - test/parser_test.rb
         | 
| 157 166 | 
             
            - test/read_mixin_test.rb
         | 
| 167 | 
            +
            - test/reload_test.rb
         | 
| 168 | 
            +
            - test/schema.rb
         | 
| 158 169 | 
             
            - test/test_models.rb
         | 
| 159 170 | 
             
            - test/transaction_test.rb
         | 
| 171 | 
            +
            - test/visitor_test.rb
         | 
| 160 172 | 
             
            homepage: http://github.com/staugaard/kasket
         | 
| 161 173 | 
             
            licenses: []
         | 
| 162 174 | 
             
            post_install_message: 
         | 
| @@ -171,13 +183,16 @@ required_ruby_version: !ruby/object:Gem::Requirement | |
| 171 183 | 
             
                  version: '0'
         | 
| 172 184 | 
             
                  segments:
         | 
| 173 185 | 
             
                  - 0
         | 
| 174 | 
            -
                  hash:  | 
| 186 | 
            +
                  hash: 2863500110980526189
         | 
| 175 187 | 
             
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 176 188 | 
             
              none: false
         | 
| 177 189 | 
             
              requirements:
         | 
| 178 190 | 
             
              - - ! '>='
         | 
| 179 191 | 
             
                - !ruby/object:Gem::Version
         | 
| 180 192 | 
             
                  version: '0'
         | 
| 193 | 
            +
                  segments:
         | 
| 194 | 
            +
                  - 0
         | 
| 195 | 
            +
                  hash: 2863500110980526189
         | 
| 181 196 | 
             
            requirements: []
         | 
| 182 197 | 
             
            rubyforge_project: 
         | 
| 183 198 | 
             
            rubygems_version: 1.8.24
         | 
| @@ -192,11 +207,15 @@ test_files: | |
| 192 207 | 
             
            - test/dirty_test.rb
         | 
| 193 208 | 
             
            - test/find_one_test.rb
         | 
| 194 209 | 
             
            - test/find_some_test.rb
         | 
| 210 | 
            +
            - test/fixtures/authors.yml
         | 
| 195 211 | 
             
            - test/fixtures/blogs.yml
         | 
| 196 212 | 
             
            - test/fixtures/comments.yml
         | 
| 197 213 | 
             
            - test/fixtures/posts.yml
         | 
| 198 214 | 
             
            - test/helper.rb
         | 
| 199 215 | 
             
            - test/parser_test.rb
         | 
| 200 216 | 
             
            - test/read_mixin_test.rb
         | 
| 217 | 
            +
            - test/reload_test.rb
         | 
| 218 | 
            +
            - test/schema.rb
         | 
| 201 219 | 
             
            - test/test_models.rb
         | 
| 202 220 | 
             
            - test/transaction_test.rb
         | 
| 221 | 
            +
            - test/visitor_test.rb
         | 
| @@ -1,57 +0,0 @@ | |
| 1 | 
            -
            # -*- encoding: utf-8 -*-
         | 
| 2 | 
            -
            module Kasket
         | 
| 3 | 
            -
              module FixForAssociationAccessorMethods
         | 
| 4 | 
            -
                def association_accessor_methods(reflection, association_proxy_class)
         | 
| 5 | 
            -
                  define_method(reflection.name) do |*params|
         | 
| 6 | 
            -
                    force_reload = params.first unless params.empty?
         | 
| 7 | 
            -
                    association = association_instance_get(reflection.name)
         | 
| 8 | 
            -
             | 
| 9 | 
            -
                    if association.nil? || force_reload
         | 
| 10 | 
            -
                      association = association_proxy_class.new(self, reflection)
         | 
| 11 | 
            -
                      retval = force_reload ? association.reload : association.__send__(:load_target)
         | 
| 12 | 
            -
                      if retval.nil? and association_proxy_class == ActiveRecord::Associations::BelongsToAssociation
         | 
| 13 | 
            -
                        association_instance_set(reflection.name, nil)
         | 
| 14 | 
            -
                        return nil
         | 
| 15 | 
            -
                      end
         | 
| 16 | 
            -
                      association_instance_set(reflection.name, association)
         | 
| 17 | 
            -
                    end
         | 
| 18 | 
            -
             | 
| 19 | 
            -
                    association.target.nil? ? nil : association
         | 
| 20 | 
            -
                  end
         | 
| 21 | 
            -
             | 
| 22 | 
            -
                  define_method("loaded_#{reflection.name}?") do
         | 
| 23 | 
            -
                    association = association_instance_get(reflection.name)
         | 
| 24 | 
            -
                    association && association.loaded?
         | 
| 25 | 
            -
                  end
         | 
| 26 | 
            -
             | 
| 27 | 
            -
                  define_method("#{reflection.name}=") do |new_value|
         | 
| 28 | 
            -
                    association = association_instance_get(reflection.name)
         | 
| 29 | 
            -
             | 
| 30 | 
            -
                    if association.nil? || association.target != new_value
         | 
| 31 | 
            -
                      association = association_proxy_class.new(self, reflection)
         | 
| 32 | 
            -
                    end
         | 
| 33 | 
            -
             | 
| 34 | 
            -
                    if association_proxy_class == ActiveRecord::Associations::HasOneThroughAssociation
         | 
| 35 | 
            -
                      association.create_through_record(new_value)
         | 
| 36 | 
            -
                      if new_record?
         | 
| 37 | 
            -
                        association_instance_set(reflection.name, new_value.nil? ? nil : association)
         | 
| 38 | 
            -
                      else
         | 
| 39 | 
            -
                        self.send(reflection.name, new_value)
         | 
| 40 | 
            -
                      end
         | 
| 41 | 
            -
                    else
         | 
| 42 | 
            -
                      association.replace(new_value)
         | 
| 43 | 
            -
                      association_instance_set(reflection.name, new_value.nil? ? nil : association)
         | 
| 44 | 
            -
                    end
         | 
| 45 | 
            -
                  end
         | 
| 46 | 
            -
             | 
| 47 | 
            -
                  define_method("set_#{reflection.name}_target") do |target|
         | 
| 48 | 
            -
                    return if target.nil? and association_proxy_class == ActiveRecord::Associations::BelongsToAssociation
         | 
| 49 | 
            -
                    association = association_proxy_class.new(self, reflection)
         | 
| 50 | 
            -
                    association.target = target
         | 
| 51 | 
            -
                    association_instance_set(reflection.name, association)
         | 
| 52 | 
            -
                  end
         | 
| 53 | 
            -
                end
         | 
| 54 | 
            -
              end
         | 
| 55 | 
            -
            end
         | 
| 56 | 
            -
             | 
| 57 | 
            -
            ActiveRecord::Base.extend Kasket::FixForAssociationAccessorMethods
         |