rubocop-migration 0.2.0 → 0.4.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 +5 -5
- data/.rubocop.yml +58 -0
- data/CODE_OF_CONDUCT.md +84 -0
- data/Gemfile +11 -1
- data/Gemfile.lock +89 -0
- data/LICENSE.txt +21 -0
- data/README.md +42 -18
- data/Rakefile +9 -3
- data/config/default.yml +151 -0
- data/data/reserved_words_mysql.txt +750 -0
- data/lib/rubocop/cop/migration/add_check_constraint.rb +111 -0
- data/lib/rubocop/cop/migration/add_column_with_default_value.rb +229 -0
- data/lib/rubocop/cop/migration/add_foreign_key.rb +166 -0
- data/lib/rubocop/cop/migration/add_index_columns_count.rb +114 -0
- data/lib/rubocop/cop/migration/add_index_concurrently.rb +164 -0
- data/lib/rubocop/cop/migration/add_index_duplicate.rb +183 -0
- data/lib/rubocop/cop/migration/batch_in_batches.rb +95 -0
- data/lib/rubocop/cop/migration/batch_in_transaction.rb +83 -0
- data/lib/rubocop/cop/migration/batch_with_throttling.rb +108 -0
- data/lib/rubocop/cop/migration/change_column.rb +113 -0
- data/lib/rubocop/cop/migration/change_column_null.rb +208 -0
- data/lib/rubocop/cop/migration/create_table_force.rb +89 -0
- data/lib/rubocop/cop/migration/jsonb.rb +131 -0
- data/lib/rubocop/cop/migration/remove_column.rb +246 -0
- data/lib/rubocop/cop/migration/rename_column.rb +81 -0
- data/lib/rubocop/cop/migration/rename_table.rb +79 -0
- data/lib/rubocop/cop/migration/reserved_word_mysql.rb +232 -0
- data/lib/rubocop/migration/config_loader.rb +51 -0
- data/lib/rubocop/migration/cop_concerns/batch_processing.rb +34 -0
- data/lib/rubocop/migration/cop_concerns/column_type_method.rb +28 -0
- data/lib/rubocop/migration/cop_concerns/disable_ddl_transaction.rb +51 -0
- data/lib/rubocop/migration/cop_concerns.rb +11 -0
- data/lib/rubocop/migration/rubocop_extension.rb +15 -0
- data/lib/rubocop/migration/version.rb +4 -2
- data/lib/rubocop/migration.rb +23 -0
- data/rubocop-migration.gemspec +25 -31
- metadata +49 -137
- data/.gitignore +0 -15
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/circle.yml +0 -6
- data/lib/rubocop/cop/migration/unsafe_migration.rb +0 -69
- data/lib/rubocop/migration/strong_migrations_checker.rb +0 -47
- data/lib/rubocop-migration.rb +0 -16
- data/vendor/.gitkeep +0 -0
@@ -0,0 +1,164 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Migration
|
6
|
+
# Use `algorithm: :concurrently` on adding indexes to existing tables in PostgreSQL.
|
7
|
+
#
|
8
|
+
# To avoid blocking writes.
|
9
|
+
#
|
10
|
+
# @safety
|
11
|
+
# Only meaningful in PostgreSQL.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# # bad
|
15
|
+
# class AddIndexToUsersName < ActiveRecord::Migration[7.0]
|
16
|
+
# def change
|
17
|
+
# add_index :users, :name
|
18
|
+
# end
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# # good
|
22
|
+
# class AddIndexToUsersNameConcurrently < ActiveRecord::Migration[7.0]
|
23
|
+
# disable_ddl_transaction!
|
24
|
+
#
|
25
|
+
# def change
|
26
|
+
# add_index :users, :name, algorithm: :concurrently
|
27
|
+
# end
|
28
|
+
# end
|
29
|
+
class AddIndexConcurrently < RuboCop::Cop::Base
|
30
|
+
extend AutoCorrector
|
31
|
+
|
32
|
+
include ::RuboCop::Migration::CopConcerns::DisableDdlTransaction
|
33
|
+
|
34
|
+
MSG = 'Use `algorithm: :concurrently` on adding indexes to existing tables in PostgreSQL.'
|
35
|
+
|
36
|
+
RESTRICT_ON_SEND = %i[
|
37
|
+
add_index
|
38
|
+
index
|
39
|
+
].freeze
|
40
|
+
|
41
|
+
# @param node [RuboCop::AST::SendNode]
|
42
|
+
# @return [void]
|
43
|
+
def on_send(node)
|
44
|
+
return unless bad?(node)
|
45
|
+
|
46
|
+
add_offense(node) do |corrector|
|
47
|
+
autocorrect(corrector, node)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
# @!method add_index?(node)
|
54
|
+
# @param node [RuboCop::AST::SendNode]
|
55
|
+
# @return [Boolean]
|
56
|
+
def_node_matcher :add_index?, <<~PATTERN
|
57
|
+
(send
|
58
|
+
nil?
|
59
|
+
:add_index
|
60
|
+
_
|
61
|
+
_
|
62
|
+
...
|
63
|
+
)
|
64
|
+
PATTERN
|
65
|
+
|
66
|
+
# @!method add_index_concurrently?(node)
|
67
|
+
# @param node [RuboCop::AST::SendNode]
|
68
|
+
# @return [Boolean]
|
69
|
+
def_node_matcher :add_index_concurrently?, <<~PATTERN
|
70
|
+
(send
|
71
|
+
nil?
|
72
|
+
:add_index
|
73
|
+
_
|
74
|
+
_
|
75
|
+
(hash
|
76
|
+
<
|
77
|
+
(pair
|
78
|
+
(sym :algorithm)
|
79
|
+
(sym :concurrently)
|
80
|
+
)
|
81
|
+
...
|
82
|
+
>
|
83
|
+
)
|
84
|
+
)
|
85
|
+
PATTERN
|
86
|
+
|
87
|
+
# @!method index?(node)
|
88
|
+
# @param node [RuboCop::AST::SendNode]
|
89
|
+
# @return [Boolean]
|
90
|
+
def_node_matcher :index?, <<~PATTERN
|
91
|
+
(send
|
92
|
+
lvar
|
93
|
+
:index
|
94
|
+
_
|
95
|
+
...
|
96
|
+
)
|
97
|
+
PATTERN
|
98
|
+
|
99
|
+
# @!method index_concurrently?(node)
|
100
|
+
# @param node [RuboCop::AST::SendNode]
|
101
|
+
# @return [Boolean]
|
102
|
+
def_node_matcher :index_concurrently?, <<~PATTERN
|
103
|
+
(send
|
104
|
+
lvar
|
105
|
+
:index
|
106
|
+
_
|
107
|
+
(hash
|
108
|
+
<
|
109
|
+
(pair
|
110
|
+
(sym :algorithm)
|
111
|
+
(sym :concurrently)
|
112
|
+
)
|
113
|
+
...
|
114
|
+
>
|
115
|
+
)
|
116
|
+
)
|
117
|
+
PATTERN
|
118
|
+
|
119
|
+
# @param corrector [RuboCop::Cop::Corrector]
|
120
|
+
# @param node [RuboCop::AST::SendNode]
|
121
|
+
# @return [void]
|
122
|
+
def autocorrect(
|
123
|
+
corrector,
|
124
|
+
node
|
125
|
+
)
|
126
|
+
insert_disable_ddl_transaction(corrector, node) unless within_disable_ddl_transaction?(node)
|
127
|
+
insert_algorithm_option(corrector, node)
|
128
|
+
end
|
129
|
+
|
130
|
+
# @param node [RuboCop::AST::SendNode]
|
131
|
+
# @return [Boolean]
|
132
|
+
def bad?(node)
|
133
|
+
case node.method_name
|
134
|
+
when :add_index
|
135
|
+
add_index?(node) && !add_index_concurrently?(node)
|
136
|
+
when :index
|
137
|
+
index?(node) && in_change_table?(node) && !index_concurrently?(node)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# @param node [RuboCop::AST::SendNode]
|
142
|
+
# @return [Boolean]
|
143
|
+
def in_change_table?(node)
|
144
|
+
node.each_ancestor(:block).first&.method?(:change_table)
|
145
|
+
end
|
146
|
+
|
147
|
+
# @param corrector [RuboCop::Cop::Corrector]
|
148
|
+
# @param node [RuboCop::AST::SendNode]
|
149
|
+
# @return [void]
|
150
|
+
def insert_algorithm_option(
|
151
|
+
corrector,
|
152
|
+
node
|
153
|
+
)
|
154
|
+
target_node = node.last_argument
|
155
|
+
target_node = target_node.pairs.last if target_node.hash_type?
|
156
|
+
corrector.insert_after(
|
157
|
+
target_node,
|
158
|
+
', algorithm: :concurrently'
|
159
|
+
)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
@@ -0,0 +1,183 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rubocop/rails/schema_loader'
|
4
|
+
require 'rubocop/rails/schema_loader/schema'
|
5
|
+
|
6
|
+
module RuboCop
|
7
|
+
module Cop
|
8
|
+
module Migration
|
9
|
+
# Avoid adding duplicate indexes.
|
10
|
+
#
|
11
|
+
# @safety
|
12
|
+
# This cop tries to find existing indexes from db/schema.rb, but it cannnot be found.
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# # bad
|
16
|
+
# class AddIndexToUsersNameAndEmail < ActiveRecord::Migration[7.0]
|
17
|
+
# def change
|
18
|
+
# add_index :users, %w[name index]
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# class AddIndexToUsersName < ActiveRecord::Migration[7.0]
|
23
|
+
# def change
|
24
|
+
# add_index :users, :name
|
25
|
+
# end
|
26
|
+
# end
|
27
|
+
class AddIndexDuplicate < RuboCop::Cop::Base
|
28
|
+
MSG = 'Avoid adding duplicate indexes.'
|
29
|
+
|
30
|
+
RESTRICT_ON_SEND = %i[
|
31
|
+
add_index
|
32
|
+
index
|
33
|
+
].freeze
|
34
|
+
|
35
|
+
# @param node [RuboCop::AST::SendNode]
|
36
|
+
# @return [void]
|
37
|
+
def on_send(node)
|
38
|
+
return unless bad?(node)
|
39
|
+
|
40
|
+
add_offense(node)
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
# @!method add_index?(node)
|
46
|
+
# @param node [RuboCop::AST::SendNode]
|
47
|
+
# @return [Boolean]
|
48
|
+
def_node_matcher :add_index?, <<~PATTERN
|
49
|
+
(send
|
50
|
+
nil?
|
51
|
+
:add_index
|
52
|
+
...
|
53
|
+
)
|
54
|
+
PATTERN
|
55
|
+
|
56
|
+
# @!method index?(node)
|
57
|
+
# @param node [RuboCop::AST::SendNode]
|
58
|
+
# @return [Boolean]
|
59
|
+
def_node_matcher :index?, <<~PATTERN
|
60
|
+
(send
|
61
|
+
lvar
|
62
|
+
:index
|
63
|
+
...
|
64
|
+
)
|
65
|
+
PATTERN
|
66
|
+
|
67
|
+
# @param node [RuboCop::AST::SendNode]
|
68
|
+
# @return [Boolean]
|
69
|
+
def adding_duplicated_index?(node)
|
70
|
+
indexed_column_names = indexed_column_names_from(node)
|
71
|
+
return false unless indexed_column_names
|
72
|
+
|
73
|
+
table_name = table_name_from(node)
|
74
|
+
return false unless table_name
|
75
|
+
|
76
|
+
existing_indexes_for(table_name).any? do |existing_index_column_names|
|
77
|
+
leftmost_match?(
|
78
|
+
haystack: existing_index_column_names,
|
79
|
+
needle: indexed_column_names
|
80
|
+
)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# @param node [RuboCop::AST::SendNode]
|
85
|
+
# @return [Boolean]
|
86
|
+
def bad?(node)
|
87
|
+
return false unless target_method?(node)
|
88
|
+
|
89
|
+
adding_duplicated_index?(node)
|
90
|
+
end
|
91
|
+
|
92
|
+
# @param table_name [String]
|
93
|
+
# @return [Array<String>]
|
94
|
+
def existing_indexes_for(table_name)
|
95
|
+
return [] unless schema
|
96
|
+
|
97
|
+
table = schema.table_by(name: table_name)
|
98
|
+
return [] unless table
|
99
|
+
|
100
|
+
table.indices.map do |index|
|
101
|
+
index.columns.map(&:to_s)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
# @param node [RuboCop::AST::SendNode]
|
106
|
+
# @return [Array<String>, nil]
|
107
|
+
def indexed_column_names_from(node)
|
108
|
+
indexed_columns_node = indexed_columns_node_from(node)
|
109
|
+
case indexed_columns_node&.type
|
110
|
+
when :array
|
111
|
+
indexed_columns_node.children.map(&:value).map(&:to_s)
|
112
|
+
when :str, :sym
|
113
|
+
[indexed_columns_node.value.to_s]
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
# @param node [RuboCop::AST::SendNode]
|
118
|
+
# @return [RuboCop::AST::Node, nil]
|
119
|
+
def indexed_columns_node_from(node)
|
120
|
+
case node.method_name
|
121
|
+
when :add_index
|
122
|
+
node.arguments[1]
|
123
|
+
when :index
|
124
|
+
node.arguments[0]
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# @param haystack [Array<String>]
|
129
|
+
# @param needle [Array<String>]
|
130
|
+
# @return [Boolean]
|
131
|
+
def leftmost_match?(
|
132
|
+
haystack:,
|
133
|
+
needle:
|
134
|
+
)
|
135
|
+
haystack.join(',').start_with?(needle.join(','))
|
136
|
+
end
|
137
|
+
|
138
|
+
# @return [RuboCop::Rails::SchemaLoader::Schema, nil]
|
139
|
+
def schema
|
140
|
+
@schema ||= ::RuboCop::Rails::SchemaLoader.load(target_ruby_version)
|
141
|
+
end
|
142
|
+
|
143
|
+
# @param node [RuboCop::AST::SendNode]
|
144
|
+
# @return [String, nil]
|
145
|
+
def table_name_from(node)
|
146
|
+
table_name_value_node = table_name_value_node_from(node)
|
147
|
+
return unless table_name_value_node.respond_to?(:value)
|
148
|
+
|
149
|
+
table_name_value_node.value.to_s
|
150
|
+
end
|
151
|
+
|
152
|
+
# @param node [RuboCop::AST::SendNode]
|
153
|
+
# @return [RuboCop::AST::Node, nil]
|
154
|
+
def table_name_value_node_from(node)
|
155
|
+
case node.method_name
|
156
|
+
when :add_index
|
157
|
+
table_name_value_node_from_add_index(node)
|
158
|
+
when :index
|
159
|
+
table_name_value_node_from_index(node)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# @param node [RuboCop::AST::SendNode]
|
164
|
+
# @return [RuboCop::AST::Node, nil]
|
165
|
+
def table_name_value_node_from_add_index(node)
|
166
|
+
node.first_argument
|
167
|
+
end
|
168
|
+
|
169
|
+
# @param node [RuboCop::AST::SendNode]
|
170
|
+
# @return [RuboCop::AST::Node, nil]
|
171
|
+
def table_name_value_node_from_index(node)
|
172
|
+
node.each_ancestor(:block).first&.send_node&.first_argument
|
173
|
+
end
|
174
|
+
|
175
|
+
# @param node [RuboCop::AST::SendNode]
|
176
|
+
# @return [Boolean]
|
177
|
+
def target_method?(node)
|
178
|
+
add_index?(node) || index?(node)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Migration
|
6
|
+
# Use `in_batches` in batch processing.
|
7
|
+
#
|
8
|
+
# For more efficient batch processing.
|
9
|
+
#
|
10
|
+
# @safety
|
11
|
+
# There are some cases where we should not do that,
|
12
|
+
# or this type of consideration might be already done in a way that we cannot detect.
|
13
|
+
#
|
14
|
+
# @example
|
15
|
+
# # bad
|
16
|
+
# class BackfillSomeColumn < ActiveRecord::Migration[7.0]
|
17
|
+
# disable_ddl_transaction!
|
18
|
+
#
|
19
|
+
# def change
|
20
|
+
# User.update_all(some_column: 'some value')
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# # good
|
25
|
+
# class BackfillSomeColumnToUsers < ActiveRecord::Migration[7.0]
|
26
|
+
# disable_ddl_transaction!
|
27
|
+
#
|
28
|
+
# def up
|
29
|
+
# User.within_in_batches do |relation|
|
30
|
+
# relation.update_all(some_column: 'some value')
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
class BatchInBatches < RuboCop::Cop::Base
|
35
|
+
extend AutoCorrector
|
36
|
+
|
37
|
+
include ::RuboCop::Migration::CopConcerns::BatchProcessing
|
38
|
+
|
39
|
+
MSG = 'Use `in_batches` in batch processing.'
|
40
|
+
|
41
|
+
RESTRICT_ON_SEND = %i[
|
42
|
+
delete_all
|
43
|
+
update_all
|
44
|
+
].freeze
|
45
|
+
|
46
|
+
# @param node [RuboCop::AST::SendNode]
|
47
|
+
# @return [void]
|
48
|
+
def on_send(node)
|
49
|
+
return unless wrong?(node)
|
50
|
+
|
51
|
+
add_offense(node) do |corrector|
|
52
|
+
autocorrect(corrector, node)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
# @param corrector [RuboCop::Cop::Corrector]
|
59
|
+
# @param node [RuboCop::AST::SendNode]
|
60
|
+
# @return [void]
|
61
|
+
def autocorrect(
|
62
|
+
corrector,
|
63
|
+
node
|
64
|
+
)
|
65
|
+
range = node.location.selector.with(
|
66
|
+
end_pos: node.location.expression.end_pos
|
67
|
+
)
|
68
|
+
corrector.replace(
|
69
|
+
range,
|
70
|
+
<<~TEXT.chomp
|
71
|
+
in_batches do |relation|
|
72
|
+
relation.#{range.source}
|
73
|
+
end
|
74
|
+
TEXT
|
75
|
+
)
|
76
|
+
end
|
77
|
+
|
78
|
+
# @param node [RuboCop::AST::Node]
|
79
|
+
# @return [Boolean]
|
80
|
+
def within_in_batches?(node)
|
81
|
+
node.each_ancestor(:block).any? do |ancestor|
|
82
|
+
ancestor.method?(:in_batches)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
# @param node [RuboCop::AST::SendNode]
|
87
|
+
# @return [Boolean]
|
88
|
+
def wrong?(node)
|
89
|
+
batch_processing?(node) &&
|
90
|
+
!within_in_batches?(node)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Migration
|
6
|
+
# Disable transaction in batch processing.
|
7
|
+
#
|
8
|
+
# To avoid locking the table.
|
9
|
+
#
|
10
|
+
# @safety
|
11
|
+
# There are some cases where transaction is really needed.
|
12
|
+
#
|
13
|
+
# @example
|
14
|
+
# # bad
|
15
|
+
# class AddSomeColumnToUsersThenBackfillSomeColumn < ActiveRecord::Migration[7.0]
|
16
|
+
# def change
|
17
|
+
# add_column :users, :some_column, :text
|
18
|
+
# User.update_all(some_column: 'some value')
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# # good
|
23
|
+
# class AddSomeColumnToUsers < ActiveRecord::Migration[7.0]
|
24
|
+
# def change
|
25
|
+
# add_column :users, :some_column, :text
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# class BackfillSomeColumnToUsers < ActiveRecord::Migration[7.0]
|
30
|
+
# disable_ddl_transaction!
|
31
|
+
#
|
32
|
+
# def up
|
33
|
+
# User.unscoped.in_batches do |relation|
|
34
|
+
# relation.update_all(some_column: 'some value')
|
35
|
+
# sleep(0.01)
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
# end
|
39
|
+
class BatchInTransaction < RuboCop::Cop::Base
|
40
|
+
extend AutoCorrector
|
41
|
+
|
42
|
+
include ::RuboCop::Migration::CopConcerns::BatchProcessing
|
43
|
+
include ::RuboCop::Migration::CopConcerns::DisableDdlTransaction
|
44
|
+
|
45
|
+
MSG = 'Disable transaction in batch processing.'
|
46
|
+
|
47
|
+
RESTRICT_ON_SEND = %i[
|
48
|
+
delete_all
|
49
|
+
update_all
|
50
|
+
].freeze
|
51
|
+
|
52
|
+
# @param node [RuboCop::AST::SendNode]
|
53
|
+
# @return [void]
|
54
|
+
def on_send(node)
|
55
|
+
return unless wrong?(node)
|
56
|
+
|
57
|
+
add_offense(node) do |corrector|
|
58
|
+
autocorrect(corrector, node)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
# @param corrector [RuboCop::Cop::Corrector]
|
65
|
+
# @param node [RuboCop::AST::SendNode]
|
66
|
+
# @return [void]
|
67
|
+
def autocorrect(
|
68
|
+
corrector,
|
69
|
+
node
|
70
|
+
)
|
71
|
+
insert_disable_ddl_transaction(corrector, node)
|
72
|
+
end
|
73
|
+
|
74
|
+
# @param node [RuboCop::AST::SendNode]
|
75
|
+
# @return [Boolean]
|
76
|
+
def wrong?(node)
|
77
|
+
batch_processing?(node) &&
|
78
|
+
!within_disable_ddl_transaction?(node)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RuboCop
|
4
|
+
module Cop
|
5
|
+
module Migration
|
6
|
+
# Use throttling in batch processing.
|
7
|
+
#
|
8
|
+
# @safety
|
9
|
+
# There are some cases where we should not do throttling,
|
10
|
+
# or the throttling might be already done in a way that we cannot detect.
|
11
|
+
#
|
12
|
+
# @example
|
13
|
+
# # bad
|
14
|
+
# class BackfillSomeColumn < ActiveRecord::Migration[7.0]
|
15
|
+
# disable_ddl_transaction!
|
16
|
+
#
|
17
|
+
# def change
|
18
|
+
# User.in_batches do |relation|
|
19
|
+
# relation.update_all(some_column: 'some value')
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# # good
|
25
|
+
# class BackfillSomeColumnToUsers < ActiveRecord::Migration[7.0]
|
26
|
+
# disable_ddl_transaction!
|
27
|
+
#
|
28
|
+
# def up
|
29
|
+
# User.in_batches do |relation|
|
30
|
+
# relation.update_all(some_column: 'some value')
|
31
|
+
# sleep(0.01)
|
32
|
+
# end
|
33
|
+
# end
|
34
|
+
# end
|
35
|
+
class BatchWithThrottling < RuboCop::Cop::Base
|
36
|
+
extend AutoCorrector
|
37
|
+
|
38
|
+
include ::RuboCop::Migration::CopConcerns::BatchProcessing
|
39
|
+
|
40
|
+
MSG = 'Use throttling in batch processing.'
|
41
|
+
|
42
|
+
RESTRICT_ON_SEND = %i[
|
43
|
+
delete_all
|
44
|
+
update_all
|
45
|
+
].freeze
|
46
|
+
|
47
|
+
# @param node [RuboCop::AST::SendNode]
|
48
|
+
# @return [void]
|
49
|
+
def on_send(node)
|
50
|
+
return unless wrong?(node)
|
51
|
+
|
52
|
+
add_offense(node) do |corrector|
|
53
|
+
autocorrect(corrector, node)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
# @!method sleep?(node)
|
60
|
+
# @param node [RuboCop::AST::Node]
|
61
|
+
# @return [Boolean]
|
62
|
+
def_node_matcher :sleep?, <<~PATTERN
|
63
|
+
(send
|
64
|
+
nil?
|
65
|
+
:sleep
|
66
|
+
...
|
67
|
+
)
|
68
|
+
PATTERN
|
69
|
+
|
70
|
+
# @param corrector [RuboCop::Cop::Corrector]
|
71
|
+
# @param node [RuboCop::AST::SendNode]
|
72
|
+
# @return [void]
|
73
|
+
def autocorrect(
|
74
|
+
corrector,
|
75
|
+
node
|
76
|
+
)
|
77
|
+
corrector.insert_after(
|
78
|
+
node,
|
79
|
+
"\n#{' ' * node.location.column}sleep(0.01)"
|
80
|
+
)
|
81
|
+
end
|
82
|
+
|
83
|
+
# @param node [RuboCop::AST::Node]
|
84
|
+
# @return [Boolean]
|
85
|
+
def in_block?(node)
|
86
|
+
node.parent&.block_type? ||
|
87
|
+
(node.parent&.begin_type? && node.parent.parent&.block_type?)
|
88
|
+
end
|
89
|
+
|
90
|
+
# @param node [RuboCop::AST::SendNode]
|
91
|
+
# @return [Boolean]
|
92
|
+
def with_throttling?(node)
|
93
|
+
(node.left_siblings + node.right_siblings).any? do |sibling|
|
94
|
+
sleep?(sibling)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
# @param node [RuboCop::AST::SendNode]
|
99
|
+
# @return [Boolean]
|
100
|
+
def wrong?(node)
|
101
|
+
batch_processing?(node) &&
|
102
|
+
in_block?(node) &&
|
103
|
+
!with_throttling?(node)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|