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 +4 -4
- data/README.md +1 -1
- data/lib/kasket/query_parser.rb +23 -6
- data/lib/kasket/read_mixin.rb +19 -2
- data/lib/kasket/relation_mixin.rb +5 -2
- data/lib/kasket/select_manager_mixin.rb +1 -0
- data/lib/kasket/version.rb +1 -1
- data/lib/kasket/visitor.rb +25 -9
- data/lib/kasket.rb +10 -5
- metadata +7 -119
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5b7ff0fe3b5afa5c01eb36fc7f89bfe94669b98340bb1537546fe6019b0d61db
|
4
|
+
data.tar.gz: d69c8e0d2f9303456f5f647867a67496b49d95e8af114f51ec54acf95947d257
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 610b4879bebfdd27f4796ce840aac305d30502702befab16623f8c1e7bd316dd7529ab5af4513d05e565b25cb716089e7b71ed34aad8c75099bf5493137c3e31
|
7
|
+
data.tar.gz: b6710e8aa232d3105822aec11431a8d3c76618c7d124e3e1773528c3decc3de57617ffa17f6a60425729e694a4cde8e281d74ed0bd7e88fcd3b34e371cae245b
|
data/README.md
CHANGED
data/lib/kasket/query_parser.rb
CHANGED
@@ -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+|\?|(?:(?:[^']|''|\\')*))'
|
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+(
|
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
|
47
|
+
return if query[:attributes].nil?
|
31
48
|
|
32
|
-
if query[:attributes].size > 1 && query[:attributes].map(&:last).any?
|
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
|
51
|
+
return
|
35
52
|
end
|
36
53
|
|
37
54
|
query[:index] = query[:attributes].map(&:first)
|
data/lib/kasket/read_mixin.rb
CHANGED
@@ -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
|
-
|
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.
|
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
|
-
|
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
|
-
|
16
|
+
nil
|
14
17
|
end
|
15
18
|
end
|
16
19
|
end
|
data/lib/kasket/version.rb
CHANGED
data/lib/kasket/visitor.rb
CHANGED
@@ -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.
|
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
|
-
|
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
|
116
|
-
|
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
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
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:
|
21
|
-
default_expires_in:
|
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.
|
37
|
-
Arel::SelectManager.
|
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.
|
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.
|
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:
|
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.
|
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
|
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: []
|