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,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Migration
6
+ # Avoid changing column type that is in use.
7
+ #
8
+ # Changing the type of a column causes the entire table to be rewritten.
9
+ # During this time, reads and writes are blocked in Postgres, and writes are blocked in MySQL and MariaDB.
10
+ #
11
+ # Some changes don’t require a table rewrite and are safe in PostgreSQL:
12
+ #
13
+ # Type | Safe Changes
14
+ # --- | ---
15
+ # `cidr` | Changing to `inet`
16
+ # `citext` | Changing to `text` if not indexed, changing to `string` with no `:limit` if not indexed
17
+ # `datetime` | Increasing or removing `:precision`, changing to `timestamptz` when session time zone is UTC in Postgres 12+
18
+ # `decimal` | Increasing `:precision` at same `:scale`, removing `:precision` and `:scale`
19
+ # `interval` | Increasing or removing `:precision`
20
+ # `numeric` | Increasing `:precision` at same `:scale`, removing `:precision` and `:scale`
21
+ # `string` | Increasing or removing `:limit`, changing to `text`, changing `citext` if not indexed
22
+ # `text` | Changing to `string` with no `:limit`, changing to `citext` if not indexed
23
+ # `time` | Increasing or removing `:precision`
24
+ # `timestamptz` | Increasing or removing `:limit`, changing to `datetime` when session time zone is UTC in Postgres 12+
25
+ #
26
+ # And some in MySQL and MariaDB:
27
+ #
28
+ # Type | Safe Changes
29
+ # --- | ---
30
+ # `string` | Increasing `:limit` from under 255 up to 255, increasing `:limit` from over 255 to the max
31
+ #
32
+ # A safer approach is to:
33
+ #
34
+ # 1. Create a new column
35
+ # 2. Write to both columns
36
+ # 3. Backfill data from the old column to the new column
37
+ # 4. Move reads from the old column to the new column
38
+ # 5. Stop writing to the old column
39
+ # 6. Drop the old column
40
+ #
41
+ # @safety
42
+ # Only meaningful if the table is in use and the type change is really unsafe as described above.
43
+ #
44
+ # @example
45
+ # # bad
46
+ # class ChangeUsersSomeColumnType < ActiveRecord::Migration[7.0]
47
+ # def change
48
+ # change_column :users, :some_column, :new_type
49
+ # end
50
+ # end
51
+ #
52
+ # # good
53
+ # class AddUsersAnotherColumn < ActiveRecord::Migration[7.0]
54
+ # def change
55
+ # add_column :users, :another_column, :new_type
56
+ # end
57
+ # end
58
+ #
59
+ # class RemoveUsersSomeColumn < ActiveRecord::Migration[7.0]
60
+ # def change
61
+ # remove_column :users, :some_column
62
+ # end
63
+ # end
64
+ class ChangeColumn < RuboCop::Cop::Base
65
+ MSG = 'Avoid changing column type that is in use.'
66
+
67
+ RESTRICT_ON_SEND = %i[
68
+ change
69
+ change_column
70
+ ].freeze
71
+
72
+ # @param node [RuboCop::AST::SendNode]
73
+ # @return [void]
74
+ def on_send(node)
75
+ return unless bad?(node)
76
+
77
+ add_offense(node)
78
+ end
79
+
80
+ private
81
+
82
+ # @!method change?(node)
83
+ # @param node [RuboCop::AST::SendNode]
84
+ # @return [Boolean]
85
+ def_node_matcher :change?, <<~PATTERN
86
+ (send
87
+ lvar
88
+ :change
89
+ ...
90
+ )
91
+ PATTERN
92
+
93
+ # @!method change_column?(node)
94
+ # @param node [RuboCop::AST::SendNode]
95
+ # @return [Boolean]
96
+ def_node_matcher :change_column?, <<~PATTERN
97
+ (send
98
+ nil?
99
+ :change_column
100
+ ...
101
+ )
102
+ PATTERN
103
+
104
+ # @param node [RuboCop::AST::SendNode]
105
+ # @return [Boolean]
106
+ def bad?(node)
107
+ change?(node) ||
108
+ change_column?(node)
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Migration
6
+ # Avoid simply setting `NOT NULL` constraint on an existing column in PostgreSQL.
7
+ #
8
+ # It blocks reads and writes while every row is checked.
9
+ # In PostgreSQL 12+, you can safely set `NOT NULL` constraint if corresponding check constraint exists.
10
+ #
11
+ # @safety
12
+ # Only meaningful in PostgreSQL 12+.
13
+ #
14
+ # @example
15
+ # # bad
16
+ # class SetNotNullColumnConstraintToUsersName < ActiveRecord::Migration[7.0]
17
+ # def change
18
+ # change_column_null :users, :name, false
19
+ # end
20
+ # end
21
+ #
22
+ # # good
23
+ # class SetNotNullCheckConstraintToUsersName < ActiveRecord::Migration[7.0]
24
+ # def change
25
+ # add_check_constraint :users, 'name IS NOT NULL', name: 'users_name_is_not_null', validate: false
26
+ # end
27
+ # end
28
+ #
29
+ # class ReplaceNotNullConstraintOnUsersName < ActiveRecord::Migration[7.0]
30
+ # def change
31
+ # validate_constraint :users, name: 'users_name_is_not_null'
32
+ # change_column_null :users, :name, false
33
+ # remove_check_constraint :users, name: 'users_name_is_not_null'
34
+ # end
35
+ # end
36
+ class ChangeColumnNull < RuboCop::Cop::Base
37
+ extend AutoCorrector
38
+
39
+ MSG = 'Avoid simply setting `NOT NULL` constraint on an existing column in PostgreSQL.'
40
+
41
+ RESTRICT_ON_SEND = %i[
42
+ change_column_null
43
+ change_null
44
+ ].freeze
45
+
46
+ # @param node [RuboCop::AST::SendNode]
47
+ # @return [void]
48
+ def on_send(node)
49
+ return if called_with_validate_constraint?(node)
50
+
51
+ add_offense(node) do |corrector|
52
+ autocorrect(corrector, node)
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ # @!method validate_constraint?(node)
59
+ # @param node [RuboCop::AST::SendNode]
60
+ # @return [Boolean]
61
+ def_node_matcher :validate_constraint?, <<~PATTERN
62
+ (send nil? :validate_constraint ...)
63
+ PATTERN
64
+
65
+ # @param corrector [RuboCop::Cop::Corrector]
66
+ # @param node [RuboCop::AST::SendNode]
67
+ # @return [void]
68
+ def autocorrect(
69
+ corrector,
70
+ node
71
+ )
72
+ case node.method_name
73
+ when :change_column_null
74
+ autocorrect_change_column_null(corrector, node)
75
+ when :change_null
76
+ autocorrect_change_null(corrector, node)
77
+ end
78
+ end
79
+
80
+ # @param corrector [RuboCop::Cop::Corrector]
81
+ # @param node [RuboCop::AST::SendNode]
82
+ # @return [void]
83
+ def autocorrect_change_column_null(
84
+ corrector,
85
+ node
86
+ )
87
+ corrector.replace(
88
+ node,
89
+ format_add_check_constraint(
90
+ column_name: find_column_name_from_change_column_null(node),
91
+ table_name: find_table_name_from_change_column_null(node)
92
+ )
93
+ )
94
+ end
95
+
96
+ # @param corrector [RuboCop::Cop::Corrector]
97
+ # @param node [RuboCop::AST::SendNode]
98
+ # @return [void]
99
+ def autocorrect_change_null(
100
+ corrector,
101
+ node
102
+ )
103
+ corrector.replace(
104
+ node.location.selector.with(
105
+ end_pos: node.location.expression.end_pos
106
+ ),
107
+ format_check_constraint(
108
+ column_name: find_column_name_from_change_null(node),
109
+ table_name: find_table_name_from_change_null(node)
110
+ )
111
+ )
112
+ end
113
+
114
+ # @param node [RuboCop::AST::SendNode]
115
+ # @return [Boolean]
116
+ def called_with_validate_constraint?(node)
117
+ case node.method_name
118
+ when :change_column_null
119
+ node
120
+ when :change_null
121
+ find_ancestor_change_table(node)
122
+ end.left_siblings.any? do |sibling|
123
+ validate_constraint?(sibling)
124
+ end
125
+ end
126
+
127
+ # @param node [RuboCop::AST::SendNode]
128
+ # @return [RuboCop::AST::BlockNode]
129
+ def find_ancestor_change_table(node)
130
+ node.each_ancestor(:block).find do |ancestor|
131
+ ancestor.method?(:change_table)
132
+ end
133
+ end
134
+
135
+ # @param node [RuboCop::AST::SendNode]
136
+ # @return [String]
137
+ def find_column_name_from_change_column_null(node)
138
+ node.arguments[1].value.to_s
139
+ end
140
+
141
+ # @param node [RuboCop::AST::SendNode]
142
+ # @return [String]
143
+ def find_column_name_from_change_null(node)
144
+ node.arguments[0].value.to_s
145
+ end
146
+
147
+ # @parm node [RuboCop::AST::SendNode]
148
+ # @return [String]
149
+ def find_table_name_from_change_column_null(node)
150
+ node.arguments[0].value.to_s
151
+ end
152
+
153
+ # @param node [RuboCop::AST::SendNode]
154
+ # @return [String]
155
+ def find_table_name_from_change_null(node)
156
+ find_ancestor_change_table(node).send_node.arguments[0].value.to_s
157
+ end
158
+
159
+ # @param column_name [String]
160
+ # @param table_name [String]
161
+ # @return [String]
162
+ def format_add_check_constraint(
163
+ column_name:,
164
+ table_name:
165
+ )
166
+ format(
167
+ 'add_check_constraint :%<table_name>s, %<arguments>s',
168
+ arguments: format_check_constraint_arguments(
169
+ column_name: column_name,
170
+ table_name: table_name
171
+ ),
172
+ table_name: table_name
173
+ )
174
+ end
175
+
176
+ # @param column_name [String]
177
+ # @param table_name [String]
178
+ # @return [String]
179
+ def format_check_constraint(
180
+ column_name:,
181
+ table_name:
182
+ )
183
+ format(
184
+ 'check_constraint %<arguments>s',
185
+ arguments: format_check_constraint_arguments(
186
+ column_name: column_name,
187
+ table_name: table_name
188
+ )
189
+ )
190
+ end
191
+
192
+ # @param coumn_name [String]
193
+ # @param table_name [String]
194
+ # @return [String]
195
+ def format_check_constraint_arguments(
196
+ column_name:,
197
+ table_name:
198
+ )
199
+ format(
200
+ "'%<column_name>s IS NOT NULL', name: '%<constraint_name>s', validate: false",
201
+ column_name: column_name,
202
+ constraint_name: "#{table_name}_#{column_name}_is_not_null"
203
+ )
204
+ end
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Migration
6
+ # Create tables without `force: true` option.
7
+ #
8
+ # The `force: true` option can drop an existing table.
9
+ # If you indend to drop an existing table, explicitly call `drop_table` first.
10
+ #
11
+ # @example
12
+ # # bad
13
+ # class CreateUsers < ActiveRecord::Migration[7.0]
14
+ # def change
15
+ # create_table :users, force: true
16
+ # end
17
+ # end
18
+ #
19
+ # # good
20
+ # class CreateUsers < ActiveRecord::Migration[7.0]
21
+ # def change
22
+ # create_table :users
23
+ # end
24
+ # end
25
+ class CreateTableForce < RuboCop::Cop::Base
26
+ extend AutoCorrector
27
+
28
+ include RangeHelp
29
+
30
+ MSG = 'Create tables without `force: true` option.'
31
+
32
+ RESTRICT_ON_SEND = %i[
33
+ create_table
34
+ ].freeze
35
+
36
+ # @param node [RuboCop::AST::SendNode]
37
+ # @return [void]
38
+ def on_send(node)
39
+ option_node = option_force_true_from_create_table(node)
40
+ return unless option_node
41
+
42
+ add_offense(option_node) do |corrector|
43
+ autocorrect(corrector, option_node)
44
+ end
45
+ end
46
+
47
+ private
48
+
49
+ # @!method option_force_true_from_create_table(node)
50
+ # @param node [RuboCop::AST::SendNode]
51
+ # @return [RuboCop::AST::PairNode, nil]
52
+ def_node_matcher :option_force_true_from_create_table, <<~PATTERN
53
+ (send
54
+ nil?
55
+ :create_table
56
+ _
57
+ (hash
58
+ <
59
+ $(pair
60
+ (sym :force)
61
+ true
62
+ )
63
+ ...
64
+ >
65
+ )
66
+ )
67
+ PATTERN
68
+
69
+ # @param corrector [RuboCop::Cop::Corrector]
70
+ # @param node [RuboCop::AST::PairNode]
71
+ # @return [void]
72
+ def autocorrect(
73
+ corrector,
74
+ node
75
+ )
76
+ corrector.remove(
77
+ range_with_surrounding_comma(
78
+ range_with_surrounding_space(
79
+ node.location.expression,
80
+ side: :left
81
+ ),
82
+ :left
83
+ )
84
+ )
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,131 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RuboCop
4
+ module Cop
5
+ module Migration
6
+ # Prefer `jsonb` to `json`.
7
+ #
8
+ # In PostgreSQL, there is no equality operator for the json column type,
9
+ # which can cause errors for existing `SELECT DISTINCT` queries in your application.
10
+ #
11
+ # @safety
12
+ # Only meaningful in PostgreSQL.
13
+ #
14
+ # @example
15
+ # # bad
16
+ # add_column :users, :properties, :json
17
+ #
18
+ # # good
19
+ # add_column :users, :properties, :jsonb
20
+ class Jsonb < RuboCop::Cop::Base
21
+ extend AutoCorrector
22
+
23
+ MSG = 'Prefer `jsonb` to `json`.'
24
+
25
+ RESTRICT_ON_SEND = %i[
26
+ add_column
27
+ change
28
+ change_column
29
+ json
30
+ ].freeze
31
+
32
+ # @param node [RuboCop::AST::SendNode]
33
+ # @return [void]
34
+ def on_send(node)
35
+ json_range = json_range_from_target_send_node(node)
36
+ return unless json_range
37
+
38
+ add_offense(json_range) do |corrector|
39
+ corrector.replace(json_range, 'jsonb')
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ # @!method json_type_node_from_add_column(node)
46
+ # @param node [RuboCop::AST::SendNode]
47
+ # @return [RuboCop::AST::SymNode, nil]
48
+ def_node_matcher :json_type_node_from_add_column, <<~PATTERN
49
+ (send
50
+ nil?
51
+ _
52
+ _
53
+ _
54
+ $(sym :json)
55
+ )
56
+ PATTERN
57
+ alias json_type_node_from_change_column json_type_node_from_add_column
58
+
59
+ # @!method json_type_node_from_change(node)
60
+ # @param node [RuboCop::AST::SendNode]
61
+ # @return [RuboCop::AST::SymNode, nil]
62
+ def_node_matcher :json_type_node_from_change, <<~PATTERN
63
+ (send
64
+ lvar
65
+ _
66
+ _
67
+ $(sym :json)
68
+ )
69
+ PATTERN
70
+
71
+ # @!method json_type_node_from_json(node)
72
+ # @param node [RuboCop::AST::SendNode]
73
+ # @return [RuboCop::AST::SendNode, nil]
74
+ def_node_matcher :json_type_node_from_json, <<~PATTERN
75
+ $(send
76
+ lvar
77
+ _
78
+ ...
79
+ )
80
+ PATTERN
81
+
82
+ # @param corrector [RuboCop::Cop::Corrector]
83
+ # @param node [RuboCop::AST::SendNode, RuboCop::AST::SymNode]
84
+ # @return [void]
85
+ def autocorrect(
86
+ corrector,
87
+ node
88
+ )
89
+ corrector.replace(node, 'jsonb')
90
+ end
91
+
92
+ # @param node [RuboCop::AST::SendNode]
93
+ # @return [RuboCop::AST::SymNode, nil]
94
+ def json_node_from_target_send_node(node)
95
+ case node.method_name
96
+ when :add_column
97
+ json_type_node_from_add_column(node)
98
+ when :change
99
+ json_type_node_from_change(node)
100
+ when :change_column
101
+ json_type_node_from_change_column(node)
102
+ when :json
103
+ json_type_node_from_json(node)
104
+ end
105
+ end
106
+
107
+ # @param node [RuboCop::AST::SendNode, RuboCop::AST::SymNode]
108
+ # @return [Parser::Source::Range]
109
+ def json_range_from_json_node(node)
110
+ case node.type
111
+ when :send
112
+ node.location.selector
113
+ when :sym
114
+ node.location.expression.with(
115
+ begin_pos: node.location.expression.begin_pos + 1
116
+ )
117
+ end
118
+ end
119
+
120
+ # @param node [RuboCop::AST::SendNode]
121
+ # @return [Parser::Source::Range]
122
+ def json_range_from_target_send_node(node)
123
+ json_node = json_node_from_target_send_node(node)
124
+ return unless json_node
125
+
126
+ json_range_from_json_node(json_node)
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end