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