rubocop-migration 0.2.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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