rubocop-isucon 0.1.0 → 0.2.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/CHANGELOG.md +15 -1
- data/README.md +17 -6
- data/config/default.yml +36 -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} +15 -4
- 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 +4 -114
- data/lib/rubocop/cop/isucon/mysql2/select_asterisk.rb +1 -135
- data/lib/rubocop/cop/isucon/mysql2/where_without_index.rb +5 -70
- 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/version.rb +1 -1
- metadata +17 -5
@@ -26,144 +26,10 @@ module RuboCop
|
|
26
26
|
# db.xquery('SELECT users.id, users.name FROM users')
|
27
27
|
#
|
28
28
|
class SelectAsterisk < Base
|
29
|
-
include Mixin::DatabaseMethods
|
30
29
|
include Mixin::Mysql2XqueryMethods
|
30
|
+
include Mixin::SelectAsteriskMethods
|
31
31
|
|
32
32
|
extend AutoCorrector
|
33
|
-
|
34
|
-
MSG = "Use SELECT with column names. (e.g. `SELECT id, name FROM table_name`)"
|
35
|
-
|
36
|
-
TODO = "# TODO: Remove needless columns if necessary\n"
|
37
|
-
|
38
|
-
# @param node [RuboCop::AST::Node]
|
39
|
-
def on_send(node)
|
40
|
-
with_error_handling(node) do
|
41
|
-
with_xquery(node) do |type, root_gda|
|
42
|
-
next unless root_gda
|
43
|
-
|
44
|
-
check_and_register_offence(type: type, root_gda: root_gda, node: node)
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
private
|
50
|
-
|
51
|
-
# @param type [Symbol] Node type. one of `:str`, `:dstr`
|
52
|
-
# @param root_gda [RuboCop::Isucon::GDA::Client]
|
53
|
-
# @param node [RuboCop::AST::Node]
|
54
|
-
def check_and_register_offence(type:, root_gda:, node:)
|
55
|
-
root_gda.visit_all do |gda|
|
56
|
-
next unless gda.ast.respond_to?(:expr_list)
|
57
|
-
|
58
|
-
gda.ast.expr_list.each do |select_field_node|
|
59
|
-
check_and_register_offence_for_select_field_node(
|
60
|
-
type: type, node: node, gda: gda,
|
61
|
-
select_field_node: select_field_node
|
62
|
-
)
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
# @param type [Symbol] Node type. one of `:str`, `:dstr`
|
68
|
-
# @param node [RuboCop::AST::Node]
|
69
|
-
# @param gda [RuboCop::Isucon::GDA::Client]
|
70
|
-
# @param select_field_node [GDA::Nodes::SelectField]
|
71
|
-
def check_and_register_offence_for_select_field_node(type:, node:, gda:, select_field_node:)
|
72
|
-
return unless select_field_node.respond_to?(:expr)
|
73
|
-
|
74
|
-
select_field = parse_select_field_node(select_field_node)
|
75
|
-
|
76
|
-
return unless select_field[:column_name] == "*"
|
77
|
-
|
78
|
-
loc = offense_location(type: type, node: node, gda_location: select_field_node.expr.location)
|
79
|
-
return unless loc
|
80
|
-
|
81
|
-
add_offense(loc) do |corrector|
|
82
|
-
perform_autocorrect(corrector: corrector, loc: loc, gda: gda, node: node,
|
83
|
-
select_table_name: select_field[:table_name])
|
84
|
-
end
|
85
|
-
end
|
86
|
-
|
87
|
-
# @param select_field_node [GDA::Nodes::SelectField]
|
88
|
-
# @return [Hash<Symbol, String>] table_name, column_name
|
89
|
-
def parse_select_field_node(select_field_node)
|
90
|
-
column_elements = select_field_node.expr.value.split(".", 2)
|
91
|
-
|
92
|
-
case column_elements.count
|
93
|
-
when 1
|
94
|
-
return { column_name: column_elements[0] }
|
95
|
-
when 2
|
96
|
-
return { table_name: column_elements[0], column_name: column_elements[1] }
|
97
|
-
end
|
98
|
-
|
99
|
-
{}
|
100
|
-
end
|
101
|
-
|
102
|
-
# @param corrector [RuboCop::Cop::Corrector]
|
103
|
-
# @param loc [Parser::Source::Range]
|
104
|
-
# @param gda [RuboCop::Isucon::GDA::Client]
|
105
|
-
# @param node [RuboCop::AST::Node]
|
106
|
-
# @param select_table_name [String,nil] table names included in the SELECT clause
|
107
|
-
def perform_autocorrect(corrector:, loc:, gda:, node:, select_table_name:)
|
108
|
-
return unless enabled_database?
|
109
|
-
return if gda.table_names.empty?
|
110
|
-
|
111
|
-
if select_table_name
|
112
|
-
return unless gda.table_names.include?(select_table_name)
|
113
|
-
|
114
|
-
replace_asterisk(corrector: corrector, loc: loc, table_name: select_table_name, table_prefix: true)
|
115
|
-
else
|
116
|
-
return unless gda.table_names.length == 1
|
117
|
-
|
118
|
-
replace_asterisk(corrector: corrector, loc: loc, table_name: gda.table_names[0], table_prefix: false)
|
119
|
-
end
|
120
|
-
|
121
|
-
insert_todo_comment(corrector: corrector, node: node)
|
122
|
-
end
|
123
|
-
|
124
|
-
# @param corrector [RuboCop::Cop::Corrector]
|
125
|
-
# @param loc [Parser::Source::Range]
|
126
|
-
# @param table_name [String]
|
127
|
-
# @param table_prefix [Boolean] Whether add table name to prefix (e.g. `users`.`name`)
|
128
|
-
def replace_asterisk(corrector:, loc:, table_name:, table_prefix:)
|
129
|
-
select_columns = columns_in_select_clause(table_name: table_name, table_prefix: table_prefix)
|
130
|
-
corrector.replace(loc, select_columns)
|
131
|
-
end
|
132
|
-
|
133
|
-
# @param table_name [String]
|
134
|
-
# @param table_prefix [Boolean] Whether add table name to prefix (e.g. `users`.`name`)
|
135
|
-
# @return [String]
|
136
|
-
def columns_in_select_clause(table_name:, table_prefix:)
|
137
|
-
column_names = connection.column_names(table_name)
|
138
|
-
|
139
|
-
column_names.map do |column|
|
140
|
-
if table_prefix
|
141
|
-
"`#{table_name}`.`#{column}`"
|
142
|
-
else
|
143
|
-
"`#{column}`"
|
144
|
-
end
|
145
|
-
end.join(", ")
|
146
|
-
end
|
147
|
-
|
148
|
-
# @param corrector [RuboCop::Cop::Corrector]
|
149
|
-
# @param node [RuboCop::AST::Node]
|
150
|
-
def insert_todo_comment(corrector:, node:)
|
151
|
-
current_line = node.loc.expression.line
|
152
|
-
current_line_range = node.loc.expression.source_buffer.line_range(current_line)
|
153
|
-
|
154
|
-
indent = node_indent_level(node)
|
155
|
-
comment_line = (" " * indent) + TODO
|
156
|
-
corrector.insert_before(current_line_range, comment_line)
|
157
|
-
end
|
158
|
-
|
159
|
-
# @param node [RuboCop::AST::Node]
|
160
|
-
# @return [Integer]
|
161
|
-
def node_indent_level(node)
|
162
|
-
node.loc.expression.source_line =~ /^(\s+)/
|
163
|
-
return 0 unless Regexp.last_match(1)
|
164
|
-
|
165
|
-
Regexp.last_match(1).length
|
166
|
-
end
|
167
33
|
end
|
168
34
|
end
|
169
35
|
end
|
@@ -19,84 +19,19 @@ module RuboCop
|
|
19
19
|
# db.xquery('SELECT id, title FROM articles WHERE id = ?', id)
|
20
20
|
#
|
21
21
|
class WhereWithoutIndex < Base
|
22
|
-
include Mixin::DatabaseMethods
|
23
22
|
include Mixin::Mysql2XqueryMethods
|
23
|
+
include Mixin::WhereWithoutIndexMethods
|
24
24
|
|
25
25
|
MSG = "This where clause doesn't seem to have an index. " \
|
26
26
|
"(e.g. `ALTER TABLE %<table_name>s ADD INDEX index_%<column_name>s (%<column_name>s)`)"
|
27
27
|
|
28
|
-
# @param node [RuboCop::AST::Node]
|
29
|
-
def on_send(node)
|
30
|
-
with_error_handling(node) do
|
31
|
-
return unless enabled_database?
|
32
|
-
|
33
|
-
with_xquery(node) do |type, root_gda|
|
34
|
-
next unless root_gda
|
35
|
-
next if exists_index_in_where_clause_columns?(root_gda)
|
36
|
-
|
37
|
-
register_offense(type: type, node: node, root_gda: root_gda)
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
28
|
private
|
43
29
|
|
44
|
-
# @param type [Symbol] Node type. one of `:str`, `:dstr`
|
45
|
-
# @param node [RuboCop::AST::Node]
|
46
|
-
# @param root_gda [RuboCop::Isucon::GDA::Client]
|
47
|
-
def register_offense(type:, node:, root_gda:)
|
48
|
-
root_gda.visit_all do |gda|
|
49
|
-
next if gda.where_nodes.empty?
|
50
|
-
|
51
|
-
loc = offense_location(type: type, node: node, gda_location: gda.where_nodes.first.location)
|
52
|
-
next unless loc
|
53
|
-
|
54
|
-
message = offense_message(gda)
|
55
|
-
add_offense(loc, message: message)
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
# @param gda [RuboCop::Isucon::GDA::Client]
|
60
|
-
def offense_message(gda)
|
61
|
-
column_name = gda.where_conditions[0].column_operand
|
62
|
-
table_name = find_table_name_from_column_name(table_names: gda.table_names, column_name: column_name)
|
63
|
-
format(MSG, table_name: table_name, column_name: column_name)
|
64
|
-
end
|
65
|
-
|
66
|
-
# @param root_gda [RuboCop::Isucon::GDA::Client]
|
67
|
-
# @return [Boolean]
|
68
|
-
def exists_index_in_where_clause_columns?(root_gda)
|
69
|
-
root_gda.visit_all do |gda|
|
70
|
-
gda.table_names.each do |table_name|
|
71
|
-
return true if covered_where_column_in_index?(gda: gda, table_name: table_name)
|
72
|
-
return true if covered_where_column_in_primary_key?(gda: gda, table_name: table_name)
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
false
|
77
|
-
end
|
78
|
-
|
79
|
-
# @param gda [RuboCop::Isucon::GDA::Client]
|
80
|
-
# @param table_name [String]
|
81
|
-
# @return [Boolean]
|
82
|
-
def covered_where_column_in_index?(gda:, table_name:)
|
83
|
-
indexes = connection.indexes(table_name)
|
84
|
-
index_first_columns = indexes.map { |index| index.columns[0] }
|
85
|
-
|
86
|
-
gda.where_conditions.any? do |condition|
|
87
|
-
index_first_columns.include?(condition.column_operand)
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
# @param gda [RuboCop::Isucon::GDA::Client]
|
92
30
|
# @param table_name [String]
|
93
|
-
# @
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
where_columns = gda.where_conditions.map(&:column_operand)
|
99
|
-
primary_keys.all? { |primary_key| where_columns.include?(primary_key) }
|
31
|
+
# @param column_name [String]
|
32
|
+
# @return [String]
|
33
|
+
def generate_offense_message(table_name:, column_name:)
|
34
|
+
format(MSG, table_name: table_name, column_name: column_name)
|
100
35
|
end
|
101
36
|
end
|
102
37
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Isucon
|
6
|
+
module Sqlite3
|
7
|
+
# Check for `JOIN` without index
|
8
|
+
#
|
9
|
+
# @note If `Database` isn't configured, this cop's feature (offense detection and auto-correct) will not be available.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# # bad (user_id is not indexed)
|
13
|
+
# db.execute('SELECT id, title FROM articles JOIN users ON users.id = articles.user_id')
|
14
|
+
#
|
15
|
+
# # good (user_id is indexed)
|
16
|
+
# db.execute('SELECT id, title FROM articles JOIN users ON users.id = articles.user_id')
|
17
|
+
#
|
18
|
+
class JoinWithoutIndex < Base
|
19
|
+
include Mixin::Sqlite3ExecuteMethods
|
20
|
+
include Mixin::JoinWithoutIndexMethods
|
21
|
+
|
22
|
+
MSG = "This join clause doesn't seem to have an index. " \
|
23
|
+
"(e.g. `CREATE INDEX index_%<table_name>s_%<column_name>s ON %<table_name>s (%<column_name>s)`)"
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
# @param table_name [String]
|
28
|
+
# @param column_name [String]
|
29
|
+
# @return [String]
|
30
|
+
def generate_offense_message(table_name:, column_name:)
|
31
|
+
format(MSG, table_name: table_name, column_name: column_name)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Isucon
|
6
|
+
module Sqlite3
|
7
|
+
# Check if SQL contains many JOINs
|
8
|
+
#
|
9
|
+
# @example CountTables: 3 (default)
|
10
|
+
# # bad
|
11
|
+
# totals = db.execute(
|
12
|
+
# "SELECT IFNULL(SUM(`submissions`.`score`), 0) AS `total_score`" \
|
13
|
+
# " FROM `users`" \
|
14
|
+
# " JOIN `registrations` ON `users`.`id` = `registrations`.`user_id`" \
|
15
|
+
# " JOIN `courses` ON `registrations`.`course_id` = `courses`.`id`" \
|
16
|
+
# " LEFT JOIN `classes` ON `courses`.`id` = `classes`.`course_id`" \
|
17
|
+
# " LEFT JOIN `submissions` ON `users`.`id` = `submissions`.`user_id` AND `submissions`.`class_id` = `classes`.`id`" \
|
18
|
+
# " WHERE `courses`.`id` = ?" \
|
19
|
+
# " GROUP BY `users`.`id`",
|
20
|
+
# [course[:id]]
|
21
|
+
# ).map { |_| _[:total_score] }
|
22
|
+
#
|
23
|
+
# # good
|
24
|
+
# registration_users_count =
|
25
|
+
# db.execute("SELECT COUNT(`user_id`) AS cnt FROM `registrations` WHERE `course_id` = ?", [course[:id]]).first[:cnt]
|
26
|
+
#
|
27
|
+
# totals = db.execute(<<~SQL, course[:id]).map { |_| _[:total_score] }
|
28
|
+
# SELECT IFNULL(SUM(`submissions`.`score`), 0) AS `total_score`
|
29
|
+
# FROM `submissions`
|
30
|
+
# JOIN `classes` ON `classes`.`id` = `submissions`.`class_id`
|
31
|
+
# WHERE `classes`.`course_id` = ?
|
32
|
+
# GROUP BY `submissions`.`user_id`
|
33
|
+
# SQL
|
34
|
+
#
|
35
|
+
# if totals.count < registration_users_count
|
36
|
+
# no_submissions_count = registration_users_count - totals.count
|
37
|
+
# totals += [0] * no_submissions_count
|
38
|
+
# end
|
39
|
+
#
|
40
|
+
# @example CountTables: 5
|
41
|
+
# # good
|
42
|
+
# totals = db.execute(
|
43
|
+
# "SELECT IFNULL(SUM(`submissions`.`score`), 0) AS `total_score`" \
|
44
|
+
# " FROM `users`" \
|
45
|
+
# " JOIN `registrations` ON `users`.`id` = `registrations`.`user_id`" \
|
46
|
+
# " JOIN `courses` ON `registrations`.`course_id` = `courses`.`id`" \
|
47
|
+
# " LEFT JOIN `classes` ON `courses`.`id` = `classes`.`course_id`" \
|
48
|
+
# " LEFT JOIN `submissions` ON `users`.`id` = `submissions`.`user_id` AND `submissions`.`class_id` = `classes`.`id`" \
|
49
|
+
# " WHERE `courses`.`id` = ?" \
|
50
|
+
# " GROUP BY `users`.`id`",
|
51
|
+
# [course[:id]]
|
52
|
+
# ).map { |_| _[:total_score] }
|
53
|
+
#
|
54
|
+
class ManyJoinTable < Base
|
55
|
+
include Mixin::Sqlite3ExecuteMethods
|
56
|
+
include Mixin::ManyJoinTableMethods
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Isucon
|
6
|
+
module Sqlite3
|
7
|
+
# rubocop:disable Layout/LineLength
|
8
|
+
|
9
|
+
# Checks that N+1 query is not used
|
10
|
+
#
|
11
|
+
# @note If `Database` isn't configured, auto-correct will not be available. (Only offense detection can be used)
|
12
|
+
#
|
13
|
+
# @note For the number of N+1 queries that can be detected by this cop, there are too few that can be corrected automatically
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# # bad
|
17
|
+
# reservations = db.execute('SELECT * FROM `reservations` WHERE `schedule_id` = ?', [schedule_id]).map do |reservation|
|
18
|
+
# reservation[:user] = db.execute('SELECT * FROM `users` WHERE `id` = ? LIMIT 1', [id]).first
|
19
|
+
# reservation
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# # good
|
23
|
+
# rows = db.xquery(<<~SQL, [schedule_id])
|
24
|
+
# SELECT
|
25
|
+
# r.id AS reservation_id,
|
26
|
+
# r.schedule_id AS reservation_schedule_id,
|
27
|
+
# r.user_id AS reservation_user_id,
|
28
|
+
# r.created_at AS reservation_created_at,
|
29
|
+
# u.id AS user_id,
|
30
|
+
# u.email AS user_email,
|
31
|
+
# u.nickname AS user_nickname,
|
32
|
+
# u.staff AS user_staff,
|
33
|
+
# u.created_at AS user_created_at
|
34
|
+
# FROM `reservations` AS r
|
35
|
+
# INNER JOIN users u ON u.id = r.user_id
|
36
|
+
# WHERE r.schedule_id = ?
|
37
|
+
# SQL
|
38
|
+
#
|
39
|
+
# # bad
|
40
|
+
# courses.map do |course|
|
41
|
+
# teacher = db.execute('SELECT * FROM `users` WHERE `id` = ?', [course[:teacher_id]]).first
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# # good
|
45
|
+
# # This is similar to ActiveRecord's preload
|
46
|
+
# # c.f. https://guides.rubyonrails.org/active_record_querying.html#preload
|
47
|
+
# courses.map do |course|
|
48
|
+
# @users_by_id ||= db.execute('SELECT * FROM `users` WHERE `id` IN (?)', [courses.map { |course| course[:teacher_id] }]).each_with_object({}) { |v, hash| hash[v[:id]] = v }
|
49
|
+
# teacher = @users_by_id[course[:teacher_id]]
|
50
|
+
# end
|
51
|
+
#
|
52
|
+
class NPlusOneQuery < Base
|
53
|
+
# rubocop:enable Layout/LineLength
|
54
|
+
|
55
|
+
include Mixin::Sqlite3ExecuteMethods
|
56
|
+
include Mixin::NPlusOneQueryMethods
|
57
|
+
|
58
|
+
extend AutoCorrector
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# [Boolean]
|
63
|
+
def array_arg?
|
64
|
+
true
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Isucon
|
6
|
+
module Sqlite3
|
7
|
+
# Avoid `SELECT *` in `db.execute`
|
8
|
+
#
|
9
|
+
# @note If `Database` isn't configured, auto-correct will not be available. (Only offense detection can be used)
|
10
|
+
#
|
11
|
+
# @note This cop replaces `SELECT *` with a `SELECT` by the columns present in the table (e.g. `SELECT id, name`),
|
12
|
+
# but does not check whether the columns are actually used.
|
13
|
+
# Please manually delete unused columns after auto corrected
|
14
|
+
#
|
15
|
+
# @example
|
16
|
+
# # bad
|
17
|
+
# db.execute('SELECT * FROM users')
|
18
|
+
#
|
19
|
+
# # bad
|
20
|
+
# db.execute('SELECT users.* FROM users')
|
21
|
+
#
|
22
|
+
# # good
|
23
|
+
# db.execute('SELECT id, name FROM users')
|
24
|
+
#
|
25
|
+
# # good
|
26
|
+
# db.execute('SELECT users.id, users.name FROM users')
|
27
|
+
#
|
28
|
+
class SelectAsterisk < Base
|
29
|
+
include Mixin::Sqlite3ExecuteMethods
|
30
|
+
include Mixin::SelectAsteriskMethods
|
31
|
+
|
32
|
+
extend AutoCorrector
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Isucon
|
6
|
+
module Sqlite3
|
7
|
+
# Check for `WHERE` without index
|
8
|
+
#
|
9
|
+
# @note If `Database` isn't configured, this cop's feature (offense detection and auto-correct) will not be available.
|
10
|
+
#
|
11
|
+
# @example
|
12
|
+
# # bad (user_id is not indexed)
|
13
|
+
# db.execute('SELECT id, title FROM articles WHERE used_id = ?', [user_id])
|
14
|
+
#
|
15
|
+
# # good (user_id is indexed)
|
16
|
+
# db.execute('SELECT id, title FROM articles WHERE used_id = ?', [user_id])
|
17
|
+
#
|
18
|
+
# # good (id is primary key)
|
19
|
+
# db.execute('SELECT id, title FROM articles WHERE id = ?', [id])
|
20
|
+
#
|
21
|
+
class WhereWithoutIndex < Base
|
22
|
+
include Mixin::Sqlite3ExecuteMethods
|
23
|
+
include Mixin::WhereWithoutIndexMethods
|
24
|
+
|
25
|
+
MSG = "This where clause doesn't seem to have an index. " \
|
26
|
+
"(e.g. `CREATE INDEX index_%<table_name>s_%<column_name>s ON %<table_name>s (%<column_name>s)`)"
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
# @param table_name [String]
|
31
|
+
# @param column_name [String]
|
32
|
+
# @return [String]
|
33
|
+
def generate_offense_message(table_name:, column_name:)
|
34
|
+
format(MSG, table_name: table_name, column_name: column_name)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -1,10 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require_relative "isucon/mixin/database_methods"
|
4
|
+
require_relative "isucon/mixin/join_without_index_methods"
|
5
|
+
require_relative "isucon/mixin/offense_location_methods"
|
6
|
+
require_relative "isucon/mixin/many_join_table_methods"
|
4
7
|
require_relative "isucon/mixin/mysql2_xquery_methods"
|
8
|
+
require_relative "isucon/mixin/n_plus_one_query_methods"
|
9
|
+
require_relative "isucon/mixin/select_asterisk_methods"
|
5
10
|
require_relative "isucon/mixin/sinatra_methods"
|
11
|
+
require_relative "isucon/mixin/sqlite3_execute_methods"
|
12
|
+
require_relative "isucon/mixin/where_without_index_methods"
|
6
13
|
|
7
|
-
require_relative "isucon/correctors/
|
14
|
+
require_relative "isucon/correctors/n_plus_one_query_corrector"
|
8
15
|
|
9
16
|
require_relative "isucon/mysql2/join_without_index"
|
10
17
|
require_relative "isucon/mysql2/many_join_table"
|
@@ -18,3 +25,8 @@ require_relative "isucon/sinatra/rack_logger"
|
|
18
25
|
require_relative "isucon/sinatra/serve_static_file"
|
19
26
|
require_relative "isucon/shell/backtick"
|
20
27
|
require_relative "isucon/shell/system"
|
28
|
+
require_relative "isucon/sqlite3/join_without_index"
|
29
|
+
require_relative "isucon/sqlite3/many_join_table"
|
30
|
+
require_relative "isucon/sqlite3/n_plus_one_query"
|
31
|
+
require_relative "isucon/sqlite3/select_asterisk"
|
32
|
+
require_relative "isucon/sqlite3/where_without_index"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rubocop-isucon
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- sue445
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-07-
|
11
|
+
date: 2022-07-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -221,12 +221,19 @@ files:
|
|
221
221
|
- gemfiles/activerecord_6_1.gemfile
|
222
222
|
- gemfiles/activerecord_7_0.gemfile
|
223
223
|
- lib/rubocop-isucon.rb
|
224
|
-
- lib/rubocop/cop/isucon/correctors/
|
225
|
-
- lib/rubocop/cop/isucon/correctors/
|
226
|
-
- lib/rubocop/cop/isucon/correctors/
|
224
|
+
- lib/rubocop/cop/isucon/correctors/n_plus_one_query_corrector.rb
|
225
|
+
- lib/rubocop/cop/isucon/correctors/n_plus_one_query_corrector/correctable_methods.rb
|
226
|
+
- lib/rubocop/cop/isucon/correctors/n_plus_one_query_corrector/replace_methods.rb
|
227
227
|
- lib/rubocop/cop/isucon/mixin/database_methods.rb
|
228
|
+
- lib/rubocop/cop/isucon/mixin/join_without_index_methods.rb
|
229
|
+
- lib/rubocop/cop/isucon/mixin/many_join_table_methods.rb
|
228
230
|
- lib/rubocop/cop/isucon/mixin/mysql2_xquery_methods.rb
|
231
|
+
- lib/rubocop/cop/isucon/mixin/n_plus_one_query_methods.rb
|
232
|
+
- lib/rubocop/cop/isucon/mixin/offense_location_methods.rb
|
233
|
+
- lib/rubocop/cop/isucon/mixin/select_asterisk_methods.rb
|
229
234
|
- lib/rubocop/cop/isucon/mixin/sinatra_methods.rb
|
235
|
+
- lib/rubocop/cop/isucon/mixin/sqlite3_execute_methods.rb
|
236
|
+
- lib/rubocop/cop/isucon/mixin/where_without_index_methods.rb
|
230
237
|
- lib/rubocop/cop/isucon/mysql2/join_without_index.rb
|
231
238
|
- lib/rubocop/cop/isucon/mysql2/many_join_table.rb
|
232
239
|
- lib/rubocop/cop/isucon/mysql2/n_plus_one_query.rb
|
@@ -239,6 +246,11 @@ files:
|
|
239
246
|
- lib/rubocop/cop/isucon/sinatra/logger.rb
|
240
247
|
- lib/rubocop/cop/isucon/sinatra/rack_logger.rb
|
241
248
|
- lib/rubocop/cop/isucon/sinatra/serve_static_file.rb
|
249
|
+
- lib/rubocop/cop/isucon/sqlite3/join_without_index.rb
|
250
|
+
- lib/rubocop/cop/isucon/sqlite3/many_join_table.rb
|
251
|
+
- lib/rubocop/cop/isucon/sqlite3/n_plus_one_query.rb
|
252
|
+
- lib/rubocop/cop/isucon/sqlite3/select_asterisk.rb
|
253
|
+
- lib/rubocop/cop/isucon/sqlite3/where_without_index.rb
|
242
254
|
- lib/rubocop/cop/isucon_cops.rb
|
243
255
|
- lib/rubocop/isucon.rb
|
244
256
|
- lib/rubocop/isucon/database_connection.rb
|