strong_migrations 1.6.0 → 2.8.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.
@@ -25,20 +25,25 @@ class Backfill%{migration_name} < ActiveRecord::Migration%{migration_suffix}
25
25
  end
26
26
  end",
27
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.
28
+ add_column_default_callable:
29
+ "Strong Migrations does not support inspecting callable default values.
30
+
31
+ If the default value is %{default_type}, add the column without a default value, then change the default.
31
32
 
32
33
  class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
33
- def change
34
- %{command}
34
+ def up
35
+ %{add_command}
36
+ %{change_command}, -> { ... }
35
37
  end
36
- end",
37
38
 
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.",
39
+ def down
40
+ %{remove_command}
41
+ end
42
+ end
43
+
44
+ Then backfill the existing rows in the Rails console or a separate migration with disable_ddl_transaction!.
45
+
46
+ Otherwise, wrap this step in a safety_assured { ... } block.",
42
47
 
43
48
  add_column_json:
44
49
  "There's no equality operator for the json column type, which can cause errors for
@@ -53,6 +58,9 @@ end",
53
58
  add_column_generated_stored:
54
59
  "Adding a stored generated column blocks %{rewrite_blocks} while the entire table is rewritten.",
55
60
 
61
+ add_column_auto_incrementing:
62
+ "Adding an auto-incrementing column blocks %{rewrite_blocks} while the entire table is rewritten.",
63
+
56
64
  change_column:
57
65
  "Changing the type of an existing column blocks %{rewrite_blocks}
58
66
  while the entire table is rewritten. A safer approach is to:
@@ -67,6 +75,22 @@ while the entire table is rewritten. A safer approach is to:
67
75
  change_column_with_not_null:
68
76
  "Changing the type is safe, but setting NOT NULL is not.",
69
77
 
78
+ change_column_constraint: "Changing the type of a column that has check constraints blocks reads and writes
79
+ while every row is checked. Drop the check constraints on the column before
80
+ changing the type and add them back afterwards.
81
+
82
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
83
+ def change
84
+ %{change_column_code}
85
+ end
86
+ end
87
+
88
+ class Validate%{migration_name} < ActiveRecord::Migration%{migration_suffix}
89
+ def change
90
+ %{validate_constraint_code}
91
+ end
92
+ end",
93
+
70
94
  remove_column: "Active Record caches attributes, which causes problems
71
95
  when removing columns. Be sure to ignore the column%{column_suffix}:
72
96
 
@@ -88,18 +112,43 @@ in your application. A safer approach is to:
88
112
 
89
113
  1. Create a new column
90
114
  2. Write to both columns
91
- 3. Backfill data from the old column to new column
115
+ 3. Backfill data from the old column to the new column
92
116
  4. Move reads from the old column to the new column
93
117
  5. Stop writing to the old column
94
118
  6. Drop the old column",
95
119
 
120
+ rename_enum_value:
121
+ "Renaming an enum value that's in use will cause errors
122
+ in your application. A safer approach is to:
123
+
124
+ 1. Add a new enum value before or after the old value
125
+ 2. Update application code to handle both values and write the new value
126
+ 3. Backfill data from the old value to the new value
127
+
128
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
129
+ def up
130
+ %{command}
131
+ end
132
+ end",
133
+
134
+ rename_schema:
135
+ "Renaming a schema that's in use will cause errors
136
+ in your application. A safer approach is to:
137
+
138
+ 1. Create a new schema
139
+ 2. Write to both schemas
140
+ 3. Backfill data from the old schema to the new schema
141
+ 4. Move reads from the old schema to the new schema
142
+ 5. Stop writing to the old schema
143
+ 6. Drop the old schema",
144
+
96
145
  rename_table:
97
146
  "Renaming a table that's in use will cause errors
98
147
  in your application. A safer approach is to:
99
148
 
100
149
  1. Create a new table. Don't forget to recreate indexes from the old table
101
150
  2. Write to both tables
102
- 3. Backfill data from the old table to new table
151
+ 3. Backfill data from the old table to the new table
103
152
  4. Move reads from the old table to the new table
104
153
  5. Stop writing to the old table
105
154
  6. Drop the old table",
@@ -154,7 +203,7 @@ you're doing is safe before proceeding, then wrap it in a safety_assured { ... }
154
203
  create_table:
155
204
  "The force option will destroy existing tables.
156
205
  If this is intended, drop the existing table first.
157
- Otherwise, remove the force option.",
206
+ In any case, remove the force option.",
158
207
 
159
208
  execute:
160
209
  "Strong Migrations does not support inspecting what happens inside an
@@ -215,6 +264,28 @@ class Validate%{migration_name} < ActiveRecord::Migration%{migration_suffix}
215
264
  end
216
265
  end",
217
266
 
267
+ add_foreign_key_mysql:
268
+ "Adding a foreign key blocks writes on both tables. If you are 100% sure
269
+ all rows are valid and migrations do not use a connection pooler,
270
+ you can add the foreign key without validating existing rows.
271
+
272
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
273
+ def up
274
+ safety_assured do
275
+ begin
276
+ execute \"SET SESSION foreign_key_checks = 0\"
277
+ %{add_foreign_key_code}
278
+ ensure
279
+ execute \"SET SESSION foreign_key_checks = 1\"
280
+ end
281
+ end
282
+ end
283
+
284
+ def down
285
+ %{remove_foreign_key_code}
286
+ end
287
+ end",
288
+
218
289
  validate_foreign_key:
219
290
  "Validating a foreign key while writes are blocked is dangerous.
220
291
  Use disable_ddl_transaction! or a separate migration.",
@@ -244,7 +315,42 @@ end",
244
315
  Use disable_ddl_transaction! or a separate migration.",
245
316
 
246
317
  add_exclusion_constraint:
247
- "Adding an exclusion constraint blocks reads and writes while every row is checked."
318
+ "Adding an exclusion constraint blocks reads and writes while every row is checked.",
319
+
320
+ add_unique_constraint:
321
+ "Adding a unique constraint creates a unique index, which blocks reads and writes.
322
+ Instead, create a unique index concurrently, then use it for the constraint.
323
+
324
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
325
+ disable_ddl_transaction!
326
+
327
+ def up
328
+ %{index_command}
329
+ %{constraint_command}
330
+ end
331
+
332
+ def down
333
+ %{remove_command}
334
+ end
335
+ end",
336
+
337
+ copy_algorithm:
338
+ "Using the COPY algorithm blocks writes. Instead, use:
339
+
340
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
341
+ def change
342
+ %{command}
343
+ end
344
+ end",
345
+
346
+ lock_option:
347
+ "Using %{lock_type} locking blocks %{lock_blocks}. Instead, use:
348
+
349
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
350
+ def change
351
+ %{command}
352
+ end
353
+ end"
248
354
  }
249
355
  self.enabled_checks = (error_messages.keys - [:remove_index]).map { |k| [k, {}] }.to_h
250
356
  end
@@ -7,10 +7,7 @@ module StrongMigrations
7
7
  end
8
8
 
9
9
  def method_missing(method, *args)
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)
10
+ return super if is_a?(ActiveRecord::Schema) || is_a?(ActiveRecord::Schema::Definition)
14
11
 
15
12
  catch(:safe) do
16
13
  strong_migrations_checker.perform(method, *args) do
@@ -18,10 +15,19 @@ module StrongMigrations
18
15
  end
19
16
  end
20
17
  end
21
- ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
18
+ # same as ActiveRecord::Migration
19
+ ruby2_keywords(:method_missing)
20
+
21
+ def revert(*)
22
+ if strong_migrations_checker.version_safe?
23
+ safety_assured { super }
24
+ else
25
+ super
26
+ end
27
+ end
22
28
 
23
29
  def safety_assured
24
- strong_migrations_checker.safety_assured do
30
+ strong_migrations_checker.class.safety_assured do
25
31
  yield
26
32
  end
27
33
  end
@@ -1,9 +1,26 @@
1
1
  module StrongMigrations
2
- module DatabaseTasks
3
- # Active Record 7 adds version argument
4
- def migrate(*args)
2
+ module MigrationContext
3
+ def up(...)
5
4
  super
6
5
  rescue => e
6
+ strong_migrations_process_exception(e)
7
+ end
8
+
9
+ def down(...)
10
+ super
11
+ rescue => e
12
+ strong_migrations_process_exception(e)
13
+ end
14
+
15
+ def run(...)
16
+ super
17
+ rescue => e
18
+ strong_migrations_process_exception(e)
19
+ end
20
+
21
+ private
22
+
23
+ def strong_migrations_process_exception(e)
7
24
  if e.cause.is_a?(StrongMigrations::Error)
8
25
  # strip cause and clean backtrace
9
26
  def e.cause
@@ -1,18 +1,24 @@
1
1
  module StrongMigrations
2
2
  module Migrator
3
- def ddl_transaction(migration, *args)
4
- return super unless StrongMigrations.lock_timeout_retries > 0 && use_transaction?(migration)
3
+ def ddl_transaction(migration, ...)
4
+ retries = StrongMigrations.lock_timeout_retries > 0 && use_transaction?(migration)
5
+ return super unless retries || StrongMigrations.transaction_timeout
5
6
 
6
7
  # handle MigrationProxy class
7
- migration = migration.send(:migration) if migration.respond_to?(:migration, true)
8
+ migration = migration.send(:migration) if !migration.is_a?(ActiveRecord::Migration) && migration.respond_to?(:migration, true)
8
9
 
9
- # retry migration since the entire transaction needs to be rerun
10
10
  checker = migration.send(:strong_migrations_checker)
11
+ return super if checker.skip?
12
+
13
+ checker.set_transaction_timeout
14
+ return super unless retries
15
+
16
+ # retry migration since the entire transaction needs to be rerun
11
17
  checker.retry_lock_timeouts(check_committed: true) do
12
18
  # failed transaction reverts timeout, so need to re-apply
13
- checker.timeouts_set = false
19
+ checker.reset
14
20
 
15
- super(migration, *args)
21
+ super(migration, ...)
16
22
  end
17
23
  end
18
24
  end
@@ -1,10 +1,9 @@
1
1
  module StrongMigrations
2
2
  module SafeMethods
3
3
  def safe_by_default_method?(method)
4
- StrongMigrations.safe_by_default && [:add_index, :add_belongs_to, :add_reference, :remove_index, :add_foreign_key, :add_check_constraint, :change_column_null].include?(method)
4
+ StrongMigrations.safe_by_default && !version_safe? && [:add_index, :add_belongs_to, :add_reference, :remove_index, :add_foreign_key, :add_check_constraint, :change_column_null].include?(method)
5
5
  end
6
6
 
7
- # TODO check if invalid index with expected name exists and remove if needed
8
7
  def safe_add_index(*args, **options)
9
8
  disable_transaction
10
9
  @migration.add_index(*args, **options.merge(algorithm: :concurrently))
@@ -30,10 +29,11 @@ module StrongMigrations
30
29
  (ActiveRecord::Base.pluralize_table_names ? reference.to_s.pluralize : reference).to_sym
31
30
  end
32
31
 
32
+ foreign_key_opts = foreign_key.is_a?(Hash) ? foreign_key.except(:to_table) : {}
33
33
  if reference
34
- @migration.add_foreign_key(table, name, column: "#{reference}_id")
34
+ @migration.add_foreign_key(table, name, column: "#{reference}_id", **foreign_key_opts)
35
35
  else
36
- @migration.add_foreign_key(table, name)
36
+ @migration.add_foreign_key(table, name, **foreign_key_opts)
37
37
  end
38
38
  end
39
39
  end
@@ -46,22 +46,14 @@ module StrongMigrations
46
46
  def safe_add_foreign_key(from_table, to_table, *args, **options)
47
47
  @migration.reversible do |dir|
48
48
  dir.up do
49
- @migration.add_foreign_key(from_table, to_table, *args, **options.merge(validate: false))
50
- disable_transaction
51
- validate_options = options.slice(:column, :name)
52
- if ActiveRecord::VERSION::MAJOR >= 6
53
- @migration.validate_foreign_key(from_table, to_table, **validate_options)
54
- else
55
- @migration.validate_foreign_key(from_table, validate_options.any? ? validate_options : to_table)
49
+ if !connection.foreign_key_exists?(from_table, to_table, **options.merge(validate: false))
50
+ @migration.add_foreign_key(from_table, to_table, *args, **options.merge(validate: false))
56
51
  end
52
+ disable_transaction
53
+ @migration.validate_foreign_key(from_table, to_table, **options.slice(:column, :name))
57
54
  end
58
55
  dir.down do
59
- remove_options = options.slice(:column, :name)
60
- if ActiveRecord::VERSION::MAJOR >= 6
61
- @migration.remove_foreign_key(from_table, to_table, **remove_options)
62
- else
63
- @migration.remove_foreign_key(from_table, remove_options.any? ? remove_options : to_table)
64
- end
56
+ @migration.remove_foreign_key(from_table, to_table, **options.slice(:column, :name))
65
57
  end
66
58
  end
67
59
  end
@@ -69,7 +61,10 @@ module StrongMigrations
69
61
  def safe_add_check_constraint(table, expression, *args, add_options, validate_options)
70
62
  @migration.reversible do |dir|
71
63
  dir.up do
72
- @migration.add_check_constraint(table, expression, *args, **add_options)
64
+ # only skip invalid constraints
65
+ unless connection.check_constraints(table).any? { |c| c.options[:name] == validate_options[:name] && !c.options[:validate] }
66
+ @migration.add_check_constraint(table, expression, *args, **add_options)
67
+ end
73
68
  disable_transaction
74
69
  @migration.validate_check_constraint(table, **validate_options)
75
70
  end
@@ -79,35 +74,60 @@ module StrongMigrations
79
74
  end
80
75
  end
81
76
 
82
- def safe_change_column_null(add_code, validate_code, change_args, remove_code, default)
77
+ def safe_change_column_null(add_args, validate_args, change_args, remove_args, table, column, default, constraints)
83
78
  @migration.reversible do |dir|
84
79
  dir.up do
85
80
  unless default.nil?
86
- raise Error, "default value not supported yet with safe_by_default"
87
- end
81
+ # TODO search for parent model if needed
82
+ if connection.pool != ActiveRecord::Base.connection_pool
83
+ raise_error :change_column_null,
84
+ code: backfill_code(table, column, default)
85
+ end
86
+
87
+ model =
88
+ Class.new(ActiveRecord::Base) do
89
+ self.table_name = table
88
90
 
89
- @migration.safety_assured do
90
- @migration.execute(add_code)
91
+ def self.to_s
92
+ "Backfill"
93
+ end
94
+ end
95
+
96
+ update_sql =
97
+ model.connection_pool.with_connection do |c|
98
+ quoted_column = c.quote_column_name(column)
99
+ quoted_default = c.quote_default_expression(default, c.send(:column_for, table, column))
100
+ "#{quoted_column} = #{quoted_default}"
101
+ end
102
+
103
+ @migration.say("Backfilling default")
91
104
  disable_transaction
92
- @migration.execute(validate_code)
93
- end
94
- if change_args
95
- @migration.change_column_null(*change_args)
96
- @migration.safety_assured do
97
- @migration.execute(remove_code)
105
+ model.unscoped.in_batches(of: 10000) do |relation|
106
+ relation.where(column => nil).update_all(update_sql)
107
+ sleep(0.01)
98
108
  end
99
109
  end
110
+
111
+ add_options = add_args.extract_options!
112
+ validate_options = validate_args.extract_options!
113
+ remove_options = remove_args.extract_options!
114
+
115
+ # only skip invalid constraints
116
+ unless constraints.any? { |c| c.options[:name] == validate_options[:name] && !c.options[:validate] }
117
+ @migration.add_check_constraint(*add_args, **add_options)
118
+ end
119
+ disable_transaction
120
+
121
+ connection.begin_db_transaction
122
+ @migration.validate_check_constraint(*validate_args, **validate_options)
123
+ @migration.change_column_null(*change_args)
124
+ @migration.remove_check_constraint(*remove_args, **remove_options)
125
+ connection.commit_db_transaction
100
126
  end
101
127
  dir.down do
102
- if change_args
103
- down_args = change_args.dup
104
- down_args[2] = true
105
- @migration.change_column_null(*down_args)
106
- else
107
- @migration.safety_assured do
108
- @migration.execute(remove_code)
109
- end
110
- end
128
+ down_args = change_args.dup
129
+ down_args[2] = true
130
+ @migration.change_column_null(*down_args)
111
131
  end
112
132
  end
113
133
  end
@@ -116,13 +136,13 @@ module StrongMigrations
116
136
  # so just commit at start
117
137
  def disable_transaction
118
138
  if in_transaction? && !transaction_disabled
119
- @migration.connection.commit_db_transaction
139
+ connection.commit_db_transaction
120
140
  self.transaction_disabled = true
121
141
  end
122
142
  end
123
143
 
124
144
  def in_transaction?
125
- @migration.connection.open_transactions > 0
145
+ connection.open_transactions > 0
126
146
  end
127
147
  end
128
148
  end
@@ -1,9 +1,9 @@
1
1
  module StrongMigrations
2
2
  module SchemaDumper
3
- def initialize(connection, *args, **options)
3
+ def initialize(connection, ...)
4
4
  return super unless StrongMigrations.alphabetize_schema
5
5
 
6
- super(WrappedConnection.new(connection), *args, **options)
6
+ super(WrappedConnection.new(connection), ...)
7
7
  end
8
8
  end
9
9
 
@@ -14,8 +14,19 @@ module StrongMigrations
14
14
  @connection = connection
15
15
  end
16
16
 
17
- def columns(*args, **options)
18
- @connection.columns(*args, **options).sort_by(&:name)
17
+ def columns(...)
18
+ @connection.columns(...).sort_by(&:name)
19
+ end
20
+
21
+ # forward private methods with send
22
+ # method_missing cannot tell how method was called
23
+ # this is not ideal, but other solutions have drawbacks
24
+ def send(name, ...)
25
+ if respond_to?(name, true)
26
+ super
27
+ else
28
+ @connection.send(name, ...)
29
+ end
19
30
  end
20
31
  end
21
32
  end
@@ -1,3 +1,3 @@
1
1
  module StrongMigrations
2
- VERSION = "1.6.0"
2
+ VERSION = "2.8.0"
3
3
  end
@@ -11,8 +11,8 @@ require_relative "strong_migrations/adapters/postgresql_adapter"
11
11
  require_relative "strong_migrations/checks"
12
12
  require_relative "strong_migrations/safe_methods"
13
13
  require_relative "strong_migrations/checker"
14
- require_relative "strong_migrations/database_tasks"
15
14
  require_relative "strong_migrations/migration"
15
+ require_relative "strong_migrations/migration_context"
16
16
  require_relative "strong_migrations/migrator"
17
17
  require_relative "strong_migrations/version"
18
18
 
@@ -29,7 +29,7 @@ module StrongMigrations
29
29
  :target_postgresql_version, :target_mysql_version, :target_mariadb_version,
30
30
  :enabled_checks, :lock_timeout, :statement_timeout, :check_down, :target_version,
31
31
  :safe_by_default, :target_sql_mode, :lock_timeout_retries, :lock_timeout_retry_delay,
32
- :alphabetize_schema
32
+ :alphabetize_schema, :skipped_databases, :remove_invalid_indexes, :transaction_timeout
33
33
  attr_writer :lock_timeout_limit
34
34
  end
35
35
  self.auto_analyze = false
@@ -40,6 +40,8 @@ module StrongMigrations
40
40
  self.safe_by_default = false
41
41
  self.check_down = false
42
42
  self.alphabetize_schema = false
43
+ self.skipped_databases = []
44
+ self.remove_invalid_indexes = false
43
45
 
44
46
  # private
45
47
  def self.developer_env?
@@ -83,6 +85,10 @@ module StrongMigrations
83
85
  false
84
86
  end
85
87
  end
88
+
89
+ def self.skip_database(database)
90
+ self.skipped_databases << database
91
+ end
86
92
  end
87
93
 
88
94
  # load error messages
@@ -90,12 +96,9 @@ require_relative "strong_migrations/error_messages"
90
96
 
91
97
  ActiveSupport.on_load(:active_record) do
92
98
  ActiveRecord::Migration.prepend(StrongMigrations::Migration)
99
+ ActiveRecord::MigrationContext.prepend(StrongMigrations::MigrationContext)
93
100
  ActiveRecord::Migrator.prepend(StrongMigrations::Migrator)
94
101
 
95
- if defined?(ActiveRecord::Tasks::DatabaseTasks)
96
- ActiveRecord::Tasks::DatabaseTasks.singleton_class.prepend(StrongMigrations::DatabaseTasks)
97
- end
98
-
99
102
  require_relative "strong_migrations/schema_dumper"
100
103
  ActiveRecord::SchemaDumper.prepend(StrongMigrations::SchemaDumper)
101
104
  end
metadata CHANGED
@@ -1,16 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: strong_migrations
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.0
4
+ version: 2.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew Kane
8
8
  - Bob Remeika
9
9
  - David Waller
10
- autorequire:
11
10
  bindir: bin
12
11
  cert_chain: []
13
- date: 2023-07-22 00:00:00.000000000 Z
12
+ date: 1980-01-02 00:00:00.000000000 Z
14
13
  dependencies:
15
14
  - !ruby/object:Gem::Dependency
16
15
  name: activerecord
@@ -18,15 +17,14 @@ dependencies:
18
17
  requirements:
19
18
  - - ">="
20
19
  - !ruby/object:Gem::Version
21
- version: '5.2'
20
+ version: '7.2'
22
21
  type: :runtime
23
22
  prerelease: false
24
23
  version_requirements: !ruby/object:Gem::Requirement
25
24
  requirements:
26
25
  - - ">="
27
26
  - !ruby/object:Gem::Version
28
- version: '5.2'
29
- description:
27
+ version: '7.2'
30
28
  email:
31
29
  - andrew@ankane.org
32
30
  - bob.remeika@gmail.com
@@ -47,9 +45,9 @@ files:
47
45
  - lib/strong_migrations/adapters/postgresql_adapter.rb
48
46
  - lib/strong_migrations/checker.rb
49
47
  - lib/strong_migrations/checks.rb
50
- - lib/strong_migrations/database_tasks.rb
51
48
  - lib/strong_migrations/error_messages.rb
52
49
  - lib/strong_migrations/migration.rb
50
+ - lib/strong_migrations/migration_context.rb
53
51
  - lib/strong_migrations/migrator.rb
54
52
  - lib/strong_migrations/railtie.rb
55
53
  - lib/strong_migrations/safe_methods.rb
@@ -60,7 +58,6 @@ homepage: https://github.com/ankane/strong_migrations
60
58
  licenses:
61
59
  - MIT
62
60
  metadata: {}
63
- post_install_message:
64
61
  rdoc_options: []
65
62
  require_paths:
66
63
  - lib
@@ -68,15 +65,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
68
65
  requirements:
69
66
  - - ">="
70
67
  - !ruby/object:Gem::Version
71
- version: '2.6'
68
+ version: '3.3'
72
69
  required_rubygems_version: !ruby/object:Gem::Requirement
73
70
  requirements:
74
71
  - - ">="
75
72
  - !ruby/object:Gem::Version
76
73
  version: '0'
77
74
  requirements: []
78
- rubygems_version: 3.4.10
79
- signing_key:
75
+ rubygems_version: 4.0.10
80
76
  specification_version: 4
81
77
  summary: Catch unsafe migrations in development
82
78
  test_files: []