strong_migrations 0.7.6 → 1.7.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.
@@ -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