kasket 1.0.4 → 2.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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