kasket 4.8.0 → 4.10.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.
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: []