rubocop-migration 0.2.0 → 0.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +5 -5
  2. data/.rubocop.yml +58 -0
  3. data/CHANGELOG.md +5 -0
  4. data/CODE_OF_CONDUCT.md +84 -0
  5. data/Gemfile +11 -1
  6. data/Gemfile.lock +89 -0
  7. data/LICENSE.txt +21 -0
  8. data/README.md +43 -18
  9. data/Rakefile +9 -3
  10. data/config/default.yml +151 -0
  11. data/data/reserved_words_mysql.txt +750 -0
  12. data/lib/rubocop/cop/migration/add_check_constraint.rb +111 -0
  13. data/lib/rubocop/cop/migration/add_column_with_default_value.rb +229 -0
  14. data/lib/rubocop/cop/migration/add_foreign_key.rb +166 -0
  15. data/lib/rubocop/cop/migration/add_index_columns_count.rb +114 -0
  16. data/lib/rubocop/cop/migration/add_index_concurrently.rb +164 -0
  17. data/lib/rubocop/cop/migration/add_index_duplicate.rb +183 -0
  18. data/lib/rubocop/cop/migration/batch_in_batches.rb +95 -0
  19. data/lib/rubocop/cop/migration/batch_in_transaction.rb +83 -0
  20. data/lib/rubocop/cop/migration/batch_with_throttling.rb +108 -0
  21. data/lib/rubocop/cop/migration/change_column.rb +113 -0
  22. data/lib/rubocop/cop/migration/change_column_null.rb +128 -0
  23. data/lib/rubocop/cop/migration/create_table_force.rb +89 -0
  24. data/lib/rubocop/cop/migration/jsonb.rb +131 -0
  25. data/lib/rubocop/cop/migration/remove_column.rb +246 -0
  26. data/lib/rubocop/cop/migration/rename_column.rb +81 -0
  27. data/lib/rubocop/cop/migration/rename_table.rb +79 -0
  28. data/lib/rubocop/cop/migration/reserved_word_mysql.rb +232 -0
  29. data/lib/rubocop/migration/config_loader.rb +51 -0
  30. data/lib/rubocop/migration/cop_concerns/batch_processing.rb +34 -0
  31. data/lib/rubocop/migration/cop_concerns/column_type_method.rb +28 -0
  32. data/lib/rubocop/migration/cop_concerns/disable_ddl_transaction.rb +51 -0
  33. data/lib/rubocop/migration/cop_concerns.rb +11 -0
  34. data/lib/rubocop/migration/rubocop_extension.rb +15 -0
  35. data/lib/rubocop/migration/version.rb +4 -2
  36. data/lib/rubocop/migration.rb +23 -0
  37. data/rubocop-migration.gemspec +25 -31
  38. metadata +50 -137
  39. data/.gitignore +0 -15
  40. data/bin/console +0 -14
  41. data/bin/setup +0 -8
  42. data/circle.yml +0 -6
  43. data/lib/rubocop/cop/migration/unsafe_migration.rb +0 -69
  44. data/lib/rubocop/migration/strong_migrations_checker.rb +0 -47
  45. data/lib/rubocop-migration.rb +0 -16
  46. 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