strong_migrations 0.7.8 → 1.0.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,402 @@
1
+ # TODO better pattern
2
+ module StrongMigrations
3
+ module Checks
4
+ private
5
+
6
+ def check_add_check_constraint(args)
7
+ table, expression, options = args
8
+ options ||= {}
9
+
10
+ if !new_table?(table)
11
+ if postgresql? && options[:validate] != false
12
+ add_options = options.merge(validate: false)
13
+ name = options[:name] || @migration.check_constraint_options(table, expression, options)[:name]
14
+ validate_options = {name: name}
15
+
16
+ if StrongMigrations.safe_by_default
17
+ safe_add_check_constraint(table, expression, add_options, validate_options)
18
+ throw :safe
19
+ end
20
+
21
+ raise_error :add_check_constraint,
22
+ add_check_constraint_code: command_str("add_check_constraint", [table, expression, add_options]),
23
+ validate_check_constraint_code: command_str("validate_check_constraint", [table, validate_options])
24
+ elsif mysql? || mariadb?
25
+ raise_error :add_check_constraint_mysql
26
+ end
27
+ end
28
+ end
29
+
30
+ def check_add_column(args)
31
+ table, column, type, options = args
32
+ options ||= {}
33
+ default = options[:default]
34
+
35
+ if !default.nil? && !adapter.add_column_default_safe?
36
+ if options[:null] == false
37
+ options = options.except(:null)
38
+ append = "
39
+
40
+ Then add the NOT NULL constraint in separate migrations."
41
+ end
42
+
43
+ raise_error :add_column_default,
44
+ add_command: command_str("add_column", [table, column, type, options.except(:default)]),
45
+ change_command: command_str("change_column_default", [table, column, default]),
46
+ remove_command: command_str("remove_column", [table, column]),
47
+ code: backfill_code(table, column, default),
48
+ append: append,
49
+ rewrite_blocks: adapter.rewrite_blocks
50
+ elsif default.is_a?(Proc) && postgresql?
51
+ # adding a column with a VOLATILE default is not safe
52
+ # https://www.postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-NOTES
53
+ # functions like random() and clock_timestamp() are VOLATILE
54
+ # check for Proc to match Active Record
55
+ raise_error :add_column_default_callable
56
+ end
57
+
58
+ if type.to_s == "json" && postgresql?
59
+ raise_error :add_column_json,
60
+ command: command_str("add_column", [table, column, :jsonb, options])
61
+ end
62
+ end
63
+
64
+ # unlike add_index, we don't make an exception here for new tables
65
+ #
66
+ # with add_index, it's fine to lock a new table even after inserting data
67
+ # since the table won't be in use by the application
68
+ #
69
+ # with add_foreign_key, this would cause issues since it locks the referenced table
70
+ #
71
+ # it's okay to allow if the table is empty, but not a fan of data-dependent checks,
72
+ # since the data in production could be different from development
73
+ #
74
+ # note: adding foreign_keys with create_table is fine
75
+ # since the table is always guaranteed to be empty
76
+ def check_add_foreign_key(args)
77
+ from_table, to_table, options = args
78
+ options ||= {}
79
+
80
+ validate = options.fetch(:validate, true)
81
+ if postgresql? && validate
82
+ if StrongMigrations.safe_by_default
83
+ safe_add_foreign_key(from_table, to_table, options)
84
+ throw :safe
85
+ end
86
+
87
+ raise_error :add_foreign_key,
88
+ add_foreign_key_code: command_str("add_foreign_key", [from_table, to_table, options.merge(validate: false)]),
89
+ validate_foreign_key_code: command_str("validate_foreign_key", [from_table, to_table])
90
+ end
91
+ end
92
+
93
+ def check_add_index(args)
94
+ table, columns, options = args
95
+ options ||= {}
96
+
97
+ if columns.is_a?(Array) && columns.size > 3 && !options[:unique]
98
+ raise_error :add_index_columns, header: "Best practice"
99
+ end
100
+
101
+ # safe to add non-concurrently to new tables (even after inserting data)
102
+ # since the table won't be in use by the application
103
+ if postgresql? && options[:algorithm] != :concurrently && !new_table?(table)
104
+ if StrongMigrations.safe_by_default
105
+ safe_add_index(table, columns, options)
106
+ throw :safe
107
+ end
108
+
109
+ raise_error :add_index, command: command_str("add_index", [table, columns, options.merge(algorithm: :concurrently)])
110
+ end
111
+ end
112
+
113
+ def check_add_reference(method, args)
114
+ table, reference, options = args
115
+ options ||= {}
116
+
117
+ if postgresql?
118
+ index_value = options.fetch(:index, true)
119
+ concurrently_set = index_value.is_a?(Hash) && index_value[:algorithm] == :concurrently
120
+ bad_index = index_value && !concurrently_set
121
+
122
+ if bad_index || options[:foreign_key]
123
+ if index_value.is_a?(Hash)
124
+ options[:index] = options[:index].merge(algorithm: :concurrently)
125
+ else
126
+ options = options.merge(index: {algorithm: :concurrently})
127
+ end
128
+
129
+ if StrongMigrations.safe_by_default
130
+ safe_add_reference(table, reference, options)
131
+ throw :safe
132
+ end
133
+
134
+ if options.delete(:foreign_key)
135
+ headline = "Adding a foreign key blocks writes on both tables."
136
+ append = "
137
+
138
+ Then add the foreign key in separate migrations."
139
+ else
140
+ headline = "Adding an index non-concurrently locks the table."
141
+ end
142
+
143
+ raise_error :add_reference,
144
+ headline: headline,
145
+ command: command_str(method, [table, reference, options]),
146
+ append: append
147
+ end
148
+ end
149
+ end
150
+
151
+ def check_change_column(args)
152
+ table, column, type, options = args
153
+ options ||= {}
154
+
155
+ safe = false
156
+ existing_column = connection.columns(table).find { |c| c.name.to_s == column.to_s }
157
+ if existing_column
158
+ existing_type = existing_column.sql_type.sub(/\(\d+(,\d+)?\)/, "")
159
+ safe = adapter.change_type_safe?(table, column, type, options, existing_column, existing_type)
160
+ end
161
+
162
+ # unsafe to set NOT NULL for safe types with Postgres
163
+ # TODO check if safe for MySQL and MariaDB
164
+ if safe && existing_column.null && options[:null] == false
165
+ raise_error :change_column_with_not_null
166
+ end
167
+
168
+ raise_error :change_column, rewrite_blocks: adapter.rewrite_blocks unless safe
169
+ end
170
+
171
+ def check_change_column_null(args)
172
+ table, column, null, default = args
173
+ if !null
174
+ if postgresql?
175
+ safe = false
176
+ safe_with_check_constraint = adapter.server_version >= Gem::Version.new("12")
177
+ if safe_with_check_constraint
178
+ safe = adapter.constraints(table).any? { |c| c["def"] == "CHECK ((#{column} IS NOT NULL))" || c["def"] == "CHECK ((#{connection.quote_column_name(column)} IS NOT NULL))" }
179
+ end
180
+
181
+ unless safe
182
+ # match https://github.com/nullobject/rein
183
+ constraint_name = "#{table}_#{column}_null"
184
+
185
+ add_code = constraint_str("ALTER TABLE %s ADD CONSTRAINT %s CHECK (%s IS NOT NULL) NOT VALID", [table, constraint_name, column])
186
+ validate_code = constraint_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [table, constraint_name])
187
+ remove_code = constraint_str("ALTER TABLE %s DROP CONSTRAINT %s", [table, constraint_name])
188
+
189
+ constraint_methods = ar_version >= 6.1
190
+
191
+ validate_constraint_code =
192
+ if constraint_methods
193
+ String.new(command_str(:validate_check_constraint, [table, {name: constraint_name}]))
194
+ else
195
+ String.new(safety_assured_str(validate_code))
196
+ end
197
+
198
+ if safe_with_check_constraint
199
+ change_args = [table, column, null]
200
+
201
+ validate_constraint_code << "\n #{command_str(:change_column_null, change_args)}"
202
+
203
+ if constraint_methods
204
+ validate_constraint_code << "\n #{command_str(:remove_check_constraint, [table, {name: constraint_name}])}"
205
+ else
206
+ validate_constraint_code << "\n #{safety_assured_str(remove_code)}"
207
+ end
208
+ end
209
+
210
+ if StrongMigrations.safe_by_default
211
+ safe_change_column_null(add_code, validate_code, change_args, remove_code, default)
212
+ throw :safe
213
+ end
214
+
215
+ add_constraint_code =
216
+ if constraint_methods
217
+ # only quote when needed
218
+ expr_column = column.to_s =~ /\A[a-z0-9_]+\z/ ? column : connection.quote_column_name(column)
219
+ command_str(:add_check_constraint, [table, "#{expr_column} IS NOT NULL", {name: constraint_name, validate: false}])
220
+ else
221
+ safety_assured_str(add_code)
222
+ end
223
+
224
+ raise_error :change_column_null_postgresql,
225
+ add_constraint_code: add_constraint_code,
226
+ validate_constraint_code: "def change\n #{validate_constraint_code}\n end"
227
+ end
228
+ elsif mysql? || mariadb?
229
+ unless adapter.strict_mode?
230
+ raise_error :change_column_null_mysql
231
+ end
232
+ end
233
+
234
+ if !default.nil?
235
+ raise_error :change_column_null,
236
+ code: backfill_code(table, column, default)
237
+ end
238
+ end
239
+ end
240
+
241
+ def check_change_table
242
+ raise_error :change_table, header: "Possibly dangerous operation"
243
+ end
244
+
245
+ def check_create_table(args)
246
+ table, options = args
247
+ options ||= {}
248
+
249
+ raise_error :create_table if options[:force]
250
+
251
+ # keep track of new tables of add_index check
252
+ @new_tables << table.to_s
253
+ end
254
+
255
+ def check_execute
256
+ raise_error :execute, header: "Possibly dangerous operation"
257
+ end
258
+
259
+ def check_remove_column(method, args)
260
+ columns =
261
+ case method
262
+ when :remove_timestamps
263
+ ["created_at", "updated_at"]
264
+ when :remove_column
265
+ [args[1].to_s]
266
+ when :remove_columns
267
+ # Active Record 6.1+ supports options
268
+ if args.last.is_a?(Hash)
269
+ args[1..-2].map(&:to_s)
270
+ else
271
+ args[1..-1].map(&:to_s)
272
+ end
273
+ else
274
+ options = args[2] || {}
275
+ reference = args[1]
276
+ cols = []
277
+ cols << "#{reference}_type" if options[:polymorphic]
278
+ cols << "#{reference}_id"
279
+ cols
280
+ end
281
+
282
+ code = "self.ignored_columns = #{columns.inspect}"
283
+
284
+ raise_error :remove_column,
285
+ model: args[0].to_s.classify,
286
+ code: code,
287
+ command: command_str(method, args),
288
+ column_suffix: columns.size > 1 ? "s" : ""
289
+ end
290
+
291
+ def check_remove_index(args)
292
+ table, options = args
293
+ unless options.is_a?(Hash)
294
+ options = {column: options}
295
+ end
296
+ options ||= {}
297
+
298
+ if postgresql? && options[:algorithm] != :concurrently && !new_table?(table)
299
+ if StrongMigrations.safe_by_default
300
+ safe_remove_index(table, options)
301
+ throw :safe
302
+ end
303
+
304
+ raise_error :remove_index, command: command_str("remove_index", [table, options.merge(algorithm: :concurrently)])
305
+ end
306
+ end
307
+
308
+ def check_rename_column
309
+ raise_error :rename_column
310
+ end
311
+
312
+ def check_rename_table
313
+ raise_error :rename_table
314
+ end
315
+
316
+ def check_validate_check_constraint
317
+ if postgresql? && adapter.writes_blocked?
318
+ raise_error :validate_check_constraint
319
+ end
320
+ end
321
+
322
+ def check_validate_foreign_key
323
+ if postgresql? && adapter.writes_blocked?
324
+ raise_error :validate_foreign_key
325
+ end
326
+ end
327
+
328
+ # helpers
329
+
330
+ def postgresql?
331
+ adapter.instance_of?(Adapters::PostgreSQLAdapter)
332
+ end
333
+
334
+ def mysql?
335
+ adapter.instance_of?(Adapters::MySQLAdapter)
336
+ end
337
+
338
+ def mariadb?
339
+ adapter.instance_of?(Adapters::MariaDBAdapter)
340
+ end
341
+
342
+ def ar_version
343
+ ActiveRecord::VERSION::STRING.to_f
344
+ end
345
+
346
+ def raise_error(message_key, header: nil, append: nil, **vars)
347
+ return unless StrongMigrations.check_enabled?(message_key, version: version)
348
+
349
+ message = StrongMigrations.error_messages[message_key] || "Missing message"
350
+ message = message + append if append
351
+
352
+ vars[:migration_name] = @migration.class.name
353
+ vars[:migration_suffix] = "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
354
+ vars[:base_model] = "ApplicationRecord"
355
+
356
+ # escape % not followed by {
357
+ message = message.gsub(/%(?!{)/, "%%") % vars if message.include?("%")
358
+ @migration.stop!(message, header: header || "Dangerous operation detected")
359
+ end
360
+
361
+ def constraint_str(statement, identifiers)
362
+ # not all identifiers are tables, but this method of quoting should be fine
363
+ statement % identifiers.map { |v| connection.quote_table_name(v) }
364
+ end
365
+
366
+ def safety_assured_str(code)
367
+ "safety_assured do\n execute '#{code}' \n end"
368
+ end
369
+
370
+ def command_str(command, args)
371
+ str_args = args[0..-2].map { |a| a.inspect }
372
+
373
+ # prettier last arg
374
+ last_arg = args[-1]
375
+ if last_arg.is_a?(Hash)
376
+ if last_arg.any?
377
+ str_args << last_arg.map do |k, v|
378
+ if v.is_a?(Hash)
379
+ # pretty index: {algorithm: :concurrently}
380
+ "#{k}: {#{v.map { |k2, v2| "#{k2}: #{v2.inspect}" }.join(", ")}}"
381
+ else
382
+ "#{k}: #{v.inspect}"
383
+ end
384
+ end.join(", ")
385
+ end
386
+ else
387
+ str_args << last_arg.inspect
388
+ end
389
+
390
+ "#{command} #{str_args.join(", ")}"
391
+ end
392
+
393
+ def backfill_code(table, column, default)
394
+ model = table.to_s.classify
395
+ "#{model}.unscoped.in_batches do |relation| \n relation.update_all #{column}: #{default.inspect}\n sleep(0.01)\n end"
396
+ end
397
+
398
+ def new_table?(table)
399
+ @new_tables.include?(table.to_s)
400
+ end
401
+ end
402
+ end
@@ -1,6 +1,7 @@
1
1
  module StrongMigrations
2
2
  module DatabaseTasks
3
- def migrate
3
+ # Active Record 7 adds version argument
4
+ def migrate(*args)
4
5
  super
5
6
  rescue => e
6
7
  if e.cause.is_a?(StrongMigrations::Error)
@@ -0,0 +1,222 @@
1
+ module StrongMigrations
2
+ self.error_messages = {
3
+ add_column_default:
4
+ "Adding a column with a non-null 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_callable:
29
+ "Strong Migrations does not support inspecting callable default values.
30
+ Please make really sure you're not calling a VOLATILE function,
31
+ then wrap it in a safety_assured { ... } block.",
32
+
33
+ add_column_json:
34
+ "There's no equality operator for the json column type, which can cause errors for
35
+ existing SELECT DISTINCT queries in your application. Use jsonb instead.
36
+
37
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
38
+ def change
39
+ %{command}
40
+ end
41
+ end",
42
+
43
+ change_column:
44
+ "Changing the type of an existing column blocks %{rewrite_blocks}
45
+ while the entire table is rewritten. A safer approach is to:
46
+
47
+ 1. Create a new column
48
+ 2. Write to both columns
49
+ 3. Backfill data from the old column to the new column
50
+ 4. Move reads from the old column to the new column
51
+ 5. Stop writing to the old column
52
+ 6. Drop the old column",
53
+
54
+ change_column_with_not_null:
55
+ "Changing the type is safe, but setting NOT NULL is not.",
56
+
57
+ remove_column: "Active Record caches attributes, which causes problems
58
+ when removing columns. Be sure to ignore the column%{column_suffix}:
59
+
60
+ class %{model} < %{base_model}
61
+ %{code}
62
+ end
63
+
64
+ Deploy the code, then wrap this step in a safety_assured { ... } block.
65
+
66
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
67
+ def change
68
+ safety_assured { %{command} }
69
+ end
70
+ end",
71
+
72
+ rename_column:
73
+ "Renaming a column that's in use will cause errors
74
+ in your application. A safer approach is to:
75
+
76
+ 1. Create a new column
77
+ 2. Write to both columns
78
+ 3. Backfill data from the old column to new column
79
+ 4. Move reads from the old column to the new column
80
+ 5. Stop writing to the old column
81
+ 6. Drop the old column",
82
+
83
+ rename_table:
84
+ "Renaming a table that's in use will cause errors
85
+ in your application. A safer approach is to:
86
+
87
+ 1. Create a new table. Don't forget to recreate indexes from the old table
88
+ 2. Write to both tables
89
+ 3. Backfill data from the old table to new table
90
+ 4. Move reads from the old table to the new table
91
+ 5. Stop writing to the old table
92
+ 6. Drop the old table",
93
+
94
+ add_reference:
95
+ "%{headline} Instead, use:
96
+
97
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
98
+ disable_ddl_transaction!
99
+
100
+ def change
101
+ %{command}
102
+ end
103
+ end",
104
+
105
+ add_index:
106
+ "Adding an index non-concurrently blocks writes. Instead, use:
107
+
108
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
109
+ disable_ddl_transaction!
110
+
111
+ def change
112
+ %{command}
113
+ end
114
+ end",
115
+
116
+ remove_index:
117
+ "Removing an index non-concurrently blocks writes. Instead, use:
118
+
119
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
120
+ disable_ddl_transaction!
121
+
122
+ def change
123
+ %{command}
124
+ end
125
+ end",
126
+
127
+ add_index_columns:
128
+ "Adding a non-unique index with more than three columns rarely improves performance.
129
+ Instead, start an index with columns that narrow down the results the most.",
130
+
131
+ change_table:
132
+ "Strong Migrations does not support inspecting what happens inside a
133
+ change_table block, so cannot help you here. Please make really sure that what
134
+ you're doing is safe before proceeding, then wrap it in a safety_assured { ... } block.",
135
+
136
+ create_table:
137
+ "The force option will destroy existing tables.
138
+ If this is intended, drop the existing table first.
139
+ Otherwise, remove the force option.",
140
+
141
+ execute:
142
+ "Strong Migrations does not support inspecting what happens inside an
143
+ execute call, so cannot help you here. Please make really sure that what
144
+ you're doing is safe before proceeding, then wrap it in a safety_assured { ... } block.",
145
+
146
+ change_column_null:
147
+ "Passing a default value to change_column_null runs a single UPDATE query,
148
+ which can cause downtime. Instead, backfill the existing rows in the
149
+ Rails console or a separate migration with disable_ddl_transaction!.
150
+
151
+ class Backfill%{migration_name} < ActiveRecord::Migration%{migration_suffix}
152
+ disable_ddl_transaction!
153
+
154
+ def up
155
+ %{code}
156
+ end
157
+ end",
158
+
159
+ change_column_null_postgresql:
160
+ "Setting NOT NULL on an existing column blocks reads and writes while every row is checked.
161
+ Instead, add a check constraint and validate it in a separate migration.
162
+
163
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
164
+ def change
165
+ %{add_constraint_code}
166
+ end
167
+ end
168
+
169
+ class Validate%{migration_name} < ActiveRecord::Migration%{migration_suffix}
170
+ %{validate_constraint_code}
171
+ end",
172
+
173
+ change_column_null_mysql:
174
+ "Setting NOT NULL on an existing column is not safe without strict mode enabled.",
175
+
176
+ add_foreign_key:
177
+ "Adding a foreign key blocks writes on both tables. Instead,
178
+ add the foreign key without validating existing rows,
179
+ then validate them in a separate migration.
180
+
181
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
182
+ def change
183
+ %{add_foreign_key_code}
184
+ end
185
+ end
186
+
187
+ class Validate%{migration_name} < ActiveRecord::Migration%{migration_suffix}
188
+ def change
189
+ %{validate_foreign_key_code}
190
+ end
191
+ end",
192
+
193
+ validate_foreign_key:
194
+ "Validating a foreign key while writes are blocked is dangerous.
195
+ Use disable_ddl_transaction! or a separate migration.",
196
+
197
+ add_check_constraint:
198
+ "Adding a check constraint key blocks reads and writes while every row is checked.
199
+ Instead, add the check constraint without validating existing rows,
200
+ then validate them in a separate migration.
201
+
202
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
203
+ def change
204
+ %{add_check_constraint_code}
205
+ end
206
+ end
207
+
208
+ class Validate%{migration_name} < ActiveRecord::Migration%{migration_suffix}
209
+ def change
210
+ %{validate_check_constraint_code}
211
+ end
212
+ end",
213
+
214
+ add_check_constraint_mysql:
215
+ "Adding a check constraint to an existing table is not safe with your database engine.",
216
+
217
+ validate_check_constraint:
218
+ "Validating a check constraint while writes are blocked is dangerous.
219
+ Use disable_ddl_transaction! or a separate migration."
220
+ }
221
+ self.enabled_checks = (error_messages.keys - [:remove_index]).map { |k| [k, {}] }.to_h
222
+ end
@@ -9,8 +9,13 @@ module StrongMigrations
9
9
  def method_missing(method, *args)
10
10
  return super if is_a?(ActiveRecord::Schema)
11
11
 
12
- strong_migrations_checker.perform(method, *args) do
13
- super
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
14
19
  end
15
20
  end
16
21
  ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
@@ -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
@@ -56,21 +56,6 @@ module StrongMigrations
56
56
  end
57
57
  end
58
58
 
59
- def safe_add_foreign_key_code(from_table, to_table, add_code, validate_code)
60
- @migration.reversible do |dir|
61
- dir.up do
62
- @migration.safety_assured do
63
- @migration.execute(add_code)
64
- disable_transaction
65
- @migration.execute(validate_code)
66
- end
67
- end
68
- dir.down do
69
- @migration.remove_foreign_key(from_table, to_table)
70
- end
71
- end
72
- end
73
-
74
59
  def safe_add_check_constraint(table, expression, add_options, validate_options)
75
60
  @migration.reversible do |dir|
76
61
  dir.up do
@@ -84,9 +69,13 @@ module StrongMigrations
84
69
  end
85
70
  end
86
71
 
87
- def safe_change_column_null(add_code, validate_code, change_args, remove_code)
72
+ def safe_change_column_null(add_code, validate_code, change_args, remove_code, default)
88
73
  @migration.reversible do |dir|
89
74
  dir.up do
75
+ unless default.nil?
76
+ raise Error, "default value not supported yet with safe_by_default"
77
+ end
78
+
90
79
  @migration.safety_assured do
91
80
  @migration.execute(add_code)
92
81
  disable_transaction