kasket 0.7.8 → 0.8.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/.gitignore CHANGED
@@ -17,7 +17,6 @@ tmtags
17
17
  coverage
18
18
  rdoc
19
19
  pkg
20
- *.gemspec
21
20
 
22
21
  ## PROJECT::SPECIFIC
23
22
  test/debug.log
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), 'lib'))
2
+
1
3
  require 'rubygems'
2
4
  require 'rake'
3
5
  require 'lib/kasket'
data/lib/kasket.rb CHANGED
@@ -13,8 +13,8 @@ module Kasket
13
13
 
14
14
  class Version
15
15
  MAJOR = 0
16
- MINOR = 7
17
- PATCH = 8
16
+ MINOR = 8
17
+ PATCH = 0
18
18
  STRING = "#{MAJOR}.#{MINOR}.#{PATCH}"
19
19
  end
20
20
 
@@ -30,17 +30,21 @@ module Kasket
30
30
  end
31
31
 
32
32
  def kasket_key_for(attribute_value_pairs)
33
- key = attribute_value_pairs.map do |attribute, value|
34
- column = columns_hash[attribute.to_s]
35
- value = nil if value.blank?
36
- attribute.to_s + '=' + connection.quote(column.type_cast(value), column)
37
- end.join('/')
38
-
39
- if key.size > (250 - kasket_key_prefix.size) || key =~ /\s/
40
- key = Digest::MD5.hexdigest(key)
33
+ if attribute_value_pairs.size == 1 && attribute_value_pairs[0][0] == :id && attribute_value_pairs[0][1].is_a?(Array)
34
+ attribute_value_pairs[0][1].map {|id| kasket_key_for_id(id)}
35
+ else
36
+ key = attribute_value_pairs.map do |attribute, value|
37
+ column = columns_hash[attribute.to_s]
38
+ value = nil if value.blank?
39
+ attribute.to_s + '=' + connection.quote(column.type_cast(value), column)
40
+ end.join('/')
41
+
42
+ if key.size > (250 - kasket_key_prefix.size) || key =~ /\s/
43
+ key = Digest::MD5.hexdigest(key)
44
+ end
45
+
46
+ kasket_key_prefix + key
41
47
  end
42
-
43
- kasket_key_prefix + key
44
48
  end
45
49
 
46
50
  def kasket_key_for_id(id)
@@ -12,7 +12,7 @@ module Kasket
12
12
  @model_class = model_class
13
13
  @supported_query_pattern = /^select \* from (?:`|")#{@model_class.table_name}(?:`|") where \((.*)\)(|\s+limit 1)\s*$/i
14
14
  @table_and_column_pattern = /(?:(?:`|")?#{@model_class.table_name}(?:`|")?\.)?(?:`|")?(\w+)(?:`|")?/ # Matches: `users`.id, `users`.`id`, users.id, id
15
- @key_eq_value_pattern = /^[\(\s]*#{@table_and_column_pattern}\s+=\s+#{VALUE}[\)\s]*$/ # Matches: KEY = VALUE, (KEY = VALUE), ()(KEY = VALUE))
15
+ @key_eq_value_pattern = /^[\(\s]*#{@table_and_column_pattern}\s+(=|IN)\s+#{VALUE}[\)\s]*$/ # Matches: KEY = VALUE, (KEY = VALUE), ()(KEY = VALUE))
16
16
  end
17
17
 
18
18
  def parse(sql)
@@ -39,10 +39,19 @@ module Kasket
39
39
  def parse_condition(conditions = '', *values)
40
40
  values = values.dup
41
41
  conditions.split(AND).inject([]) do |pairs, condition|
42
- matched, column_name, sql_value = *(@key_eq_value_pattern.match(condition))
42
+ matched, column_name, operator, sql_value = *(@key_eq_value_pattern.match(condition))
43
43
  if matched
44
- value = sql_value == '?' ? values.shift : sql_value
45
- pairs << [column_name.to_sym, value.gsub(/''|\\'/, "'")]
44
+ if operator == 'IN'
45
+ if column_name == 'id'
46
+ values = sql_value[1..(-2)].split(',').map(&:strip)
47
+ pairs << [column_name.to_sym, values]
48
+ else
49
+ return nil
50
+ end
51
+ else
52
+ value = sql_value == '?' ? values.shift : sql_value
53
+ pairs << [column_name.to_sym, value.gsub(/''|\\'/, "'")]
54
+ end
46
55
  else
47
56
  return nil
48
57
  end
@@ -8,20 +8,44 @@ module Kasket
8
8
  end
9
9
 
10
10
  def find_by_sql_with_kasket(sql)
11
- sql = sanitize_sql(sql)
12
- query = kasket_parser.parse(sql) if use_kasket?
11
+ query = kasket_parser.parse(sanitize_sql(sql)) if use_kasket?
13
12
  if query && has_kasket_index_on?(query[:index])
14
-
15
- if value = Rails.cache.read(query[:key])
16
- Array.wrap(value).collect { |record| instantiate(record.dup) }
13
+ if query[:key].is_a?(Array)
14
+ find_by_sql_with_kasket_on_id_array(sql, query)
17
15
  else
18
- store_in_kasket(query[:key], find_by_sql_without_kasket(sql))
16
+ if value = Rails.cache.read(query[:key])
17
+ Array.wrap(value).collect { |record| instantiate(record.dup) }
18
+ else
19
+ store_in_kasket(query[:key], find_by_sql_without_kasket(sql))
20
+ end
19
21
  end
20
22
  else
21
23
  find_by_sql_without_kasket(sql)
22
24
  end
23
25
  end
24
26
 
27
+ def find_by_sql_with_kasket_on_id_array(sql, query)
28
+ key_value_map = Rails.cache.read_multi(*query[:key])
29
+ missing_ids = []
30
+
31
+ key_value_map.each do |key, value|
32
+ if value.nil?
33
+ missing_ids << key.split('=').last.to_i
34
+ else
35
+ key_value_map[key] = instantiate(value.dup)
36
+ end
37
+ end
38
+
39
+ without_kasket do
40
+ find(missing_ids).each do |instance|
41
+ instance.store_in_kasket
42
+ key_value_map[instance.kasket_key] = instance
43
+ end
44
+ end
45
+
46
+ key_value_map.values
47
+ end
48
+
25
49
  protected
26
50
 
27
51
  def store_in_kasket(key, records)
@@ -33,7 +57,8 @@ module Kasket
33
57
  Rails.cache.write(key, record.instance_variable_get(:@attributes).dup)
34
58
  key
35
59
  end
36
- Rails.cache.write(key, keys)
60
+
61
+ Rails.cache.write(key, keys) if key.is_a?(String)
37
62
  end
38
63
  records
39
64
  end
@@ -3,7 +3,7 @@ require File.dirname(__FILE__) + '/helper'
3
3
  class FindSomeTest < ActiveSupport::TestCase
4
4
  fixtures :blogs, :posts
5
5
 
6
- should_eventually "cache find(id, id) calls" do
6
+ should "cache find(id, id) calls" do
7
7
  post1 = Post.first
8
8
  post2 = Post.last
9
9
 
@@ -18,14 +18,14 @@ class FindSomeTest < ActiveSupport::TestCase
18
18
  Post.find(post1.id, post2.id)
19
19
  end
20
20
 
21
- should_eventually "only lookup the records that are not in the cache" do
21
+ should "only lookup the records that are not in the cache" do
22
22
  post1 = Post.first
23
23
  post2 = Post.last
24
24
  assert_equal(post1, Post.find(post1.id))
25
25
  assert(Rails.cache.read(post1.kasket_key))
26
26
  assert_nil(Rails.cache.read(post2.kasket_key))
27
27
 
28
- Post.expects(:find_by_sql_without_kasket).with("SELECT * FROM \'posts\' WHERE (\'posts\'.\'id\' IN (#{post1.id},#{post2.id})) ").returns([post2])
28
+ Post.expects(:find_by_sql_without_kasket).with("SELECT * FROM \"posts\" WHERE (\"posts\".\"id\" = #{post2.id}) ").returns([post2])
29
29
  found_posts = Post.find(post1.id, post2.id)
30
30
  assert_equal([post1, post2].map(&:id).sort, found_posts.map(&:id).sort)
31
31
 
@@ -33,13 +33,4 @@ class FindSomeTest < ActiveSupport::TestCase
33
33
  found_posts = Post.find(post1.id, post2.id)
34
34
  assert_equal([post1, post2].map(&:id).sort, found_posts.map(&:id).sort)
35
35
  end
36
-
37
- should_eventually "cache on index other than primary key" do
38
- blog = blogs(:a_blog)
39
- posts = Post.find_all_by_blog_id(blog.id)
40
-
41
- Post.expects(:find_by_sql_without_kasket).never
42
-
43
- assert_equal(posts, Post.find_all_by_blog_id(blog.id))
44
- end
45
36
  end
data/test/helper.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'rubygems'
2
+ require 'ruby-debug'
2
3
  require 'test/unit'
3
4
  require 'mocha'
4
5
  require 'shoulda'
data/test/parser_test.rb CHANGED
@@ -25,6 +25,16 @@ class ParserTest < ActiveSupport::TestCase
25
25
  assert @parser.parse('SELECT * FROM `posts` WHERE (`posts`.`id` = 2) LIMIT 1')
26
26
  end
27
27
 
28
+ should "support IN queries on id" do
29
+ parsed_query = @parser.parse('SELECT * FROM `posts` WHERE (`posts`.`id` IN (1,2,3))')
30
+ assert(parsed_query)
31
+ assert_equal([[:id, ['1', '2', '3']]], parsed_query[:attributes])
32
+ end
33
+
34
+ should "not support IN queries on other attributes" do
35
+ assert(!@parser.parse('SELECT * FROM `posts` WHERE (`posts`.`hest` IN (1,2,3))'))
36
+ end
37
+
28
38
  should "support vaguely formatted queries" do
29
39
  assert @parser.parse('SELECT * FROM "posts" WHERE (title = red AND blog_id = big)')
30
40
  end
@@ -66,11 +76,6 @@ class ParserTest < ActiveSupport::TestCase
66
76
  should "include the OR operator" do
67
77
  assert !@parser.parse('SELECT * FROM `posts` WHERE (title = red OR blog_id = big) LIMIT 2')
68
78
  end
69
-
70
- should "include the IN operator" do
71
- assert !@parser.parse('SELECT * FROM `posts` WHERE (id IN (1,2,3))')
72
- end
73
-
74
79
  end
75
80
 
76
81
  context "key generation" do
@@ -84,6 +89,13 @@ class ParserTest < ActiveSupport::TestCase
84
89
  assert_match(/id=1\/title='title'$/, @parser.parse("SELECT * FROM `posts` WHERE (id = 1 AND title = 'title')")[:key])
85
90
  end
86
91
 
92
+ should "generate multiple keys on IN queries" do
93
+ keys = @parser.parse('SELECT * FROM `posts` WHERE (id IN (1,2))')[:key]
94
+ assert_instance_of(Array, keys)
95
+ assert_match(/id=1$/, keys[0])
96
+ assert_match(/id=2$/, keys[1])
97
+ end
98
+
87
99
  context "when limit 1" do
88
100
  should "add /first to the key if the index does not include id" do
89
101
  assert_match(/title='a'\/first$/, @parser.parse("SELECT * FROM `posts` WHERE (title = 'a') LIMIT 1")[:key])
metadata CHANGED
@@ -1,12 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kasket
3
3
  version: !ruby/object:Gem::Version
4
+ hash: 63
4
5
  prerelease: false
5
6
  segments:
6
7
  - 0
7
- - 7
8
8
  - 8
9
- version: 0.7.8
9
+ - 0
10
+ version: 0.8.0
10
11
  platform: ruby
11
12
  authors:
12
13
  - Mick Staugaard
@@ -15,16 +16,18 @@ autorequire:
15
16
  bindir: bin
16
17
  cert_chain: []
17
18
 
18
- date: 2010-07-21 00:00:00 -07:00
19
+ date: 2010-08-24 00:00:00 -07:00
19
20
  default_executable:
20
21
  dependencies:
21
22
  - !ruby/object:Gem::Dependency
22
23
  name: activerecord
23
24
  prerelease: false
24
25
  requirement: &id001 !ruby/object:Gem::Requirement
26
+ none: false
25
27
  requirements:
26
28
  - - ">="
27
29
  - !ruby/object:Gem::Version
30
+ hash: 11
28
31
  segments:
29
32
  - 2
30
33
  - 3
@@ -36,9 +39,11 @@ dependencies:
36
39
  name: activesupport
37
40
  prerelease: false
38
41
  requirement: &id002 !ruby/object:Gem::Requirement
42
+ none: false
39
43
  requirements:
40
44
  - - ">="
41
45
  - !ruby/object:Gem::Version
46
+ hash: 11
42
47
  segments:
43
48
  - 2
44
49
  - 3
@@ -50,9 +55,11 @@ dependencies:
50
55
  name: shoulda
51
56
  prerelease: false
52
57
  requirement: &id003 !ruby/object:Gem::Requirement
58
+ none: false
53
59
  requirements:
54
60
  - - ">="
55
61
  - !ruby/object:Gem::Version
62
+ hash: 3
56
63
  segments:
57
64
  - 0
58
65
  version: "0"
@@ -62,9 +69,11 @@ dependencies:
62
69
  name: mocha
63
70
  prerelease: false
64
71
  requirement: &id004 !ruby/object:Gem::Requirement
72
+ none: false
65
73
  requirements:
66
74
  - - ">="
67
75
  - !ruby/object:Gem::Version
76
+ hash: 3
68
77
  segments:
69
78
  - 0
70
79
  version: "0"
@@ -74,9 +83,11 @@ dependencies:
74
83
  name: temping
75
84
  prerelease: false
76
85
  requirement: &id005 !ruby/object:Gem::Requirement
86
+ none: false
77
87
  requirements:
78
88
  - - ">="
79
89
  - !ruby/object:Gem::Version
90
+ hash: 27
80
91
  segments:
81
92
  - 1
82
93
  - 3
@@ -129,23 +140,27 @@ rdoc_options:
129
140
  require_paths:
130
141
  - lib
131
142
  required_ruby_version: !ruby/object:Gem::Requirement
143
+ none: false
132
144
  requirements:
133
145
  - - ">="
134
146
  - !ruby/object:Gem::Version
147
+ hash: 3
135
148
  segments:
136
149
  - 0
137
150
  version: "0"
138
151
  required_rubygems_version: !ruby/object:Gem::Requirement
152
+ none: false
139
153
  requirements:
140
154
  - - ">="
141
155
  - !ruby/object:Gem::Version
156
+ hash: 3
142
157
  segments:
143
158
  - 0
144
159
  version: "0"
145
160
  requirements: []
146
161
 
147
162
  rubyforge_project:
148
- rubygems_version: 1.3.6
163
+ rubygems_version: 1.3.7
149
164
  signing_key:
150
165
  specification_version: 3
151
166
  summary: A write back caching layer on active record