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
@@ -0,0 +1,153 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Isucon
6
+ module Mixin
7
+ # Common methods for {RuboCop::Cop::Isucon::Mysql2::NPlusOneQuery}
8
+ # and {RuboCop::Cop::Isucon::Sqlite3::NPlusOneQuery}
9
+ module NPlusOneQueryMethods
10
+ include Mixin::DatabaseMethods
11
+
12
+ extend NodePattern::Macros
13
+
14
+ MSG = "This looks like N+1 query."
15
+
16
+ # @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L38
17
+ POST_CONDITION_LOOP_TYPES = %i[while_post until_post].freeze
18
+
19
+ # @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L39
20
+ LOOP_TYPES = (POST_CONDITION_LOOP_TYPES + %i[while until for]).freeze
21
+
22
+ # @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L41
23
+ ENUMERABLE_METHOD_NAMES = (Enumerable.instance_methods + [:each]).to_set.freeze
24
+
25
+ def_node_matcher :csv_loop?, <<~PATTERN
26
+ (block
27
+ (send (const nil? :CSV) :parse ...)
28
+ ...)
29
+ PATTERN
30
+
31
+ # @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L68
32
+ def_node_matcher :kernel_loop?, <<~PATTERN
33
+ (block
34
+ (send {nil? (const nil? :Kernel)} :loop)
35
+ ...)
36
+ PATTERN
37
+
38
+ # @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L74
39
+ def_node_matcher :enumerable_loop?, <<~PATTERN
40
+ (block
41
+ (send $_ #enumerable_method? ...)
42
+ ...)
43
+ PATTERN
44
+
45
+ # @param node [RuboCop::AST::Node]
46
+ def on_send(node)
47
+ with_error_handling(node) do
48
+ with_db_query(node) do |type, root_gda|
49
+ check_and_register_offence(node: node, type: type, root_gda: root_gda, is_array_arg: array_arg?)
50
+ end
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ # @param node [RuboCop::AST::Node]
57
+ # @param type [Symbol] Node type. one of `:str`, `:dstr`
58
+ # @param root_gda [RuboCop::Isucon::GDA::Client]
59
+ # @param is_array_arg [Boolean]
60
+ def check_and_register_offence(node:, type:, root_gda:, is_array_arg:) # rubocop:disable Metrics/MethodLength
61
+ return unless db_query_node?(node)
62
+
63
+ receiver, = *node.children
64
+
65
+ parent = parent_loop_node(receiver)
66
+ return unless parent
67
+
68
+ return if or_assignment_to_instance_variable?(node)
69
+
70
+ add_offense(receiver) do |corrector|
71
+ perform_autocorrect(
72
+ corrector: corrector, current_node: receiver,
73
+ parent_node: parent, type: type, gda: root_gda, is_array_arg: is_array_arg
74
+ )
75
+ end
76
+ end
77
+
78
+ # @param node [RuboCop::AST::Node]
79
+ def db_query_node?(node)
80
+ return db_query_methods.include?(node.children[1]) if node.children.count >= 3
81
+
82
+ child = node.children.first
83
+ return false unless child
84
+
85
+ child.children.count >= 3 && db_query_methods.include?(child.children[1])
86
+ end
87
+
88
+ # Whether match to `@instance_var ||=`
89
+ # @param node [RuboCop::AST::Node]
90
+ # @return [Boolean]
91
+ def or_assignment_to_instance_variable?(node)
92
+ _or_assignment_to_instance_variable?(node.parent&.parent) ||
93
+ _or_assignment_to_instance_variable?(node.parent&.parent&.parent)
94
+ end
95
+
96
+ # Whether match to `@instance_var ||=`
97
+ # @param node [RuboCop::AST::Node]
98
+ # @return [Boolean]
99
+ def _or_assignment_to_instance_variable?(node)
100
+ node&.or_asgn_type? && node.child_nodes&.first&.ivasgn_type?
101
+ end
102
+
103
+ # @param node [RuboCop::AST::Node]
104
+ # @return [RuboCop::AST::Node]
105
+ def parent_loop_node(node)
106
+ node.each_ancestor.find { |ancestor| loop?(ancestor, node) }
107
+ end
108
+
109
+ # @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L106
110
+ def loop?(ancestor, node)
111
+ keyword_loop?(ancestor.type) ||
112
+ kernel_loop?(ancestor) ||
113
+ node_within_enumerable_loop?(node, ancestor) ||
114
+ csv_loop?(ancestor)
115
+ end
116
+
117
+ # @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L112
118
+ def keyword_loop?(type)
119
+ LOOP_TYPES.include?(type)
120
+ end
121
+
122
+ # @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L116
123
+ def node_within_enumerable_loop?(node, ancestor)
124
+ enumerable_loop?(ancestor) do |receiver|
125
+ receiver != node && !receiver&.descendants&.include?(node)
126
+ end
127
+ end
128
+
129
+ # @see https://github.com/rubocop/rubocop-performance/blob/v1.11.5/lib/rubocop/cop/performance/collection_literal_in_loop.rb#L130
130
+ def enumerable_method?(method_name)
131
+ ENUMERABLE_METHOD_NAMES.include?(method_name)
132
+ end
133
+
134
+ # @param corrector [RuboCop::Cop::Corrector]
135
+ # @param current_node [RuboCop::AST::Node]
136
+ # @param parent_node [RuboCop::AST::Node]
137
+ # @param type [Symbol] Node type. one of `:str`, `:dstr`
138
+ # @param gda [RuboCop::Isucon::GDA::Client]
139
+ # @param is_array_arg [Boolean]
140
+ def perform_autocorrect(corrector:, current_node:, parent_node:, type:, gda:, is_array_arg:) # rubocop:disable Metrics/ParameterLists
141
+ return unless enabled_database?
142
+
143
+ corrector = Correctors::NPlusOneQueryCorrector.new(
144
+ corrector: corrector, current_node: current_node, is_array_arg: is_array_arg,
145
+ parent_node: parent_node, type: type, gda: gda, connection: connection
146
+ )
147
+ corrector.correct
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Isucon
6
+ module Mixin
7
+ # Calculate offense location from Ruby and SQL ASTs
8
+ module OffenceLocationMethods
9
+ # @param type [Symbol] Node type. one of `:str`, `:dstr`
10
+ # @param node [RuboCop::AST::Node]
11
+ # @param gda_location [RuboCop::Isucon::GDA::NodeLocation]
12
+ # @return [Parser::Source::Range,nil]
13
+ def offense_location(type:, node:, gda_location:)
14
+ return nil unless gda_location
15
+
16
+ begin_pos = begin_position_from_gda_location(type: type, node: node, gda_location: gda_location)
17
+ return nil unless begin_pos
18
+
19
+ end_pos = begin_pos + gda_location.length
20
+ Parser::Source::Range.new(node.loc.expression.source_buffer, begin_pos, end_pos)
21
+ end
22
+
23
+ private
24
+
25
+ # @param type [Symbol] Node type. one of `:str`, `:dstr`
26
+ # @param node [RuboCop::AST::Node]
27
+ # @param gda_location [RuboCop::Isucon::GDA::NodeLocation]
28
+ # @return [Integer,nil]
29
+ def begin_position_from_gda_location(type:, node:, gda_location:)
30
+ case type
31
+ when :str
32
+ return begin_position_from_gda_location_for_str(node: node, gda_location: gda_location)
33
+ when :dstr
34
+ return begin_position_from_gda_location_for_dstr(node: node, gda_location: gda_location)
35
+ end
36
+
37
+ nil
38
+ end
39
+
40
+ # @param node [RuboCop::AST::Node]
41
+ # @param gda_location [RuboCop::Isucon::GDA::NodeLocation]
42
+ # @return [Integer,nil]
43
+ def begin_position_from_gda_location_for_str(node:, gda_location:)
44
+ str_node = node.child_nodes[1]
45
+ return nil unless str_node&.str_type?
46
+
47
+ str_node.loc.begin.end_pos + gda_location.begin_pos
48
+ end
49
+
50
+ # @param node [RuboCop::AST::Node]
51
+ # @param gda_location [RuboCop::Isucon::GDA::NodeLocation]
52
+ # @return [Integer,nil]
53
+ def begin_position_from_gda_location_for_dstr(node:, gda_location:)
54
+ dstr_node = node.child_nodes[1]
55
+ return nil unless dstr_node&.dstr_type?
56
+
57
+ str_node = find_str_node_from_gda_location(dstr_node: dstr_node, gda_location: gda_location)
58
+ index = str_node.value.index(gda_location.body)
59
+ return nil unless index
60
+
61
+ str_node_begin_pos(str_node) + index + heredoc_indent_level(node)
62
+ end
63
+
64
+ # @param str_node [RuboCop::AST::StrNode]
65
+ # @return [Integer]
66
+ def str_node_begin_pos(str_node)
67
+ begin_pos = str_node.loc.expression.begin_pos
68
+
69
+ # e.g.
70
+ # db.xquery(
71
+ # "SELECT * " \
72
+ # "FROM users " \
73
+ # "LIMIT 10"
74
+ # )
75
+ return begin_pos + 1 if str_node.loc.expression.source_buffer.source[begin_pos] == '"'
76
+
77
+ begin_pos
78
+ end
79
+
80
+ # @param dstr_node [RuboCop::AST::DstrNode]
81
+ # @param gda_location [RuboCop::Isucon::GDA::NodeLocation]
82
+ # @return [RuboCop::AST::StrNode,nil]
83
+ def find_str_node_from_gda_location(dstr_node:, gda_location:)
84
+ return nil unless dstr_node
85
+
86
+ begin_pos = 0
87
+ dstr_node.child_nodes.each do |str_node|
88
+ return str_node if begin_pos <= gda_location.begin_pos && gda_location.begin_pos < begin_pos + str_node.value.length
89
+
90
+ begin_pos += str_node.value.length
91
+ end
92
+ nil
93
+ end
94
+
95
+ # @param node [RuboCop::AST::Node]
96
+ # @return [Integer]
97
+ def heredoc_indent_level(node)
98
+ dstr_node = node.child_nodes[1]
99
+ return 0 unless dstr_node&.dstr_type?
100
+
101
+ heredoc_indent_type = heredoc_indent_type(node)
102
+ return 0 unless heredoc_indent_type == "~"
103
+
104
+ heredoc_body = dstr_node.loc.heredoc_body.source
105
+ indent_level(heredoc_body)
106
+ end
107
+
108
+ # @param str [String]
109
+ # @return [Integer]
110
+ # @see https://github.com/rubocop/rubocop/blob/v1.21.0/lib/rubocop/cop/mixin/heredoc.rb#L23-L28
111
+ def indent_level(str)
112
+ indentations = str.lines.
113
+ map { |line| line[/^\s*/] }.
114
+ reject { |line| line.end_with?("\n") }
115
+ indentations.empty? ? 0 : indentations.min_by(&:size).size
116
+ end
117
+
118
+ # Returns '~', '-' or nil
119
+ #
120
+ # @param node [RuboCop::AST::Node]
121
+ # @return [String,nil] '~', '-' or `nil`
122
+ # @see https://github.com/rubocop/rubocop/blob/v1.21.0/lib/rubocop/cop/layout/heredoc_indentation.rb#L146-L149
123
+ def heredoc_indent_type(node)
124
+ node.source[/<<([~-])/, 1]
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,148 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Isucon
6
+ module Mixin
7
+ # Common methods for {RuboCop::Cop::Isucon::Mysql2::SelectAsterisk} and {RuboCop::Cop::Isucon::Sqlite3::SelectAsterisk}
8
+ module SelectAsteriskMethods
9
+ include Mixin::DatabaseMethods
10
+
11
+ MSG = "Use SELECT with column names. (e.g. `SELECT id, name FROM table_name`)"
12
+
13
+ TODO = "# TODO: Remove needless columns if necessary\n"
14
+
15
+ # @param node [RuboCop::AST::Node]
16
+ def on_send(node)
17
+ with_error_handling(node) do
18
+ with_db_query(node) do |type, root_gda|
19
+ check_and_register_offence(type: type, root_gda: root_gda, node: node)
20
+ end
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ # @param type [Symbol] Node type. one of `:str`, `:dstr`
27
+ # @param root_gda [RuboCop::Isucon::GDA::Client]
28
+ # @param node [RuboCop::AST::Node]
29
+ def check_and_register_offence(type:, root_gda:, node:)
30
+ return unless root_gda
31
+
32
+ root_gda.visit_all do |gda|
33
+ next unless gda.ast.respond_to?(:expr_list)
34
+
35
+ gda.ast.expr_list.each do |select_field_node|
36
+ check_and_register_offence_for_select_field_node(
37
+ type: type, node: node, gda: gda,
38
+ select_field_node: select_field_node
39
+ )
40
+ end
41
+ end
42
+ end
43
+
44
+ # @param type [Symbol] Node type. one of `:str`, `:dstr`
45
+ # @param node [RuboCop::AST::Node]
46
+ # @param gda [RuboCop::Isucon::GDA::Client]
47
+ # @param select_field_node [GDA::Nodes::SelectField]
48
+ def check_and_register_offence_for_select_field_node(type:, node:, gda:, select_field_node:)
49
+ return unless select_field_node.respond_to?(:expr)
50
+
51
+ select_field = parse_select_field_node(select_field_node)
52
+
53
+ return unless select_field[:column_name] == "*"
54
+
55
+ loc = offense_location(type: type, node: node, gda_location: select_field_node.expr.location)
56
+ return unless loc
57
+
58
+ add_offense(loc) do |corrector|
59
+ perform_autocorrect(corrector: corrector, loc: loc, gda: gda, node: node,
60
+ select_table_name: select_field[:table_name])
61
+ end
62
+ end
63
+
64
+ # @param select_field_node [GDA::Nodes::SelectField]
65
+ # @return [Hash{Symbol => String}] table_name, column_name
66
+ def parse_select_field_node(select_field_node)
67
+ column_elements = select_field_node.expr.value.split(".", 2)
68
+
69
+ case column_elements.count
70
+ when 1
71
+ return { column_name: column_elements[0] }
72
+ when 2
73
+ return { table_name: column_elements[0], column_name: column_elements[1] }
74
+ end
75
+
76
+ {}
77
+ end
78
+
79
+ # @param corrector [RuboCop::Cop::Corrector]
80
+ # @param loc [Parser::Source::Range]
81
+ # @param gda [RuboCop::Isucon::GDA::Client]
82
+ # @param node [RuboCop::AST::Node]
83
+ # @param select_table_name [String,nil] table names included in the SELECT clause
84
+ def perform_autocorrect(corrector:, loc:, gda:, node:, select_table_name:)
85
+ return unless enabled_database?
86
+ return if gda.table_names.empty?
87
+
88
+ if select_table_name
89
+ return unless gda.table_names.include?(select_table_name)
90
+
91
+ replace_asterisk(corrector: corrector, loc: loc, table_name: select_table_name, table_prefix: true)
92
+ else
93
+ return unless gda.table_names.length == 1
94
+
95
+ replace_asterisk(corrector: corrector, loc: loc, table_name: gda.table_names[0], table_prefix: false)
96
+ end
97
+
98
+ insert_todo_comment(corrector: corrector, node: node)
99
+ end
100
+
101
+ # @param corrector [RuboCop::Cop::Corrector]
102
+ # @param loc [Parser::Source::Range]
103
+ # @param table_name [String]
104
+ # @param table_prefix [Boolean] Whether add table name to prefix (e.g. `users`.`name`)
105
+ def replace_asterisk(corrector:, loc:, table_name:, table_prefix:)
106
+ select_columns = columns_in_select_clause(table_name: table_name, table_prefix: table_prefix)
107
+ corrector.replace(loc, select_columns)
108
+ end
109
+
110
+ # @param table_name [String]
111
+ # @param table_prefix [Boolean] Whether add table name to prefix (e.g. `users`.`name`)
112
+ # @return [String]
113
+ def columns_in_select_clause(table_name:, table_prefix:)
114
+ column_names = connection.column_names(table_name)
115
+
116
+ column_names.map do |column|
117
+ if table_prefix
118
+ "`#{table_name}`.`#{column}`"
119
+ else
120
+ "`#{column}`"
121
+ end
122
+ end.join(", ")
123
+ end
124
+
125
+ # @param corrector [RuboCop::Cop::Corrector]
126
+ # @param node [RuboCop::AST::Node]
127
+ def insert_todo_comment(corrector:, node:)
128
+ current_line = node.loc.expression.line
129
+ current_line_range = node.loc.expression.source_buffer.line_range(current_line)
130
+
131
+ indent = node_indent_level(node)
132
+ comment_line = (" " * indent) + TODO
133
+ corrector.insert_before(current_line_range, comment_line)
134
+ end
135
+
136
+ # @param node [RuboCop::AST::Node]
137
+ # @return [Integer]
138
+ def node_indent_level(node)
139
+ node.loc.expression.source_line =~ /^(\s+)/
140
+ return 0 unless Regexp.last_match(1)
141
+
142
+ Regexp.last_match(1).length
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Isucon
6
+ module Mixin
7
+ # Helper methods for `db.execute` in AST
8
+ module Sqlite3ExecuteMethods
9
+ extend NodePattern::Macros
10
+
11
+ include OffenceLocationMethods
12
+
13
+ # @!method find_xquery(node)
14
+ # @param node [RuboCop::AST::Node]
15
+ def_node_search :find_execute, <<~PATTERN
16
+ (send _ {:execute | :get_first_row} (${str dstr lvar ivar cvar} $...) ...)
17
+ PATTERN
18
+
19
+ NON_STRING_WARNING_MSG = "Warning: non-string was passed to `execute` or `get_first_row` 1st argument. " \
20
+ "So argument doesn't parsed as SQL (%<file_path>s:%<line_num>d)"
21
+
22
+ # @param node [RuboCop::AST::Node]
23
+ # @yieldparam type [Symbol] Node type. one of `:str`, `:dstr`
24
+ # @yieldparam root_gda [RuboCop::Isucon::GDA::Client,nil]
25
+ #
26
+ # @note If arguments of `db.xquery` isn't string, `root_gda` is `nil`
27
+ def with_db_query(node)
28
+ find_execute(node) do |type, params|
29
+ sql = execute_param(type: type, params: params)
30
+
31
+ unless sql
32
+ warn format(NON_STRING_WARNING_MSG, file_path: processed_source.file_path, line_num: node.loc.expression.line)
33
+ end
34
+
35
+ root_gda = sql ? RuboCop::Isucon::GDA::Client.new(sql) : nil
36
+
37
+ yield type, root_gda
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ # @return [Array<Symbol>]
44
+ def db_query_methods
45
+ %i[execute get_first_row]
46
+ end
47
+
48
+ # @param type [Symbol] Node type. one of `:str`, `:dstr`
49
+ # @param params [Array<RuboCop::AST::Node>]
50
+ # @return [String,nil]
51
+ def execute_param(type:, params:)
52
+ case type
53
+ when :str
54
+ return params[0]
55
+ when :dstr
56
+ if params.all? { |param| param.respond_to?(:value) }
57
+ # heredoc
58
+ return params.map(&:value).join
59
+ end
60
+ end
61
+ nil
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Isucon
6
+ module Mixin
7
+ # Common methods for {RuboCop::Cop::Isucon::Mysql2::WhereWithoutIndex}
8
+ # and {RuboCop::Cop::Isucon::Sqlite3::WhereWithoutIndex}
9
+ module WhereWithoutIndexMethods
10
+ include Mixin::DatabaseMethods
11
+
12
+ # @param node [RuboCop::AST::Node]
13
+ def on_send(node)
14
+ with_error_handling(node) do
15
+ return unless enabled_database?
16
+
17
+ with_db_query(node) do |type, root_gda|
18
+ check_and_register_offence(type: type, root_gda: root_gda, node: node)
19
+ end
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ # @param type [Symbol] Node type. one of `:str`, `:dstr`
26
+ # @param root_gda [RuboCop::Isucon::GDA::Client]
27
+ # @param node [RuboCop::AST::Node]
28
+ def check_and_register_offence(type:, root_gda:, node:)
29
+ return unless root_gda
30
+ return if exists_index_in_where_clause_columns?(root_gda)
31
+
32
+ register_offense(type: type, node: node, root_gda: root_gda)
33
+ end
34
+
35
+ # @param type [Symbol] Node type. one of `:str`, `:dstr`
36
+ # @param node [RuboCop::AST::Node]
37
+ # @param root_gda [RuboCop::Isucon::GDA::Client]
38
+ def register_offense(type:, node:, root_gda:)
39
+ root_gda.visit_all do |gda|
40
+ next if gda.where_nodes.empty?
41
+
42
+ loc = offense_location(type: type, node: node, gda_location: gda.where_nodes.first.location)
43
+ next unless loc
44
+
45
+ message = offense_message(gda)
46
+ add_offense(loc, message: message)
47
+ end
48
+ end
49
+
50
+ # @param gda [RuboCop::Isucon::GDA::Client]
51
+ def offense_message(gda)
52
+ column_name = gda.where_conditions[0].column_operand
53
+ table_name = find_table_name_from_column_name(table_names: gda.table_names, column_name: column_name)
54
+ generate_offense_message(table_name: table_name, column_name: column_name)
55
+ end
56
+
57
+ # @param root_gda [RuboCop::Isucon::GDA::Client]
58
+ # @return [Boolean]
59
+ def exists_index_in_where_clause_columns?(root_gda)
60
+ root_gda.visit_all do |gda|
61
+ gda.table_names.each do |table_name|
62
+ return true if covered_where_column_in_index?(gda: gda, table_name: table_name)
63
+ return true if covered_where_column_in_primary_key?(gda: gda, table_name: table_name)
64
+ end
65
+ end
66
+
67
+ false
68
+ end
69
+
70
+ # @param gda [RuboCop::Isucon::GDA::Client]
71
+ # @param table_name [String]
72
+ # @return [Boolean]
73
+ def covered_where_column_in_index?(gda:, table_name:)
74
+ indexes = connection.indexes(table_name)
75
+ index_first_columns = indexes.map { |index| index.columns[0] }
76
+
77
+ gda.where_conditions.any? do |condition|
78
+ index_first_columns.include?(condition.column_operand)
79
+ end
80
+ end
81
+
82
+ # @param gda [RuboCop::Isucon::GDA::Client]
83
+ # @param table_name [String]
84
+ # @return [Boolean]
85
+ def covered_where_column_in_primary_key?(gda:, table_name:)
86
+ primary_keys = connection.primary_keys(table_name)
87
+ return false if primary_keys.empty?
88
+
89
+ where_columns = gda.where_conditions.map(&:column_operand)
90
+ primary_keys.all? { |primary_key| where_columns.include?(primary_key) }
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -16,82 +16,19 @@ module RuboCop
16
16
  # db.xquery('SELECT id, title FROM articles JOIN users ON users.id = articles.user_id')
17
17
  #
18
18
  class JoinWithoutIndex < Base
19
- include Mixin::DatabaseMethods
20
19
  include Mixin::Mysql2XqueryMethods
20
+ include Mixin::JoinWithoutIndexMethods
21
21
 
22
22
  MSG = "This join clause doesn't seem to have an index. " \
23
23
  "(e.g. `ALTER TABLE %<table_name>s ADD INDEX index_%<column_name>s (%<column_name>s)`)"
24
24
 
25
- # @param node [RuboCop::AST::Node]
26
- def on_send(node)
27
- with_error_handling(node) do
28
- return unless enabled_database?
29
-
30
- with_xquery(node) do |type, root_gda|
31
- check_and_register_offence(type: type, root_gda: root_gda, node: node)
32
- end
33
- end
34
- end
35
-
36
25
  private
37
26
 
38
- # @param type [Symbol] Node type. one of `:str`, `:dstr`
39
- # @param root_gda [RuboCop::Isucon::GDA::Client]
40
- # @param node [RuboCop::AST::Node]
41
- def check_and_register_offence(type:, root_gda:, node:)
42
- return unless root_gda
43
-
44
- root_gda.visit_all do |gda|
45
- gda.join_conditions.each do |join_condition|
46
- join_operand = join_operand_without_index(join_condition)
47
- next unless join_operand
48
-
49
- register_offense(type: type, node: node, join_operand: join_operand)
50
- end
51
- end
52
- end
53
-
54
- # @param join_condition [RuboCop::Isucon::GDA::JoinCondition]
55
- # @return [RuboCop::Isucon::GDA::JoinOperand,nil]
56
- def join_operand_without_index(join_condition)
57
- join_condition.operands.each do |join_operand|
58
- next unless join_operand.table_name
59
-
60
- unless indexed_column?(table_name: join_operand.table_name, column_name: join_operand.column_name)
61
- return join_operand
62
- end
63
- end
64
-
65
- nil
66
- end
67
-
68
27
  # @param table_name [String]
69
28
  # @param column_name [String]
70
- # @return [Boolean]
71
- def indexed_column?(table_name:, column_name:)
72
- primary_keys = connection.primary_keys(table_name)
73
-
74
- return true if primary_keys&.first == column_name
75
-
76
- indexes = connection.indexes(table_name)
77
- index_first_columns = indexes.map { |index| index.columns[0] }
78
- index_first_columns.include?(column_name)
79
- end
80
-
81
- # @param type [Symbol] Node type. one of `:str`, `:dstr`
82
- # @param node [RuboCop::AST::Node]
83
- # @param join_operand [RuboCop::Isucon::GDA::JoinOperand]
84
- def register_offense(type:, node:, join_operand:)
85
- loc = offense_location(type: type, node: node, gda_location: join_operand.node.location)
86
- return unless loc
87
-
88
- message = offense_message(join_operand)
89
- add_offense(loc, message: message)
90
- end
91
-
92
- # @param join_operand [RuboCop::Isucon::GDA::JoinOperand]
93
- def offense_message(join_operand)
94
- format(MSG, table_name: join_operand.table_name, column_name: join_operand.column_name)
29
+ # @return [String]
30
+ def generate_offense_message(table_name:, column_name:)
31
+ format(MSG, table_name: table_name, column_name: column_name)
95
32
  end
96
33
  end
97
34
  end