rubocop-isucon 0.1.0 → 1.0.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/.github/dependabot.yml +10 -0
- data/.github/workflows/pages.yml +68 -0
- data/.github/workflows/test.yml +3 -7
- data/.rubocop.yml +5 -1
- data/CHANGELOG.md +21 -1
- data/README.md +24 -6
- data/config/default.yml +37 -1
- data/gemfiles/activerecord_7_1.gemfile +14 -0
- data/lib/rubocop/cop/isucon/correctors/{mysql2_n_plus_one_query_corrector → n_plus_one_query_corrector}/correctable_methods.rb +1 -1
- data/lib/rubocop/cop/isucon/correctors/{mysql2_n_plus_one_query_corrector → n_plus_one_query_corrector}/replace_methods.rb +1 -1
- data/lib/rubocop/cop/isucon/correctors/{mysql2_n_plus_one_query_corrector.rb → n_plus_one_query_corrector.rb} +16 -5
- data/lib/rubocop/cop/isucon/mixin/join_without_index_methods.rb +87 -0
- data/lib/rubocop/cop/isucon/mixin/many_join_table_methods.rb +39 -0
- data/lib/rubocop/cop/isucon/mixin/mysql2_xquery_methods.rb +7 -116
- data/lib/rubocop/cop/isucon/mixin/n_plus_one_query_methods.rb +153 -0
- data/lib/rubocop/cop/isucon/mixin/offense_location_methods.rb +130 -0
- data/lib/rubocop/cop/isucon/mixin/select_asterisk_methods.rb +148 -0
- data/lib/rubocop/cop/isucon/mixin/sqlite3_execute_methods.rb +67 -0
- data/lib/rubocop/cop/isucon/mixin/where_without_index_methods.rb +96 -0
- data/lib/rubocop/cop/isucon/mysql2/join_without_index.rb +4 -67
- data/lib/rubocop/cop/isucon/mysql2/many_join_table.rb +1 -26
- data/lib/rubocop/cop/isucon/mysql2/n_plus_one_query.rb +5 -115
- data/lib/rubocop/cop/isucon/mysql2/select_asterisk.rb +1 -135
- data/lib/rubocop/cop/isucon/mysql2/where_without_index.rb +7 -72
- data/lib/rubocop/cop/isucon/shell/backtick.rb +7 -0
- data/lib/rubocop/cop/isucon/sqlite3/join_without_index.rb +37 -0
- data/lib/rubocop/cop/isucon/sqlite3/many_join_table.rb +61 -0
- data/lib/rubocop/cop/isucon/sqlite3/n_plus_one_query.rb +70 -0
- data/lib/rubocop/cop/isucon/sqlite3/select_asterisk.rb +37 -0
- data/lib/rubocop/cop/isucon/sqlite3/where_without_index.rb +40 -0
- data/lib/rubocop/cop/isucon_cops.rb +13 -1
- data/lib/rubocop/isucon/gda/client.rb +1 -1
- data/lib/rubocop/isucon/gda/join_operand.rb +1 -1
- data/lib/rubocop/isucon/version.rb +1 -1
- data/rubocop-isucon.gemspec +3 -2
- metadata +38 -10
- data/.github/workflows/gh-pages.yml +0 -44
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6bd0eac249a23368414a86aa1ab08826542cf644fa50b19017a2bcfd6648d594
|
4
|
+
data.tar.gz: 18e829ac97ac7c7cb9696a1078f36b8a958fd340ddbb9bc1b232b2630e683df2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 58238a855da0596f653d47436977b44cc7722982a1b59e60850f79003b356d85071cba061cca6adae2b4cfa4b63ab489e121135e3c2954f366f5ca1c375e1bbf
|
7
|
+
data.tar.gz: b7747989e1219c7fee4f834b8811e419d3c9f6083336d5fab7e7b19a3f8e1f0dad86644e708b18b926b9de3a678edf8b31495097e319bff970599b80d0ab21d9
|
@@ -0,0 +1,10 @@
|
|
1
|
+
# c.f. https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
2
|
+
version: 2
|
3
|
+
|
4
|
+
updates:
|
5
|
+
- package-ecosystem: github-actions
|
6
|
+
directory: /
|
7
|
+
schedule:
|
8
|
+
interval: weekly
|
9
|
+
assignees:
|
10
|
+
- sue445
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# Simple workflow for deploying static content to GitHub Pages
|
2
|
+
name: Deploy static content to Pages
|
3
|
+
|
4
|
+
on:
|
5
|
+
# Runs on pushes targeting the default branch
|
6
|
+
push:
|
7
|
+
branches:
|
8
|
+
- main
|
9
|
+
|
10
|
+
# Allows you to run this workflow manually from the Actions tab
|
11
|
+
workflow_dispatch:
|
12
|
+
|
13
|
+
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
14
|
+
permissions:
|
15
|
+
contents: read
|
16
|
+
pages: write
|
17
|
+
id-token: write
|
18
|
+
|
19
|
+
# Allow one concurrent deployment
|
20
|
+
concurrency:
|
21
|
+
group: "pages"
|
22
|
+
cancel-in-progress: true
|
23
|
+
|
24
|
+
jobs:
|
25
|
+
# Single deploy job since we're just deploying
|
26
|
+
deploy:
|
27
|
+
environment:
|
28
|
+
name: github-pages
|
29
|
+
url: ${{ steps.deployment.outputs.page_url }}
|
30
|
+
runs-on: ubuntu-latest
|
31
|
+
steps:
|
32
|
+
- name: Checkout
|
33
|
+
uses: actions/checkout@v4
|
34
|
+
|
35
|
+
- name: Install packages
|
36
|
+
run: |
|
37
|
+
set -xe
|
38
|
+
sudo apt-get update
|
39
|
+
sudo apt-get install -y libgda-5.0
|
40
|
+
|
41
|
+
- uses: ruby/setup-ruby@v1
|
42
|
+
with:
|
43
|
+
ruby-version: ruby
|
44
|
+
bundler-cache: true
|
45
|
+
|
46
|
+
- run: bundle exec yard
|
47
|
+
|
48
|
+
- name: Setup Pages
|
49
|
+
uses: actions/configure-pages@v3
|
50
|
+
- name: Upload artifact
|
51
|
+
uses: actions/upload-pages-artifact@v2
|
52
|
+
with:
|
53
|
+
# Upload entire repository
|
54
|
+
path: './doc'
|
55
|
+
- name: Deploy to GitHub Pages
|
56
|
+
id: deployment
|
57
|
+
uses: actions/deploy-pages@main
|
58
|
+
|
59
|
+
- name: Slack Notification (not success)
|
60
|
+
uses: lazy-actions/slatify@master
|
61
|
+
if: "! success()"
|
62
|
+
continue-on-error: true
|
63
|
+
with:
|
64
|
+
job_name: "*pages*"
|
65
|
+
type: ${{ job.status }}
|
66
|
+
icon_emoji: ":octocat:"
|
67
|
+
url: ${{ secrets.SLACK_WEBHOOK }}
|
68
|
+
token: ${{ secrets.GITHUB_TOKEN }}
|
data/.github/workflows/test.yml
CHANGED
@@ -21,23 +21,19 @@ jobs:
|
|
21
21
|
|
22
22
|
matrix:
|
23
23
|
ruby:
|
24
|
-
- "2.6"
|
25
|
-
- "2.7"
|
26
24
|
- "3.0"
|
27
25
|
- "3.1"
|
26
|
+
- "3.2"
|
28
27
|
gemfile:
|
29
28
|
- activerecord_6_1
|
30
29
|
- activerecord_7_0
|
31
|
-
|
32
|
-
# activerecord 7.0+ requires Ruby 2.7+
|
33
|
-
- ruby: "2.6"
|
34
|
-
gemfile: activerecord_7_0
|
30
|
+
- activerecord_7_1
|
35
31
|
|
36
32
|
env:
|
37
33
|
BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}.gemfile
|
38
34
|
|
39
35
|
steps:
|
40
|
-
- uses: actions/checkout@
|
36
|
+
- uses: actions/checkout@v4
|
41
37
|
|
42
38
|
- name: Install packages
|
43
39
|
run: |
|
data/.rubocop.yml
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
require:
|
2
2
|
- rubocop-performance
|
3
|
+
- rubocop-yard
|
3
4
|
|
4
5
|
AllCops:
|
5
6
|
NewCops: enable
|
6
7
|
SuggestExtensions: false
|
7
|
-
TargetRubyVersion:
|
8
|
+
TargetRubyVersion: 3.0
|
8
9
|
Exclude:
|
9
10
|
- 'gemfiles/vendor/**/*'
|
10
11
|
- 'benchmark/**/*'
|
@@ -15,6 +16,9 @@ AllCops:
|
|
15
16
|
- 'vendor/**/*'
|
16
17
|
- '.git/**/*'
|
17
18
|
|
19
|
+
Gemspec/DevelopmentDependencies:
|
20
|
+
EnforcedStyle: gemspec
|
21
|
+
|
18
22
|
Layout/DotPosition:
|
19
23
|
EnforcedStyle: trailing
|
20
24
|
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,25 @@
|
|
1
1
|
## [Unreleased]
|
2
|
-
[full changelog](http://github.com/sue445/rubocop-isucon/compare/
|
2
|
+
[full changelog](http://github.com/sue445/rubocop-isucon/compare/v1.0.0...main)
|
3
|
+
|
4
|
+
## [1.0.0] - 2023-10-29
|
5
|
+
[full changelog](http://github.com/sue445/rubocop-isucon/compare/v0.2.0...v1.0.0)
|
6
|
+
|
7
|
+
* Requires Ruby 3.0+
|
8
|
+
* https://github.com/sue445/rubocop-isucon/pull/236
|
9
|
+
|
10
|
+
## [0.2.0] - 2022-07-31
|
11
|
+
[full changelog](http://github.com/sue445/rubocop-isucon/compare/v0.1.0...v0.2.0)
|
12
|
+
|
13
|
+
* Add `Isucon/Sqlite3/SelectAsterisk`
|
14
|
+
* https://github.com/sue445/rubocop-isucon/pull/202
|
15
|
+
* Add `Isucon/Sqlite3/WhereWithoutIndex`
|
16
|
+
* https://github.com/sue445/rubocop-isucon/pull/207
|
17
|
+
* Add `Isucon/Sqlite3/JoinWithoutIndex`
|
18
|
+
* https://github.com/sue445/rubocop-isucon/pull/208
|
19
|
+
* Add `Isucon/Sqlite3/ManyJoinTable`
|
20
|
+
* https://github.com/sue445/rubocop-isucon/pull/209
|
21
|
+
* Add `Isucon/Sqlite3/NPlusOneQuery`
|
22
|
+
* https://github.com/sue445/rubocop-isucon/pull/210
|
3
23
|
|
4
24
|
## [0.1.0] - 2022-07-23
|
5
25
|
|
data/README.md
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# RuboCop ISUCON
|
2
2
|
RuboCop plugin for ruby reference implementation of [ISUCON](https://github.com/isucon)
|
3
3
|
|
4
|
+
[](https://badge.fury.io/rb/rubocop-isucon)
|
4
5
|
[](https://github.com/sue445/rubocop-isucon/actions?query=workflow%3Atest)
|
5
6
|
|
6
7
|
## Installation
|
@@ -72,16 +73,25 @@ Isucon/Mysql2:
|
|
72
73
|
password: isucon
|
73
74
|
encoding: utf8
|
74
75
|
port: 3306
|
76
|
+
|
77
|
+
Isucon/Sqlite3:
|
78
|
+
Database:
|
79
|
+
adapter: sqlite3
|
80
|
+
database: # TODO: Fix this
|
75
81
|
```
|
76
82
|
|
77
83
|
`Database` isn't configured in `.rubocop.yml`, some cops doesn't work
|
78
84
|
|
79
|
-
| cop
|
80
|
-
|
81
|
-
| `Isucon/Mysql2/JoinWithoutIndex`
|
82
|
-
| `Isucon/Mysql2/NPlusOneQuery`
|
83
|
-
| `Isucon/Mysql2/SelectAsterisk`
|
84
|
-
| `Isucon/Mysql2/WhereWithoutIndex`
|
85
|
+
| cop | offense detection | auto-correct |
|
86
|
+
|------------------------------------|----------------------------|----------------------------|
|
87
|
+
| `Isucon/Mysql2/JoinWithoutIndex` | `Database` is **required** | Not supported |
|
88
|
+
| `Isucon/Mysql2/NPlusOneQuery` | `Database` is optional | `Database` is **required** |
|
89
|
+
| `Isucon/Mysql2/SelectAsterisk` | `Database` is optional | `Database` is **required** |
|
90
|
+
| `Isucon/Mysql2/WhereWithoutIndex` | `Database` is **required** | Not supported |
|
91
|
+
| `Isucon/Sqlite3/JoinWithoutIndex` | `Database` is **required** | Not supported |
|
92
|
+
| `Isucon/Sqlite3/NPlusOneQuery` | `Database` is optional | `Database` is **required** |
|
93
|
+
| `Isucon/Sqlite3/SelectAsterisk` | `Database` is optional | `Database` is **required** |
|
94
|
+
| `Isucon/Sqlite3/WhereWithoutIndex` | `Database` is **required** | Not supported |
|
85
95
|
|
86
96
|
## Documentation
|
87
97
|
See. https://sue445.github.io/rubocop-isucon/
|
@@ -89,6 +99,7 @@ See. https://sue445.github.io/rubocop-isucon/
|
|
89
99
|
* `Isucon/Mysql2` department docs : https://sue445.github.io/rubocop-isucon/RuboCop/Cop/Isucon/Mysql2.html
|
90
100
|
* `Isucon/Shell` department docs : https://sue445.github.io/rubocop-isucon/RuboCop/Cop/Isucon/Shell.html
|
91
101
|
* `Isucon/Sinatra` department docs : https://sue445.github.io/rubocop-isucon/RuboCop/Cop/Isucon/Sinatra.html
|
102
|
+
* `Isucon/Sqlite3` department docs : https://sue445.github.io/rubocop-isucon/RuboCop/Cop/Isucon/Sqlite3.html
|
92
103
|
|
93
104
|
## Benchmark
|
94
105
|
See [benchmark/](benchmark/)
|
@@ -106,3 +117,10 @@ Bug reports and pull requests are welcome on GitHub at https://github.com/sue445
|
|
106
117
|
## License
|
107
118
|
|
108
119
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
120
|
+
|
121
|
+
ISUCON is a trademark or registered trademark of LINE Corporation.
|
122
|
+
|
123
|
+
https://isucon.net
|
124
|
+
|
125
|
+
## Presentation
|
126
|
+
* [Fix SQL N\+1 queries with RuboCop](https://speakerdeck.com/sue445/fix-sql-n-plus-one-queries-with-rubocop) at [RubyKaigi 2023](https://rubykaigi.org/2013/) :gem:
|
data/config/default.yml
CHANGED
@@ -16,7 +16,7 @@ Isucon/Mysql2/ManyJoinTable:
|
|
16
16
|
CountTables: 3
|
17
17
|
|
18
18
|
Isucon/Mysql2/NPlusOneQuery:
|
19
|
-
Description: 'Checks that N+1 query
|
19
|
+
Description: 'Checks that there’s no N+1 query'
|
20
20
|
Enabled: true
|
21
21
|
VersionAdded: '0.1.0'
|
22
22
|
SafeAutoCorrect: false
|
@@ -81,3 +81,39 @@ Isucon/Sinatra/ServeStaticFile:
|
|
81
81
|
Enabled: true
|
82
82
|
VersionAdded: '0.1.0'
|
83
83
|
StyleGuide: ServeStaticFile.html
|
84
|
+
|
85
|
+
Isucon/Sqlite3:
|
86
|
+
StyleGuideBaseURL: https://sue445.github.io/rubocop-isucon/RuboCop/Cop/Isucon/Sqlite3/
|
87
|
+
Database:
|
88
|
+
|
89
|
+
Isucon/Sqlite3/JoinWithoutIndex:
|
90
|
+
Description: 'Check for `JOIN` without index'
|
91
|
+
Enabled: true
|
92
|
+
VersionAdded: '0.2.0'
|
93
|
+
StyleGuide: JoinWithoutIndex.html
|
94
|
+
|
95
|
+
Isucon/Sqlite3/ManyJoinTable:
|
96
|
+
Description: 'Check if SQL contains many JOINs'
|
97
|
+
Enabled: true
|
98
|
+
VersionAdded: '0.2.0'
|
99
|
+
StyleGuide: ManyJoinTable.html
|
100
|
+
CountTables: 3
|
101
|
+
|
102
|
+
Isucon/Sqlite3/NPlusOneQuery:
|
103
|
+
Description: 'Checks that there’s no N+1 query'
|
104
|
+
Enabled: true
|
105
|
+
VersionAdded: '0.2.0'
|
106
|
+
SafeAutoCorrect: false
|
107
|
+
StyleGuide: NPlusOneQuery.html
|
108
|
+
|
109
|
+
Isucon/Sqlite3/SelectAsterisk:
|
110
|
+
Description: 'Avoid `SELECT *` in `db.execute`'
|
111
|
+
Enabled: true
|
112
|
+
VersionAdded: '0.2.0'
|
113
|
+
StyleGuide: SelectAsterisk.html
|
114
|
+
|
115
|
+
Isucon/Sqlite3/WhereWithoutIndex:
|
116
|
+
Description: 'Check for `WHERE` without index'
|
117
|
+
Enabled: true
|
118
|
+
VersionAdded: '0.2.0'
|
119
|
+
StyleGuide: WhereWithoutIndex.html
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
source "https://rubygems.org"
|
4
|
+
|
5
|
+
gem "activerecord", "~> 7.1.0"
|
6
|
+
|
7
|
+
group :sqlite3 do
|
8
|
+
# c.f. https://github.com/rails/rails/blob/v7.1.0/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb#L14
|
9
|
+
gem "sqlite3", "~> 1.4"
|
10
|
+
end
|
11
|
+
|
12
|
+
# eval_gemfile "#{__dir__}/common.gemfile"
|
13
|
+
|
14
|
+
gemspec path: "../"
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative "
|
4
|
-
require_relative "
|
3
|
+
require_relative "n_plus_one_query_corrector/correctable_methods"
|
4
|
+
require_relative "n_plus_one_query_corrector/replace_methods"
|
5
5
|
|
6
6
|
module RuboCop
|
7
7
|
module Cop
|
@@ -9,7 +9,7 @@ module RuboCop
|
|
9
9
|
module Correctors
|
10
10
|
# rubocop:disable Layout/LineLength
|
11
11
|
|
12
|
-
# Corrector for {RuboCop::Cop::Isucon::Mysql2::NPlusOneQuery}
|
12
|
+
# Corrector for {RuboCop::Cop::Isucon::Mysql2::NPlusOneQuery} and {RuboCop::Cop::Isucon::Sqlite3::NPlusOneQuery}
|
13
13
|
#
|
14
14
|
# @example Before
|
15
15
|
# courses.map do |course|
|
@@ -21,7 +21,7 @@ module RuboCop
|
|
21
21
|
# @users_by_id ||= db.xquery('SELECT * FROM `users` WHERE `id` IN (?)', courses.map { |course| course[:teacher_id] }).each_with_object({}) { |v, hash| hash[v[:id]] = v }
|
22
22
|
# teacher = @users_by_id[course[:teacher_id]]
|
23
23
|
# end
|
24
|
-
class
|
24
|
+
class NPlusOneQueryCorrector
|
25
25
|
# rubocop:enable Layout/LineLength
|
26
26
|
|
27
27
|
include Mixin::Mysql2XqueryMethods
|
@@ -46,19 +46,24 @@ module RuboCop
|
|
46
46
|
# @return [RuboCop::Isucon::DatabaseConnection]
|
47
47
|
attr_reader :connection
|
48
48
|
|
49
|
+
# @return [Boolean]
|
50
|
+
attr_reader :is_array_arg
|
51
|
+
|
49
52
|
# @param corrector [RuboCop::Cop::Corrector]
|
50
53
|
# @param current_node [RuboCop::AST::Node]
|
51
54
|
# @param parent_node [RuboCop::AST::Node]
|
52
55
|
# @param type [Symbol] Node type. one of `:str`, `:dstr`
|
53
56
|
# @param gda [RuboCop::Isucon::GDA::Client]
|
54
57
|
# @param connection [RuboCop::Isucon::DatabaseConnection]
|
55
|
-
|
58
|
+
# @param is_array_arg [Boolean]
|
59
|
+
def initialize(corrector:, current_node:, parent_node:, type:, gda:, connection:, is_array_arg:) # rubocop:disable Metrics/ParameterLists
|
56
60
|
@corrector = corrector
|
57
61
|
@current_node = current_node
|
58
62
|
@parent_node = parent_node
|
59
63
|
@type = type
|
60
64
|
@gda = gda
|
61
65
|
@connection = connection
|
66
|
+
@is_array_arg = is_array_arg
|
62
67
|
end
|
63
68
|
|
64
69
|
def correct
|
@@ -67,6 +72,10 @@ module RuboCop
|
|
67
72
|
|
68
73
|
private
|
69
74
|
|
75
|
+
def array_arg?
|
76
|
+
is_array_arg
|
77
|
+
end
|
78
|
+
|
70
79
|
# @return [RuboCop::AST::Node,nil]
|
71
80
|
def parent_receiver
|
72
81
|
parent_node.child_nodes&.first&.receiver
|
@@ -88,6 +97,8 @@ module RuboCop
|
|
88
97
|
|
89
98
|
# @return [RuboCop::AST::Node]
|
90
99
|
def xquery_arg
|
100
|
+
return current_node.child_nodes[2].child_nodes[0] if array_arg? && current_node.child_nodes[2]&.array_type?
|
101
|
+
|
91
102
|
current_node.child_nodes[2]
|
92
103
|
end
|
93
104
|
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Isucon
|
6
|
+
module Mixin
|
7
|
+
# Common methods for {RuboCop::Cop::Isucon::Mysql2::JoinWithoutIndex}
|
8
|
+
# and {RuboCop::Cop::Isucon::Sqlite3::JoinWithoutIndex}
|
9
|
+
module JoinWithoutIndexMethods
|
10
|
+
include Mixin::DatabaseMethods
|
11
|
+
|
12
|
+
# @param node [RuboCop::AST::Node]
|
13
|
+
def on_send(node)
|
14
|
+
with_error_handling(node) do
|
15
|
+
return unless enabled_database?
|
16
|
+
|
17
|
+
with_db_query(node) do |type, root_gda|
|
18
|
+
check_and_register_offence(type: type, root_gda: root_gda, node: node)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
# @param type [Symbol] Node type. one of `:str`, `:dstr`
|
26
|
+
# @param root_gda [RuboCop::Isucon::GDA::Client]
|
27
|
+
# @param node [RuboCop::AST::Node]
|
28
|
+
def check_and_register_offence(type:, root_gda:, node:)
|
29
|
+
return unless root_gda
|
30
|
+
|
31
|
+
root_gda.visit_all do |gda|
|
32
|
+
gda.join_conditions.each do |join_condition|
|
33
|
+
join_operand = join_operand_without_index(join_condition)
|
34
|
+
next unless join_operand
|
35
|
+
|
36
|
+
register_offense(type: type, node: node, join_operand: join_operand)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# @param join_condition [RuboCop::Isucon::GDA::JoinCondition]
|
42
|
+
# @return [RuboCop::Isucon::GDA::JoinOperand,nil]
|
43
|
+
def join_operand_without_index(join_condition)
|
44
|
+
join_condition.operands.each do |join_operand|
|
45
|
+
next unless join_operand.table_name
|
46
|
+
|
47
|
+
unless indexed_column?(table_name: join_operand.table_name, column_name: join_operand.column_name)
|
48
|
+
return join_operand
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
|
55
|
+
# @param table_name [String]
|
56
|
+
# @param column_name [String]
|
57
|
+
# @return [Boolean]
|
58
|
+
def indexed_column?(table_name:, column_name:)
|
59
|
+
primary_keys = connection.primary_keys(table_name)
|
60
|
+
|
61
|
+
return true if primary_keys&.first == column_name
|
62
|
+
|
63
|
+
indexes = connection.indexes(table_name)
|
64
|
+
index_first_columns = indexes.map { |index| index.columns[0] }
|
65
|
+
index_first_columns.include?(column_name)
|
66
|
+
end
|
67
|
+
|
68
|
+
# @param type [Symbol] Node type. one of `:str`, `:dstr`
|
69
|
+
# @param node [RuboCop::AST::Node]
|
70
|
+
# @param join_operand [RuboCop::Isucon::GDA::JoinOperand]
|
71
|
+
def register_offense(type:, node:, join_operand:)
|
72
|
+
loc = offense_location(type: type, node: node, gda_location: join_operand.node.location)
|
73
|
+
return unless loc
|
74
|
+
|
75
|
+
message = offense_message(join_operand)
|
76
|
+
add_offense(loc, message: message)
|
77
|
+
end
|
78
|
+
|
79
|
+
# @param join_operand [RuboCop::Isucon::GDA::JoinOperand]
|
80
|
+
def offense_message(join_operand)
|
81
|
+
generate_offense_message(table_name: join_operand.table_name, column_name: join_operand.column_name)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Isucon
|
6
|
+
module Mixin
|
7
|
+
# Common methods for {RuboCop::Cop::Isucon::Mysql2::ManyJoinTable}
|
8
|
+
# and {RuboCop::Cop::Isucon::Sqlite3::ManyJoinTable}
|
9
|
+
module ManyJoinTableMethods
|
10
|
+
MSG = "Avoid SQL with lots of JOINs"
|
11
|
+
|
12
|
+
# @param node [RuboCop::AST::Node]
|
13
|
+
def on_send(node)
|
14
|
+
with_db_query(node) do |_, root_gda|
|
15
|
+
check_and_register_offence(root_gda: root_gda, node: node)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
# @param root_gda [RuboCop::Isucon::GDA::Client]
|
22
|
+
# @param node [RuboCop::AST::Node]
|
23
|
+
def check_and_register_offence(root_gda:, node:)
|
24
|
+
return unless root_gda
|
25
|
+
|
26
|
+
root_gda.visit_all do |gda|
|
27
|
+
add_offense(node) if gda.table_names.count > count_tables
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [Integer]
|
32
|
+
def count_tables
|
33
|
+
cop_config["CountTables"]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -8,6 +8,8 @@ module RuboCop
|
|
8
8
|
module Mysql2XqueryMethods
|
9
9
|
extend NodePattern::Macros
|
10
10
|
|
11
|
+
include OffenceLocationMethods
|
12
|
+
|
11
13
|
# @!method find_xquery(node)
|
12
14
|
# @param node [RuboCop::AST::Node]
|
13
15
|
def_node_search :find_xquery, <<~PATTERN
|
@@ -22,7 +24,7 @@ module RuboCop
|
|
22
24
|
# @yieldparam root_gda [RuboCop::Isucon::GDA::Client,nil]
|
23
25
|
#
|
24
26
|
# @note If arguments of `db.xquery` isn't string, `root_gda` is `nil`
|
25
|
-
def
|
27
|
+
def with_db_query(node)
|
26
28
|
find_xquery(node) do |type, params|
|
27
29
|
sql = xquery_param(type: type, params: params)
|
28
30
|
|
@@ -36,22 +38,13 @@ module RuboCop
|
|
36
38
|
end
|
37
39
|
end
|
38
40
|
|
39
|
-
|
40
|
-
# @param node [RuboCop::AST::Node]
|
41
|
-
# @param gda_location [RuboCop::Isucon::GDA::NodeLocation]
|
42
|
-
# @return [Parser::Source::Range,nil]
|
43
|
-
def offense_location(type:, node:, gda_location:)
|
44
|
-
return nil unless gda_location
|
45
|
-
|
46
|
-
begin_pos = begin_position_from_gda_location(type: type, node: node, gda_location: gda_location)
|
47
|
-
return nil unless begin_pos
|
41
|
+
private
|
48
42
|
|
49
|
-
|
50
|
-
|
43
|
+
# @return [Array<Symbol>]
|
44
|
+
def db_query_methods
|
45
|
+
%i[xquery query]
|
51
46
|
end
|
52
47
|
|
53
|
-
private
|
54
|
-
|
55
48
|
# @param type [Symbol] Node type. one of `:str`, `:dstr`
|
56
49
|
# @param params [Array<RuboCop::AST::Node>]
|
57
50
|
# @return [String,nil]
|
@@ -67,108 +60,6 @@ module RuboCop
|
|
67
60
|
end
|
68
61
|
nil
|
69
62
|
end
|
70
|
-
|
71
|
-
# @param type [Symbol] Node type. one of `:str`, `:dstr`
|
72
|
-
# @param node [RuboCop::AST::Node]
|
73
|
-
# @param gda_location [RuboCop::Isucon::GDA::NodeLocation]
|
74
|
-
# @return [Integer,nil]
|
75
|
-
def begin_position_from_gda_location(type:, node:, gda_location:)
|
76
|
-
case type
|
77
|
-
when :str
|
78
|
-
return begin_position_from_gda_location_for_str(node: node, gda_location: gda_location)
|
79
|
-
when :dstr
|
80
|
-
return begin_position_from_gda_location_for_dstr(node: node, gda_location: gda_location)
|
81
|
-
end
|
82
|
-
|
83
|
-
nil
|
84
|
-
end
|
85
|
-
|
86
|
-
# @param node [RuboCop::AST::Node]
|
87
|
-
# @param gda_location [RuboCop::Isucon::GDA::NodeLocation]
|
88
|
-
# @return [Integer,nil]
|
89
|
-
def begin_position_from_gda_location_for_str(node:, gda_location:)
|
90
|
-
str_node = node.child_nodes[1]
|
91
|
-
return nil unless str_node&.str_type?
|
92
|
-
|
93
|
-
str_node.loc.begin.end_pos + gda_location.begin_pos
|
94
|
-
end
|
95
|
-
|
96
|
-
# @param node [RuboCop::AST::Node]
|
97
|
-
# @param gda_location [RuboCop::Isucon::GDA::NodeLocation]
|
98
|
-
# @return [Integer,nil]
|
99
|
-
def begin_position_from_gda_location_for_dstr(node:, gda_location:)
|
100
|
-
dstr_node = node.child_nodes[1]
|
101
|
-
return nil unless dstr_node&.dstr_type?
|
102
|
-
|
103
|
-
str_node = find_str_node_from_gda_location(dstr_node: dstr_node, gda_location: gda_location)
|
104
|
-
index = str_node.value.index(gda_location.body)
|
105
|
-
return nil unless index
|
106
|
-
|
107
|
-
str_node_begin_pos(str_node) + index + heredoc_indent_level(node)
|
108
|
-
end
|
109
|
-
|
110
|
-
# @param str_node [RuboCop::AST::StrNode]
|
111
|
-
# @return [Integer]
|
112
|
-
def str_node_begin_pos(str_node)
|
113
|
-
begin_pos = str_node.loc.expression.begin_pos
|
114
|
-
|
115
|
-
# e.g.
|
116
|
-
# db.xquery(
|
117
|
-
# "SELECT * " \
|
118
|
-
# "FROM users " \
|
119
|
-
# "LIMIT 10"
|
120
|
-
# )
|
121
|
-
return begin_pos + 1 if str_node.loc.expression.source_buffer.source[begin_pos] == '"'
|
122
|
-
|
123
|
-
begin_pos
|
124
|
-
end
|
125
|
-
|
126
|
-
# @param dstr_node [RuboCop::AST::DstrNode]
|
127
|
-
# @param gda_location [RuboCop::Isucon::GDA::NodeLocation]
|
128
|
-
# @return [RuboCop::AST::StrNode,nil]
|
129
|
-
def find_str_node_from_gda_location(dstr_node:, gda_location:)
|
130
|
-
return nil unless dstr_node
|
131
|
-
|
132
|
-
begin_pos = 0
|
133
|
-
dstr_node.child_nodes.each do |str_node|
|
134
|
-
return str_node if begin_pos <= gda_location.begin_pos && gda_location.begin_pos < begin_pos + str_node.value.length
|
135
|
-
|
136
|
-
begin_pos += str_node.value.length
|
137
|
-
end
|
138
|
-
nil
|
139
|
-
end
|
140
|
-
|
141
|
-
# @param node [RuboCop::AST::Node]
|
142
|
-
# @return [Integer]
|
143
|
-
def heredoc_indent_level(node)
|
144
|
-
dstr_node = node.child_nodes[1]
|
145
|
-
return 0 unless dstr_node&.dstr_type?
|
146
|
-
|
147
|
-
heredoc_indent_type = heredoc_indent_type(node)
|
148
|
-
return 0 unless heredoc_indent_type == "~"
|
149
|
-
|
150
|
-
heredoc_body = dstr_node.loc.heredoc_body.source
|
151
|
-
indent_level(heredoc_body)
|
152
|
-
end
|
153
|
-
|
154
|
-
# @param str [String]
|
155
|
-
# @return [Integer]
|
156
|
-
# @see https://github.com/rubocop/rubocop/blob/v1.21.0/lib/rubocop/cop/mixin/heredoc.rb#L23-L28
|
157
|
-
def indent_level(str)
|
158
|
-
indentations = str.lines.
|
159
|
-
map { |line| line[/^\s*/] }.
|
160
|
-
reject { |line| line.end_with?("\n") }
|
161
|
-
indentations.empty? ? 0 : indentations.min_by(&:size).size
|
162
|
-
end
|
163
|
-
|
164
|
-
# Returns '~', '-' or nil
|
165
|
-
#
|
166
|
-
# @param node [RuboCop::AST::Node]
|
167
|
-
# @return [String,nil] '~', '-' or `nil`
|
168
|
-
# @see https://github.com/rubocop/rubocop/blob/v1.21.0/lib/rubocop/cop/layout/heredoc_indentation.rb#L146-L149
|
169
|
-
def heredoc_indent_type(node)
|
170
|
-
node.source[/<<([~-])/, 1]
|
171
|
-
end
|
172
63
|
end
|
173
64
|
end
|
174
65
|
end
|