online_migrations 0.5.3 → 0.6.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/BACKGROUND_MIGRATIONS.md +3 -3
- data/CHANGELOG.md +11 -0
- data/README.md +44 -4
- data/lib/generators/online_migrations/templates/initializer.rb.tt +8 -8
- data/lib/online_migrations/background_migrations/config.rb +2 -2
- data/lib/online_migrations/change_column_type_helpers.rb +27 -9
- data/lib/online_migrations/command_checker.rb +12 -1
- data/lib/online_migrations/config.rb +1 -1
- data/lib/online_migrations/error_messages.rb +9 -2
- data/lib/online_migrations/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 625229a6ef3cc2b9a6a6201ae2033b4560ca395482d797115563abd1dace6716
|
4
|
+
data.tar.gz: bc24c264f8a6be48e9232959a6e654e5a814c9872317d88e788eca8260dd6a7e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8054e3f5fec6fbc066cc3d6184a3002a2e77b21dee5ee7adca1025fa907c3e2263fae206b25150b662e37cd1dfac46cd66a54c82d725848020c30ed0cecef5c9
|
7
|
+
data.tar.gz: 2ad342552fbf328c341875192aba31c441b2bd5a72d3fac9edfef9caa9fa2eab003c9ae7d441d4778b3425f82aed140290de82df3291c030e0b1e7867871ae47
|
data/BACKGROUND_MIGRATIONS.md
CHANGED
@@ -120,7 +120,7 @@ enqueue_background_migration("MyMigrationWithArgs", arg1, arg2, ...)
|
|
120
120
|
* `CopyColumn` - copies data from one column(s) to other(s) (enqueue using `copy_column_in_background`)
|
121
121
|
* `DeleteAssociatedRecords` - deletes records associated with a parent object (enqueue using `delete_associated_records_in_background`)
|
122
122
|
* `DeleteOrphanedRecords` - deletes records with one or more missing relations (enqueue using `delete_orphaned_records_in_background`)
|
123
|
-
* `PerformActionOnRelation` - performs specific action on a relation or
|
123
|
+
* `PerformActionOnRelation` - performs specific action on a relation or individual records (enqueue using `perform_action_on_relation_in_background`)
|
124
124
|
* `ResetCounters` - resets one or more counter caches to their correct value (enqueue using `reset_counters_in_background`)
|
125
125
|
|
126
126
|
## Testing
|
@@ -240,7 +240,7 @@ Specify the throttle condition as a block:
|
|
240
240
|
```ruby
|
241
241
|
# config/initializers/online_migrations.rb
|
242
242
|
|
243
|
-
OnlineMigrations.config.
|
243
|
+
OnlineMigrations.config.background_migrations.throttler = -> { DatabaseStatus.unhealthy? }
|
244
244
|
```
|
245
245
|
|
246
246
|
Note that it's up to you to define a throttling condition that makes sense for your app. For example, you can check various PostgreSQL metrics such as replication lag, DB threads, whether DB writes are available, etc.
|
@@ -254,7 +254,7 @@ If you want to integrate with an exception monitoring service (e.g. Bugsnag), yo
|
|
254
254
|
```ruby
|
255
255
|
# config/initializers/online_migrations.rb
|
256
256
|
|
257
|
-
OnlineMigrations.config.
|
257
|
+
OnlineMigrations.config.background_migrations.error_handler = ->(error, errored_job) do
|
258
258
|
Bugsnag.notify(error) do |notification|
|
259
259
|
notification.add_metadata(:background_migration, { name: errored_job.migration_name })
|
260
260
|
end
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,16 @@
|
|
1
1
|
## master (unreleased)
|
2
2
|
|
3
|
+
## 0.6.0 (2023-02-04)
|
4
|
+
|
5
|
+
- Ignore internal Active Record migrations compatibility related options when suggesting a safe column type change
|
6
|
+
- Added check for `add_exclusion_constraint`
|
7
|
+
- Fix preserving old column options (`:comment` and `:collation`) when changing column type
|
8
|
+
- Set `NOT NULL` during new column creation when changing column type for PostgreSQL >= 11
|
9
|
+
|
10
|
+
## 0.5.4 (2023-01-03)
|
11
|
+
|
12
|
+
- Support ruby 3.2.0
|
13
|
+
|
3
14
|
## 0.5.3 (2022-11-10)
|
4
15
|
|
5
16
|
- Fix removing index by name
|
data/README.md
CHANGED
@@ -20,6 +20,8 @@ See [comparison to `strong_migrations`](#comparison-to-strong_migrations)
|
|
20
20
|
- Rails 4.2+
|
21
21
|
- PostgreSQL 9.6+
|
22
22
|
|
23
|
+
**Note**: Since some migration helpers use database `VIEW`s to implement their logic, it is recommended to use `structure.sql` schema format, or otherwise add some gem (like [scenic](https://github.com/scenic-views/scenic)) to be able to dump them into the `schema.rb`.
|
24
|
+
|
23
25
|
## Installation
|
24
26
|
|
25
27
|
Add this line to your application's Gemfile:
|
@@ -69,7 +71,7 @@ class AddAdminToUsers < ActiveRecord::Migration[7.0]
|
|
69
71
|
# Do not wrap the migration in a transaction so that locks are held for a shorter time.
|
70
72
|
disable_ddl_transaction!
|
71
73
|
|
72
|
-
def
|
74
|
+
def up
|
73
75
|
# Lower PostgreSQL's lock timeout to avoid statement queueing.
|
74
76
|
execute "SET lock_timeout TO '5s'" # The lock_timeout duration is customizable.
|
75
77
|
|
@@ -88,6 +90,10 @@ class AddAdminToUsers < ActiveRecord::Migration[7.0]
|
|
88
90
|
execute "SET statement_timeout TO '5s'"
|
89
91
|
change_column_null :users, :admin, false
|
90
92
|
end
|
93
|
+
|
94
|
+
def down
|
95
|
+
remove_column :users, :admin
|
96
|
+
end
|
91
97
|
end
|
92
98
|
```
|
93
99
|
|
@@ -136,6 +142,7 @@ Potentially dangerous operations:
|
|
136
142
|
- [replacing an index](#replacing-an-index)
|
137
143
|
- [adding a reference](#adding-a-reference)
|
138
144
|
- [adding a foreign key](#adding-a-foreign-key)
|
145
|
+
- [adding an exclusion constraint](#adding-an-exclusion-constraint)
|
139
146
|
- [adding a json column](#adding-a-json-column)
|
140
147
|
- [using primary key with short integer type](#using-primary-key-with-short-integer-type)
|
141
148
|
- [hash indexes](#hash-indexes)
|
@@ -317,6 +324,9 @@ A safer approach can be accomplished in several steps:
|
|
317
324
|
end
|
318
325
|
```
|
319
326
|
|
327
|
+
**Note**: `initialize_column_type_change` accepts additional options (like `:limit`, `:default` etc)
|
328
|
+
which will be passed to `add_column` when creating a new column, so you can override previous values.
|
329
|
+
|
320
330
|
2. Backfill data from the old column to the new column:
|
321
331
|
|
322
332
|
```ruby
|
@@ -391,7 +401,7 @@ For the previous example, to rename `name` column to `first_name` of the `users`
|
|
391
401
|
```sql
|
392
402
|
BEGIN;
|
393
403
|
ALTER TABLE users RENAME TO users_column_rename;
|
394
|
-
CREATE VIEW users AS SELECT *, first_name AS name FROM
|
404
|
+
CREATE VIEW users AS SELECT *, first_name AS name FROM users_column_rename;
|
395
405
|
COMMIT;
|
396
406
|
```
|
397
407
|
|
@@ -410,9 +420,21 @@ OnlineMigrations.config.column_renames = {
|
|
410
420
|
}
|
411
421
|
}
|
412
422
|
```
|
423
|
+
NOTE: You also need to temporarily enable partial writes (is disabled by default in Active Record >= 7)
|
424
|
+
until the process of column rename is fully done.
|
425
|
+
```ruby
|
426
|
+
# config/application.rb
|
427
|
+
# For Active Record >= 7
|
428
|
+
config.active_record.partial_inserts = true
|
429
|
+
|
430
|
+
# Or for Active Record < 7
|
431
|
+
config.active_record.partial_writes = true
|
432
|
+
```
|
413
433
|
|
414
434
|
2. Deploy
|
415
|
-
3.
|
435
|
+
3. Tell the database that you are going to rename a column. This will not actually rename any columns,
|
436
|
+
nor any data/indexes/foreign keys copying will be made, so will be instantaneous.
|
437
|
+
It will use a combination of a VIEW and column aliasing to work with both column names simultaneously
|
416
438
|
|
417
439
|
```ruby
|
418
440
|
class InitializeRenameUsersNameToFirstName < ActiveRecord::Migration[7.0]
|
@@ -425,7 +447,7 @@ end
|
|
425
447
|
4. Replace usages of the old column with a new column in the codebase
|
426
448
|
5. Deploy
|
427
449
|
6. Remove the column rename config from step 1
|
428
|
-
7. Remove the VIEW created in step 3:
|
450
|
+
7. Remove the VIEW created in step 3 and finally rename the column:
|
429
451
|
|
430
452
|
```ruby
|
431
453
|
class FinalizeRenameUsersNameToFirstName < ActiveRecord::Migration[7.0]
|
@@ -794,6 +816,24 @@ end
|
|
794
816
|
|
795
817
|
**Note**: If you forget `disable_ddl_transaction!`, the migration will fail.
|
796
818
|
|
819
|
+
### Adding an exclusion constraint
|
820
|
+
|
821
|
+
:x: **Bad**
|
822
|
+
|
823
|
+
Adding an exclusion constraint blocks reads and writes while every row is checked.
|
824
|
+
|
825
|
+
```ruby
|
826
|
+
class AddExclusionContraint < ActiveRecord::Migration[7.1]
|
827
|
+
def change
|
828
|
+
add_exclusion_constraint :users, "number WITH =", using: :gist
|
829
|
+
end
|
830
|
+
end
|
831
|
+
```
|
832
|
+
|
833
|
+
:white_check_mark: **Good**
|
834
|
+
|
835
|
+
[Let us know](https://github.com/fatkodima/online_migrations/issues/new) if you have a safe way to do this (exclusion constraints cannot be marked `NOT VALID`).
|
836
|
+
|
797
837
|
### Adding a json column
|
798
838
|
|
799
839
|
:x: **Bad**
|
@@ -38,7 +38,7 @@ OnlineMigrations.configure do |config|
|
|
38
38
|
# migration failure in production. This is also useful in development to get
|
39
39
|
# a better grasp of what is going on for high-level statements like add_column_with_default.
|
40
40
|
#
|
41
|
-
# Note: It can be
|
41
|
+
# Note: It can be overridden by `ONLINE_MIGRATIONS_VERBOSE_SQL_LOGS` environment variable.
|
42
42
|
config.verbose_sql_logs = defined?(Rails) && Rails.env.production?
|
43
43
|
|
44
44
|
# Lock retries.
|
@@ -67,25 +67,25 @@ OnlineMigrations.configure do |config|
|
|
67
67
|
|
68
68
|
# ==> Background migrations configuration
|
69
69
|
# The number of rows to process in a single background migration run.
|
70
|
-
# config.
|
70
|
+
# config.background_migrations.batch_size = 20_000
|
71
71
|
|
72
72
|
# The smaller batches size that the batches will be divided into.
|
73
|
-
# config.
|
73
|
+
# config.background_migrations.sub_batch_size = 1000
|
74
74
|
|
75
75
|
# The pause interval between each background migration job's execution (in seconds).
|
76
|
-
# config.
|
76
|
+
# config.background_migrations.batch_pause = 0.seconds
|
77
77
|
|
78
78
|
# The number of milliseconds to sleep between each sub_batch execution.
|
79
|
-
# config.
|
79
|
+
# config.background_migrations.sub_batch_pause_ms = 100
|
80
80
|
|
81
81
|
# Maximum number of batch run attempts.
|
82
82
|
# When attempts are exhausted, the individual batch is marked as failed.
|
83
|
-
# config.
|
83
|
+
# config.background_migrations.batch_max_attempts = 5
|
84
84
|
|
85
85
|
# Configure custom throttler for background migrations.
|
86
86
|
# It will be called before each batch run.
|
87
87
|
# If throttled, the current run will be retried next time.
|
88
|
-
# config.
|
88
|
+
# config.background_migrations.throttler = -> { DatabaseStatus.unhealthy? }
|
89
89
|
|
90
90
|
# The number of seconds that must pass before the running job is considered stuck.
|
91
91
|
# config.background_migrations.stuck_jobs_timeout = 1.hour
|
@@ -95,7 +95,7 @@ OnlineMigrations.configure do |config|
|
|
95
95
|
config.background_migrations.backtrace_cleaner = Rails.backtrace_cleaner
|
96
96
|
|
97
97
|
# The callback to perform when an error occurs in the migration job.
|
98
|
-
# config.
|
98
|
+
# config.background_migrations.error_handler = ->(error, errored_job) do
|
99
99
|
# Bugsnag.notify(error) do |notification|
|
100
100
|
# notification.add_metadata(:background_migration, { name: errored_job.migration_name })
|
101
101
|
# end
|
@@ -43,7 +43,7 @@ module OnlineMigrations
|
|
43
43
|
# @return [Proc]
|
44
44
|
#
|
45
45
|
# @example
|
46
|
-
# OnlineMigrations.config.
|
46
|
+
# OnlineMigrations.config.background_migrations.throttler = -> { DatabaseStatus.unhealthy? }
|
47
47
|
#
|
48
48
|
attr_reader :throttler
|
49
49
|
|
@@ -64,7 +64,7 @@ module OnlineMigrations
|
|
64
64
|
# The callback to perform when an error occurs in the migration job.
|
65
65
|
#
|
66
66
|
# @example
|
67
|
-
# OnlineMigrations.config.
|
67
|
+
# OnlineMigrations.config.background_migrations.error_handler = ->(error, errored_job) do
|
68
68
|
# Bugsnag.notify(error) do |notification|
|
69
69
|
# notification.add_metadata(:background_migration, { name: errored_job.migration_name })
|
70
70
|
# end
|
@@ -107,19 +107,26 @@ module OnlineMigrations
|
|
107
107
|
transaction do
|
108
108
|
columns_and_types.each do |(column_name, new_type)|
|
109
109
|
old_col = __column_for(table_name, column_name)
|
110
|
+
old_col_options = __options_from_column(old_col, [:collation, :comment])
|
110
111
|
column_options = options[column_name] || {}
|
111
112
|
tmp_column_name = conversions[column_name]
|
112
113
|
|
113
|
-
if raw_connection.server_version >= 11_00_00
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
114
|
+
if raw_connection.server_version >= 11_00_00
|
115
|
+
if primary_key(table_name) == column_name.to_s && old_col.type == :integer
|
116
|
+
# If the column to be converted is a Primary Key, set it to
|
117
|
+
# `NOT NULL DEFAULT 0` and we'll copy the correct values when backfilling.
|
118
|
+
# That way, we skip the expensive validation step required to add
|
119
|
+
# a `NOT NULL` constraint at the end of the process.
|
120
|
+
add_column(table_name, tmp_column_name, new_type,
|
121
|
+
**old_col_options.merge(column_options).merge(default: old_col.default || 0, null: false))
|
122
|
+
else
|
123
|
+
unless old_col.default.nil?
|
124
|
+
old_col_options = old_col_options.merge(default: old_col.default, null: old_col.null)
|
125
|
+
end
|
126
|
+
add_column(table_name, tmp_column_name, new_type, **old_col_options.merge(column_options))
|
127
|
+
end
|
121
128
|
else
|
122
|
-
add_column(table_name, tmp_column_name, new_type, **column_options)
|
129
|
+
add_column(table_name, tmp_column_name, new_type, **old_col_options.merge(column_options))
|
123
130
|
change_column_default(table_name, tmp_column_name, old_col.default) unless old_col.default.nil?
|
124
131
|
end
|
125
132
|
end
|
@@ -377,6 +384,17 @@ module OnlineMigrations
|
|
377
384
|
"#{column_name}_for_type_change"
|
378
385
|
end
|
379
386
|
|
387
|
+
def __options_from_column(column, options)
|
388
|
+
result = {}
|
389
|
+
options.each do |option|
|
390
|
+
if column.respond_to?(option)
|
391
|
+
value = column.public_send(option)
|
392
|
+
result[option] = value unless value.nil?
|
393
|
+
end
|
394
|
+
end
|
395
|
+
result
|
396
|
+
end
|
397
|
+
|
380
398
|
def __copy_triggers_name(table_name, from_column, to_column)
|
381
399
|
CopyTrigger.on_table(table_name, connection: self).name(from_column, to_column)
|
382
400
|
end
|
@@ -42,6 +42,7 @@ module OnlineMigrations
|
|
42
42
|
|
43
43
|
true
|
44
44
|
end
|
45
|
+
ruby2_keywords(:check) if respond_to?(:ruby2_keywords, true)
|
45
46
|
|
46
47
|
private
|
47
48
|
def check_database_version
|
@@ -228,6 +229,10 @@ module OnlineMigrations
|
|
228
229
|
|
229
230
|
type = type.to_sym
|
230
231
|
|
232
|
+
# Ignore internal Active Record migrations compatibility related
|
233
|
+
# options, like `_uses_legacy_table_name` etc. They all are starting with "_".
|
234
|
+
options = options.reject { |key, _| key.to_s.start_with?("_") }
|
235
|
+
|
231
236
|
existing_column = column_for(table_name, column_name)
|
232
237
|
if existing_column
|
233
238
|
existing_type = existing_column.type.to_sym
|
@@ -271,7 +276,7 @@ module OnlineMigrations
|
|
271
276
|
!options[:limit] || (existing_column.limit && options[:limit] >= existing_column.limit)
|
272
277
|
end
|
273
278
|
when :numeric, :decimal
|
274
|
-
# numeric and decimal are equivalent and can be used
|
279
|
+
# numeric and decimal are equivalent and can be used interchangeably
|
275
280
|
[:numeric, :decimal].include?(existing_type) &&
|
276
281
|
(
|
277
282
|
(
|
@@ -517,6 +522,12 @@ module OnlineMigrations
|
|
517
522
|
end
|
518
523
|
end
|
519
524
|
|
525
|
+
def add_exclusion_constraint(table_name, _expression, **_options)
|
526
|
+
unless new_or_small_table?(table_name)
|
527
|
+
raise_error :add_exclusion_constraint
|
528
|
+
end
|
529
|
+
end
|
530
|
+
|
520
531
|
def add_check_constraint(table_name, expression, **options)
|
521
532
|
if !new_or_small_table?(table_name) && options[:validate] != false
|
522
533
|
name = options[:name] || check_constraint_name(table_name, expression)
|
@@ -153,7 +153,7 @@ module OnlineMigrations
|
|
153
153
|
# This feature is enabled by default in a production Rails environment.
|
154
154
|
# @return [Boolean]
|
155
155
|
#
|
156
|
-
# @note: It can be
|
156
|
+
# @note: It can be overridden by `ONLINE_MIGRATIONS_VERBOSE_SQL_LOGS` environment variable.
|
157
157
|
#
|
158
158
|
attr_accessor :verbose_sql_logs
|
159
159
|
|
@@ -131,7 +131,8 @@ migration_helpers provides a safer approach to do this:
|
|
131
131
|
}
|
132
132
|
}
|
133
133
|
<% unless partial_writes %>
|
134
|
-
NOTE: You also need to temporarily enable partial writes
|
134
|
+
NOTE: You also need to temporarily enable partial writes (is disabled by default in Active Record >= 7)
|
135
|
+
until the process of column rename is fully done.
|
135
136
|
# config/application.rb
|
136
137
|
config.active_record.<%= partial_writes_setting %> = true
|
137
138
|
<% end %>
|
@@ -150,7 +151,7 @@ It will use a combination of a VIEW and column aliasing to work with both column
|
|
150
151
|
4. Replace usages of the old column with a new column in the codebase
|
151
152
|
5. Deploy
|
152
153
|
6. Remove the column rename config from step 1
|
153
|
-
7. Remove the VIEW created in step 3:
|
154
|
+
7. Remove the VIEW created in step 3 and finally rename the column:
|
154
155
|
|
155
156
|
class Finalize<%= migration_name %> < <%= migration_parent %>
|
156
157
|
def change
|
@@ -175,6 +176,9 @@ A safer approach can be accomplished in several steps:
|
|
175
176
|
end
|
176
177
|
end
|
177
178
|
|
179
|
+
**Note**: `initialize_column_type_change` accepts additional options (like `:limit`, `:default` etc)
|
180
|
+
which will be passed to `add_column` when creating a new column, so you can override previous values.
|
181
|
+
|
178
182
|
2. Backfill data from the old column to the new column:
|
179
183
|
|
180
184
|
class Backfill<%= migration_name %> < <%= migration_parent %>
|
@@ -380,6 +384,9 @@ end",
|
|
380
384
|
"Validating a foreign key while holding heavy locks on tables is dangerous.
|
381
385
|
Use disable_ddl_transaction! or a separate migration.",
|
382
386
|
|
387
|
+
add_exclusion_constraint:
|
388
|
+
"Adding an exclusion constraint blocks reads and writes while every row is checked.",
|
389
|
+
|
383
390
|
add_check_constraint:
|
384
391
|
"Adding a check constraint blocks reads and writes while every row is checked.
|
385
392
|
A safer approach is to add the check constraint without validating existing rows,
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: online_migrations
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.6.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- fatkodima
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-02-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -101,7 +101,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
101
101
|
- !ruby/object:Gem::Version
|
102
102
|
version: '0'
|
103
103
|
requirements: []
|
104
|
-
rubygems_version: 3.
|
104
|
+
rubygems_version: 3.4.3
|
105
105
|
signing_key:
|
106
106
|
specification_version: 4
|
107
107
|
summary: Catch unsafe PostgreSQL migrations in development and run them easier in
|