ar-query-matchers 0.2.0 → 0.5.2.pre.5

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: 5265f76d235538ee76fd8ed78d460357bd35efc1b9f96286cd674c646dfa9080
4
- data.tar.gz: 368284e4b46d07c1331805e1224f15f66f82757a69dcc2d2eccb097331e154e9
3
+ metadata.gz: 2aaeaac19a8660c885b5ac75c32077ab6b0f835c53aaefa1bb3d3b7bab701fd4
4
+ data.tar.gz: 3746c66186e7d067fc82895a8d7bb90dc230eb7ddef7553ad2bb9631bd69d097
5
5
  SHA512:
6
- metadata.gz: 6b6f19694c17e3462faccfcb5928e907a269947264f2a9902d7bcfb4a5ea0c88aeb99deff97198abbe5f42d25d6f844dcdff738967ef783e6a1230cadb3ce0aa
7
- data.tar.gz: 55615a4c940d4dfdfbba8625fea16bcc64c537e31c31f690fc7fcc1d2b3ca7a775d5b68e145e472a478b0c8a787239fdc3c2cb8d42fbf66fcf581231499b4131
6
+ metadata.gz: 7d43af021387d6a1ea871bf13d876a512e19cf98eb702f85f9ff0446970c8f6bfbf40356d1eb0c1417664fd8b47ab4e8aab115785f06f8f22f547ab284dfa809
7
+ data.tar.gz: f8785fb4accfb4f7a2573ced84211769793cc72bb6784de3884665f39a63346ebcc83046e737e6fca3e56a9fd1dbafc563805800b49df32ee77951137c76f8aa
data/CHANGELOG.md CHANGED
@@ -6,6 +6,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.5.2] - 2021-05-18
10
+ ### Changed
11
+ - Removes 'SELECT' from MODEL_SQL_PATTERN to allow for more granular SQL statement matching
12
+
13
+ ## [0.5.1] - 2020-11-19
14
+ ### Changed
15
+ - Removes zero count expectations from hash before comparing
16
+
17
+ ## [0.5.0] - 2020-07-23
18
+ ### Changed
19
+ - Add time information to query counter
20
+
21
+ ## [0.4.0] - 2020-07-20
22
+ ### Changed
23
+ - Upgrade the Rails dependency to allow for Rails 6.1
24
+
25
+ ## [0.3.0] - 2020-03-13
26
+ ### Changed
27
+ - Correct the Rails dependency to allow for Rails 6.0
28
+
9
29
  ## [0.2.0] - 2019-09-15
10
30
  ### Changed
11
31
  - Package the CHANGELOG and README in the gem.
@@ -15,6 +35,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
15
35
  ### Added
16
36
  - First versions as a public ruby gem.
17
37
 
18
- [Unreleased]: https://github.com/gusto/ar-query-matchers/compare/v0.2.0...HEAD
38
+ [Unreleased]: https://github.com/gusto/ar-query-matchers/compare/v0.5.1...HEAD
39
+ [0.5.1]: https://github.com/gusto/ar-query-matchers/releases/tag/v0.5.1
40
+ [0.5.0]: https://github.com/gusto/ar-query-matchers/releases/tag/v0.5.0
41
+ [0.4.0]: https://github.com/gusto/ar-query-matchers/releases/tag/v0.4.0
42
+ [0.3.0]: https://github.com/gusto/ar-query-matchers/releases/tag/v0.3.0
19
43
  [0.2.0]: https://github.com/gusto/ar-query-matchers/releases/tag/v0.2.0
20
- [0.1.0]: https://github.com/gusto/ar-query-matchers/releases/tag/v0.1.0
44
+ [0.1.0]: https://github.com/gusto/ar-query-matchers/releases/tag/v0.1.0
data/README.md CHANGED
@@ -1,25 +1,28 @@
1
1
  ## AR Query Matchers
2
2
  ![badge](https://action-badges.now.sh/gusto/ar-query-matchers?action=Run%20Tests)
3
3
 
4
- These RSpec matchers allows guarding against N+1 queries by specifying
4
+ These RSpec matchers allow guarding against N+1 queries by specifying
5
5
  exactly how many queries you expect each of your ActiveRecord models to perform.
6
6
 
7
- They also help us reason about the type of record interactions happening in a block of code.
7
+ They could also help reasoning about which database interactions are happening inside a block of code.
8
8
 
9
9
  This pattern is a based on how Rails itself tests queries:
10
10
  https://github.com/rails/rails/blob/ac2bc00482c1cf47a57477edb6ab1426a3ba593c/activerecord/test/cases/test_case.rb#L104-L141
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
+
12
15
  ### Usage
13
16
  Include it in your Gemfile:
14
17
  ```ruby
15
18
  group :test do
16
- gem 'ar-query-matchers', '~> 0.1.0', require: false
19
+ gem 'ar-query-matchers', '~> 0.2.0', require: false
17
20
  end
18
21
  ```
19
22
 
20
23
  Start using it:
21
24
  ```ruby
22
- require 'ar-query-matchers'
25
+ require 'ar_query_matchers'
23
26
 
24
27
  RSpec.describe Employee do
25
28
  it 'creating an employee creates exactly one record' do
@@ -31,7 +34,7 @@ end
31
34
  ```
32
35
 
33
36
  ### Matchers
34
- This module defines a few categories of matchers:
37
+ This gem defines a few categories of matchers:
35
38
  - **Create**: Which models are created during a block
36
39
  - **Load**: Which models are fetched during a block
37
40
  - **Update**: Which models are updated during a block
@@ -55,7 +58,7 @@ expect { some_code() }.to only_load_models(
55
58
  )
56
59
  ```
57
60
 
58
- The following spec will pass only if there are exactly no select queries.
61
+ The following spec will pass only if there are no select queries.
59
62
  ```ruby
60
63
  expect { some_code() }.to not_load_models
61
64
  ```
@@ -84,11 +87,9 @@ Expected to run queries to load models exactly {"Address"=>1, "Payroll"=>1, "Use
84
87
  ```
85
88
 
86
89
  ### High Level Design:
87
- The RSpec matcher delegates to the query counters, asserts expectations and formats error messages to provide meaningful failures.
88
-
89
-
90
+ The RSpec matcher delegates to "query counters", asserts expectations and formats error messages to provide meaningful failures.
90
91
  The matchers are pretty simple, and delegate instrumentation into specialized QueryCounter classes.
91
- The QueryCounters are different classes instruments a ruby block by listening on all sql, parsing the queries and returning structured data describing the interactions.
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.
92
93
 
93
94
  ```
94
95
  ┌────────────────────────────────────────────────────────────────────────────────────────┐
@@ -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
@@ -23,7 +23,7 @@ module ArQueryMatchers
23
23
  # for inserts, name is always 'SQL', we have to rely on pattern matching the query string.
24
24
  select_from_table = sql.match(TABLE_NAME_SQL_PATTERN)
25
25
 
26
- TableName.new(select_from_table[:table_name]) if select_from_table
26
+ [TableName.new(select_from_table[:table_name])] if select_from_table
27
27
  end
28
28
  end
29
29
  end
@@ -21,18 +21,21 @@ module ArQueryMatchers
21
21
 
22
22
  # Matches unnamed SQL operations like the following:
23
23
  # "SELECT COUNT(*) FROM `users` ..."
24
- MODEL_SQL_PATTERN = /SELECT .* FROM [`"](?<table_name>[^`"]+)[`"]/.freeze
24
+ MODEL_SQL_PATTERN = /FROM [`"](?<table_name>[^`"]+)[`"]/.freeze
25
25
 
26
- def filter_map(name, sql)
26
+ def filter_map(_name, sql)
27
27
  # First check for a `SELECT * FROM` query that ActiveRecord has
28
28
  # helpfully named for us in the payload
29
- match = name.match(MODEL_LOAD_PATTERN)
30
- return ModelName.new(match[:model_name]) if match
29
+ #
30
+ # NOTE: This misses possible subqueries and prevents us from getting
31
+ # to the below matcher
32
+ # match = name.match(MODEL_LOAD_PATTERN)
33
+ # return [ModelName.new(match[:model_name])] if match
31
34
 
32
35
  # Fall back to pattern-matching on the table name in a COUNT and looking
33
36
  # up the table name from ActiveRecord's loaded descendants.
34
- select_from_table = sql.match(MODEL_SQL_PATTERN)
35
- TableName.new(select_from_table[:table_name]) if select_from_table
37
+ selects_from_table = sql.scan(MODEL_SQL_PATTERN)
38
+ selects_from_table.map { |(table_name)| TableName.new(table_name) } unless selects_from_table.empty?
36
39
  end
37
40
  end
38
41
  end
@@ -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,18 +75,23 @@ module ArQueryMatchers
75
75
  private_constant :MARGINALIA_SQL_COMMENT_PATTERN
76
76
 
77
77
  def to_proc(queries)
78
- lambda do |_name, _start, _finish, _message_id, payload|
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 :ame key that makes this
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
- model_name = @query_filter.filter_map(payload[:name] || '', payload[:sql] || '')&.model_name
84
+ results = @query_filter.filter_map(payload[:name] || '', payload[:sql] || '')
85
+
86
+ # Round to microseconds
87
+ results&.each do |result|
88
+ model_name = result.model_name
89
+ next unless model_name
85
90
 
86
- if model_name
87
91
  comment = payload[:sql].match(MARGINALIA_SQL_COMMENT_PATTERN)
88
92
  queries[model_name][:lines] << comment[:line] if comment
89
93
  queries[model_name][:count] += 1
94
+ queries[model_name][:time] += (finish - start).round(6) # Round to microseconds
90
95
  end
91
96
  end
92
97
  end
@@ -22,7 +22,7 @@ module ArQueryMatchers
22
22
  def filter_map(_name, sql)
23
23
  # for updates, name is always 'SQL', we have to rely on pattern matching on the query string instead.
24
24
  select_from_table = sql.match(TABLE_NAME_SQL_PATTERN)
25
- TableName.new(select_from_table[:table_name]) if select_from_table
25
+ [TableName.new(select_from_table[:table_name])] if select_from_table
26
26
  end
27
27
  end
28
28
  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.2.0
4
+ version: 0.5.2.pre.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matan Zruya
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-09-15 00:00:00.000000000 Z
11
+ date: 2021-05-25 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: '6.0'
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: '6.0'
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: '6.0'
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: '6.0'
52
+ version: '7.0'
53
53
  - !ruby/object:Gem::Dependency
54
54
  name: rspec
55
55
  requirement: !ruby/object:Gem::Requirement
@@ -148,7 +148,7 @@ dependencies:
148
148
  - - ">="
149
149
  - !ruby/object:Gem::Version
150
150
  version: '0'
151
- description: These RSpec matchers allows guarding against N+1 queries by specifying
151
+ description: These RSpec matchers allow guarding against N+1 queries by specifying
152
152
  exactly how many queries you expect each of your ActiveRecord models to perform.
153
153
  email:
154
154
  - mzruya@gmail.com
@@ -186,11 +186,11 @@ required_ruby_version: !ruby/object:Gem::Requirement
186
186
  version: '0'
187
187
  required_rubygems_version: !ruby/object:Gem::Requirement
188
188
  requirements:
189
- - - ">="
189
+ - - ">"
190
190
  - !ruby/object:Gem::Version
191
- version: '0'
191
+ version: 1.3.1
192
192
  requirements: []
193
- rubygems_version: 3.0.3
193
+ rubygems_version: 3.0.3.1
194
194
  signing_key:
195
195
  specification_version: 4
196
196
  summary: Ruby test matchers for instrumenting ActiveRecord query counts