strong_migrations 0.7.6 → 1.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,267 @@
1
+ module StrongMigrations
2
+ self.error_messages = {
3
+ add_column_default:
4
+ "Adding a column with a %{default_type} default blocks %{rewrite_blocks} while the entire table is rewritten.
5
+ Instead, add the column without a default value, then change the default.
6
+
7
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
8
+ def up
9
+ %{add_command}
10
+ %{change_command}
11
+ end
12
+
13
+ def down
14
+ %{remove_command}
15
+ end
16
+ end
17
+
18
+ Then backfill the existing rows in the Rails console or a separate migration with disable_ddl_transaction!.
19
+
20
+ class Backfill%{migration_name} < ActiveRecord::Migration%{migration_suffix}
21
+ disable_ddl_transaction!
22
+
23
+ def up
24
+ %{code}
25
+ end
26
+ end",
27
+
28
+ add_column_default_null:
29
+ "Adding a column with a null default blocks %{rewrite_blocks} while the entire table is rewritten.
30
+ Instead, add the column without a default value.
31
+
32
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
33
+ def change
34
+ %{command}
35
+ end
36
+ end",
37
+
38
+ add_column_default_callable:
39
+ "Strong Migrations does not support inspecting callable default values.
40
+ Please make really sure you're not calling a VOLATILE function,
41
+ then wrap it in a safety_assured { ... } block.",
42
+
43
+ add_column_json:
44
+ "There's no equality operator for the json column type, which can cause errors for
45
+ existing SELECT DISTINCT queries in your application. Use jsonb instead.
46
+
47
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
48
+ def change
49
+ %{command}
50
+ end
51
+ end",
52
+
53
+ add_column_generated_stored:
54
+ "Adding a stored generated column blocks %{rewrite_blocks} while the entire table is rewritten.",
55
+
56
+ change_column:
57
+ "Changing the type of an existing column blocks %{rewrite_blocks}
58
+ while the entire table is rewritten. A safer approach is to:
59
+
60
+ 1. Create a new column
61
+ 2. Write to both columns
62
+ 3. Backfill data from the old column to the new column
63
+ 4. Move reads from the old column to the new column
64
+ 5. Stop writing to the old column
65
+ 6. Drop the old column",
66
+
67
+ change_column_with_not_null:
68
+ "Changing the type is safe, but setting NOT NULL is not.",
69
+
70
+ remove_column: "Active Record caches attributes, which causes problems
71
+ when removing columns. Be sure to ignore the column%{column_suffix}:
72
+
73
+ class %{model} < %{base_model}
74
+ %{code}
75
+ end
76
+
77
+ Deploy the code, then wrap this step in a safety_assured { ... } block.
78
+
79
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
80
+ def change
81
+ safety_assured { %{command} }
82
+ end
83
+ end",
84
+
85
+ rename_column:
86
+ "Renaming a column that's in use will cause errors
87
+ in your application. A safer approach is to:
88
+
89
+ 1. Create a new column
90
+ 2. Write to both columns
91
+ 3. Backfill data from the old column to the new column
92
+ 4. Move reads from the old column to the new column
93
+ 5. Stop writing to the old column
94
+ 6. Drop the old column",
95
+
96
+ rename_table:
97
+ "Renaming a table that's in use will cause errors
98
+ in your application. A safer approach is to:
99
+
100
+ 1. Create a new table. Don't forget to recreate indexes from the old table
101
+ 2. Write to both tables
102
+ 3. Backfill data from the old table to the new table
103
+ 4. Move reads from the old table to the new table
104
+ 5. Stop writing to the old table
105
+ 6. Drop the old table",
106
+
107
+ add_reference:
108
+ "%{headline} Instead, use:
109
+
110
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
111
+ disable_ddl_transaction!
112
+
113
+ def change
114
+ %{command}
115
+ end
116
+ end",
117
+
118
+ add_index:
119
+ "Adding an index non-concurrently blocks writes. Instead, use:
120
+
121
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
122
+ disable_ddl_transaction!
123
+
124
+ def change
125
+ %{command}
126
+ end
127
+ end",
128
+
129
+ remove_index:
130
+ "Removing an index non-concurrently blocks writes. Instead, use:
131
+
132
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
133
+ disable_ddl_transaction!
134
+
135
+ def change
136
+ %{command}
137
+ end
138
+ end",
139
+
140
+ add_index_columns:
141
+ "Adding a non-unique index with more than three columns rarely improves performance.
142
+ Instead, start an index with columns that narrow down the results the most.",
143
+
144
+ add_index_corruption:
145
+ "Adding an index concurrently can cause silent data corruption in Postgres 14.0 to 14.3.
146
+ Upgrade Postgres before adding new indexes, or wrap this step in a safety_assured { ... } block
147
+ to accept the risk.",
148
+
149
+ change_table:
150
+ "Strong Migrations does not support inspecting what happens inside a
151
+ change_table block, so cannot help you here. Please make really sure that what
152
+ you're doing is safe before proceeding, then wrap it in a safety_assured { ... } block.",
153
+
154
+ create_table:
155
+ "The force option will destroy existing tables.
156
+ If this is intended, drop the existing table first.
157
+ Otherwise, remove the force option.",
158
+
159
+ execute:
160
+ "Strong Migrations does not support inspecting what happens inside an
161
+ execute call, so cannot help you here. Please make really sure that what
162
+ you're doing is safe before proceeding, then wrap it in a safety_assured { ... } block.",
163
+
164
+ change_column_default:
165
+ "Partial writes are enabled, which can cause incorrect values
166
+ to be inserted when changing the default value of a column.
167
+ Disable partial writes in config/application.rb:
168
+
169
+ config.active_record.%{config} = false",
170
+
171
+ change_column_null:
172
+ "Passing a default value to change_column_null runs a single UPDATE query,
173
+ which can cause downtime. Instead, backfill the existing rows in the
174
+ Rails console or a separate migration with disable_ddl_transaction!.
175
+
176
+ class Backfill%{migration_name} < ActiveRecord::Migration%{migration_suffix}
177
+ disable_ddl_transaction!
178
+
179
+ def up
180
+ %{code}
181
+ end
182
+ end",
183
+
184
+ change_column_null_postgresql:
185
+ "Setting NOT NULL on an existing column blocks reads and writes while every row is checked.
186
+ Instead, add a check constraint and validate it in a separate migration.
187
+
188
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
189
+ def change
190
+ %{add_constraint_code}
191
+ end
192
+ end
193
+
194
+ class Validate%{migration_name} < ActiveRecord::Migration%{migration_suffix}
195
+ %{validate_constraint_code}
196
+ end",
197
+
198
+ change_column_null_mysql:
199
+ "Setting NOT NULL on an existing column is not safe without strict mode enabled.",
200
+
201
+ add_foreign_key:
202
+ "Adding a foreign key blocks writes on both tables. Instead,
203
+ add the foreign key without validating existing rows,
204
+ then validate them in a separate migration.
205
+
206
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
207
+ def change
208
+ %{add_foreign_key_code}
209
+ end
210
+ end
211
+
212
+ class Validate%{migration_name} < ActiveRecord::Migration%{migration_suffix}
213
+ def change
214
+ %{validate_foreign_key_code}
215
+ end
216
+ end",
217
+
218
+ validate_foreign_key:
219
+ "Validating a foreign key while writes are blocked is dangerous.
220
+ Use disable_ddl_transaction! or a separate migration.",
221
+
222
+ add_check_constraint:
223
+ "Adding a check constraint key blocks reads and writes while every row is checked.
224
+ Instead, add the check constraint without validating existing rows,
225
+ then validate them in a separate migration.
226
+
227
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
228
+ def change
229
+ %{add_check_constraint_code}
230
+ end
231
+ end
232
+
233
+ class Validate%{migration_name} < ActiveRecord::Migration%{migration_suffix}
234
+ def change
235
+ %{validate_check_constraint_code}
236
+ end
237
+ end",
238
+
239
+ add_check_constraint_mysql:
240
+ "Adding a check constraint to an existing table is not safe with your database engine.",
241
+
242
+ validate_check_constraint:
243
+ "Validating a check constraint while writes are blocked is dangerous.
244
+ Use disable_ddl_transaction! or a separate migration.",
245
+
246
+ add_exclusion_constraint:
247
+ "Adding an exclusion constraint blocks reads and writes while every row is checked.",
248
+
249
+ add_unique_constraint:
250
+ "Adding a unique constraint creates a unique index, which blocks reads and writes.
251
+ Instead, create a unique index concurrently, then use it for the constraint.
252
+
253
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
254
+ disable_ddl_transaction!
255
+
256
+ def up
257
+ %{index_command}
258
+ %{constraint_command}
259
+ end
260
+
261
+ def down
262
+ %{remove_command}
263
+ end
264
+ end"
265
+ }
266
+ self.enabled_checks = (error_messages.keys - [:remove_index]).map { |k| [k, {}] }.to_h
267
+ end
@@ -7,14 +7,29 @@ module StrongMigrations
7
7
  end
8
8
 
9
9
  def method_missing(method, *args)
10
- strong_migrations_checker.perform(method, *args) do
11
- super
10
+ return super if is_a?(ActiveRecord::Schema)
11
+
12
+ # Active Record 7.0.2+ versioned schema
13
+ return super if defined?(ActiveRecord::Schema::Definition) && is_a?(ActiveRecord::Schema::Definition)
14
+
15
+ catch(:safe) do
16
+ strong_migrations_checker.perform(method, *args) do
17
+ super
18
+ end
12
19
  end
13
20
  end
14
21
  ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
15
22
 
23
+ def revert(*)
24
+ if strong_migrations_checker.version_safe?
25
+ safety_assured { super }
26
+ else
27
+ super
28
+ end
29
+ end
30
+
16
31
  def safety_assured
17
- strong_migrations_checker.safety_assured do
32
+ strong_migrations_checker.class.safety_assured do
18
33
  yield
19
34
  end
20
35
  end
@@ -0,0 +1,19 @@
1
+ module StrongMigrations
2
+ module Migrator
3
+ def ddl_transaction(migration, *args)
4
+ return super unless StrongMigrations.lock_timeout_retries > 0 && use_transaction?(migration)
5
+
6
+ # handle MigrationProxy class
7
+ migration = migration.send(:migration) if migration.respond_to?(:migration, true)
8
+
9
+ # retry migration since the entire transaction needs to be rerun
10
+ checker = migration.send(:strong_migrations_checker)
11
+ checker.retry_lock_timeouts(check_committed: true) do
12
+ # failed transaction reverts timeout, so need to re-apply
13
+ checker.timeouts_set = false
14
+
15
+ super(migration, *args)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -5,22 +5,22 @@ module StrongMigrations
5
5
  end
6
6
 
7
7
  # TODO check if invalid index with expected name exists and remove if needed
8
- def safe_add_index(table, columns, options)
8
+ def safe_add_index(*args, **options)
9
9
  disable_transaction
10
- @migration.add_index(table, columns, **options.merge(algorithm: :concurrently))
10
+ @migration.add_index(*args, **options.merge(algorithm: :concurrently))
11
11
  end
12
12
 
13
- def safe_remove_index(table, options)
13
+ def safe_remove_index(*args, **options)
14
14
  disable_transaction
15
- @migration.remove_index(table, **options.merge(algorithm: :concurrently))
15
+ @migration.remove_index(*args, **options.merge(algorithm: :concurrently))
16
16
  end
17
17
 
18
- def safe_add_reference(table, reference, options)
18
+ def safe_add_reference(table, reference, *args, **options)
19
19
  @migration.reversible do |dir|
20
20
  dir.up do
21
21
  disable_transaction
22
22
  foreign_key = options.delete(:foreign_key)
23
- @migration.add_reference(table, reference, **options)
23
+ @migration.add_reference(table, reference, *args, **options)
24
24
  if foreign_key
25
25
  # same as Active Record
26
26
  name =
@@ -30,7 +30,12 @@ module StrongMigrations
30
30
  (ActiveRecord::Base.pluralize_table_names ? reference.to_s.pluralize : reference).to_sym
31
31
  end
32
32
 
33
- @migration.add_foreign_key(table, name)
33
+ foreign_key_opts = foreign_key.is_a?(Hash) ? foreign_key.except(:to_table) : {}
34
+ if reference
35
+ @migration.add_foreign_key(table, name, column: "#{reference}_id", **foreign_key_opts)
36
+ else
37
+ @migration.add_foreign_key(table, name, **foreign_key_opts)
38
+ end
34
39
  end
35
40
  end
36
41
  dir.down do
@@ -39,50 +44,49 @@ module StrongMigrations
39
44
  end
40
45
  end
41
46
 
42
- def safe_add_foreign_key(from_table, to_table, options)
47
+ def safe_add_foreign_key(from_table, to_table, *args, **options)
43
48
  @migration.reversible do |dir|
44
49
  dir.up do
45
- @migration.add_foreign_key(from_table, to_table, **options.merge(validate: false))
50
+ @migration.add_foreign_key(from_table, to_table, *args, **options.merge(validate: false))
46
51
  disable_transaction
47
- @migration.validate_foreign_key(from_table, to_table)
48
- end
49
- dir.down do
50
- @migration.remove_foreign_key(from_table, to_table)
51
- end
52
- end
53
- end
54
-
55
- def safe_add_foreign_key_code(from_table, to_table, add_code, validate_code)
56
- @migration.reversible do |dir|
57
- dir.up do
58
- @migration.safety_assured do
59
- @migration.execute(add_code)
60
- disable_transaction
61
- @migration.execute(validate_code)
52
+ validate_options = options.slice(:column, :name)
53
+ if ActiveRecord::VERSION::MAJOR >= 6
54
+ @migration.validate_foreign_key(from_table, to_table, **validate_options)
55
+ else
56
+ @migration.validate_foreign_key(from_table, validate_options.any? ? validate_options : to_table)
62
57
  end
63
58
  end
64
59
  dir.down do
65
- @migration.remove_foreign_key(from_table, to_table)
60
+ remove_options = options.slice(:column, :name)
61
+ if ActiveRecord::VERSION::MAJOR >= 6
62
+ @migration.remove_foreign_key(from_table, to_table, **remove_options)
63
+ else
64
+ @migration.remove_foreign_key(from_table, remove_options.any? ? remove_options : to_table)
65
+ end
66
66
  end
67
67
  end
68
68
  end
69
69
 
70
- def safe_add_check_constraint(table, expression, add_options, validate_options)
70
+ def safe_add_check_constraint(table, expression, *args, add_options, validate_options)
71
71
  @migration.reversible do |dir|
72
72
  dir.up do
73
- @migration.add_check_constraint(table, expression, **add_options)
73
+ @migration.add_check_constraint(table, expression, *args, **add_options)
74
74
  disable_transaction
75
75
  @migration.validate_check_constraint(table, **validate_options)
76
76
  end
77
77
  dir.down do
78
- @migration.remove_check_constraint(table, expression, **add_options)
78
+ @migration.remove_check_constraint(table, expression, **add_options.except(:validate))
79
79
  end
80
80
  end
81
81
  end
82
82
 
83
- def safe_change_column_null(add_code, validate_code, change_args, remove_code)
83
+ def safe_change_column_null(add_code, validate_code, change_args, remove_code, default)
84
84
  @migration.reversible do |dir|
85
85
  dir.up do
86
+ unless default.nil?
87
+ raise Error, "default value not supported yet with safe_by_default"
88
+ end
89
+
86
90
  @migration.safety_assured do
87
91
  @migration.execute(add_code)
88
92
  disable_transaction
@@ -0,0 +1,21 @@
1
+ module StrongMigrations
2
+ module SchemaDumper
3
+ def initialize(connection, *args, **options)
4
+ return super unless StrongMigrations.alphabetize_schema
5
+
6
+ super(WrappedConnection.new(connection), *args, **options)
7
+ end
8
+ end
9
+
10
+ class WrappedConnection
11
+ delegate_missing_to :@connection
12
+
13
+ def initialize(connection)
14
+ @connection = connection
15
+ end
16
+
17
+ def columns(*args, **options)
18
+ @connection.columns(*args, **options).sort_by(&:name)
19
+ end
20
+ end
21
+ end
@@ -1,3 +1,3 @@
1
1
  module StrongMigrations
2
- VERSION = "0.7.6"
2
+ VERSION = "1.7.0"
3
3
  end