kasket 4.8.0 → 4.10.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e5cec0e5e944a74b977fd8e8113983160e1a5075eade70a499e6b16863abfde6
4
- data.tar.gz: d9ee5a1d6a974e998191ad656b4b962b5a7ea74e0327be3944813790bf6e7e8c
3
+ metadata.gz: 5b7ff0fe3b5afa5c01eb36fc7f89bfe94669b98340bb1537546fe6019b0d61db
4
+ data.tar.gz: d69c8e0d2f9303456f5f647867a67496b49d95e8af114f51ec54acf95947d257
5
5
  SHA512:
6
- metadata.gz: 9aca01c0f69b403d2476b5db05b610604899f2a38f450b0c29d8f19b60f5f2e3e67777c172690672ae83a1a4ce14e316c85175989d5dd4df826afdbf6b8e530c
7
- data.tar.gz: 1645f52ca823c5c628d9fa5c1aba45393feffc9ac3f0c0e70e1552582bd55de79df20f8870f72deca67d8e0faae036d5fc5d1fa67b7be80dcf836d86808b7c68
6
+ metadata.gz: 610b4879bebfdd27f4796ce840aac305d30502702befab16623f8c1e7bd316dd7529ab5af4513d05e565b25cb716089e7b71ed34aad8c75099bf5493137c3e31
7
+ data.tar.gz: b6710e8aa232d3105822aec11431a8d3c76618c7d124e3e1773528c3decc3de57617ffa17f6a60425729e694a4cde8e281d74ed0bd7e88fcd3b34e371cae245b
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Kasket [![Build status](https://circleci.com/gh/zendesk/kasket.svg?style=svg)](https://circleci.com/gh/zendesk/kasket)
1
+ # Kasket
2
2
 
3
3
  ### Puts a cap on your queries
4
4
  A caching layer for ActiveRecord (3.x and 4.x)
@@ -6,32 +6,49 @@ module Kasket
6
6
  # SELECT * FROM `users` WHERE (`users`.`id` = 2) LIMIT 1
7
7
  # 'SELECT * FROM \'posts\' WHERE (\'posts\'.\'id\' = 574019247) '
8
8
 
9
- AND = /\s+AND\s+/i
10
- VALUE = /'?(\d+|\?|(?:(?:[^']|''|\\')*))'?/ # Matches: 123, ?, '123', '12''3'
9
+ AND = /\s+AND\s+/i.freeze
10
+ VALUE = /'?(\d+|\?|(?:(?:[^']|''|\\')*))'?/.freeze # Matches: 123, ?, '123', '12''3'
11
11
 
12
12
  def initialize(model_class)
13
13
  @model_class = model_class
14
14
 
15
- @supported_query_pattern = /^select\s+((?:`|")#{@model_class.table_name}(?:`|")\.)?\* from (?:`|")#{@model_class.table_name}(?:`|") where (.*?)(|\s+limit 1)\s*$/i
15
+ @supported_query_pattern = /^select\s+(.+?)\s+from (?:`|")#{@model_class.table_name}(?:`|") where (.*?)(|\s+limit 1)\s*$/i
16
16
 
17
+ @star_pattern = /^((`|")#{@model_class.table_name}\2\.)?\*$/
17
18
  # Matches: `users`.id, `users`.`id`, users.id, id
18
19
  @table_and_column_pattern = /(?:(?:`|")?#{@model_class.table_name}(?:`|")?\.)?(?:`|")?([a-zA-Z]\w*)(?:`|")?/
19
20
  # Matches: KEY = VALUE, (KEY = VALUE), ()(KEY = VALUE))
20
21
  @key_eq_value_pattern = /^[\(\s]*#{@table_and_column_pattern}\s+(=|IN)\s+#{VALUE}[\)\s]*$/
21
22
  end
22
23
 
24
+ ##
25
+ # Parses a SQL query to produce a kasket query
26
+ #
27
+ # @param sql [String] the sql query to parse
28
+ # @return [Hash|nil] the kasket query, or nil if the sql query is not supported
23
29
  def parse(sql)
24
30
  if match = @supported_query_pattern.match(sql)
31
+ select = match[1]
32
+ unless @star_pattern.match? select
33
+ # If we're not selecting all columns using star, then ensure all columns are selected explicitly
34
+ select_columns = select.split(/\s*,\s*/).map do |s|
35
+ break unless column_match = @table_and_column_pattern.match(s)
36
+
37
+ column_match[1]
38
+ end.uniq
39
+ columns = @model_class.column_names
40
+ return unless columns.size == select_columns.size && (columns - select_columns).empty?
41
+ end
25
42
  where = match[2]
26
43
  limit = match[3]
27
44
 
28
45
  query = {}
29
46
  query[:attributes] = sorted_attribute_value_pairs(where)
30
- return nil if query[:attributes].nil?
47
+ return if query[:attributes].nil?
31
48
 
32
- if query[:attributes].size > 1 && query[:attributes].map(&:last).any? {|a| a.is_a?(Array)}
49
+ if query[:attributes].size > 1 && query[:attributes].map(&:last).any?(Array)
33
50
  # this is a query with IN conditions AND other conditions
34
- return nil
51
+ return
35
52
  end
36
53
 
37
54
  query[:index] = query[:attributes].map(&:first)
@@ -8,6 +8,7 @@ module Kasket
8
8
  end
9
9
  end
10
10
 
11
+ # *args can be replaced with (sql, *args) once we stop supporting Rails < 5.2
11
12
  def find_by_sql_with_kasket(*args)
12
13
  sql = args[0]
13
14
 
@@ -32,11 +33,27 @@ module Kasket
32
33
  find_by_sql_with_kasket_on_id_array(query[:key])
33
34
  else
34
35
  if value = Kasket.cache.read(query[:key])
35
- if value.is_a?(Array)
36
+ # Identified a specific edge case where memcached server returns 0x00 binary protocol response with no data
37
+ # when the node is being rebooted which causes the Dalli memcached client to return a TrueClass object instead of nil
38
+ # see: https://github.com/petergoldstein/dalli/blob/31dabf19d3dd94b348a00a59fe5a7b8fa80ce3ad/lib/dalli/server.rb#L520
39
+ # and: https://github.com/petergoldstein/dalli/issues/390
40
+ #
41
+ # The code in this first condition of TrueClass === true will
42
+ # skip the kasket cache for these specific objects and go directly to SQL for retrieval.
43
+ result_set = if value.is_a?(TrueClass)
44
+ find_by_sql_without_kasket(*args)
45
+ elsif value.is_a?(Array)
36
46
  filter_pending_records(find_by_sql_with_kasket_on_id_array(value))
37
47
  else
38
48
  filter_pending_records(Array.wrap(value).collect { |record| instantiate(record.dup) })
39
49
  end
50
+
51
+ payload = {
52
+ record_count: result_set.length,
53
+ class_name: to_s
54
+ }
55
+
56
+ ActiveSupport::Notifications.instrument('instantiation.active_record', payload) { result_set }
40
57
  else
41
58
  store_in_kasket(query[:key], find_by_sql_without_kasket(*args))
42
59
  end
@@ -80,7 +97,7 @@ module Kasket
80
97
  if records.size == 1
81
98
  records.first.store_in_kasket(key)
82
99
  elsif records.empty?
83
- ActiveRecord::Base.logger.info("[KASKET] would have stored an empty resultset") if ActiveRecord::Base.logger
100
+ ActiveRecord::Base.logger.debug("[KASKET] would have stored an empty resultset") if ActiveRecord::Base.logger
84
101
  elsif records.size <= Kasket::CONFIGURATION[:max_collection_size]
85
102
  if records.all?(&:kasket_cacheable?)
86
103
  instance_keys = records.map(&:store_in_kasket)
@@ -1,16 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
  module Kasket
3
3
  module RelationMixin
4
+ # binds can be removed when support for Rails < 5 is removed
4
5
  def to_kasket_query(binds = nil)
5
6
  if arel.is_a?(Arel::SelectManager)
6
7
  if ActiveRecord::VERSION::MAJOR < 5
7
8
  arel.to_kasket_query(klass, (binds || bind_values))
8
- else
9
+ elsif ActiveRecord::VERSION::STRING < '5.2'
9
10
  arel.to_kasket_query(klass, (@values[:where].to_h.values + Array(@values[:limit])))
11
+ else
12
+ arel.to_kasket_query(klass)
10
13
  end
11
14
  end
12
15
  rescue TypeError # unsupported object in ast
13
- return nil
16
+ nil
14
17
  end
15
18
  end
16
19
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  module Kasket
3
3
  module SelectManagerMixin
4
+ # binds can be removed once we stop supporting Rails < 5.2
4
5
  def to_kasket_query(klass, binds = [])
5
6
  begin
6
7
  query = Kasket::Visitor.new(klass, binds).accept(ast)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  module Kasket
3
- VERSION = '4.8.0'
3
+ VERSION = '4.10.0'
4
4
  class Version
5
5
  MAJOR = Kasket::VERSION.split('.')[0]
6
6
  MINOR = Kasket::VERSION.split('.')[1]
@@ -3,6 +3,7 @@ require 'arel'
3
3
 
4
4
  module Kasket
5
5
  class Visitor < Arel::Visitors::Visitor
6
+ # binds can be removed once we stop supporting Rails < 5.2
6
7
  def initialize(model_class, binds)
7
8
  @model_class = model_class
8
9
  @binds = binds.dup
@@ -51,11 +52,18 @@ module Kasket
51
52
  return :unsupported if ActiveRecord::VERSION::MAJOR < 5 ? node.having : node.havings.present?
52
53
  return :unsupported if node.set_quantifier
53
54
  return :unsupported if !node.source || node.source.empty?
54
- return :unsupported if node.projections.size != 1
55
+ return :unsupported if node.projections.empty?
55
56
 
56
57
  select = node.projections[0]
57
58
  select = select.name if select.respond_to?(:name)
58
- return :unsupported if select != '*'
59
+ if select != '*'
60
+ # If we're not selecting all columns using star, then ensure all columns are selected explicitly
61
+ column_names = @model_class.column_names
62
+ return :unsupported unless node.projections.size == column_names.size
63
+
64
+ projection_names = node.projections.map { |p| p.name if p.respond_to?(:name) }.compact
65
+ return unless (column_names - projection_names).empty?
66
+ end
59
67
 
60
68
  parts = [visit(node.source)]
61
69
 
@@ -75,6 +83,7 @@ module Kasket
75
83
  def visit_Arel_Nodes_JoinSource(node, *_)
76
84
  return :unsupported if !node.left || node.right.any?
77
85
  return :unsupported unless node.left.is_a?(Arel::Table)
86
+
78
87
  visit(node.left)
79
88
  end
80
89
 
@@ -85,6 +94,7 @@ module Kasket
85
94
  def visit_Arel_Nodes_And(node, *_)
86
95
  attributes = node.children.map { |child| visit(child) }
87
96
  return :unsupported if attributes.include?(:unsupported)
97
+
88
98
  attributes.sort! { |pair1, pair2| pair1[0].to_s <=> pair2[0].to_s }
89
99
  { attributes: attributes }
90
100
  end
@@ -112,8 +122,12 @@ module Kasket
112
122
  end
113
123
 
114
124
  def literal(node, *_)
115
- if node == '?'
116
- @binds.shift.last.to_s
125
+ if ActiveRecord::VERSION::STRING < '5.2'
126
+ if node == '?'
127
+ @binds.shift.last.to_s
128
+ else
129
+ node.to_s
130
+ end
117
131
  else
118
132
  node.to_s
119
133
  end
@@ -135,11 +149,13 @@ module Kasket
135
149
  node.map {|value| visit(value) }
136
150
  end
137
151
 
138
- def visit_Arel_Nodes_Casted(node, *_)
139
- case node.val
140
- when nil then nil
141
- when String then node.val
142
- else quoted(node.val)
152
+ if ActiveRecord::VERSION::STRING < '5.2'
153
+ def visit_Arel_Nodes_Casted(node, *_)
154
+ case node.val
155
+ when nil then nil
156
+ when String then node.val
157
+ else quoted(node.val)
158
+ end
143
159
  end
144
160
  end
145
161
 
data/lib/kasket.rb CHANGED
@@ -17,8 +17,8 @@ module Kasket
17
17
 
18
18
  CONFIGURATION = { # rubocop:disable Style/MutableConstant
19
19
  max_collection_size: 100,
20
- write_through: false,
21
- default_expires_in: nil
20
+ write_through: false,
21
+ default_expires_in: nil
22
22
  }
23
23
 
24
24
  module_function
@@ -33,8 +33,8 @@ module Kasket
33
33
  ActiveRecord::Base.extend(Kasket::ConfigurationMixin)
34
34
 
35
35
  if defined?(ActiveRecord::Relation)
36
- ActiveRecord::Relation.send(:include, Kasket::RelationMixin)
37
- Arel::SelectManager.send(:include, Kasket::SelectManagerMixin)
36
+ ActiveRecord::Relation.include Kasket::RelationMixin
37
+ Arel::SelectManager.include Kasket::SelectManagerMixin
38
38
  end
39
39
  end
40
40
 
@@ -42,10 +42,15 @@ module Kasket
42
42
  @cache_store = ActiveSupport::Cache.lookup_store(options)
43
43
  end
44
44
 
45
- def self.cache
45
+ def self.cache_store
46
46
  @cache_store ||= Rails.cache
47
47
  end
48
48
 
49
+ # Alias cache_store to cache
50
+ class << self
51
+ alias_method :cache, :cache_store
52
+ end
53
+
49
54
  # Keys are the records being saved.
50
55
  # Values are either the saved record, or nil if the record has been destroyed.
51
56
  def self.pending_records
metadata CHANGED
@@ -1,15 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kasket
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.8.0
4
+ version: 4.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mick Staugaard
8
8
  - Eric Chapweske
9
- autorequire:
9
+ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2020-08-26 00:00:00.000000000 Z
12
+ date: 2022-04-05 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -31,118 +31,6 @@ dependencies:
31
31
  - - "<"
32
32
  - !ruby/object:Gem::Version
33
33
  version: '6.1'
34
- - !ruby/object:Gem::Dependency
35
- name: rake
36
- requirement: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
- type: :development
42
- prerelease: false
43
- version_requirements: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '0'
48
- - !ruby/object:Gem::Dependency
49
- name: bundler
50
- requirement: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: '0'
55
- type: :development
56
- prerelease: false
57
- version_requirements: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - ">="
60
- - !ruby/object:Gem::Version
61
- version: '0'
62
- - !ruby/object:Gem::Dependency
63
- name: mocha
64
- requirement: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - ">="
67
- - !ruby/object:Gem::Version
68
- version: '0'
69
- type: :development
70
- prerelease: false
71
- version_requirements: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- - !ruby/object:Gem::Dependency
77
- name: wwtd
78
- requirement: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
83
- type: :development
84
- prerelease: false
85
- version_requirements: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - ">="
88
- - !ruby/object:Gem::Version
89
- version: '0'
90
- - !ruby/object:Gem::Dependency
91
- name: bump
92
- requirement: !ruby/object:Gem::Requirement
93
- requirements:
94
- - - ">="
95
- - !ruby/object:Gem::Version
96
- version: '0'
97
- type: :development
98
- prerelease: false
99
- version_requirements: !ruby/object:Gem::Requirement
100
- requirements:
101
- - - ">="
102
- - !ruby/object:Gem::Version
103
- version: '0'
104
- - !ruby/object:Gem::Dependency
105
- name: minitest
106
- requirement: !ruby/object:Gem::Requirement
107
- requirements:
108
- - - ">="
109
- - !ruby/object:Gem::Version
110
- version: '0'
111
- type: :development
112
- prerelease: false
113
- version_requirements: !ruby/object:Gem::Requirement
114
- requirements:
115
- - - ">="
116
- - !ruby/object:Gem::Version
117
- version: '0'
118
- - !ruby/object:Gem::Dependency
119
- name: minitest-rg
120
- requirement: !ruby/object:Gem::Requirement
121
- requirements:
122
- - - ">="
123
- - !ruby/object:Gem::Version
124
- version: '0'
125
- type: :development
126
- prerelease: false
127
- version_requirements: !ruby/object:Gem::Requirement
128
- requirements:
129
- - - ">="
130
- - !ruby/object:Gem::Version
131
- version: '0'
132
- - !ruby/object:Gem::Dependency
133
- name: timecop
134
- requirement: !ruby/object:Gem::Requirement
135
- requirements:
136
- - - ">="
137
- - !ruby/object:Gem::Version
138
- version: '0'
139
- type: :development
140
- prerelease: false
141
- version_requirements: !ruby/object:Gem::Requirement
142
- requirements:
143
- - - ">="
144
- - !ruby/object:Gem::Version
145
- version: '0'
146
34
  description: puts a cap on your queries
147
35
  email:
148
36
  - mick@zendesk.com
@@ -165,7 +53,7 @@ homepage: http://github.com/zendesk/kasket
165
53
  licenses:
166
54
  - Apache License Version 2.0
167
55
  metadata: {}
168
- post_install_message:
56
+ post_install_message:
169
57
  rdoc_options: []
170
58
  require_paths:
171
59
  - lib
@@ -173,15 +61,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
173
61
  requirements:
174
62
  - - ">="
175
63
  - !ruby/object:Gem::Version
176
- version: 2.3.0
64
+ version: 2.5.0
177
65
  required_rubygems_version: !ruby/object:Gem::Requirement
178
66
  requirements:
179
67
  - - ">="
180
68
  - !ruby/object:Gem::Version
181
69
  version: '0'
182
70
  requirements: []
183
- rubygems_version: 3.1.4
184
- signing_key:
71
+ rubygems_version: 3.0.3.1
72
+ signing_key:
185
73
  specification_version: 4
186
74
  summary: A write back caching layer on active record
187
75
  test_files: []