online_migrations 0.29.3 → 0.30.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 +4 -4
- data/CHANGELOG.md +5 -0
- data/docs/configuring.md +123 -0
- data/lib/online_migrations/lock_retrier.rb +45 -28
- data/lib/online_migrations/migration.rb +2 -2
- data/lib/online_migrations/migrator.rb +5 -0
- data/lib/online_migrations/schema_statements.rb +4 -4
- data/lib/online_migrations/version.rb +1 -1
- metadata +3 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 428fd65e363b915608f720370a2cd91ec14749cd17933ec4e26351bb4745084a
|
|
4
|
+
data.tar.gz: ae292bba4cd1b557694f78240a7da761c8d890aa691cc6b485ddd9f71fd339c5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 59c0573c24d75488c849fa746d6346c31e439d66032725264ce724e97ab8ffbd16cebb03a57ad75071e2b29bf884be0b47696abdcadc72d974effaafd73fe8b4
|
|
7
|
+
data.tar.gz: 73de95254d001d4d285a48ad4d3adb60726ab6bf0bb86b085dde8466b6e1e85fbb668738f09121c38718504ad253615afb3da5e24f56bc48bd01ce9d369cdd4c
|
data/CHANGELOG.md
CHANGED
data/docs/configuring.md
CHANGED
|
@@ -113,6 +113,129 @@ When a statement within transaction fails - the whole transaction is retried. If
|
|
|
113
113
|
|
|
114
114
|
**Note**: Statements are retried by default, unless lock retries are disabled. It is possible to implement more sophisticated lock retriers. See [source code](https://github.com/fatkodima/online_migrations/blob/master/lib/online_migrations/lock_retrier.rb) for the examples.
|
|
115
115
|
|
|
116
|
+
### Command-specific lock retry configuration
|
|
117
|
+
|
|
118
|
+
For migrations using `disable_ddl_transaction!`, you can implement command-specific lock retry behavior. This is useful when different DDL operations have different locking characteristics:
|
|
119
|
+
|
|
120
|
+
- `add_index` with `algorithm: :concurrently` uses `ShareUpdateExclusiveLock` (less restrictive), so can use longer timeouts
|
|
121
|
+
- `add_foreign_key` uses `AccessExclusiveLock` (blocks all access), so should use shorter timeouts to fail fast
|
|
122
|
+
|
|
123
|
+
**Note**: Command-specific configuration only works for migrations with `disable_ddl_transaction!`. For migrations running within transactions (the default), the lock retrier wraps the entire transaction and doesn't have visibility into individual DDL commands.
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
module OnlineMigrations
|
|
127
|
+
class CommandAwareLockRetrier < LockRetrier
|
|
128
|
+
# You can vary the number of attempts based on the command
|
|
129
|
+
def attempts(command = nil, arguments = [])
|
|
130
|
+
case command
|
|
131
|
+
when :add_index
|
|
132
|
+
# Concurrent index creation uses longer individual timeouts,
|
|
133
|
+
# so fewer attempts are needed to reach the same overall window
|
|
134
|
+
10
|
|
135
|
+
when :add_foreign_key
|
|
136
|
+
# Foreign keys use shorter timeouts to fail fast,
|
|
137
|
+
# so more attempts are needed to reach the same overall window
|
|
138
|
+
60
|
|
139
|
+
else
|
|
140
|
+
# Default attempts for other operations
|
|
141
|
+
30
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def lock_timeout(attempt, command = nil, arguments = [])
|
|
146
|
+
case command
|
|
147
|
+
when :add_index
|
|
148
|
+
# Concurrent index creation is less restrictive, use longer timeout
|
|
149
|
+
30.seconds
|
|
150
|
+
when :add_foreign_key
|
|
151
|
+
# Foreign keys block all access, use shorter timeout to fail fast
|
|
152
|
+
5.seconds
|
|
153
|
+
else
|
|
154
|
+
# Default timeout for other operations
|
|
155
|
+
10.seconds
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def delay(attempt, command = nil, arguments = [])
|
|
160
|
+
case command
|
|
161
|
+
when :add_index
|
|
162
|
+
# Longer delay for index operations since they take time anyway
|
|
163
|
+
3.seconds
|
|
164
|
+
when :add_foreign_key
|
|
165
|
+
# Shorter delay to retry faster for quick FK operations
|
|
166
|
+
1.second
|
|
167
|
+
else
|
|
168
|
+
# Default delay for other operations
|
|
169
|
+
2.seconds
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
config.lock_retrier = OnlineMigrations::CommandAwareLockRetrier.new
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
All three methods (`attempts`, `lock_timeout`, and `delay`) can receive command-specific parameters:
|
|
179
|
+
- `command` - the migration method being called (e.g., `:add_index`, `:add_column`, `:add_foreign_key`), or `nil` for transaction-wrapped migrations
|
|
180
|
+
- `arguments` - an array of arguments passed to the migration method
|
|
181
|
+
|
|
182
|
+
Additionally, `lock_timeout` and `delay` receive:
|
|
183
|
+
- `attempt` - the current retry attempt number (1-indexed)
|
|
184
|
+
|
|
185
|
+
This allows you to fine-tune the retry strategy for different commands. For example, to maintain roughly the same total timeout window:
|
|
186
|
+
- `add_index`: 10 attempts × (30s lock + 3s delay) = ~5.5 minute window
|
|
187
|
+
- `add_foreign_key`: 60 attempts × (5s lock + 1s delay) = ~6 minute window
|
|
188
|
+
|
|
189
|
+
#### Alternative: Configuration Hash Approach
|
|
190
|
+
|
|
191
|
+
For simpler use cases, you can use a configuration hash instead of case statements:
|
|
192
|
+
|
|
193
|
+
```ruby
|
|
194
|
+
module OnlineMigrations
|
|
195
|
+
class ConfigurableLockRetrier < LockRetrier
|
|
196
|
+
COMMAND_CONFIGS = {
|
|
197
|
+
add_index: {
|
|
198
|
+
attempts: 10,
|
|
199
|
+
lock_timeout: 30.seconds,
|
|
200
|
+
delay: 3.seconds
|
|
201
|
+
},
|
|
202
|
+
add_foreign_key: {
|
|
203
|
+
attempts: 60,
|
|
204
|
+
lock_timeout: 5.seconds,
|
|
205
|
+
delay: 1.second
|
|
206
|
+
},
|
|
207
|
+
default: {
|
|
208
|
+
attempts: 30,
|
|
209
|
+
lock_timeout: 10.seconds,
|
|
210
|
+
delay: 2.seconds
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
def attempts(command = nil, arguments = [])
|
|
215
|
+
config_for(command)[:attempts]
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def lock_timeout(attempt, command = nil, arguments = [])
|
|
219
|
+
config_for(command)[:lock_timeout]
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def delay(attempt, command = nil, arguments = [])
|
|
223
|
+
config_for(command)[:delay]
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
private
|
|
227
|
+
|
|
228
|
+
def config_for(command)
|
|
229
|
+
COMMAND_CONFIGS[command] || COMMAND_CONFIGS[:default]
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
config.lock_retrier = OnlineMigrations::ConfigurableLockRetrier.new
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
This approach is more concise and easier to maintain when you have simple static configurations per command. The case statement approach (shown above) is better when you need conditional logic or want to use the `attempt` parameter dynamically.
|
|
238
|
+
|
|
116
239
|
To temporarily disable lock retries while running migrations, set `DISABLE_LOCK_RETRIES` env variable. This is useful when you are deploying a hotfix and do not want to wait too long while the lock retrier safely tries to acquire the lock, but try to acquire the lock immediately with the default configured lock timeout value.
|
|
117
240
|
|
|
118
241
|
To permanently disable lock retries, you can set `lock_retrier` to `nil`.
|
|
@@ -36,7 +36,7 @@ module OnlineMigrations
|
|
|
36
36
|
# TIMINGS.size
|
|
37
37
|
# end
|
|
38
38
|
#
|
|
39
|
-
# def lock_timeout(attempt)
|
|
39
|
+
# def lock_timeout(attempt, command = nil, arguments = [])
|
|
40
40
|
# TIMINGS[attempt - 1][0]
|
|
41
41
|
# end
|
|
42
42
|
#
|
|
@@ -48,21 +48,34 @@ module OnlineMigrations
|
|
|
48
48
|
class LockRetrier
|
|
49
49
|
# Returns the number of retrying attempts
|
|
50
50
|
#
|
|
51
|
-
|
|
51
|
+
# @param _command [Symbol, nil] the migration method being executed (e.g., :add_index, :add_column).
|
|
52
|
+
# Will be nil when called from a transaction-wrapped migration (the default).
|
|
53
|
+
# Only populated for migrations using `disable_ddl_transaction!`.
|
|
54
|
+
# @param _arguments [Array] the arguments passed to the migration method
|
|
55
|
+
#
|
|
56
|
+
def attempts(_command = nil, _arguments = [])
|
|
52
57
|
raise NotImplementedError
|
|
53
58
|
end
|
|
54
59
|
|
|
55
60
|
# Returns database lock timeout value (in seconds) for specified attempt number
|
|
56
61
|
#
|
|
57
62
|
# @param _attempt [Integer] attempt number
|
|
63
|
+
# @param _command [Symbol, nil] the migration method being executed (e.g., :add_index, :add_column).
|
|
64
|
+
# Will be nil when called from a transaction-wrapped migration (the default).
|
|
65
|
+
# Only populated for migrations using `disable_ddl_transaction!`.
|
|
66
|
+
# @param _arguments [Array] the arguments passed to the migration method
|
|
58
67
|
#
|
|
59
|
-
def lock_timeout(_attempt); end
|
|
68
|
+
def lock_timeout(_attempt, _command = nil, _arguments = []); end
|
|
60
69
|
|
|
61
70
|
# Returns sleep time after unsuccessful lock attempt (in seconds)
|
|
62
71
|
#
|
|
63
72
|
# @param _attempt [Integer] attempt number
|
|
73
|
+
# @param _command [Symbol, nil] the migration method being executed (e.g., :add_index, :add_column).
|
|
74
|
+
# Will be nil when called from a transaction-wrapped migration (the default).
|
|
75
|
+
# Only populated for migrations using `disable_ddl_transaction!`.
|
|
76
|
+
# @param _arguments [Array] the arguments passed to the migration method
|
|
64
77
|
#
|
|
65
|
-
def delay(_attempt)
|
|
78
|
+
def delay(_attempt, _command = nil, _arguments = [])
|
|
66
79
|
raise NotImplementedError
|
|
67
80
|
end
|
|
68
81
|
|
|
@@ -77,7 +90,7 @@ module OnlineMigrations
|
|
|
77
90
|
# add_column(:users, :name, :string)
|
|
78
91
|
# end
|
|
79
92
|
#
|
|
80
|
-
def with_lock_retries(connection, &block)
|
|
93
|
+
def with_lock_retries(connection, command = nil, *arguments, &block)
|
|
81
94
|
return yield if lock_retries_disabled?
|
|
82
95
|
|
|
83
96
|
current_attempt = 0
|
|
@@ -85,15 +98,15 @@ module OnlineMigrations
|
|
|
85
98
|
begin
|
|
86
99
|
current_attempt += 1
|
|
87
100
|
|
|
88
|
-
current_lock_timeout = lock_timeout(current_attempt)
|
|
101
|
+
current_lock_timeout = lock_timeout(current_attempt, command, arguments)
|
|
89
102
|
if current_lock_timeout
|
|
90
103
|
with_lock_timeout(connection, current_lock_timeout.in_milliseconds, &block)
|
|
91
104
|
else
|
|
92
105
|
yield
|
|
93
106
|
end
|
|
94
107
|
rescue ActiveRecord::LockWaitTimeout, ActiveRecord::Deadlocked => e
|
|
95
|
-
if current_attempt <= attempts
|
|
96
|
-
current_delay = delay(current_attempt)
|
|
108
|
+
if current_attempt <= attempts(command, arguments)
|
|
109
|
+
current_delay = delay(current_attempt, command, arguments)
|
|
97
110
|
|
|
98
111
|
problem = e.is_a?(ActiveRecord::Deadlocked) ? "Deadlock detected." : "Lock timeout."
|
|
99
112
|
Utils.say("#{problem} Retrying in #{current_delay} seconds...")
|
|
@@ -130,13 +143,6 @@ module OnlineMigrations
|
|
|
130
143
|
# config.retrier = OnlineMigrations::ConstantLockRetrier.new(attempts: 5, delay: 2.seconds, lock_timeout: 0.05.seconds)
|
|
131
144
|
#
|
|
132
145
|
class ConstantLockRetrier < LockRetrier
|
|
133
|
-
# LockRetrier API implementation
|
|
134
|
-
#
|
|
135
|
-
# @return [Integer] Number of retrying attempts
|
|
136
|
-
# @see LockRetrier#attempts
|
|
137
|
-
#
|
|
138
|
-
attr_reader :attempts
|
|
139
|
-
|
|
140
146
|
# Create a new ConstantLockRetrier instance
|
|
141
147
|
#
|
|
142
148
|
# @param attempts [Integer] Maximum number of attempts
|
|
@@ -150,12 +156,21 @@ module OnlineMigrations
|
|
|
150
156
|
@lock_timeout = lock_timeout
|
|
151
157
|
end
|
|
152
158
|
|
|
159
|
+
# LockRetrier API implementation
|
|
160
|
+
#
|
|
161
|
+
# @return [Integer] Number of retrying attempts
|
|
162
|
+
# @see LockRetrier#attempts
|
|
163
|
+
#
|
|
164
|
+
def attempts(_command = nil, _arguments = [])
|
|
165
|
+
@attempts
|
|
166
|
+
end
|
|
167
|
+
|
|
153
168
|
# LockRetrier API implementation
|
|
154
169
|
#
|
|
155
170
|
# @return [Numeric] Database lock timeout value (in seconds)
|
|
156
171
|
# @see LockRetrier#lock_timeout
|
|
157
172
|
#
|
|
158
|
-
def lock_timeout(_attempt)
|
|
173
|
+
def lock_timeout(_attempt, _command = nil, _arguments = [])
|
|
159
174
|
@lock_timeout
|
|
160
175
|
end
|
|
161
176
|
|
|
@@ -164,7 +179,7 @@ module OnlineMigrations
|
|
|
164
179
|
# @return [Numeric] Sleep time after unsuccessful lock attempt (in seconds)
|
|
165
180
|
# @see LockRetrier#delay
|
|
166
181
|
#
|
|
167
|
-
def delay(_attempt)
|
|
182
|
+
def delay(_attempt, _command = nil, _arguments = [])
|
|
168
183
|
@delay
|
|
169
184
|
end
|
|
170
185
|
end
|
|
@@ -180,13 +195,6 @@ module OnlineMigrations
|
|
|
180
195
|
# base_delay: 0.01.seconds, max_delay: 1.minute, lock_timeout: 0.2.seconds)
|
|
181
196
|
#
|
|
182
197
|
class ExponentialLockRetrier < LockRetrier
|
|
183
|
-
# LockRetrier API implementation
|
|
184
|
-
#
|
|
185
|
-
# @return [Integer] Number of retrying attempts
|
|
186
|
-
# @see LockRetrier#attempts
|
|
187
|
-
#
|
|
188
|
-
attr_reader :attempts
|
|
189
|
-
|
|
190
198
|
# Create a new ExponentialLockRetrier instance
|
|
191
199
|
#
|
|
192
200
|
# @param attempts [Integer] Maximum number of attempts
|
|
@@ -202,12 +210,21 @@ module OnlineMigrations
|
|
|
202
210
|
@lock_timeout = lock_timeout
|
|
203
211
|
end
|
|
204
212
|
|
|
213
|
+
# LockRetrier API implementation
|
|
214
|
+
#
|
|
215
|
+
# @return [Integer] Number of retrying attempts
|
|
216
|
+
# @see LockRetrier#attempts
|
|
217
|
+
#
|
|
218
|
+
def attempts(_command = nil, _arguments = [])
|
|
219
|
+
@attempts
|
|
220
|
+
end
|
|
221
|
+
|
|
205
222
|
# LockRetrier API implementation
|
|
206
223
|
#
|
|
207
224
|
# @return [Numeric] Database lock timeout value (in seconds)
|
|
208
225
|
# @see LockRetrier#lock_timeout
|
|
209
226
|
#
|
|
210
|
-
def lock_timeout(_attempt)
|
|
227
|
+
def lock_timeout(_attempt, _command = nil, _arguments = [])
|
|
211
228
|
@lock_timeout
|
|
212
229
|
end
|
|
213
230
|
|
|
@@ -217,21 +234,21 @@ module OnlineMigrations
|
|
|
217
234
|
# @see LockRetrier#delay
|
|
218
235
|
# @see https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/
|
|
219
236
|
#
|
|
220
|
-
def delay(attempt)
|
|
237
|
+
def delay(attempt, _command = nil, _arguments = [])
|
|
221
238
|
(rand * [@max_delay, @base_delay * (2**(attempt - 1))].min).ceil
|
|
222
239
|
end
|
|
223
240
|
end
|
|
224
241
|
|
|
225
242
|
# @private
|
|
226
243
|
class NullLockRetrier < LockRetrier
|
|
227
|
-
def attempts(
|
|
244
|
+
def attempts(_command = nil, _arguments = [])
|
|
228
245
|
0
|
|
229
246
|
end
|
|
230
247
|
|
|
231
248
|
def lock_timeout(*)
|
|
232
249
|
end
|
|
233
250
|
|
|
234
|
-
def delay(
|
|
251
|
+
def delay(_attempt, _command = nil, _arguments = [])
|
|
235
252
|
end
|
|
236
253
|
|
|
237
254
|
def with_lock_retries(_connection)
|
|
@@ -23,9 +23,9 @@ module OnlineMigrations
|
|
|
23
23
|
if in_transaction?
|
|
24
24
|
super
|
|
25
25
|
elsif method == :with_lock_retries
|
|
26
|
-
connection.with_lock_retries(*args, &block)
|
|
26
|
+
connection.with_lock_retries(method, *args, &block)
|
|
27
27
|
else
|
|
28
|
-
connection.with_lock_retries { super }
|
|
28
|
+
connection.with_lock_retries(method, *args) { super }
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
end
|
|
@@ -12,6 +12,11 @@ module OnlineMigrations
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
if use_transaction?(migration)
|
|
15
|
+
# Wrap the entire transaction with lock retries so that if the transaction
|
|
16
|
+
# fails to acquire any locks, the whole migration is retried.
|
|
17
|
+
# Note: at this point we don't have visibility into individual DDL commands,
|
|
18
|
+
# so command and arguments will be nil when lock_timeout is called.
|
|
19
|
+
# For command-specific retry behavior, migrations must use `disable_ddl_transaction!`
|
|
15
20
|
migration.connection.with_lock_retries do
|
|
16
21
|
super
|
|
17
22
|
end
|
|
@@ -801,7 +801,7 @@ module OnlineMigrations
|
|
|
801
801
|
#
|
|
802
802
|
# @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_check_constraint
|
|
803
803
|
#
|
|
804
|
-
def add_check_constraint(table_name, expression, **options)
|
|
804
|
+
def add_check_constraint(table_name, expression, if_not_exists: false, **options)
|
|
805
805
|
if check_constraint_exists?(table_name, expression: expression, **options)
|
|
806
806
|
Utils.say(<<~MSG.squish)
|
|
807
807
|
Check constraint was not created because it already exists (this may be due to an aborted migration or similar).
|
|
@@ -816,7 +816,7 @@ module OnlineMigrations
|
|
|
816
816
|
#
|
|
817
817
|
# @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-remove_check_constraint
|
|
818
818
|
#
|
|
819
|
-
def remove_check_constraint(table_name, expression = nil, **options)
|
|
819
|
+
def remove_check_constraint(table_name, expression = nil, if_exists: false, **options)
|
|
820
820
|
if check_constraint_exists?(table_name, expression: expression, **options)
|
|
821
821
|
super
|
|
822
822
|
else
|
|
@@ -862,11 +862,11 @@ module OnlineMigrations
|
|
|
862
862
|
# Executes the block with a retry mechanism that alters the `lock_timeout`
|
|
863
863
|
# and sleep time between attempts.
|
|
864
864
|
#
|
|
865
|
-
def with_lock_retries(&block)
|
|
865
|
+
def with_lock_retries(command = nil, *args, &block)
|
|
866
866
|
__ensure_not_in_transaction!
|
|
867
867
|
|
|
868
868
|
retrier = OnlineMigrations.config.lock_retrier
|
|
869
|
-
retrier.with_lock_retries(self, &block)
|
|
869
|
+
retrier.with_lock_retries(self, command, *args, &block)
|
|
870
870
|
end
|
|
871
871
|
|
|
872
872
|
private
|
metadata
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: online_migrations
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.30.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- fatkodima
|
|
8
|
-
autorequire:
|
|
9
8
|
bindir: bin
|
|
10
9
|
cert_chain: []
|
|
11
|
-
date: 2025-
|
|
10
|
+
date: 2025-10-17 00:00:00.000000000 Z
|
|
12
11
|
dependencies:
|
|
13
12
|
- !ruby/object:Gem::Dependency
|
|
14
13
|
name: activerecord
|
|
@@ -24,7 +23,6 @@ dependencies:
|
|
|
24
23
|
- - ">="
|
|
25
24
|
- !ruby/object:Gem::Version
|
|
26
25
|
version: '7.1'
|
|
27
|
-
description:
|
|
28
26
|
email:
|
|
29
27
|
- fatkodima123@gmail.com
|
|
30
28
|
executables: []
|
|
@@ -101,7 +99,6 @@ metadata:
|
|
|
101
99
|
homepage_uri: https://github.com/fatkodima/online_migrations
|
|
102
100
|
source_code_uri: https://github.com/fatkodima/online_migrations
|
|
103
101
|
changelog_uri: https://github.com/fatkodima/online_migrations/blob/master/CHANGELOG.md
|
|
104
|
-
post_install_message:
|
|
105
102
|
rdoc_options: []
|
|
106
103
|
require_paths:
|
|
107
104
|
- lib
|
|
@@ -116,8 +113,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
116
113
|
- !ruby/object:Gem::Version
|
|
117
114
|
version: '0'
|
|
118
115
|
requirements: []
|
|
119
|
-
rubygems_version: 3.
|
|
120
|
-
signing_key:
|
|
116
|
+
rubygems_version: 3.6.2
|
|
121
117
|
specification_version: 4
|
|
122
118
|
summary: Catch unsafe PostgreSQL migrations in development and run them easier in
|
|
123
119
|
production
|