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,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