rubocop-isucon 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|