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.
Files changed (45) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +58 -0
  3. data/CODE_OF_CONDUCT.md +84 -0
  4. data/Gemfile +11 -1
  5. data/Gemfile.lock +89 -0
  6. data/LICENSE.txt +21 -0
  7. data/README.md +42 -18
  8. data/Rakefile +9 -3
  9. data/config/default.yml +151 -0
  10. data/data/reserved_words_mysql.txt +750 -0
  11. data/lib/rubocop/cop/migration/add_check_constraint.rb +111 -0
  12. data/lib/rubocop/cop/migration/add_column_with_default_value.rb +229 -0
  13. data/lib/rubocop/cop/migration/add_foreign_key.rb +166 -0
  14. data/lib/rubocop/cop/migration/add_index_columns_count.rb +114 -0
  15. data/lib/rubocop/cop/migration/add_index_concurrently.rb +164 -0
  16. data/lib/rubocop/cop/migration/add_index_duplicate.rb +183 -0
  17. data/lib/rubocop/cop/migration/batch_in_batches.rb +95 -0
  18. data/lib/rubocop/cop/migration/batch_in_transaction.rb +83 -0
  19. data/lib/rubocop/cop/migration/batch_with_throttling.rb +108 -0
  20. data/lib/rubocop/cop/migration/change_column.rb +113 -0
  21. data/lib/rubocop/cop/migration/change_column_null.rb +208 -0
  22. data/lib/rubocop/cop/migration/create_table_force.rb +89 -0
  23. data/lib/rubocop/cop/migration/jsonb.rb +131 -0
  24. data/lib/rubocop/cop/migration/remove_column.rb +246 -0
  25. data/lib/rubocop/cop/migration/rename_column.rb +81 -0
  26. data/lib/rubocop/cop/migration/rename_table.rb +79 -0
  27. data/lib/rubocop/cop/migration/reserved_word_mysql.rb +232 -0
  28. data/lib/rubocop/migration/config_loader.rb +51 -0
  29. data/lib/rubocop/migration/cop_concerns/batch_processing.rb +34 -0
  30. data/lib/rubocop/migration/cop_concerns/column_type_method.rb +28 -0
  31. data/lib/rubocop/migration/cop_concerns/disable_ddl_transaction.rb +51 -0
  32. data/lib/rubocop/migration/cop_concerns.rb +11 -0
  33. data/lib/rubocop/migration/rubocop_extension.rb +15 -0
  34. data/lib/rubocop/migration/version.rb +4 -2
  35. data/lib/rubocop/migration.rb +23 -0
  36. data/rubocop-migration.gemspec +25 -31
  37. metadata +49 -137
  38. data/.gitignore +0 -15
  39. data/bin/console +0 -14
  40. data/bin/setup +0 -8
  41. data/circle.yml +0 -6
  42. data/lib/rubocop/cop/migration/unsafe_migration.rb +0 -69
  43. data/lib/rubocop/migration/strong_migrations_checker.rb +0 -47
  44. data/lib/rubocop-migration.rb +0 -16
  45. data/vendor/.gitkeep +0 -0
@@ -0,0 +1,111 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Migration
6
+ # Activate a check constraint in a separate migration in PostgreSQL.
7
+ #
8
+ # Adding a check constraint without `NOT VALID` blocks reads and writes in Postgres and
9
+ # blocks writes in MySQL and MariaDB while every row is checked.
10
+ #
11
+ # @safety
12
+ # Only meaningful in PostgreSQL.
13
+ #
14
+ # @example
15
+ # # bad
16
+ # class AddCheckConstraintToOrdersPrice < ActiveRecord::Migration[7.0]
17
+ # def change
18
+ # add_check_constraint :orders, 'price > 0', name: 'orders_price_positive'
19
+ # end
20
+ # end
21
+ #
22
+ # # good
23
+ # class AddCheckConstraintToOrdersPriceWithoutValidation < ActiveRecord::Migration[7.0]
24
+ # def change
25
+ # add_check_constraint :orders, 'price > 0', name: 'orders_price_positive', validate: false
26
+ # end
27
+ # end
28
+ #
29
+ # class ActivateCheckConstraintOnOrdersPrice < ActiveRecord::Migration[7.0]
30
+ # def change
31
+ # validate_check_constraint :orders, name: 'orders_price_positive'
32
+ # end
33
+ # end
34
+ class AddCheckConstraint < RuboCop::Cop::Base
35
+ extend AutoCorrector
36
+
37
+ MSG = 'Activate a check constraint in a separate migration in PostgreSQL.'
38
+
39
+ RESTRICT_ON_SEND = %i[
40
+ add_check_constraint
41
+ ].freeze
42
+
43
+ # @param node [RuboCop::AST::SendNode]
44
+ # @return [void]
45
+ def on_send(node)
46
+ return unless bad?(node)
47
+
48
+ add_offense(node) do |corrector|
49
+ autocorrect(corrector, node)
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ # @!method add_check_constraint?(node)
56
+ # @param node [RuboCop::AST::SendNode]
57
+ # @return [Boolean]
58
+ def_node_matcher :add_check_constraint?, <<~PATTERN
59
+ (send
60
+ nil?
61
+ :add_check_constraint
62
+ ...
63
+ )
64
+ PATTERN
65
+
66
+ # @!method add_check_constraint_with_validate_false?(node)
67
+ # @param node [RuboCop::AST::SendNode]
68
+ # @return [Boolean]
69
+ def_node_matcher :add_check_constraint_with_validate_false?, <<~PATTERN
70
+ (send
71
+ nil?
72
+ :add_check_constraint
73
+ _
74
+ _
75
+ (hash
76
+ <
77
+ (pair
78
+ (sym :validate)
79
+ false
80
+ )
81
+ ...
82
+ >
83
+ )
84
+ )
85
+ PATTERN
86
+
87
+ # @param corrector [RuboCop::Cop::Corrector]
88
+ # @param node [RuboCop::AST::SendNode]
89
+ # @return [void]
90
+ def autocorrect(
91
+ corrector,
92
+ node
93
+ )
94
+ target = node.last_argument
95
+ target = target.pairs.last if target.hash_type?
96
+ corrector.insert_after(
97
+ target,
98
+ ', validate: false'
99
+ )
100
+ end
101
+
102
+ # @param node [RuboCop::AST::SendNode]
103
+ # @return [Boolean]
104
+ def bad?(node)
105
+ add_check_constraint?(node) &&
106
+ !add_check_constraint_with_validate_false?(node)
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,229 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Migration
6
+ # Add the column without a default value then change the default.
7
+ #
8
+ # In earlier versions of Postgres, MySQL, and MariaDB,
9
+ # adding a column with a default value to an existing table causes the entire table to be rewritten.
10
+ # During this time, reads and writes are blocked in Postgres, and writes are blocked in MySQL and MariaDB.
11
+ #
12
+ # @safety
13
+ # Only meaningful in earlier versions of Postgres, MySQL, and MariaDB.
14
+ #
15
+ # @example
16
+ # # bad
17
+ # class AddSomeColumnToUsers < ActiveRecord::Migration[7.0]
18
+ # def change
19
+ # add_column :users, :some_column, :string, default: 'some value'
20
+ # end
21
+ # end
22
+ #
23
+ # # good
24
+ # class AddSomeColumnToUsers < ActiveRecord::Migration[7.0]
25
+ # def change
26
+ # add_column :users, :some_column, :string
27
+ # change_column_default :users, :some_column, 'some value'
28
+ # end
29
+ # end
30
+ class AddColumnWithDefaultValue < RuboCop::Cop::Base
31
+ extend AutoCorrector
32
+
33
+ include RangeHelp
34
+ include ::RuboCop::Migration::CopConcerns::ColumnTypeMethod
35
+
36
+ MSG = 'Add the column without a default value then change the default.'
37
+
38
+ RESTRICT_ON_SEND = [
39
+ :add_column,
40
+ *COLUMN_TYPE_METHOD_NAMES
41
+ ].freeze
42
+
43
+ # @param node [RuboCop::AST::SendNode]
44
+ # @return [void]
45
+ def on_send(node)
46
+ return unless target_method?(node)
47
+
48
+ default_option_node = non_nil_default_option_node_from(node)
49
+ return unless default_option_node
50
+
51
+ add_offense(default_option_node) do |corrector|
52
+ autocorrect(
53
+ corrector,
54
+ default_option_node: default_option_node,
55
+ send_node: node
56
+ )
57
+ end
58
+ end
59
+
60
+ private
61
+
62
+ # @!method add_column?(node)
63
+ # @param node [RuboCop::AST::SendNode]
64
+ # @return [Boolean]
65
+ def_node_matcher :add_column?, <<~PATTERN
66
+ (send
67
+ nil?
68
+ :add_column
69
+ ...
70
+ )
71
+ PATTERN
72
+
73
+ # @!method column_type_method?(node)
74
+ # @param node [RuboCop::AST::SendNode]
75
+ # @return [Boolean]
76
+ def_node_matcher :column_type_method?, <<~PATTERN
77
+ (send
78
+ lvar
79
+ COLUMN_TYPE_METHOD_NAMES
80
+ ...
81
+ )
82
+ PATTERN
83
+
84
+ # @!method non_nil_default_option_node_from(node)
85
+ # @param node [RuboCop::AST::SendNode]
86
+ # @return [RuboCop::AST::PairNode, nil]
87
+ def_node_matcher :non_nil_default_option_node_from, <<~PATTERN
88
+ (send
89
+ _
90
+ _
91
+ ...
92
+ (hash
93
+ <
94
+ $(pair
95
+ (sym :default)
96
+ !nil
97
+ )
98
+ >
99
+ ...
100
+ )
101
+ )
102
+ PATTERN
103
+
104
+ # @param corrector [RuboCop::Cop::Corrector]
105
+ # @param default_option_node [RuboCop::AST::PairNode]
106
+ # @param send_node [RuboCop::AST::SendNode]
107
+ # @return [void]
108
+ def autocorrect(
109
+ corrector,
110
+ default_option_node:,
111
+ send_node:
112
+ )
113
+ remove_pair(
114
+ corrector,
115
+ default_option_node
116
+ )
117
+ insert_change_column_default(
118
+ corrector,
119
+ default_option_node: default_option_node,
120
+ send_node: send_node
121
+ )
122
+ end
123
+
124
+ # @param node [RuboCop::AST::SendNode]
125
+ # @return [RuboCop::AST::SymNode]
126
+ def find_column_node_from(node)
127
+ case node.method_name
128
+ when :add_column
129
+ node.arguments[1]
130
+ else
131
+ node.first_argument
132
+ end
133
+ end
134
+
135
+ # @param node [RuboCop::AST::SendNode]
136
+ # @return [RuboCop::AST::SendNode]
137
+ def find_insertion_target_node_from(node)
138
+ case node.method_name
139
+ when :add_column
140
+ node
141
+ else
142
+ node.each_ancestor(:block).first
143
+ end
144
+ end
145
+
146
+ # @param node [RuboCop::AST::SendNode]
147
+ # @return [RuboCop::AST::SymNode]
148
+ def find_table_node_from(node)
149
+ case node.method_name
150
+ when :add_column
151
+ node.first_argument
152
+ else
153
+ node.each_ancestor(:block).first.send_node.first_argument
154
+ end
155
+ end
156
+
157
+ # @param node [RuboCop::AST::SendNode]
158
+ # @return [Boolean]
159
+ def in_change_table?(node)
160
+ node.each_ancestor(:block).first&.method?(:change_table)
161
+ end
162
+
163
+ # @param corrector [RuboCop::Cop::Corrector]
164
+ # @param node [RuboCop::AST::Node]
165
+ # @param string [String]
166
+ def insert_after_with_same_indentation(
167
+ corrector,
168
+ node,
169
+ string
170
+ )
171
+ corrector.insert_after(
172
+ node,
173
+ format(
174
+ "\n%<indentation>s%<string>s",
175
+ indentation: ' ' * node.location.column,
176
+ string: string
177
+ )
178
+ )
179
+ end
180
+
181
+ # @param corrector [RuboCop::Cop::Corrector]
182
+ # @param default_option_node [RuboCop::AST::PairNode]
183
+ # @param send_node [RuboCop::AST::SendNode]
184
+ # @return [void]
185
+ def insert_change_column_default(
186
+ corrector,
187
+ default_option_node:,
188
+ send_node:
189
+ )
190
+ insert_after_with_same_indentation(
191
+ corrector,
192
+ find_insertion_target_node_from(send_node),
193
+ format(
194
+ 'change_column_default %<table>s, %<column>s, %<default>s',
195
+ column: find_column_node_from(send_node).source,
196
+ default: default_option_node.value.source,
197
+ table: find_table_node_from(send_node).source
198
+ )
199
+ )
200
+ end
201
+
202
+ # @param corrector [RuboCop::Cop::Corrector]
203
+ # @param node [RuboCop::AST::Node]
204
+ # @return [void]
205
+ def remove_pair(
206
+ corrector,
207
+ node
208
+ )
209
+ corrector.remove(
210
+ range_with_surrounding_comma(
211
+ range_with_surrounding_space(
212
+ node.location.expression,
213
+ side: :left
214
+ ),
215
+ :left
216
+ )
217
+ )
218
+ end
219
+
220
+ # @param node [RuboCop::AST::SendNode]
221
+ # @return [Boolean]
222
+ def target_method?(node)
223
+ add_column?(node) ||
224
+ (column_type_method?(node) && in_change_table?(node))
225
+ end
226
+ end
227
+ end
228
+ end
229
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Migration
6
+ # Activate foreign key validation in a separate migration in PostgreSQL.
7
+ #
8
+ # To avoid blocking writes on both tables.
9
+ #
10
+ # @safety
11
+ # Only meaningful in PostgreSQL.
12
+ #
13
+ # @example
14
+ # # bad
15
+ # class AddForeignKeyFromArticlesToUsers < ActiveRecord::Migration[7.0]
16
+ # def change
17
+ # add_foreign_key :articles, :users
18
+ # end
19
+ # end
20
+ #
21
+ # # good
22
+ # class AddForeignKeyFromArticlesToUsersWithoutValidation < ActiveRecord::Migration[7.0]
23
+ # def change
24
+ # add_foreign_key :articles, :users, validate: false
25
+ # end
26
+ # end
27
+ #
28
+ # class ActivateForeignKeyValidationFromArticlesToUsers < ActiveRecord::Migration[7.0]
29
+ # def change
30
+ # validate_foreign_key :articles, :users
31
+ # end
32
+ # end
33
+ class AddForeignKey < RuboCop::Cop::Base
34
+ extend AutoCorrector
35
+
36
+ include RangeHelp
37
+
38
+ MSG = 'Activate foreign key validation in a separate migration in PostgreSQL.'
39
+
40
+ RESTRICT_ON_SEND = %i[
41
+ add_foreign_key
42
+ add_reference
43
+ ].freeze
44
+
45
+ # @param node [RuboCop::AST::SendNode]
46
+ # @return [void]
47
+ def on_send(node)
48
+ return unless bad?(node)
49
+
50
+ add_offense(node) do |corrector|
51
+ autocorrect(corrector, node)
52
+ end
53
+ end
54
+
55
+ private
56
+
57
+ # @!method add_foreign_key_without_validate_option?(node)
58
+ # @param node [RuboCop::AST::SendNode]
59
+ # @return [Boolean]
60
+ def_node_matcher :add_foreign_key_without_validate_option?, <<~PATTERN
61
+ (send
62
+ nil?
63
+ :add_foreign_key
64
+ _
65
+ _
66
+ (hash
67
+ (pair
68
+ !(sym :validate)
69
+ _
70
+ )*
71
+ )?
72
+ )
73
+ PATTERN
74
+
75
+ # @!method option_validate_true_value_node_from_add_foreign_key(node)
76
+ # @param node [RuboCop::AST::SendNode]
77
+ # @return [RuboCop::AST::PairNode]
78
+ def_node_matcher :option_validate_true_value_node_from_add_foreign_key, <<~PATTERN
79
+ (send
80
+ nil?
81
+ :add_foreign_key
82
+ _
83
+ _
84
+ (hash
85
+ <
86
+ (pair
87
+ (sym :validate)
88
+ $true
89
+ )
90
+ ...
91
+ >
92
+ )
93
+ )
94
+ PATTERN
95
+
96
+ # @!method option_foreign_key_true_node_from_add_reference(node)
97
+ # @param node [RuboCop::AST::SendNode]
98
+ # @return [RuboCop::AST::PairNode]
99
+ def_node_matcher :option_foreign_key_true_node_from_add_reference, <<~PATTERN
100
+ (send
101
+ nil?
102
+ :add_reference
103
+ _
104
+ _
105
+ (hash
106
+ <
107
+ $(pair
108
+ (sym :foreign_key)
109
+ true
110
+ )
111
+ ...
112
+ >
113
+ )
114
+ )
115
+ PATTERN
116
+
117
+ # @param node [RuboCop::AST::SendNode]
118
+ # @return [Boolean]
119
+ def add_foreign_key_with_validate_option_true?(node)
120
+ option_validate_true_value_node_from_add_foreign_key(node)
121
+ end
122
+
123
+ # @param node [RuboCop::AST::SendNode]
124
+ # @return [Boolean]
125
+ def add_reference_with_validate_option_true?(node)
126
+ option_foreign_key_true_node_from_add_reference(node)
127
+ end
128
+
129
+ # @param corrector [RuboCop::Cop::Corrector]
130
+ # @param node [RuboCop::AST::SendNode]
131
+ # @return [void]
132
+ def autocorrect(
133
+ corrector,
134
+ node
135
+ )
136
+ if add_foreign_key_without_validate_option?(node)
137
+ corrector.insert_after(node.last_argument, ', validate: false')
138
+ elsif add_foreign_key_with_validate_option_true?(node)
139
+ corrector.replace(
140
+ option_validate_true_value_node_from_add_foreign_key(node),
141
+ 'false'
142
+ )
143
+ elsif add_reference_with_validate_option_true?(node)
144
+ corrector.remove(
145
+ range_with_surrounding_comma(
146
+ range_with_surrounding_space(
147
+ option_foreign_key_true_node_from_add_reference(node).location.expression,
148
+ side: :left
149
+ ),
150
+ :left
151
+ )
152
+ )
153
+ end
154
+ end
155
+
156
+ # @param node [RuboCop::AST::SendNode]
157
+ # @return [Boolean]
158
+ def bad?(node)
159
+ add_foreign_key_without_validate_option?(node) ||
160
+ add_foreign_key_with_validate_option_true?(node) ||
161
+ add_reference_with_validate_option_true?(node)
162
+ end
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Migration
6
+ # Keep non-unique index columns count less than a specified number.
7
+ #
8
+ # Adding a non-unique index with more than three columns rarely improves performance.
9
+ # Instead, start an index with columns that narrow down the results the most.
10
+ #
11
+ # @example
12
+ # # bad
13
+ # add_index :users, %i[a b c d]
14
+ #
15
+ # # good (`MaxColumnsCount: 3` by default)
16
+ # add_index :users, %i[a b c]
17
+ class AddIndexColumnsCount < RuboCop::Cop::Base
18
+ RESTRICT_ON_SEND = %i[
19
+ add_index
20
+ index
21
+ ].freeze
22
+
23
+ # @param node [RuboCop::AST::SendNode]
24
+ # @return [void]
25
+ def on_send(node)
26
+ return if with_unique_option?(node)
27
+
28
+ column_names_node = column_names_node_from(node)
29
+ return unless column_names_node
30
+
31
+ column_names_count = columns_count_from(column_names_node)
32
+ return if column_names_count <= max_columns_count
33
+
34
+ add_offense(
35
+ column_names_node,
36
+ message: "Keep unique index columns count less than #{max_columns_count}."
37
+ )
38
+ end
39
+
40
+ private
41
+
42
+ # @!method column_names_node_from_add_index(node)
43
+ # @param node [RuboCop::AST::SendNode]
44
+ # @return [RuboCop::AST::Node, nil]
45
+ def_node_matcher :column_names_node_from_add_index, <<~PATTERN
46
+ (send
47
+ nil?
48
+ :add_index
49
+ _
50
+ $({array | str | sym} ...)
51
+ ...
52
+ )
53
+ PATTERN
54
+
55
+ # @!method column_names_node_from_index(node)
56
+ # @param node [RuboCop::AST::SendNode]
57
+ # @return [RuboCop::AST::Node, nil]
58
+ def_node_matcher :column_names_node_from_index, <<~PATTERN
59
+ (send
60
+ lvar
61
+ :index
62
+ $({array | str | sym} ...)
63
+ ...
64
+ )
65
+ PATTERN
66
+
67
+ # @!method with_unique_option?(node)
68
+ # @param node [RuboCop::AST::SendNode]
69
+ # @return [Boolean]
70
+ def_node_matcher :with_unique_option?, <<~PATTERN
71
+ (send
72
+ ...
73
+ (hash
74
+ <
75
+ (pair
76
+ (sym :unique)
77
+ true
78
+ )
79
+ ...
80
+ >
81
+ )
82
+ )
83
+ PATTERN
84
+
85
+ # @param node [RuboCop::AST::SendNode]
86
+ # @return [RuboCop::AST::Node, nil]
87
+ def column_names_node_from(node)
88
+ case node.method_name
89
+ when :add_index
90
+ column_names_node_from_add_index(node)
91
+ when :index
92
+ column_names_node_from_index(node)
93
+ end
94
+ end
95
+
96
+ # @param node [RuboCop::AST::Node]
97
+ # @return [Integer, nil]
98
+ def columns_count_from(node)
99
+ case node.type
100
+ when :array
101
+ node.values.count
102
+ when :sym
103
+ 1
104
+ end
105
+ end
106
+
107
+ # @return [Integer]
108
+ def max_columns_count
109
+ cop_config['MaxColumnsCount']
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end