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.
@@ -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
 
@@ -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 :ConfigurationMixin, 'kasket/configuration_mixin'
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, 'kasket/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.extended_by.member?(Kasket::ConfigurationMixin)
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::Associations::BelongsToPolymorphicAssociation.send(:include, Kasket::ReloadAssociationMixin)
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::STRING}/#{table_name}/version=#{column_names.join.sum}/"
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)
@@ -11,15 +11,26 @@ module Kasket
11
11
 
12
12
  def initialize(model_class)
13
13
  @model_class = model_class
14
- @supported_query_pattern = /^select \* from (?:`|")#{@model_class.table_name}(?:`|") where \((.*)\)(|\s+limit 1)\s*$/i
15
- @table_and_column_pattern = /(?:(?:`|")?#{@model_class.table_name}(?:`|")?\.)?(?:`|")?(\D\w*)(?:`|")?/ # Matches: `users`.id, `users`.`id`, users.id, id
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(match[1])
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] = match[2].blank? ? nil : 1
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
@@ -8,8 +8,17 @@ module Kasket
8
8
  end
9
9
  end
10
10
 
11
- def find_by_sql_with_kasket(sql)
12
- query = kasket_parser.parse(sanitize_sql(sql)) if use_kasket?
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(sql))
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(sql)
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
- key_value_map = Kasket.cache.read_multi(*keys)
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
- if missing_ids.any?
45
- without_kasket do
46
- find_all_by_id(missing_ids).each do |instance|
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
- key_value_map.values.compact
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?
@@ -0,0 +1,9 @@
1
+ module Kasket
2
+ module RelationMixin
3
+ def to_kasket_query(binds = [])
4
+ if arel.is_a?(Arel::SelectManager)
5
+ arel.to_kasket_query(klass, binds)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -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
- target_class = proxy_reflection.options[:polymorphic] ? association_class : proxy_reflection.klass
10
- Kasket.clear_local if target_class.include?(WriteMixin)
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
@@ -1,8 +1,10 @@
1
+ # -*- encoding: utf-8 -*-
1
2
  module Kasket
2
3
  class Version
3
- MAJOR = 1
4
- MINOR = 0
5
- PATCH = 4
4
+ MAJOR = 2
5
+ MINOR = 1
6
+ PATCH = 0
6
7
  STRING = "#{MAJOR}.#{MINOR}.#{PATCH}"
8
+ PROTOCOL = 3
7
9
  end
8
10
  end
@@ -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::STRING}/posts/version=3558/blog_id=null"
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::STRING}/posts/version=3558/title='this'"
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
@@ -4,6 +4,4 @@ test:
4
4
  database: kasket_test
5
5
  username: root
6
6
  password:
7
- socket: /tmp/mysql.sock
8
-
9
-
7
+ host: 127.0.0.1
@@ -0,0 +1,5 @@
1
+ mick:
2
+ name: Mick Staugaard
3
+
4
+ eric:
5
+ name: Eric Chapweske
@@ -1,12 +1,14 @@
1
1
  few_comments_1:
2
- post: has_two_comments
2
+ id: 1
3
+ post_id: 2
3
4
  body: what ever body 1
4
5
 
5
6
  few_comments_2:
6
- post: has_two_comments
7
+ id: 2
8
+ post_id: 2
7
9
  body: what ever body 2
8
10
 
9
- <% (1..10000).each do |i| %>
11
+ <% (1..10).each do |i| %>
10
12
  many_comments_<%= i %>:
11
13
  post: has_many_comments
12
14
  body: what ever body <%= i %>
@@ -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
@@ -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
@@ -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
- kasket_query = @parser.parse('SELECT * FROM `posts` WHERE (0 = 1)')
13
- assert(!kasket_query)
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
- parsed_query = @parser.parse('SELECT * FROM `posts` WHERE (`posts`.`id` IN (1,2,3) AND `posts`.`is_active` = 1)')
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
- assert_equal [[:blog_id, "big"], [:title, "red"]], @parser.parse('SELECT * FROM `posts` WHERE (`posts`.`title` = red AND `posts`.`blog_id` = big)')[:attributes]
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], @parser.parse('SELECT * FROM `posts` WHERE (`posts`.`title` = red AND `posts`.`blog_id` = big)')[:index]
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 !@parser.parse('SELECT * FROM `apples` WHERE (`users`.`id` = 2) ')
60
+ assert !parse(:conditions => {'users.id' => 2}, :from => 'apples')
31
61
  end
32
62
 
33
63
  should "support cachable queries" do
34
- assert @parser.parse('SELECT * FROM `posts` WHERE (`posts`.`id` = 2) ')
35
- assert @parser.parse('SELECT * FROM `posts` WHERE (`posts`.`id` = 2) LIMIT 1')
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
- parsed_query = @parser.parse('SELECT * FROM `posts` WHERE (`posts`.`id` IN (1,2,3))')
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(!@parser.parse('SELECT * FROM `posts` WHERE (`posts`.`hest` IN (1,2,3))'))
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
- sql = 'SELECT * FROM `posts` WHERE (`posts`.`id` = 2)'
56
- assert_equal nil, @parser.parse(sql)[:limit]
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 !@parser.parse('SELECT * FROM `posts` WHERE (title = red AND blog_id = big) LIMIT 2')
89
+ assert !parse(:conditions => {:title => 'red', :blog_id => 1}, :limit => 2)
68
90
  end
69
91
 
70
92
  should "include joins" do
71
- assert !@parser.parse('SELECT * FROM `posts`, `trees` JOIN ON apple.tree_id = tree.id WHERE (title = red)')
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 !@parser.parse('SELECT id FROM `posts` WHERE (title = red)')
98
+ assert !parse(:conditions => {:title => 'red'}, :select => :id)
76
99
  end
77
100
 
78
101
  should "include offset" do
79
- assert !@parser.parse('SELECT * FROM `posts` WHERE (title = red) LIMIT 1 OFFSET 2')
102
+ assert !parse(:conditions => {:title => 'red'}, :limit => 1, :offset => 2)
80
103
  end
81
104
 
82
105
  should "include order" do
83
- assert !@parser.parse('SELECT * FROM `posts` WHERE (title = red) ORDER DESC')
106
+ assert !parse(:conditions => {:title => 'red'}, :order => :title)
84
107
  end
85
108
 
86
109
  should "include the OR operator" do
87
- assert !@parser.parse('SELECT * FROM `posts` WHERE (title = red OR blog_id = big) LIMIT 2')
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
- assert_match(/^kasket-#{Kasket::Version::STRING}\/posts\/version=3558\//, @parser.parse('SELECT * FROM `posts` WHERE (id = 1)')[:key])
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
- assert_match(/id=1$/, @parser.parse('SELECT * FROM `posts` WHERE (id = 1)')[:key])
98
- assert_match(/blog_id=2\/id=1$/, @parser.parse('SELECT * FROM `posts` WHERE (id = 1 AND blog_id = 2)')[:key])
99
- assert_match(/id=1\/title='title'$/, @parser.parse("SELECT * FROM `posts` WHERE (id = 1 AND title = 'title')")[:key])
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 = @parser.parse('SELECT * FROM `posts` WHERE (id IN (1,2))')[:key]
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$/, @parser.parse("SELECT * FROM `posts` WHERE (title = 'a') LIMIT 1")[:key])
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$/, @parser.parse("SELECT * FROM `posts` WHERE (id = 1) LIMIT 1")[:key])
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
@@ -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::STRING}/posts/version=3558/id=1", @post_database_result)
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::STRING}/posts/version=3558/id=1")
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::STRING}/comments/version=3476/post_id=1")
36
- assert_equal(["kasket-#{Kasket::Version::STRING}/comments/version=3476/id=1", "kasket-#{Kasket::Version::STRING}/comments/version=3476/id=2"], stored_value)
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::STRING}/posts/version=3558/id=1", @post_database_result)
47
- @record = Post.find_by_sql('SELECT * FROM `posts` WHERE (id = 1)').first
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('SELECT * FROM `posts` WHERE (id = 1)').first
59
+ same_record = Post.find_by_sql(@sql).first
53
60
 
54
61
  assert_not_equal @record, same_record
55
62
  end
@@ -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
@@ -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
@@ -1,29 +1,29 @@
1
- require 'temping'
2
- include Temping
3
-
4
- create_model :comment do
5
- with_columns do |t|
6
- t.text "body"
7
- t.integer "post_id"
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
- create_model :post do
18
- with_columns do |t|
19
- t.string "title"
20
- t.integer "blog_id"
21
- t.datetime "created_at"
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
- create_model :blog do
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
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-19 00:00:00.000000000 Z
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: 2.3.4
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: 2.3.4
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: shoulda
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: mocha
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: temping
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: 1.3.0
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: 1.3.0
116
+ version: 0.10.5
111
117
  - !ruby/object:Gem::Dependency
112
- name: sqlite3
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: '0'
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: '0'
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: 4530626145164262218
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