rubocop-isucon 0.1.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 +7 -0
- data/.github/workflows/gh-pages.yml +44 -0
- data/.github/workflows/test.yml +91 -0
- data/.gitignore +13 -0
- data/.rspec +3 -0
- data/.rubocop.yml +43 -0
- data/.yardopts +7 -0
- data/CHANGELOG.md +6 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +108 -0
- data/Rakefile +35 -0
- data/benchmark/README.md +69 -0
- data/benchmark/memorize.rb +86 -0
- data/benchmark/parse_table.rb +103 -0
- data/benchmark/shell.rb +26 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/config/default.yml +83 -0
- data/config/enable-only-performance.yml +30 -0
- data/gemfiles/activerecord_6_1.gemfile +14 -0
- data/gemfiles/activerecord_7_0.gemfile +14 -0
- data/lib/rubocop/cop/isucon/correctors/mysql2_n_plus_one_query_corrector/correctable_methods.rb +66 -0
- data/lib/rubocop/cop/isucon/correctors/mysql2_n_plus_one_query_corrector/replace_methods.rb +127 -0
- data/lib/rubocop/cop/isucon/correctors/mysql2_n_plus_one_query_corrector.rb +112 -0
- data/lib/rubocop/cop/isucon/mixin/database_methods.rb +59 -0
- data/lib/rubocop/cop/isucon/mixin/mysql2_xquery_methods.rb +176 -0
- data/lib/rubocop/cop/isucon/mixin/sinatra_methods.rb +37 -0
- data/lib/rubocop/cop/isucon/mysql2/join_without_index.rb +100 -0
- data/lib/rubocop/cop/isucon/mysql2/many_join_table.rb +86 -0
- data/lib/rubocop/cop/isucon/mysql2/n_plus_one_query.rb +179 -0
- data/lib/rubocop/cop/isucon/mysql2/prepare_execute.rb +136 -0
- data/lib/rubocop/cop/isucon/mysql2/select_asterisk.rb +171 -0
- data/lib/rubocop/cop/isucon/mysql2/where_without_index.rb +105 -0
- data/lib/rubocop/cop/isucon/shell/backtick.rb +36 -0
- data/lib/rubocop/cop/isucon/shell/system.rb +36 -0
- data/lib/rubocop/cop/isucon/sinatra/disable_logging.rb +83 -0
- data/lib/rubocop/cop/isucon/sinatra/logger.rb +52 -0
- data/lib/rubocop/cop/isucon/sinatra/rack_logger.rb +58 -0
- data/lib/rubocop/cop/isucon/sinatra/serve_static_file.rb +73 -0
- data/lib/rubocop/cop/isucon_cops.rb +20 -0
- data/lib/rubocop/isucon/database_connection.rb +42 -0
- data/lib/rubocop/isucon/gda/client.rb +184 -0
- data/lib/rubocop/isucon/gda/gda_ext.rb +119 -0
- data/lib/rubocop/isucon/gda/join_condition.rb +25 -0
- data/lib/rubocop/isucon/gda/join_operand.rb +46 -0
- data/lib/rubocop/isucon/gda/node_location.rb +42 -0
- data/lib/rubocop/isucon/gda/node_patcher.rb +101 -0
- data/lib/rubocop/isucon/gda/where_condition.rb +73 -0
- data/lib/rubocop/isucon/gda/where_operand.rb +32 -0
- data/lib/rubocop/isucon/gda.rb +28 -0
- data/lib/rubocop/isucon/inject.rb +20 -0
- data/lib/rubocop/isucon/memorize_methods.rb +38 -0
- data/lib/rubocop/isucon/version.rb +7 -0
- data/lib/rubocop/isucon.rb +20 -0
- data/lib/rubocop-isucon.rb +16 -0
- data/rubocop-isucon.gemspec +52 -0
- metadata +286 -0
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Isucon
|
6
|
+
module Sinatra
|
7
|
+
# Serve static files on front server (e.g. nginx) instead of sinatra app
|
8
|
+
#
|
9
|
+
# @example
|
10
|
+
# # bad
|
11
|
+
# class App < Sinatra::Base
|
12
|
+
# get '/' do
|
13
|
+
# content_type :html
|
14
|
+
# File.read(File.join(__dir__, '..', 'public', 'index.html'))
|
15
|
+
# end
|
16
|
+
# end
|
17
|
+
#
|
18
|
+
# # good (e.g. Serve on nginx)
|
19
|
+
# location / {
|
20
|
+
# try_files $uri $uri/ /index.html;
|
21
|
+
# }
|
22
|
+
#
|
23
|
+
class ServeStaticFile < Base
|
24
|
+
include Mixin::SinatraMethods
|
25
|
+
|
26
|
+
MSG = "Serve static files on front server (e.g. nginx) instead of sinatra app"
|
27
|
+
|
28
|
+
# @!method file_read_method?(node)
|
29
|
+
# @param node [RuboCop::AST::Node]
|
30
|
+
# @return [Boolean]
|
31
|
+
def_node_matcher :file_read_method?, <<~PATTERN
|
32
|
+
(send (const nil? :File) :read ...)
|
33
|
+
PATTERN
|
34
|
+
|
35
|
+
# @!method get_block?(node)
|
36
|
+
# @param node [RuboCop::AST::Node]
|
37
|
+
# @return [Boolean]
|
38
|
+
def_node_matcher :get_block?, <<~PATTERN
|
39
|
+
(block (send nil? :get ...) ...)
|
40
|
+
PATTERN
|
41
|
+
|
42
|
+
# @param node [RuboCop::AST::Node]
|
43
|
+
def on_send(node)
|
44
|
+
return unless parent_is_sinatra_app?(node)
|
45
|
+
return unless file_read_method?(node)
|
46
|
+
|
47
|
+
parent = parent_get_node(node)
|
48
|
+
return unless parent
|
49
|
+
|
50
|
+
return unless end_of_block?(node: node, parent: parent)
|
51
|
+
|
52
|
+
add_offense(parent)
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
# @param node [RuboCop::AST::Node]
|
58
|
+
# @return [RuboCop::AST::Node]
|
59
|
+
def parent_get_node(node)
|
60
|
+
node.each_ancestor.find { |ancestor| get_block?(ancestor) }
|
61
|
+
end
|
62
|
+
|
63
|
+
# @param node [RuboCop::AST::Node]
|
64
|
+
# @param parent [RuboCop::AST::Node]
|
65
|
+
# @return [Boolean]
|
66
|
+
def end_of_block?(node:, parent:)
|
67
|
+
parent.child_nodes.last&.child_nodes&.last == node
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "isucon/mixin/database_methods"
|
4
|
+
require_relative "isucon/mixin/mysql2_xquery_methods"
|
5
|
+
require_relative "isucon/mixin/sinatra_methods"
|
6
|
+
|
7
|
+
require_relative "isucon/correctors/mysql2_n_plus_one_query_corrector"
|
8
|
+
|
9
|
+
require_relative "isucon/mysql2/join_without_index"
|
10
|
+
require_relative "isucon/mysql2/many_join_table"
|
11
|
+
require_relative "isucon/mysql2/n_plus_one_query"
|
12
|
+
require_relative "isucon/mysql2/prepare_execute"
|
13
|
+
require_relative "isucon/mysql2/select_asterisk"
|
14
|
+
require_relative "isucon/mysql2/where_without_index"
|
15
|
+
require_relative "isucon/sinatra/disable_logging"
|
16
|
+
require_relative "isucon/sinatra/logger"
|
17
|
+
require_relative "isucon/sinatra/rack_logger"
|
18
|
+
require_relative "isucon/sinatra/serve_static_file"
|
19
|
+
require_relative "isucon/shell/backtick"
|
20
|
+
require_relative "isucon/shell/system"
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Isucon
|
5
|
+
# Manage database connection
|
6
|
+
class DatabaseConnection
|
7
|
+
# @param database_config [Hash] Same as `ActiveRecord::Base.establish_connection` argument
|
8
|
+
# @see https://api.rubyonrails.org/classes/ActiveRecord/ConnectionHandling.html#method-i-establish_connection
|
9
|
+
def initialize(database_config)
|
10
|
+
ActiveRecord::Base.establish_connection(database_config)
|
11
|
+
@column_names_by_table = {}
|
12
|
+
@indexes_by_table = {}
|
13
|
+
@primary_keys_by_table = {}
|
14
|
+
end
|
15
|
+
|
16
|
+
# @param table_name [String]
|
17
|
+
# @return [Array<String>]
|
18
|
+
def column_names(table_name)
|
19
|
+
@column_names_by_table[table_name] ||= ActiveRecord::Base.connection.columns(table_name).map(&:name)
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param table_name [String]
|
23
|
+
# @return [Array<ActiveRecord::ConnectionAdapters::IndexDefinition>]
|
24
|
+
# @see https://github.com/rails/rails/blob/v6.1.4.1/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb#L8
|
25
|
+
def indexes(table_name)
|
26
|
+
@indexes_by_table[table_name] ||= ActiveRecord::Base.connection.indexes(table_name)
|
27
|
+
end
|
28
|
+
|
29
|
+
# @param table_name [String]
|
30
|
+
# @return [Array<Array<String>>] column names of indexes
|
31
|
+
def unique_index_columns(table_name)
|
32
|
+
indexes(table_name).select(&:unique).map(&:columns)
|
33
|
+
end
|
34
|
+
|
35
|
+
# @param table_name [String]
|
36
|
+
# @return [Array<String>] primary key's column names
|
37
|
+
def primary_keys(table_name)
|
38
|
+
@primary_keys_by_table[table_name] ||= ActiveRecord::Base.connection.primary_keys(table_name)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Isucon
|
5
|
+
module GDA
|
6
|
+
# Client for `GDA`
|
7
|
+
class Client # rubocop:disable Metrics/ClassLength
|
8
|
+
# @return [GDA::Nodes::Select]
|
9
|
+
attr_reader :ast
|
10
|
+
|
11
|
+
# @return [String]
|
12
|
+
attr_reader :sql
|
13
|
+
|
14
|
+
# @param sql [String,nil]
|
15
|
+
# @param ast [GDA::Nodes::Select]
|
16
|
+
# @note if `sql` is `nil`, `ast` is required
|
17
|
+
def initialize(sql, ast: nil)
|
18
|
+
@sql = sql
|
19
|
+
|
20
|
+
if ast
|
21
|
+
# called from subquery AST
|
22
|
+
@ast = ast
|
23
|
+
else
|
24
|
+
# called from root AST
|
25
|
+
@ast = statement.ast
|
26
|
+
RuboCop::Isucon::GDA::NodePatcher.new(sql).accept(@ast)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [Array<String>]
|
31
|
+
def table_names
|
32
|
+
return @table_names if @table_names
|
33
|
+
|
34
|
+
@table_names =
|
35
|
+
if from_targets?
|
36
|
+
ast.from.targets.map(&:table_name).compact.uniq
|
37
|
+
else
|
38
|
+
[]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# @return [Array<RuboCop::Isucon::GDA::WhereCondition>]
|
43
|
+
def where_conditions
|
44
|
+
where_nodes.
|
45
|
+
map do |node|
|
46
|
+
where_operands = node.operands.map do |operand|
|
47
|
+
create_where_operand(operand)
|
48
|
+
end
|
49
|
+
|
50
|
+
WhereCondition.new(
|
51
|
+
operator: node.operator,
|
52
|
+
operands: where_operands,
|
53
|
+
)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# @return [Array<RuboCop::Isucon::GDA::JoinCondition>]
|
58
|
+
def join_conditions
|
59
|
+
return [] unless from_joins?
|
60
|
+
|
61
|
+
ast.from.joins.map do |node|
|
62
|
+
join_operands = node.expr.cond.operands.map do |operand|
|
63
|
+
create_join_operand(operand)
|
64
|
+
end
|
65
|
+
|
66
|
+
JoinCondition.new(
|
67
|
+
operator: node.expr.cond.operator,
|
68
|
+
operands: join_operands,
|
69
|
+
)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# @return [Array<GDA::Nodes::Operation>]
|
74
|
+
def where_nodes
|
75
|
+
return [] unless ast.respond_to?(:where_cond)
|
76
|
+
|
77
|
+
ast.where_cond.to_a.
|
78
|
+
select { |node| node.instance_of?(::GDA::Nodes::Operation) && node.operator }
|
79
|
+
end
|
80
|
+
|
81
|
+
# @return [Hash,nil]
|
82
|
+
def serialize_statement
|
83
|
+
return nil unless @sql
|
84
|
+
|
85
|
+
JSON.parse(statement.serialize)
|
86
|
+
end
|
87
|
+
|
88
|
+
# @yieldparam gda [RuboCop::Isucon::GDA::Client]
|
89
|
+
def visit_subquery_recursive(&block)
|
90
|
+
return unless from_targets?
|
91
|
+
|
92
|
+
ast.from.targets.each do |target|
|
93
|
+
next unless target.expr.select
|
94
|
+
|
95
|
+
gda = Client.new(nil, ast: target.expr.select)
|
96
|
+
yield(gda)
|
97
|
+
gda.visit_subquery_recursive(&block)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
# @yieldparam gda [RuboCop::Isucon::GDA::Client]
|
102
|
+
def visit_all(&block)
|
103
|
+
yield(self)
|
104
|
+
visit_subquery_recursive(&block)
|
105
|
+
end
|
106
|
+
|
107
|
+
# @return [Boolean]
|
108
|
+
def select_query?
|
109
|
+
ast.is_a?(::GDA::Nodes::Select)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Whether `SELECT` clause contains aggregate functions (`COUNT`, `MAX`, `MIN`, `SUM` or `AVG`)
|
113
|
+
# @return [Boolean]
|
114
|
+
def contains_aggregate_functions?
|
115
|
+
aggregate_function_names = %w[COUNT MAX MIN SUM AVG]
|
116
|
+
ast.expr_list.any? do |select_field_node|
|
117
|
+
aggregate_function_names.include?(select_field_node.expr.func&.function_name&.upcase)
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# Whether AST has `GROUP BY` clause
|
122
|
+
# @return [Boolean]
|
123
|
+
def group_by_clause?
|
124
|
+
!ast.group_by.empty?
|
125
|
+
end
|
126
|
+
|
127
|
+
# Whether AST has `LIMIT` clause
|
128
|
+
# @return [Boolean]
|
129
|
+
def limit_clause?
|
130
|
+
!!ast.limit_count
|
131
|
+
end
|
132
|
+
|
133
|
+
# @return [Boolean]
|
134
|
+
def from_joins?
|
135
|
+
ast.respond_to?(:from) && ast.from.respond_to?(:joins)
|
136
|
+
end
|
137
|
+
|
138
|
+
# @return [Boolean]
|
139
|
+
def from_targets?
|
140
|
+
ast.respond_to?(:from) && ast.from.respond_to?(:targets)
|
141
|
+
end
|
142
|
+
|
143
|
+
private
|
144
|
+
|
145
|
+
# @return [GDA::SQL::Statement]
|
146
|
+
# @raise [ArgumentError] called from subquery
|
147
|
+
def statement
|
148
|
+
return @statement if @statement
|
149
|
+
|
150
|
+
raise ArgumentError, "@sql is required" unless @sql
|
151
|
+
|
152
|
+
@statement = ::GDA::SQL::Parser.new.parse(RuboCop::Isucon::GDA.normalize_sql(@sql))
|
153
|
+
end
|
154
|
+
|
155
|
+
# @param operand [GDA::Nodes::Expr]
|
156
|
+
# @return [RuboCop::Isucon::GDA::WhereOperand]
|
157
|
+
def create_where_operand(operand)
|
158
|
+
WhereOperand.new(value: operand.value.gsub(/^.+\./, ""), node: operand)
|
159
|
+
end
|
160
|
+
|
161
|
+
# @param operand [GDA::Nodes::Expr]
|
162
|
+
# @return [RuboCop::Isucon::GDA::JoinOperand]
|
163
|
+
def create_join_operand(operand)
|
164
|
+
table_name_or_as, column_name = operand.value.split(".", 2)
|
165
|
+
|
166
|
+
if (target = from_targets.find { |t| table_name_or_as == t[:table_name] })
|
167
|
+
return JoinOperand.new(table_name: target[:table_name], column_name: column_name, as: nil, node: operand)
|
168
|
+
end
|
169
|
+
|
170
|
+
if (target = from_targets.find { |t| table_name_or_as == t[:as] })
|
171
|
+
return JoinOperand.new(table_name: target[:table_name], column_name: column_name, as: target[:as], node: operand)
|
172
|
+
end
|
173
|
+
|
174
|
+
JoinOperand.new(table_name: nil, column_name: column_name, as: nil, node: operand)
|
175
|
+
end
|
176
|
+
|
177
|
+
# @return [Hash]
|
178
|
+
def from_targets
|
179
|
+
@from_targets ||= ast.from.targets.map { |target| { table_name: target.table_name, as: target.as } }
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
@@ -0,0 +1,119 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
GDA::Nodes::Node.class_eval do
|
4
|
+
# @!attribute [rw] location
|
5
|
+
# @return [RuboCop::Isucon::GDA::NodeLocation]
|
6
|
+
attr_accessor :location
|
7
|
+
|
8
|
+
# @return [String]
|
9
|
+
def inspect
|
10
|
+
# NOTE: Suppress the inclusion of instance variables in `#inspect`
|
11
|
+
encoded_object_id = super[/#<#{self.class.name}:0x([0-9a-z]{16})/, 1]
|
12
|
+
"#<#{self.class.name}:0x#{encoded_object_id}>"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
GDA::Nodes::Select.class_eval do
|
17
|
+
extend RuboCop::Isucon::MemorizeMethods
|
18
|
+
|
19
|
+
memorize :distinct_expr
|
20
|
+
memorize :expr_list
|
21
|
+
memorize :from
|
22
|
+
memorize :where_cond
|
23
|
+
memorize :group_by
|
24
|
+
memorize :having_cond
|
25
|
+
memorize :order_by
|
26
|
+
memorize :limit_count
|
27
|
+
memorize :limit_offset
|
28
|
+
end
|
29
|
+
|
30
|
+
GDA::Nodes::Insert.class_eval do
|
31
|
+
extend RuboCop::Isucon::MemorizeMethods
|
32
|
+
|
33
|
+
memorize :table
|
34
|
+
memorize :fields_list
|
35
|
+
memorize :values_list
|
36
|
+
memorize :select
|
37
|
+
end
|
38
|
+
|
39
|
+
GDA::Nodes::Update.class_eval do
|
40
|
+
extend RuboCop::Isucon::MemorizeMethods
|
41
|
+
|
42
|
+
memorize :table
|
43
|
+
memorize :fields_list
|
44
|
+
memorize :expr_list
|
45
|
+
memorize :cond
|
46
|
+
end
|
47
|
+
|
48
|
+
GDA::Nodes::Join.class_eval do
|
49
|
+
extend RuboCop::Isucon::MemorizeMethods
|
50
|
+
|
51
|
+
memorize :expr
|
52
|
+
memorize :use
|
53
|
+
end
|
54
|
+
|
55
|
+
GDA::Nodes::Delete.class_eval do
|
56
|
+
extend RuboCop::Isucon::MemorizeMethods
|
57
|
+
|
58
|
+
memorize :table
|
59
|
+
memorize :cond
|
60
|
+
end
|
61
|
+
|
62
|
+
GDA::Nodes::SelectField.class_eval do
|
63
|
+
extend RuboCop::Isucon::MemorizeMethods
|
64
|
+
|
65
|
+
memorize :expr
|
66
|
+
end
|
67
|
+
|
68
|
+
GDA::Nodes::Expr.class_eval do
|
69
|
+
extend RuboCop::Isucon::MemorizeMethods
|
70
|
+
|
71
|
+
memorize :func
|
72
|
+
memorize :cond
|
73
|
+
memorize :select
|
74
|
+
memorize :case_s
|
75
|
+
memorize :param_spec
|
76
|
+
end
|
77
|
+
|
78
|
+
GDA::Nodes::From.class_eval do
|
79
|
+
extend RuboCop::Isucon::MemorizeMethods
|
80
|
+
|
81
|
+
memorize :targets
|
82
|
+
memorize :joins
|
83
|
+
end
|
84
|
+
|
85
|
+
GDA::Nodes::Target.class_eval do
|
86
|
+
extend RuboCop::Isucon::MemorizeMethods
|
87
|
+
|
88
|
+
memorize :expr
|
89
|
+
end
|
90
|
+
|
91
|
+
GDA::Nodes::Operation.class_eval do
|
92
|
+
extend RuboCop::Isucon::MemorizeMethods
|
93
|
+
|
94
|
+
memorize :operands
|
95
|
+
end
|
96
|
+
|
97
|
+
GDA::Nodes::Function.class_eval do
|
98
|
+
extend RuboCop::Isucon::MemorizeMethods
|
99
|
+
|
100
|
+
memorize :args_list
|
101
|
+
end
|
102
|
+
|
103
|
+
GDA::Nodes::Order.class_eval do
|
104
|
+
extend RuboCop::Isucon::MemorizeMethods
|
105
|
+
|
106
|
+
memorize :expr
|
107
|
+
end
|
108
|
+
|
109
|
+
GDA::Nodes::Unknown.class_eval do
|
110
|
+
extend RuboCop::Isucon::MemorizeMethods
|
111
|
+
|
112
|
+
memorize :expressions
|
113
|
+
end
|
114
|
+
|
115
|
+
GDA::Nodes::Compound.class_eval do
|
116
|
+
extend RuboCop::Isucon::MemorizeMethods
|
117
|
+
|
118
|
+
memorize :stmt_list
|
119
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Isucon
|
5
|
+
module GDA
|
6
|
+
# response of {RuboCop::Isucon::GDA::Client#join_conditions}
|
7
|
+
class JoinCondition
|
8
|
+
# @!attribute [rw] operator
|
9
|
+
# @return [String]
|
10
|
+
attr_accessor :operator
|
11
|
+
|
12
|
+
# @!attribute [rw] operands
|
13
|
+
# @return [Array<RuboCop::Isucon::GDA::JoinOperand>]
|
14
|
+
attr_accessor :operands
|
15
|
+
|
16
|
+
# @param operator [String]
|
17
|
+
# @param operands [Array<RuboCop::Isucon::GDA::JoinOperand>]
|
18
|
+
def initialize(operator: nil, operands: [])
|
19
|
+
@operator = operator
|
20
|
+
@operands = operands
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Isucon
|
5
|
+
module GDA
|
6
|
+
# response of {RuboCop::Isucon::GDA::Client#join_conditions}
|
7
|
+
class JoinOperand
|
8
|
+
# @!attribute [rw] table_name
|
9
|
+
# @return [String]
|
10
|
+
attr_accessor :table_name
|
11
|
+
|
12
|
+
# @!attribute [rw] column_name
|
13
|
+
# @return [String]
|
14
|
+
attr_accessor :column_name
|
15
|
+
|
16
|
+
# @!attribute [rw] as
|
17
|
+
# @return [String]
|
18
|
+
attr_accessor :as
|
19
|
+
|
20
|
+
# @!attribute [rw] node
|
21
|
+
# @return [GDA::Nodes::Expr]
|
22
|
+
attr_accessor :node
|
23
|
+
|
24
|
+
# @param table_name [String]
|
25
|
+
# @param column_name [String]
|
26
|
+
# @param as [String]
|
27
|
+
# @param node [GDA::Nodes::Expr]
|
28
|
+
def initialize(table_name: nil, column_name: nil, as: nil, node: nil) # rubocop:disable Naming/MethodParameterName
|
29
|
+
@table_name = table_name
|
30
|
+
@column_name = column_name
|
31
|
+
@as = as
|
32
|
+
@node = node
|
33
|
+
end
|
34
|
+
|
35
|
+
# @param other [RuboCop::Isucon::GDA::JoinOperand]
|
36
|
+
# @return [Boolean]
|
37
|
+
def ==(other)
|
38
|
+
other.is_a?(JoinOperand) &&
|
39
|
+
table_name == other.table_name &&
|
40
|
+
column_name == other.column_name &&
|
41
|
+
as == other.as
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Isucon
|
5
|
+
module GDA
|
6
|
+
# Location in SQL
|
7
|
+
class NodeLocation
|
8
|
+
# @return [Integer]
|
9
|
+
attr_reader :begin_pos
|
10
|
+
|
11
|
+
# @return [Integer]
|
12
|
+
attr_reader :end_pos
|
13
|
+
|
14
|
+
# @return [String]
|
15
|
+
attr_reader :body
|
16
|
+
|
17
|
+
# @param begin_pos [Integer]
|
18
|
+
# @param end_pos [Integer]
|
19
|
+
# @param body [String]
|
20
|
+
def initialize(begin_pos:, end_pos:, body:)
|
21
|
+
@begin_pos = begin_pos
|
22
|
+
@end_pos = end_pos
|
23
|
+
@body = body
|
24
|
+
end
|
25
|
+
|
26
|
+
# @param other [RuboCop::Isucon::GDA::NodeLocation]
|
27
|
+
# @return [Boolean]
|
28
|
+
def ==(other)
|
29
|
+
other.is_a?(NodeLocation) &&
|
30
|
+
begin_pos == other.begin_pos &&
|
31
|
+
end_pos == other.end_pos &&
|
32
|
+
body == other.body
|
33
|
+
end
|
34
|
+
|
35
|
+
# @return [Integer]
|
36
|
+
def length
|
37
|
+
end_pos - begin_pos
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Isucon
|
5
|
+
module GDA
|
6
|
+
# Monkey patching to `GDA::Nodes::Node`
|
7
|
+
class NodePatcher < ::GDA::Visitors::Visitor
|
8
|
+
# @param sql [String]
|
9
|
+
def initialize(sql)
|
10
|
+
@sql = sql
|
11
|
+
@normalized_sql = RuboCop::Isucon::GDA.normalize_sql(sql)
|
12
|
+
@current_operation_pos = 0
|
13
|
+
@current_expr_pos = 0
|
14
|
+
super()
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
# @param node [GDA::Nodes::Operation]
|
20
|
+
def visit_GDA_Nodes_Operation(node) # rubocop:disable Naming/MethodName -- This method is called from `GDA::Visitors::Visitor#visit` c.f. https://github.com/tenderlove/gda/blob/v1.1.0/lib/gda/visitors/visitor.rb#L13-L17
|
21
|
+
return super unless node.operator
|
22
|
+
|
23
|
+
pattern = operand_pattern(node)
|
24
|
+
return super unless pattern
|
25
|
+
|
26
|
+
node.location = search_operation_location(pattern)
|
27
|
+
|
28
|
+
super
|
29
|
+
end
|
30
|
+
|
31
|
+
# @param node [GDA::Nodes::Operation]
|
32
|
+
# @return [Regexp,nil]
|
33
|
+
def operand_pattern(node)
|
34
|
+
operand0 = Regexp.escape(node.operands[0].value)
|
35
|
+
operator = Regexp.escape(node.operator)
|
36
|
+
|
37
|
+
case node.operands.count
|
38
|
+
when 1
|
39
|
+
/#{operand0}\s*#{operator}/
|
40
|
+
when 2
|
41
|
+
/#{operand0}\s*#{operator}\s*#{Regexp.escape(node.operands[1].value)}/
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# @param pattern [Regexp]
|
46
|
+
# @return [RuboCop::Isucon::GDA::NodeLocation,nil]
|
47
|
+
def search_operation_location(pattern)
|
48
|
+
result = search_location(pattern, @current_operation_pos)
|
49
|
+
return nil unless result
|
50
|
+
|
51
|
+
@current_operation_pos = result[:current_pos] if result[:current_pos]
|
52
|
+
result[:location]
|
53
|
+
end
|
54
|
+
|
55
|
+
# @param pattern [Regexp]
|
56
|
+
# @param current_pos [Integer]
|
57
|
+
# @return [Hash]
|
58
|
+
def search_location(pattern, current_pos)
|
59
|
+
begin_pos = @normalized_sql.index(pattern, current_pos)
|
60
|
+
|
61
|
+
return nil unless Regexp.last_match
|
62
|
+
|
63
|
+
length = Regexp.last_match[0].length
|
64
|
+
end_pos = begin_pos + length
|
65
|
+
|
66
|
+
begin_pos -= 1 if @sql[begin_pos - 1] == "`"
|
67
|
+
end_pos += 1 if @sql[end_pos] == "`"
|
68
|
+
|
69
|
+
{
|
70
|
+
location: NodeLocation.new(begin_pos: begin_pos, end_pos: end_pos, body: @sql[begin_pos...end_pos]),
|
71
|
+
current_pos: end_pos,
|
72
|
+
}
|
73
|
+
end
|
74
|
+
|
75
|
+
# @param node [GDA::Nodes::Expr]
|
76
|
+
def visit_GDA_Nodes_Expr(node) # rubocop:disable Naming/MethodName -- This method is called from `GDA::Visitors::Visitor#visit` c.f. https://github.com/tenderlove/gda/blob/v1.1.0/lib/gda/visitors/visitor.rb#L13-L17
|
77
|
+
return super unless node.value
|
78
|
+
|
79
|
+
escaped_value = Regexp.escape(node.value).gsub("\\.", "\\s*\\.\\s*")
|
80
|
+
node.location = search_expr_location(/(?<=[\s,])#{escaped_value}(?=[\s,])/)
|
81
|
+
|
82
|
+
begin
|
83
|
+
super
|
84
|
+
rescue RuntimeError => e
|
85
|
+
raise unless e.message.strip == "unknown node type: 505"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# @param pattern [Regexp]
|
90
|
+
# @return [RuboCop::Isucon::GDA::NodeLocation,nil]
|
91
|
+
def search_expr_location(pattern)
|
92
|
+
result = search_location(pattern, @current_expr_pos)
|
93
|
+
return nil unless result
|
94
|
+
|
95
|
+
@current_expr_pos = result[:current_pos] if result[:current_pos]
|
96
|
+
result[:location]
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|