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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.github/dependabot.yml +10 -0
  3. data/.github/workflows/pages.yml +68 -0
  4. data/.github/workflows/test.yml +3 -7
  5. data/.rubocop.yml +5 -1
  6. data/CHANGELOG.md +21 -1
  7. data/README.md +24 -6
  8. data/config/default.yml +37 -1
  9. data/gemfiles/activerecord_7_1.gemfile +14 -0
  10. data/lib/rubocop/cop/isucon/correctors/{mysql2_n_plus_one_query_corrector → n_plus_one_query_corrector}/correctable_methods.rb +1 -1
  11. data/lib/rubocop/cop/isucon/correctors/{mysql2_n_plus_one_query_corrector → n_plus_one_query_corrector}/replace_methods.rb +1 -1
  12. data/lib/rubocop/cop/isucon/correctors/{mysql2_n_plus_one_query_corrector.rb → n_plus_one_query_corrector.rb} +16 -5
  13. data/lib/rubocop/cop/isucon/mixin/join_without_index_methods.rb +87 -0
  14. data/lib/rubocop/cop/isucon/mixin/many_join_table_methods.rb +39 -0
  15. data/lib/rubocop/cop/isucon/mixin/mysql2_xquery_methods.rb +7 -116
  16. data/lib/rubocop/cop/isucon/mixin/n_plus_one_query_methods.rb +153 -0
  17. data/lib/rubocop/cop/isucon/mixin/offense_location_methods.rb +130 -0
  18. data/lib/rubocop/cop/isucon/mixin/select_asterisk_methods.rb +148 -0
  19. data/lib/rubocop/cop/isucon/mixin/sqlite3_execute_methods.rb +67 -0
  20. data/lib/rubocop/cop/isucon/mixin/where_without_index_methods.rb +96 -0
  21. data/lib/rubocop/cop/isucon/mysql2/join_without_index.rb +4 -67
  22. data/lib/rubocop/cop/isucon/mysql2/many_join_table.rb +1 -26
  23. data/lib/rubocop/cop/isucon/mysql2/n_plus_one_query.rb +5 -115
  24. data/lib/rubocop/cop/isucon/mysql2/select_asterisk.rb +1 -135
  25. data/lib/rubocop/cop/isucon/mysql2/where_without_index.rb +7 -72
  26. data/lib/rubocop/cop/isucon/shell/backtick.rb +7 -0
  27. data/lib/rubocop/cop/isucon/sqlite3/join_without_index.rb +37 -0
  28. data/lib/rubocop/cop/isucon/sqlite3/many_join_table.rb +61 -0
  29. data/lib/rubocop/cop/isucon/sqlite3/n_plus_one_query.rb +70 -0
  30. data/lib/rubocop/cop/isucon/sqlite3/select_asterisk.rb +37 -0
  31. data/lib/rubocop/cop/isucon/sqlite3/where_without_index.rb +40 -0
  32. data/lib/rubocop/cop/isucon_cops.rb +13 -1
  33. data/lib/rubocop/isucon/gda/client.rb +1 -1
  34. data/lib/rubocop/isucon/gda/join_operand.rb +1 -1
  35. data/lib/rubocop/isucon/version.rb +1 -1
  36. data/rubocop-isucon.gemspec +3 -2
  37. metadata +38 -10
  38. 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 is not used
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
- # Whether match to `@instance_var ||=`
116
- # @param node [RuboCop::AST::Node]
117
- # @return [Boolean]
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 used_id = ?', user_id)
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 used_id = ?', user_id)
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
- # @return [Boolean]
94
- def covered_where_column_in_primary_key?(gda:, table_name:)
95
- primary_keys = connection.primary_keys(table_name)
96
- return false if primary_keys.empty?
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