online_migrations 0.33.1 → 0.34.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5ea2552d8a879bacf5a5f8360a13aeafa9b36bec50943b359eb2d0501532ceb0
4
- data.tar.gz: 251b3bee148c70f37d86892ce985a93ee664a2623111de38b9fcff47b6be4baa
3
+ metadata.gz: b7f1dd28a4540742f77c2a33d7336f4f6f2b12a4f267a1cc556975f7b7122ba4
4
+ data.tar.gz: db448dca841dc39418cd92e5d86108ee0b59b69b72eeda391b0a81a30aa7ce71
5
5
  SHA512:
6
- metadata.gz: 295c89b41757157ce16b7b2585d3516cd2900b1ba0291523c2e689991002669e08ba3928fc23b1047a92dbd0f225436f0b2f8ed9c7b6803fee8a4e56c4ef4239
7
- data.tar.gz: 16c0ada6d9a4e07271eae3a1a509f23a4756a5af3eebf172ca4971680b1c799f148c2f7bbeeffdcfce2e600cf24095e1663f03d6de00dd692af803ccdad22f62
6
+ metadata.gz: b423580e44eefc2f1ff71b2ebdcd15e4ff449f869231aaebe38c4598dc364d817eb3b484a82b7a0cc493727f94d956610ced75ac409092227c2e5cc727935eee
7
+ data.tar.gz: 3287e23ec07a09a1026790ab4747427eafbc70f675fd2cbf7382f302a86f748c3fde45f5a7546b3e66848cd1e12eb86f073ff3e5461471bb35ba79f5b0a160ec
data/CHANGELOG.md CHANGED
@@ -1,5 +1,28 @@
1
1
  ## master (unreleased)
2
2
 
3
+ ## 0.34.0 (2026-05-29)
4
+
5
+ - Drop support for Rails < 7.2
6
+
7
+ - Do not schedule background data migrations with non existent data migration classes
8
+
9
+ This prevents a situation when the background data migration was enqueued and started running
10
+ before the deploy completes and the data migration class is available.
11
+
12
+ - Add ability to enqueue background migrations for specific shards (if using sharding)
13
+
14
+ ```ruby
15
+ add_index_in_background(:users, :name, connection_class_name: "ApplicationRecord", shard: :shard_one)
16
+ enqueue_background_data_migration("MyMigration", shard: :shard_one)
17
+ ```
18
+
19
+ - Fix validating foreign key in `add_reference_concurrently` when multiple foreign keys target the same table
20
+
21
+ ## 0.33.2 (2026-03-16)
22
+
23
+ - Fix message for adding to `ignored_columns` when renaming a column
24
+ - Schedule background data migration jobs only after committing the database transaction
25
+
3
26
  ## 0.33.1 (2026-02-05)
4
27
 
5
28
  - Allow "running" to "enqueued" state transition for stuck background data migrations
@@ -17,7 +40,7 @@
17
40
  - Add ability to create delayed background migrations
18
41
 
19
42
  ```ruby
20
- add_index_in_background(:users, :name, delay: true)
43
+ add_index_in_background(:users, :name, connection_class_name: "ApplicationRecord", delay: true)
21
44
  enqueue_background_data_migration("MyMigration", delay: true)
22
45
  ```
23
46
 
@@ -94,7 +117,7 @@
94
117
  - Drop support for Ruby < 3.1 and Rails < 7.1
95
118
  - Add check for `change_column` for columns with check constraints
96
119
 
97
- - Allow to require safety reason explanation when calling `safery_assured`
120
+ - Allow to require safety reason explanation when calling `safety_assured`
98
121
 
99
122
  ```ruby
100
123
  # config/initializers/online_migrations.rb
data/README.md CHANGED
@@ -17,7 +17,7 @@ See [comparison to `strong_migrations`](#comparison-to-strong_migrations)
17
17
  ## Requirements
18
18
 
19
19
  - Ruby 3.1+
20
- - Rails 7.1+
20
+ - Rails 7.2+
21
21
  - PostgreSQL 12+
22
22
 
23
23
  For older Ruby and Rails versions you can use older versions of this gem.
@@ -441,8 +441,9 @@ It will use a combination of a VIEW and column aliasing to work with both column
441
441
  ```
442
442
 
443
443
  4. Replace usages of the old column with a new column in the codebase
444
- 5. If you enabled Active Record `enumerate_columns_in_select_statements` setting in your application
445
- (is disabled by default in Active Record >= 7), then you need to ignore old column:
444
+ 5. If the model has `ignored_columns` set, or you enabled Active Record
445
+ `enumerate_columns_in_select_statements` setting (is disabled by default in Active Record >= 7),
446
+ then you need to ignore the old column:
446
447
 
447
448
  ```ruby
448
449
  class User < ApplicationRecord
data/docs/configuring.md CHANGED
@@ -40,7 +40,7 @@ Check the [source code](https://github.com/fatkodima/online_migrations/blob/mast
40
40
 
41
41
  ## Requiring safety_assured reason
42
42
 
43
- To require safety reason explanation when calling `safery_assured` (disabled by default):
43
+ To require safety reason explanation when calling `safety_assured` (disabled by default):
44
44
 
45
45
  ```ruby
46
46
  config.require_safety_assured_reason = true
@@ -1,4 +1,4 @@
1
- class BackgroundDataMigrationsChangeStatusDefault < <%= migration_parent %>
1
+ class BackgroundMigrationsChangeStatusDefault < <%= migration_parent %>
2
2
  def change
3
3
  safety_assured do
4
4
  change_column_default :background_data_migrations, :status, from: "enqueued", to: "pending"
@@ -12,7 +12,7 @@ OnlineMigrations.configure do |config|
12
12
  # Set the version of the production database so the right checks are run in development.
13
13
  # config.target_version = 17
14
14
 
15
- # Configure whether to require safety reason explanation when calling #safery_assured.
15
+ # Configure whether to require safety reason explanation when calling #safety_assured.
16
16
  config.require_safety_assured_reason = false
17
17
 
18
18
  # Configure whether to perform checks when migrating down.
@@ -339,6 +339,7 @@ module OnlineMigrations
339
339
  # @param migration_name [String, Class] Background migration class name
340
340
  # @param arguments [Array] Extra arguments to pass to the migration instance when the migration runs
341
341
  # @param delay [Boolean] Whether this migration should be delayed and approved by the user to start running.
342
+ # @param shard [String, Symbol] Specific shard this migration will be enqueued for. Defaults to all shards.
342
343
  # @option options [Integer] :max_attempts (5) Maximum number of batch run attempts
343
344
  # @option options [String, nil] :connection_class_name Class name to use to get connections
344
345
  #
@@ -369,7 +370,7 @@ module OnlineMigrations
369
370
  # @note For convenience, the enqueued background data migration is run inline
370
371
  # in development and test environments
371
372
  #
372
- def enqueue_background_data_migration(migration_name, *arguments, delay: false, **options)
373
+ def enqueue_background_data_migration(migration_name, *arguments, delay: false, shard: nil, **options)
373
374
  options.assert_valid_keys(:max_attempts, :iteration_pause, :connection_class_name)
374
375
 
375
376
  migration_name = migration_name.name if migration_name.is_a?(Class)
@@ -380,8 +381,16 @@ module OnlineMigrations
380
381
  end
381
382
 
382
383
  connection_class = options[:connection_class_name].constantize
383
- shards = Utils.shard_names(connection_class)
384
- shards = [nil] if shards.size == 1
384
+ shards = Utils.shard_names(connection_class).map(&:to_s)
385
+ if shards.size == 1
386
+ shards = [nil]
387
+ elsif shard
388
+ shard = shard.to_s
389
+ raise "Unknown shard: #{shard}" if !shards.include?(shard)
390
+
391
+ shards = [shard]
392
+ end
393
+
385
394
  status = delay ? :delayed : :pending
386
395
 
387
396
  shards.each do |shard|
@@ -33,20 +33,27 @@ module OnlineMigrations
33
33
  relation = Migration.queue_order
34
34
  relation = relation.where(shard: shard) if shard
35
35
 
36
+ migrations_to_enqueue = []
37
+
36
38
  with_lock do
37
39
  stuck_migrations, active_migrations = relation.running.partition(&:stuck?)
38
- runnable_migrations = relation.pending + stuck_migrations
40
+ runnable_migrations = migrations_with_existing_classes(relation.pending) + stuck_migrations
39
41
 
40
42
  # Ensure no more than 'concurrency' migrations are running at the same time.
41
43
  remaining_to_enqueue = concurrency - active_migrations.count
42
44
  if remaining_to_enqueue > 0
43
- migrations_to_enqueue = runnable_migrations.take(remaining_to_enqueue)
44
- migrations_to_enqueue.each do |migration|
45
- enqueue_migration(migration)
45
+ runnable_migrations.take(remaining_to_enqueue).each do |migration|
46
+ migration.update!(status: :enqueued)
47
+
48
+ migrations_to_enqueue << migration
46
49
  end
47
50
  end
48
51
  end
49
52
 
53
+ migrations_to_enqueue.each do |migration|
54
+ enqueue_migration(migration)
55
+ end
56
+
50
57
  true
51
58
  end
52
59
 
@@ -64,10 +71,20 @@ module OnlineMigrations
64
71
  end
65
72
  end
66
73
 
74
+ def migrations_with_existing_classes(migrations)
75
+ migrations.select do |migration|
76
+ # Detect if the data migration class exists.
77
+ # It may not yet exist if the data migration was enqueued before the deploy finished.
78
+ migration.data_migration
79
+ true
80
+ rescue DataMigration::NotFoundError
81
+ false
82
+ end
83
+ end
84
+
67
85
  def enqueue_migration(migration)
68
86
  job = OnlineMigrations.config.background_data_migrations.job
69
87
  job_class = job.constantize
70
- migration.update!(status: :enqueued)
71
88
 
72
89
  jid = job_class.perform_async(migration.id)
73
90
  if jid
@@ -151,28 +151,28 @@ module OnlineMigrations
151
151
  # @private
152
152
  def run
153
153
  on_shard_if_present do
154
- connection = connection_class.connection
155
-
156
- connection.with_lock_retries do
157
- statement_timeout = self.statement_timeout || OnlineMigrations.config.statement_timeout
158
-
159
- with_statement_timeout(connection, statement_timeout) do
160
- if index_addition?
161
- index = connection.indexes(table_name).find { |i| name.match?(/\b#{i.name}\b/) }
162
- if index
163
- if index.valid?
164
- return
165
- else
166
- connection.remove_index(table_name, name: index.name, algorithm: :concurrently)
154
+ with_connection do |connection|
155
+ connection.with_lock_retries do
156
+ statement_timeout = self.statement_timeout || OnlineMigrations.config.statement_timeout
157
+
158
+ with_statement_timeout(connection, statement_timeout) do
159
+ if index_addition?
160
+ index = connection.indexes(table_name).find { |i| name.match?(/\b#{i.name}\b/) }
161
+ if index
162
+ if index.valid?
163
+ return
164
+ else
165
+ connection.remove_index(table_name, name: index.name, algorithm: :concurrently)
166
+ end
167
167
  end
168
168
  end
169
- end
170
169
 
171
- connection.execute(definition)
170
+ connection.execute(definition)
172
171
 
173
- # Outdated statistics + a new index can hurt performance of existing queries.
174
- if OnlineMigrations.config.auto_analyze
175
- connection.execute("ANALYZE #{table_name}")
172
+ # Outdated statistics + a new index can hurt performance of existing queries.
173
+ if OnlineMigrations.config.auto_analyze
174
+ connection.execute("ANALYZE #{table_name}")
175
+ end
176
176
  end
177
177
  end
178
178
  end
@@ -206,6 +206,12 @@ module OnlineMigrations
206
206
  indexes_in_progress.include?(name)
207
207
  end
208
208
 
209
+ # Extension point, do not remove this method.
210
+ def with_connection(&block)
211
+ # https://github.com/rails/rails/pull/51083
212
+ connection_class.with_connection(&block)
213
+ end
214
+
209
215
  def with_statement_timeout(connection, timeout)
210
216
  return yield if timeout.nil?
211
217
 
@@ -4,7 +4,7 @@ module OnlineMigrations
4
4
  module BackgroundSchemaMigrations
5
5
  module MigrationHelpers
6
6
  def add_index_in_background(table_name, column_name, **options)
7
- migration_options = options.extract!(:max_attempts, :statement_timeout, :connection_class_name, :delay)
7
+ migration_options = options.extract!(:max_attempts, :statement_timeout, :connection_class_name, :delay, :shard)
8
8
 
9
9
  options[:algorithm] = :concurrently
10
10
  index, algorithm, if_not_exists = add_index_options(table_name, column_name, **options)
@@ -34,7 +34,7 @@ module OnlineMigrations
34
34
  def remove_index_in_background(table_name, column_name = nil, name:, **options)
35
35
  raise ArgumentError, "Index name must be specified" if name.blank?
36
36
 
37
- migration_options = options.extract!(:max_attempts, :statement_timeout, :connection_class_name, :delay)
37
+ migration_options = options.extract!(:max_attempts, :statement_timeout, :connection_class_name, :delay, :shard)
38
38
 
39
39
  if !index_exists?(table_name, column_name, **options, name: name)
40
40
  Utils.raise_or_say("Index deletion was not enqueued because the index does not exist.")
@@ -46,7 +46,7 @@ module OnlineMigrations
46
46
  end
47
47
 
48
48
  def validate_foreign_key_in_background(from_table, to_table = nil, **options)
49
- migration_options = options.extract!(:max_attempts, :statement_timeout, :connection_class_name, :delay)
49
+ migration_options = options.extract!(:max_attempts, :statement_timeout, :connection_class_name, :delay, :shard)
50
50
 
51
51
  if !foreign_key_exists?(from_table, to_table, **options)
52
52
  Utils.raise_or_say("Foreign key validation was not enqueued because the foreign key does not exist.")
@@ -87,7 +87,7 @@ module OnlineMigrations
87
87
  end
88
88
  end
89
89
 
90
- def enqueue_background_schema_migration(migration_name, table_name, connection_class_name: nil, delay: false, **options)
90
+ def enqueue_background_schema_migration(migration_name, table_name, connection_class_name: nil, delay: false, shard: nil, **options)
91
91
  options.assert_valid_keys(:definition, :max_attempts, :statement_timeout)
92
92
 
93
93
  if Utils.multiple_databases? && !connection_class_name
@@ -104,8 +104,15 @@ module OnlineMigrations
104
104
  # Normalize to the real connection class name.
105
105
  connection_class_name = connection_class.name
106
106
 
107
- shards = Utils.shard_names(connection_class)
108
- shards = [nil] if shards.size == 1
107
+ shards = Utils.shard_names(connection_class).map(&:to_s)
108
+ if shards.size == 1
109
+ shards = [nil]
110
+ elsif shard
111
+ shard = shard.to_s
112
+ raise "Unknown shard: #{shard}" if !shards.include?(shard)
113
+
114
+ shards = [shard]
115
+ end
109
116
 
110
117
  status = delay ? :delayed : :pending
111
118
 
@@ -73,7 +73,7 @@ module OnlineMigrations
73
73
  end
74
74
  end
75
75
 
76
- # Whether to require safety reason explanation when calling #safery_assured
76
+ # Whether to require safety reason explanation when calling #safety_assured
77
77
  #
78
78
  # Disabled by default
79
79
  # @return [Boolean]
@@ -16,6 +16,7 @@ module OnlineMigrations
16
16
  # @return [DataMigration] the Data Migration with the given name.
17
17
  #
18
18
  # @raise [NotFoundError] if a Data Migration with the given name does not exist.
19
+ # @raise [ArgumentError] if a Data Migration with the given name is not a subclass of DataMigration.
19
20
  #
20
21
  def named(name)
21
22
  namespace = OnlineMigrations.config.background_data_migrations.migrations_module.constantize
@@ -26,7 +27,7 @@ module OnlineMigrations
26
27
 
27
28
  raise NotFoundError.new("Data Migration #{name} not found", name) if migration.nil?
28
29
  if !(migration.is_a?(Class) && migration < self)
29
- raise NotFoundError.new("#{name} is not a Data Migration", name)
30
+ raise ArgumentError, "#{name} is not a Data Migration"
30
31
  end
31
32
 
32
33
  migration
@@ -148,14 +148,14 @@ It will use a combination of a VIEW and column aliasing to work with both column
148
148
  end
149
149
 
150
150
  4. Replace usages of the old column with a new column in the codebase
151
- <% if ActiveRecord::Base.enumerate_columns_in_select_statements %>
152
- 5. Ignore old column
151
+ 5. If the model has `ignored_columns` set, or you enabled
152
+ `enumerate_columns_in_select_statements`, ignore the old column:
153
153
 
154
154
  self.ignored_columns += [:<%= column_name %>]
155
155
 
156
156
  6. Deploy
157
157
  7. Remove the column rename config from step 1
158
- 8. Remove the column ignore from step 5
158
+ 8. Remove the column ignore from step 5, if added
159
159
  9. Remove the VIEW created in step 3 and finally rename the column:
160
160
 
161
161
  class Finalize<%= migration_name %> < <%= migration_parent %>
@@ -165,19 +165,7 @@ It will use a combination of a VIEW and column aliasing to work with both column
165
165
  end
166
166
 
167
167
  10. Deploy
168
- <% else %>
169
- 5. Deploy
170
- 6. Remove the column rename config from step 1
171
- 7. Remove the VIEW created in step 3 and finally rename the column:
172
-
173
- class Finalize<%= migration_name %> < <%= migration_parent %>
174
- def change
175
- finalize_column_rename :<%= table_name %>, :<%= column_name %>, :<%= new_column %>
176
- end
177
- end
178
-
179
- 8. Deploy
180
- <% end %>",
168
+ ",
181
169
 
182
170
  change_column_with_not_null:
183
171
  "Changing the type is safe, but setting NOT NULL is not.",
@@ -3,83 +3,6 @@
3
3
  module OnlineMigrations
4
4
  # @private
5
5
  module SchemaCache
6
- def primary_keys(connection, table_name)
7
- if (renamed_table = renamed_table?(connection, table_name))
8
- super(connection, renamed_table)
9
- elsif renamed_column?(connection, table_name)
10
- super(connection, column_rename_table(table_name))
11
- else
12
- super
13
- end
14
- end
15
-
16
- def columns(connection, table_name)
17
- if (renamed_table = renamed_table?(connection, table_name))
18
- super(connection, renamed_table)
19
- elsif renamed_column?(connection, table_name)
20
- columns = super(connection, column_rename_table(table_name))
21
- OnlineMigrations.config.column_renames[table_name].each do |old_column_name, new_column_name|
22
- duplicate_column(old_column_name, new_column_name, columns)
23
- end
24
- columns
25
- else
26
- super.reject { |column| column.name.end_with?("_for_type_change") }
27
- end
28
- end
29
-
30
- def indexes(connection, table_name)
31
- if (renamed_table = renamed_table?(connection, table_name))
32
- super(connection, renamed_table)
33
- elsif renamed_column?(connection, table_name)
34
- super(connection, column_rename_table(table_name))
35
- else
36
- super
37
- end
38
- end
39
-
40
- def clear_data_source_cache!(connection, name)
41
- if (renamed_table = renamed_table?(connection, name))
42
- super(connection, renamed_table)
43
- end
44
-
45
- if renamed_column?(connection, name)
46
- super(connection, column_rename_table(name))
47
- end
48
-
49
- super
50
- end
51
-
52
- private
53
- def renamed_table?(connection, table_name)
54
- table_renames = OnlineMigrations.config.table_renames
55
- if table_renames.key?(table_name) && connection.view_exists?(table_name)
56
- table_renames[table_name]
57
- end
58
- end
59
-
60
- def renamed_column?(connection, table_name)
61
- column_renames = OnlineMigrations.config.column_renames
62
- column_renames.key?(table_name) && connection.view_exists?(table_name)
63
- end
64
-
65
- def column_rename_table(table_name)
66
- "#{table_name}_column_rename"
67
- end
68
-
69
- def duplicate_column(old_column_name, new_column_name, columns)
70
- old_column = columns.find { |column| column.name == old_column_name }
71
- new_column = old_column.dup
72
- # Active Record defines only reader for :name
73
- new_column.instance_variable_set(:@name, new_column_name)
74
- # Correspond to the Active Record freezing of each column
75
- columns << new_column.freeze
76
- end
77
- end
78
-
79
- # @private
80
- module SchemaCache72
81
- # Active Record >= 7.2 changed signature of the methods,
82
- # see https://github.com/rails/rails/pull/48716.
83
6
  def primary_keys(pool, table_name)
84
7
  if (renamed_table = renamed_table?(pool, table_name))
85
8
  super(pool, renamed_table)
@@ -705,7 +705,7 @@ module OnlineMigrations
705
705
  add_foreign_key(table_name, foreign_table_name, **foreign_key, column: column_name, validate: false)
706
706
 
707
707
  if foreign_key[:validate] != false
708
- validate_foreign_key(table_name, foreign_table_name, **foreign_key)
708
+ validate_foreign_key(table_name, foreign_table_name, **foreign_key, column: column_name)
709
709
  end
710
710
  end
711
711
  end
@@ -992,12 +992,7 @@ module OnlineMigrations
992
992
 
993
993
  def __tmp_table_name_for_column_rename(table_name)
994
994
  suffix = "_column_rename"
995
-
996
- # On ActiveRecord 7.1 can use table_name_length instead of max_identifier_length,
997
- # see https://github.com/rails/rails/pull/45136.
998
- # Also we need to account for "_pkey", because older versions does not correctly rename
999
- # tables with long names. Remove when supporting newer versions only.
1000
- prefix_length = max_identifier_length - "_pkey".size - suffix.length
995
+ prefix_length = max_identifier_length - suffix.length
1001
996
  table_name[0, prefix_length] + suffix
1002
997
  end
1003
998
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OnlineMigrations
4
- VERSION = "0.33.1"
4
+ VERSION = "0.34.0"
5
5
  end
@@ -116,16 +116,11 @@ module OnlineMigrations
116
116
  ActiveRecord::Migration.prepend(OnlineMigrations::Migration)
117
117
  ActiveRecord::Migrator.prepend(OnlineMigrations::Migrator)
118
118
  ActiveRecord::SchemaDumper.prepend(OnlineMigrations::SchemaDumper)
119
+ ActiveRecord::ConnectionAdapters::SchemaCache.prepend(OnlineMigrations::SchemaCache)
119
120
 
120
121
  ActiveRecord::Tasks::DatabaseTasks.singleton_class.prepend(OnlineMigrations::DatabaseTasks)
121
122
  ActiveRecord::Migration::CommandRecorder.include(OnlineMigrations::CommandRecorder)
122
123
 
123
- if OnlineMigrations::Utils.ar_version >= 7.2
124
- ActiveRecord::ConnectionAdapters::SchemaCache.prepend(OnlineMigrations::SchemaCache72)
125
- else
126
- ActiveRecord::ConnectionAdapters::SchemaCache.prepend(OnlineMigrations::SchemaCache)
127
- end
128
-
129
124
  if !ActiveRecord::Batches::BatchEnumerator.method_defined?(:use_ranges)
130
125
  ActiveRecord::Batches::BatchEnumerator.include(OnlineMigrations::ActiveRecordBatchEnumerator)
131
126
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: online_migrations
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.33.1
4
+ version: 0.34.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - fatkodima
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - ">="
17
17
  - !ruby/object:Gem::Version
18
- version: '7.1'
18
+ version: '7.2'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - ">="
24
24
  - !ruby/object:Gem::Version
25
- version: '7.1'
25
+ version: '7.2'
26
26
  email:
27
27
  - fatkodima123@gmail.com
28
28
  executables: []
@@ -115,7 +115,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
115
115
  - !ruby/object:Gem::Version
116
116
  version: '0'
117
117
  requirements: []
118
- rubygems_version: 4.0.3
118
+ rubygems_version: 4.0.10
119
119
  specification_version: 4
120
120
  summary: Catch unsafe PostgreSQL migrations in development and run them easier in
121
121
  production