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
@@ -53,32 +53,7 @@ module RuboCop
|
|
53
53
|
#
|
54
54
|
class ManyJoinTable < Base
|
55
55
|
include Mixin::Mysql2XqueryMethods
|
56
|
-
|
57
|
-
MSG = "Avoid SQL with lots of JOINs"
|
58
|
-
|
59
|
-
# @param node [RuboCop::AST::Node]
|
60
|
-
def on_send(node)
|
61
|
-
with_xquery(node) do |_, root_gda|
|
62
|
-
check_and_register_offence(root_gda: root_gda, node: node)
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
private
|
67
|
-
|
68
|
-
# @param root_gda [RuboCop::Isucon::GDA::Client]
|
69
|
-
# @param node [RuboCop::AST::Node]
|
70
|
-
def check_and_register_offence(root_gda:, node:)
|
71
|
-
return unless root_gda
|
72
|
-
|
73
|
-
root_gda.visit_all do |gda|
|
74
|
-
add_offense(node) if gda.table_names.count > count_tables
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
# @return [Integer]
|
79
|
-
def count_tables
|
80
|
-
cop_config["CountTables"]
|
81
|
-
end
|
56
|
+
include Mixin::ManyJoinTableMethods
|
82
57
|
end
|
83
58
|
end
|
84
59
|
end
|
@@ -6,7 +6,7 @@ module RuboCop
|
|
6
6
|
module Mysql2
|
7
7
|
# rubocop:disable Layout/LineLength
|
8
8
|
|
9
|
-
# Checks that N+1 query
|
9
|
+
# Checks that there’s no N+1 query
|
10
10
|
#
|
11
11
|
# @note If `Database` isn't configured, auto-correct will not be available. (Only offense detection can be used)
|
12
12
|
#
|
@@ -51,126 +51,16 @@ module RuboCop
|
|
51
51
|
class NPlusOneQuery < Base
|
52
52
|
# rubocop:enable Layout/LineLength
|
53
53
|
|
54
|
-
include Mixin::DatabaseMethods
|
55
54
|
include Mixin::Mysql2XqueryMethods
|
55
|
+
include Mixin::NPlusOneQueryMethods
|
56
56
|
|
57
57
|
extend AutoCorrector
|
58
58
|
|
59
|
-
MSG = "This looks like N+1 query."
|
60
|
-
|
61
|
-
# @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L38
|
62
|
-
POST_CONDITION_LOOP_TYPES = %i[while_post until_post].freeze
|
63
|
-
|
64
|
-
# @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L39
|
65
|
-
LOOP_TYPES = (POST_CONDITION_LOOP_TYPES + %i[while until for]).freeze
|
66
|
-
|
67
|
-
# @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L41
|
68
|
-
ENUMERABLE_METHOD_NAMES = (Enumerable.instance_methods + [:each]).to_set.freeze
|
69
|
-
|
70
|
-
def_node_matcher :csv_loop?, <<~PATTERN
|
71
|
-
(block
|
72
|
-
(send (const nil? :CSV) :parse ...)
|
73
|
-
...)
|
74
|
-
PATTERN
|
75
|
-
|
76
|
-
# @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L68
|
77
|
-
def_node_matcher :kernel_loop?, <<~PATTERN
|
78
|
-
(block
|
79
|
-
(send {nil? (const nil? :Kernel)} :loop)
|
80
|
-
...)
|
81
|
-
PATTERN
|
82
|
-
|
83
|
-
# @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L74
|
84
|
-
def_node_matcher :enumerable_loop?, <<~PATTERN
|
85
|
-
(block
|
86
|
-
(send $_ #enumerable_method? ...)
|
87
|
-
...)
|
88
|
-
PATTERN
|
89
|
-
|
90
|
-
# @param node [RuboCop::AST::Node]
|
91
|
-
def on_send(node) # rubocop:disable Metrics/MethodLength
|
92
|
-
with_error_handling(node) do
|
93
|
-
with_xquery(node) do |type, root_gda|
|
94
|
-
receiver, = *node.children
|
95
|
-
|
96
|
-
next unless receiver.send_type?
|
97
|
-
|
98
|
-
parent = parent_loop_node(receiver)
|
99
|
-
next unless parent
|
100
|
-
|
101
|
-
next if or_assignment_to_instance_variable?(node)
|
102
|
-
|
103
|
-
add_offense(receiver) do |corrector|
|
104
|
-
perform_autocorrect(
|
105
|
-
corrector: corrector, current_node: receiver,
|
106
|
-
parent_node: parent, type: type, gda: root_gda
|
107
|
-
)
|
108
|
-
end
|
109
|
-
end
|
110
|
-
end
|
111
|
-
end
|
112
|
-
|
113
59
|
private
|
114
60
|
|
115
|
-
#
|
116
|
-
|
117
|
-
|
118
|
-
def or_assignment_to_instance_variable?(node)
|
119
|
-
_or_assignment_to_instance_variable?(node.parent&.parent) ||
|
120
|
-
_or_assignment_to_instance_variable?(node.parent&.parent&.parent)
|
121
|
-
end
|
122
|
-
|
123
|
-
# Whether match to `@instance_var ||=`
|
124
|
-
# @param node [RuboCop::AST::Node]
|
125
|
-
# @return [Boolean]
|
126
|
-
def _or_assignment_to_instance_variable?(node)
|
127
|
-
node&.or_asgn_type? && node.child_nodes&.first&.ivasgn_type?
|
128
|
-
end
|
129
|
-
|
130
|
-
# @param node [RuboCop::AST::Node]
|
131
|
-
# @return [RuboCop::AST::Node]
|
132
|
-
def parent_loop_node(node)
|
133
|
-
node.each_ancestor.find { |ancestor| loop?(ancestor, node) }
|
134
|
-
end
|
135
|
-
|
136
|
-
# @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L106
|
137
|
-
def loop?(ancestor, node)
|
138
|
-
keyword_loop?(ancestor.type) ||
|
139
|
-
kernel_loop?(ancestor) ||
|
140
|
-
node_within_enumerable_loop?(node, ancestor) ||
|
141
|
-
csv_loop?(ancestor)
|
142
|
-
end
|
143
|
-
|
144
|
-
# @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L112
|
145
|
-
def keyword_loop?(type)
|
146
|
-
LOOP_TYPES.include?(type)
|
147
|
-
end
|
148
|
-
|
149
|
-
# @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L116
|
150
|
-
def node_within_enumerable_loop?(node, ancestor)
|
151
|
-
enumerable_loop?(ancestor) do |receiver|
|
152
|
-
receiver != node && !receiver&.descendants&.include?(node)
|
153
|
-
end
|
154
|
-
end
|
155
|
-
|
156
|
-
# @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L130
|
157
|
-
def enumerable_method?(method_name)
|
158
|
-
ENUMERABLE_METHOD_NAMES.include?(method_name)
|
159
|
-
end
|
160
|
-
|
161
|
-
# @param corrector [RuboCop::Cop::Corrector]
|
162
|
-
# @param current_node [RuboCop::AST::Node]
|
163
|
-
# @param parent_node [RuboCop::AST::Node]
|
164
|
-
# @param type [Symbol] Node type. one of `:str`, `:dstr`
|
165
|
-
# @param gda [RuboCop::Isucon::GDA::Client]
|
166
|
-
def perform_autocorrect(corrector:, current_node:, parent_node:, type:, gda:)
|
167
|
-
return unless enabled_database?
|
168
|
-
|
169
|
-
corrector = Correctors::Mysql2NPlusOneQueryCorrector.new(
|
170
|
-
corrector: corrector, current_node: current_node,
|
171
|
-
parent_node: parent_node, type: type, gda: gda, connection: connection
|
172
|
-
)
|
173
|
-
corrector.correct
|
61
|
+
# [Boolean]
|
62
|
+
def array_arg?
|
63
|
+
false
|
174
64
|
end
|
175
65
|
end
|
176
66
|
end
|
@@ -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
|
@@ -10,93 +10,28 @@ module RuboCop
|
|
10
10
|
#
|
11
11
|
# @example
|
12
12
|
# # bad (user_id is not indexed)
|
13
|
-
# db.xquery('SELECT id, title FROM articles WHERE
|
13
|
+
# db.xquery('SELECT id, title FROM articles WHERE user_id = ?', user_id)
|
14
14
|
#
|
15
15
|
# # good (user_id is indexed)
|
16
|
-
# db.xquery('SELECT id, title FROM articles WHERE
|
16
|
+
# db.xquery('SELECT id, title FROM articles WHERE user_id = ?', user_id)
|
17
17
|
#
|
18
18
|
# # good (id is primary key)
|
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
|
@@ -22,6 +22,13 @@ module RuboCop
|
|
22
22
|
# OpenSSL::Digest::SHA512.hexdigest(src)
|
23
23
|
# end
|
24
24
|
#
|
25
|
+
# # bad
|
26
|
+
# `curl -s https://example.com`
|
27
|
+
#
|
28
|
+
# # good
|
29
|
+
# require "open-uri"
|
30
|
+
# URI.open("https://example.com").read
|
31
|
+
#
|
25
32
|
class Backtick < Base
|
26
33
|
MSG = "Use pure-ruby code instead of external command execution if possible"
|
27
34
|
|
@@ -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 there’s no N+1 query
|
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 user_id = ?', [user_id])
|
14
|
+
#
|
15
|
+
# # good (user_id is indexed)
|
16
|
+
# db.execute('SELECT id, title FROM articles WHERE user_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
|