ar-query-matchers 0.1.0 → 0.5.1
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/CHANGELOG.md +40 -0
- data/LICENSE.txt +21 -0
- data/README.md +34 -10
- data/lib/ar_query_matchers.rb +10 -3
- data/lib/ar_query_matchers/queries/query_counter.rb +4 -3
- metadata +43 -26
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 9c028b70b3fbf9ea3e703744480dfb98f6a1ef9b6ab2e90fc4c05879cb2eecff
|
4
|
+
data.tar.gz: c1191c7af4628af475e2e180ec3dd0233e80d69c0d5991d8278292fed6e1aec6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f220e3dff11f0b978b6de20314646783814fadfc7cc15ad51d59236a76def60e98f77fe5495217f572046fffbd15a568b0f5827c56874a1b8dd71ec4c3822e04
|
7
|
+
data.tar.gz: 10ee5a3e0c6a39836d59649a0df91eb64f5967e8cc5df0d874755e33f1894d6f251f3f9e82bbc4395e97b67a1c820aaab929c3df05226e4d9c52e91fc133ddf7
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
# Changelog
|
2
|
+
All notable changes to this project will be documented in this file.
|
3
|
+
|
4
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
5
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
6
|
+
|
7
|
+
## [Unreleased]
|
8
|
+
|
9
|
+
## [0.5.1] - 2020-11-19
|
10
|
+
### Changed
|
11
|
+
- Removes zero count expectations from hash before comparing
|
12
|
+
|
13
|
+
## [0.5.0] - 2020-07-23
|
14
|
+
### Changed
|
15
|
+
- Add time information to query counter
|
16
|
+
|
17
|
+
## [0.4.0] - 2020-07-20
|
18
|
+
### Changed
|
19
|
+
- Upgrade the Rails dependency to allow for Rails 6.1
|
20
|
+
|
21
|
+
## [0.3.0] - 2020-03-13
|
22
|
+
### Changed
|
23
|
+
- Correct the Rails dependency to allow for Rails 6.0
|
24
|
+
|
25
|
+
## [0.2.0] - 2019-09-15
|
26
|
+
### Changed
|
27
|
+
- Package the CHANGELOG and README in the gem.
|
28
|
+
- Add additional gemspec metadata
|
29
|
+
|
30
|
+
## [0.1.0] - 2019-09-14
|
31
|
+
### Added
|
32
|
+
- First versions as a public ruby gem.
|
33
|
+
|
34
|
+
[Unreleased]: https://github.com/gusto/ar-query-matchers/compare/v0.5.1...HEAD
|
35
|
+
[0.5.1]: https://github.com/gusto/ar-query-matchers/releases/tag/v0.5.1
|
36
|
+
[0.5.0]: https://github.com/gusto/ar-query-matchers/releases/tag/v0.5.0
|
37
|
+
[0.4.0]: https://github.com/gusto/ar-query-matchers/releases/tag/v0.4.0
|
38
|
+
[0.3.0]: https://github.com/gusto/ar-query-matchers/releases/tag/v0.3.0
|
39
|
+
[0.2.0]: https://github.com/gusto/ar-query-matchers/releases/tag/v0.2.0
|
40
|
+
[0.1.0]: https://github.com/gusto/ar-query-matchers/releases/tag/v0.1.0
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2019 Matan Zruya
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
CHANGED
@@ -1,14 +1,40 @@
|
|
1
1
|
## AR Query Matchers
|
2
|
+

|
2
3
|
|
3
|
-
These RSpec matchers
|
4
|
-
exactly how many queries
|
4
|
+
These RSpec matchers allow guarding against N+1 queries by specifying
|
5
|
+
exactly how many queries you expect each of your ActiveRecord models to perform.
|
5
6
|
|
6
|
-
They also help
|
7
|
+
They could also help reasoning about which database interactions are happening inside a block of code.
|
7
8
|
|
8
9
|
This pattern is a based on how Rails itself tests queries:
|
9
10
|
https://github.com/rails/rails/blob/ac2bc00482c1cf47a57477edb6ab1426a3ba593c/activerecord/test/cases/test_case.rb#L104-L141
|
10
11
|
|
11
|
-
|
12
|
+
Currently, this gem only supports RSpec matchers, but the code is meant to be adapted to support other testing frameworks.
|
13
|
+
If you'd like to pick that up, please have a look at: https://github.com/Gusto/ar-query-matchers/issues/13
|
14
|
+
|
15
|
+
### Usage
|
16
|
+
Include it in your Gemfile:
|
17
|
+
```ruby
|
18
|
+
group :test do
|
19
|
+
gem 'ar-query-matchers', '~> 0.2.0', require: false
|
20
|
+
end
|
21
|
+
```
|
22
|
+
|
23
|
+
Start using it:
|
24
|
+
```ruby
|
25
|
+
require 'ar_query_matchers'
|
26
|
+
|
27
|
+
RSpec.describe Employee do
|
28
|
+
it 'creating an employee creates exactly one record' do
|
29
|
+
expect {
|
30
|
+
Employee.create!(first_name: 'John', last_name: 'Doe')
|
31
|
+
}.to only_create_models('Employee' => '1')
|
32
|
+
end
|
33
|
+
end
|
34
|
+
```
|
35
|
+
|
36
|
+
### Matchers
|
37
|
+
This gem defines a few categories of matchers:
|
12
38
|
- **Create**: Which models are created during a block
|
13
39
|
- **Load**: Which models are fetched during a block
|
14
40
|
- **Update**: Which models are updated during a block
|
@@ -32,7 +58,7 @@ expect { some_code() }.to only_load_models(
|
|
32
58
|
)
|
33
59
|
```
|
34
60
|
|
35
|
-
The following spec will pass only if there are
|
61
|
+
The following spec will pass only if there are no select queries.
|
36
62
|
```ruby
|
37
63
|
expect { some_code() }.to not_load_models
|
38
64
|
```
|
@@ -61,11 +87,9 @@ Expected to run queries to load models exactly {"Address"=>1, "Payroll"=>1, "Use
|
|
61
87
|
```
|
62
88
|
|
63
89
|
### High Level Design:
|
64
|
-
The RSpec matcher delegates to
|
65
|
-
|
66
|
-
|
90
|
+
The RSpec matcher delegates to "query counters", asserts expectations and formats error messages to provide meaningful failures.
|
67
91
|
The matchers are pretty simple, and delegate instrumentation into specialized QueryCounter classes.
|
68
|
-
The QueryCounters are different classes
|
92
|
+
The QueryCounters are different classes which instrument a ruby block by listening on all sql, parsing the queries and returning structured data describing the interactions.
|
69
93
|
|
70
94
|
```
|
71
95
|
┌────────────────────────────────────────────────────────────────────────────────────────┐
|
@@ -88,4 +112,4 @@ For more information, see:
|
|
88
112
|
### Known problems
|
89
113
|
- The Rails 4 `ActiveRecord::Base#pluck` method doesn't issue a
|
90
114
|
`Load` or `Exists` named query and therefore we don't capture the counts with
|
91
|
-
this tool. This may be fixed in Rails 5/6.
|
115
|
+
this tool. This may be fixed in Rails 5/6.
|
data/lib/ar_query_matchers.rb
CHANGED
@@ -3,9 +3,16 @@
|
|
3
3
|
require 'ar_query_matchers/queries/create_counter'
|
4
4
|
require 'ar_query_matchers/queries/load_counter'
|
5
5
|
require 'ar_query_matchers/queries/update_counter'
|
6
|
+
require 'bigdecimal'
|
6
7
|
|
7
8
|
module ArQueryMatchers
|
8
9
|
module ArQueryMatchers
|
10
|
+
class Utility
|
11
|
+
def self.remove_superfluous_expectations(expected)
|
12
|
+
expected.select { |_, v| v.positive? }
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
9
16
|
module CreateModels
|
10
17
|
# The following will succeed:
|
11
18
|
# expect {
|
@@ -22,7 +29,7 @@ module ArQueryMatchers
|
|
22
29
|
|
23
30
|
match do |block|
|
24
31
|
@query_stats = Queries::CreateCounter.instrument(&block)
|
25
|
-
expected == @query_stats.query_counts
|
32
|
+
Utility.remove_superfluous_expectations(expected) == @query_stats.query_counts
|
26
33
|
end
|
27
34
|
|
28
35
|
def failure_text
|
@@ -82,7 +89,7 @@ module ArQueryMatchers
|
|
82
89
|
|
83
90
|
match do |block|
|
84
91
|
@query_stats = Queries::LoadCounter.instrument(&block)
|
85
|
-
expected == @query_stats.query_counts
|
92
|
+
Utility.remove_superfluous_expectations(expected) == @query_stats.query_counts
|
86
93
|
end
|
87
94
|
|
88
95
|
def failure_text
|
@@ -146,7 +153,7 @@ module ArQueryMatchers
|
|
146
153
|
|
147
154
|
match do |block|
|
148
155
|
@query_stats = Queries::UpdateCounter.instrument(&block)
|
149
|
-
expected == @query_stats.query_counts
|
156
|
+
Utility.remove_superfluous_expectations(expected) == @query_stats.query_counts
|
150
157
|
end
|
151
158
|
|
152
159
|
def failure_text
|
@@ -62,7 +62,7 @@ module ArQueryMatchers
|
|
62
62
|
# @param [block] block to instrument
|
63
63
|
# @return [QueryStats] stats about all the SQL queries executed during the block
|
64
64
|
def instrument(&block)
|
65
|
-
queries = Hash.new { |h, k| h[k] = { count: 0, lines: [] } }
|
65
|
+
queries = Hash.new { |h, k| h[k] = { count: 0, lines: [], time: BigDecimal(0) } }
|
66
66
|
ActiveSupport::Notifications.subscribed(to_proc(queries), 'sql.active_record', &block)
|
67
67
|
QueryStats.new(queries)
|
68
68
|
end
|
@@ -75,11 +75,11 @@ module ArQueryMatchers
|
|
75
75
|
private_constant :MARGINALIA_SQL_COMMENT_PATTERN
|
76
76
|
|
77
77
|
def to_proc(queries)
|
78
|
-
lambda do |_name,
|
78
|
+
lambda do |_name, start, finish, _message_id, payload|
|
79
79
|
return if payload[:cached]
|
80
80
|
|
81
81
|
# Given a `sql.active_record` event, figure out which model is being
|
82
|
-
# accessed. Some of the simpler queries have a :
|
82
|
+
# accessed. Some of the simpler queries have a :name key that makes this
|
83
83
|
# really easy. Others require parsing the SQL by hand.
|
84
84
|
model_name = @query_filter.filter_map(payload[:name] || '', payload[:sql] || '')&.model_name
|
85
85
|
|
@@ -87,6 +87,7 @@ module ArQueryMatchers
|
|
87
87
|
comment = payload[:sql].match(MARGINALIA_SQL_COMMENT_PATTERN)
|
88
88
|
queries[model_name][:lines] << comment[:line] if comment
|
89
89
|
queries[model_name][:count] += 1
|
90
|
+
queries[model_name][:time] += (finish - start).round(6) # Round to microseconds
|
90
91
|
end
|
91
92
|
end
|
92
93
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ar-query-matchers
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1
|
4
|
+
version: 0.5.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Matan Zruya
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-11-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -17,9 +17,9 @@ dependencies:
|
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '4.0'
|
20
|
-
- - "
|
20
|
+
- - "<"
|
21
21
|
- !ruby/object:Gem::Version
|
22
|
-
version: '
|
22
|
+
version: '7.0'
|
23
23
|
type: :runtime
|
24
24
|
prerelease: false
|
25
25
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -27,9 +27,9 @@ dependencies:
|
|
27
27
|
- - ">="
|
28
28
|
- !ruby/object:Gem::Version
|
29
29
|
version: '4.0'
|
30
|
-
- - "
|
30
|
+
- - "<"
|
31
31
|
- !ruby/object:Gem::Version
|
32
|
-
version: '
|
32
|
+
version: '7.0'
|
33
33
|
- !ruby/object:Gem::Dependency
|
34
34
|
name: activesupport
|
35
35
|
requirement: !ruby/object:Gem::Requirement
|
@@ -37,9 +37,9 @@ dependencies:
|
|
37
37
|
- - ">="
|
38
38
|
- !ruby/object:Gem::Version
|
39
39
|
version: '4.0'
|
40
|
-
- - "
|
40
|
+
- - "<"
|
41
41
|
- !ruby/object:Gem::Version
|
42
|
-
version: '
|
42
|
+
version: '7.0'
|
43
43
|
type: :runtime
|
44
44
|
prerelease: false
|
45
45
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -47,9 +47,9 @@ dependencies:
|
|
47
47
|
- - ">="
|
48
48
|
- !ruby/object:Gem::Version
|
49
49
|
version: '4.0'
|
50
|
-
- - "
|
50
|
+
- - "<"
|
51
51
|
- !ruby/object:Gem::Version
|
52
|
-
version: '
|
52
|
+
version: '7.0'
|
53
53
|
- !ruby/object:Gem::Dependency
|
54
54
|
name: rspec
|
55
55
|
requirement: !ruby/object:Gem::Requirement
|
@@ -64,6 +64,20 @@ dependencies:
|
|
64
64
|
- - "~>"
|
65
65
|
- !ruby/object:Gem::Version
|
66
66
|
version: '3.0'
|
67
|
+
- !ruby/object:Gem::Dependency
|
68
|
+
name: appraisal
|
69
|
+
requirement: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: '0'
|
74
|
+
type: :development
|
75
|
+
prerelease: false
|
76
|
+
version_requirements: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
67
81
|
- !ruby/object:Gem::Dependency
|
68
82
|
name: bundler
|
69
83
|
requirement: !ruby/object:Gem::Requirement
|
@@ -82,30 +96,30 @@ dependencies:
|
|
82
96
|
name: rake
|
83
97
|
requirement: !ruby/object:Gem::Requirement
|
84
98
|
requirements:
|
85
|
-
- - "
|
99
|
+
- - ">="
|
86
100
|
- !ruby/object:Gem::Version
|
87
|
-
version: '
|
101
|
+
version: '0'
|
88
102
|
type: :development
|
89
103
|
prerelease: false
|
90
104
|
version_requirements: !ruby/object:Gem::Requirement
|
91
105
|
requirements:
|
92
|
-
- - "
|
106
|
+
- - ">="
|
93
107
|
- !ruby/object:Gem::Version
|
94
|
-
version: '
|
108
|
+
version: '0'
|
95
109
|
- !ruby/object:Gem::Dependency
|
96
110
|
name: rspec
|
97
111
|
requirement: !ruby/object:Gem::Requirement
|
98
112
|
requirements:
|
99
|
-
- - "
|
113
|
+
- - ">="
|
100
114
|
- !ruby/object:Gem::Version
|
101
|
-
version: '
|
115
|
+
version: '0'
|
102
116
|
type: :development
|
103
117
|
prerelease: false
|
104
118
|
version_requirements: !ruby/object:Gem::Requirement
|
105
119
|
requirements:
|
106
|
-
- - "
|
120
|
+
- - ">="
|
107
121
|
- !ruby/object:Gem::Version
|
108
|
-
version: '
|
122
|
+
version: '0'
|
109
123
|
- !ruby/object:Gem::Dependency
|
110
124
|
name: rubocop
|
111
125
|
requirement: !ruby/object:Gem::Requirement
|
@@ -124,23 +138,26 @@ dependencies:
|
|
124
138
|
name: sqlite3
|
125
139
|
requirement: !ruby/object:Gem::Requirement
|
126
140
|
requirements:
|
127
|
-
- - "
|
141
|
+
- - ">="
|
128
142
|
- !ruby/object:Gem::Version
|
129
|
-
version: '
|
143
|
+
version: '0'
|
130
144
|
type: :development
|
131
145
|
prerelease: false
|
132
146
|
version_requirements: !ruby/object:Gem::Requirement
|
133
147
|
requirements:
|
134
|
-
- - "
|
148
|
+
- - ">="
|
135
149
|
- !ruby/object:Gem::Version
|
136
|
-
version: '
|
137
|
-
description:
|
150
|
+
version: '0'
|
151
|
+
description: These RSpec matchers allow guarding against N+1 queries by specifying
|
152
|
+
exactly how many queries you expect each of your ActiveRecord models to perform.
|
138
153
|
email:
|
139
154
|
- mzruya@gmail.com
|
140
155
|
executables: []
|
141
156
|
extensions: []
|
142
157
|
extra_rdoc_files: []
|
143
158
|
files:
|
159
|
+
- CHANGELOG.md
|
160
|
+
- LICENSE.txt
|
144
161
|
- README.md
|
145
162
|
- lib/ar_query_matchers.rb
|
146
163
|
- lib/ar_query_matchers/queries/create_counter.rb
|
@@ -157,7 +174,7 @@ licenses:
|
|
157
174
|
metadata:
|
158
175
|
homepage_uri: https://github.com/Gusto/ar-query-matchers
|
159
176
|
source_code_uri: https://github.com/Gusto/ar-query-matchers
|
160
|
-
changelog_uri: https://github.com/Gusto/ar-query-matchers
|
177
|
+
changelog_uri: https://github.com/Gusto/ar-query-matchers/blob/master/CHANGELOG.md
|
161
178
|
post_install_message:
|
162
179
|
rdoc_options: []
|
163
180
|
require_paths:
|
@@ -173,8 +190,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
173
190
|
- !ruby/object:Gem::Version
|
174
191
|
version: '0'
|
175
192
|
requirements: []
|
176
|
-
rubygems_version: 3.
|
193
|
+
rubygems_version: 3.1.4
|
177
194
|
signing_key:
|
178
195
|
specification_version: 4
|
179
|
-
summary:
|
196
|
+
summary: Ruby test matchers for instrumenting ActiveRecord query counts
|
180
197
|
test_files: []
|