online_migrations 0.8.1 → 0.9.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 +16 -1
- data/README.md +68 -68
- data/docs/configuring.md +1 -1
- data/lib/online_migrations/background_migration.rb +2 -2
- data/lib/online_migrations/background_migrations/background_migration_class_validator.rb +1 -1
- data/lib/online_migrations/background_migrations/config.rb +1 -1
- data/lib/online_migrations/background_migrations/delete_associated_records.rb +1 -1
- data/lib/online_migrations/background_migrations/delete_orphaned_records.rb +1 -1
- data/lib/online_migrations/background_migrations/migration_job_status_validator.rb +2 -2
- data/lib/online_migrations/background_migrations/migration_status_validator.rb +2 -2
- data/lib/online_migrations/background_migrations/reset_counters.rb +2 -2
- data/lib/online_migrations/batch_iterator.rb +3 -3
- data/lib/online_migrations/change_column_type_helpers.rb +41 -25
- data/lib/online_migrations/command_checker.rb +28 -20
- data/lib/online_migrations/command_recorder.rb +4 -4
- data/lib/online_migrations/config.rb +1 -1
- data/lib/online_migrations/error_messages.rb +3 -3
- data/lib/online_migrations/migration.rb +10 -1
- data/lib/online_migrations/schema_statements.rb +2 -2
- data/lib/online_migrations/utils.rb +1 -1
- data/lib/online_migrations/verbose_sql_logs.rb +7 -1
- 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: 3f7396712072756a2c7de52a7057f0c46da8ce26bf59270f0f2dea42bf43d0b3
|
4
|
+
data.tar.gz: a3a26397aa2a279e996e8af2e7c698ebabc73ee38bad89b84e98cbddb609feb4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d897c787f1b4e23436ecc362dd49a97eba529e7f0613fe71d38b4fde1cd740a40e0fa6ba2ef59d32f0ab6c1050ebc98728a1830d22679f98671305e1d82d6eb3
|
7
|
+
data.tar.gz: 10c2d6cc695042052c994111f71df5e19d17cf2b81a72b4ca2bce835ecced1a91dad0bfe5d2091079661c1d97dbbf066e641b80ceca196be7dd5c0e859f8ea09
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,20 @@
|
|
1
1
|
## master (unreleased)
|
2
2
|
|
3
|
+
## 0.9.0 (2023-10-27)
|
4
|
+
|
5
|
+
- Add ability to use custom raw sql for `backfill_column_for_type_change`'s `type_cast_function`
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
backfill_column_for_type_change(:users, :company_id, type_cast_function: Arel.sql("company_id::integer"))
|
9
|
+
```
|
10
|
+
|
11
|
+
- Fix version safety with `revert`
|
12
|
+
|
13
|
+
## 0.8.2 (2023-09-26)
|
14
|
+
|
15
|
+
- Promote check constraint to `NOT NULL` on PostgreSQL >= 12 when changing column type
|
16
|
+
- Fix `safety_assured` with `revert`
|
17
|
+
|
3
18
|
## 0.8.1 (2023-08-04)
|
4
19
|
|
5
20
|
- Fix `update_columns_in_batches` when multiple columns are passed
|
@@ -8,7 +23,7 @@
|
|
8
23
|
## 0.8.0 (2023-07-24)
|
9
24
|
|
10
25
|
- Add check for `change_column_default`
|
11
|
-
- Add check for `
|
26
|
+
- Add check for `add_unique_constraint` (Active Record >= 7.1)
|
12
27
|
- Add check for `add_column` with stored generated columns
|
13
28
|
|
14
29
|
## 0.7.3 (2023-05-30)
|
data/README.md
CHANGED
@@ -55,7 +55,7 @@ An operation is classified as dangerous if it either:
|
|
55
55
|
Consider the following migration:
|
56
56
|
|
57
57
|
```ruby
|
58
|
-
class AddAdminToUsers < ActiveRecord::Migration[7.
|
58
|
+
class AddAdminToUsers < ActiveRecord::Migration[7.1]
|
59
59
|
def change
|
60
60
|
add_column :users, :admin, :boolean, default: false, null: false
|
61
61
|
end
|
@@ -67,7 +67,7 @@ If the `users` table is large, running this migration on a live PostgreSQL < 11
|
|
67
67
|
A safer approach would be to run something like the following:
|
68
68
|
|
69
69
|
```ruby
|
70
|
-
class AddAdminToUsers < ActiveRecord::Migration[7.
|
70
|
+
class AddAdminToUsers < ActiveRecord::Migration[7.1]
|
71
71
|
# Do not wrap the migration in a transaction so that locks are held for a shorter time.
|
72
72
|
disable_ddl_transaction!
|
73
73
|
|
@@ -112,7 +112,7 @@ A safer approach is to:
|
|
112
112
|
|
113
113
|
add_column_with_default takes care of all this steps:
|
114
114
|
|
115
|
-
class AddAdminToUsers < ActiveRecord::Migration[7.
|
115
|
+
class AddAdminToUsers < ActiveRecord::Migration[7.1]
|
116
116
|
disable_ddl_transaction!
|
117
117
|
|
118
118
|
def change
|
@@ -143,7 +143,7 @@ Potentially dangerous operations:
|
|
143
143
|
- [adding a reference](#adding-a-reference)
|
144
144
|
- [adding a foreign key](#adding-a-foreign-key)
|
145
145
|
- [adding an exclusion constraint](#adding-an-exclusion-constraint)
|
146
|
-
- [adding a unique
|
146
|
+
- [adding a unique constraint](#adding-a-unique-constraint)
|
147
147
|
- [adding a json column](#adding-a-json-column)
|
148
148
|
- [adding a stored generated column](#adding-a-stored-generated-column)
|
149
149
|
- [using primary key with short integer type](#using-primary-key-with-short-integer-type)
|
@@ -166,7 +166,7 @@ You can also add [custom checks](#custom-checks) or [disable specific checks](#d
|
|
166
166
|
Active Record caches database columns at runtime, so if you drop a column, it can cause exceptions until your app reboots.
|
167
167
|
|
168
168
|
```ruby
|
169
|
-
class RemoveNameFromUsers < ActiveRecord::Migration[7.
|
169
|
+
class RemoveNameFromUsers < ActiveRecord::Migration[7.1]
|
170
170
|
def change
|
171
171
|
remove_column :users, :name
|
172
172
|
end
|
@@ -195,7 +195,7 @@ end
|
|
195
195
|
3. Wrap column removing in a `safety_assured` block:
|
196
196
|
|
197
197
|
```ruby
|
198
|
-
class RemoveNameFromUsers < ActiveRecord::Migration[7.
|
198
|
+
class RemoveNameFromUsers < ActiveRecord::Migration[7.1]
|
199
199
|
def change
|
200
200
|
safety_assured { remove_column :users, :name }
|
201
201
|
end
|
@@ -212,7 +212,7 @@ end
|
|
212
212
|
In earlier versions of PostgreSQL adding a column with a non-null default value to an existing table blocks reads and writes while the entire table is rewritten.
|
213
213
|
|
214
214
|
```ruby
|
215
|
-
class AddAdminToUsers < ActiveRecord::Migration[7.
|
215
|
+
class AddAdminToUsers < ActiveRecord::Migration[7.1]
|
216
216
|
def change
|
217
217
|
add_column :users, :admin, :boolean, default: false
|
218
218
|
end
|
@@ -232,7 +232,7 @@ A safer approach is to:
|
|
232
232
|
`add_column_with_default` helper takes care of all this steps:
|
233
233
|
|
234
234
|
```ruby
|
235
|
-
class AddAdminToUsers < ActiveRecord::Migration[7.
|
235
|
+
class AddAdminToUsers < ActiveRecord::Migration[7.1]
|
236
236
|
disable_ddl_transaction!
|
237
237
|
|
238
238
|
def change
|
@@ -250,7 +250,7 @@ end
|
|
250
250
|
Active Record wraps each migration in a transaction, and backfilling in the same transaction that alters a table keeps the table locked for the [duration of the backfill](https://wework.github.io/data/2015/11/05/add-columns-with-default-values-to-large-tables-in-rails-postgres/).
|
251
251
|
|
252
252
|
```ruby
|
253
|
-
class AddAdminToUsers < ActiveRecord::Migration[7.
|
253
|
+
class AddAdminToUsers < ActiveRecord::Migration[7.1]
|
254
254
|
def change
|
255
255
|
add_column :users, :admin, :boolean
|
256
256
|
User.update_all(admin: false)
|
@@ -265,13 +265,13 @@ Also, running a single query to update data can cause issues for large tables.
|
|
265
265
|
There are three keys to backfilling safely: batching, throttling, and running it outside a transaction. Use a `update_column_in_batches` helper in a separate migration with `disable_ddl_transaction!`.
|
266
266
|
|
267
267
|
```ruby
|
268
|
-
class AddAdminToUsers < ActiveRecord::Migration[7.
|
268
|
+
class AddAdminToUsers < ActiveRecord::Migration[7.1]
|
269
269
|
def change
|
270
270
|
add_column :users, :admin, :boolean
|
271
271
|
end
|
272
272
|
end
|
273
273
|
|
274
|
-
class BackfillUsersAdminColumn < ActiveRecord::Migration[7.
|
274
|
+
class BackfillUsersAdminColumn < ActiveRecord::Migration[7.1]
|
275
275
|
disable_ddl_transaction!
|
276
276
|
|
277
277
|
def up
|
@@ -290,7 +290,7 @@ end
|
|
290
290
|
Changing the type of an existing column blocks reads and writes while the entire table is rewritten.
|
291
291
|
|
292
292
|
```ruby
|
293
|
-
class ChangeFilesSizeType < ActiveRecord::Migration[7.
|
293
|
+
class ChangeFilesSizeType < ActiveRecord::Migration[7.1]
|
294
294
|
def change
|
295
295
|
change_column :files, :size, :bigint
|
296
296
|
end
|
@@ -323,7 +323,7 @@ A safer approach can be accomplished in several steps:
|
|
323
323
|
1. Create a new column and keep column's data in sync:
|
324
324
|
|
325
325
|
```ruby
|
326
|
-
class InitializeChangeFilesSizeType < ActiveRecord::Migration[7.
|
326
|
+
class InitializeChangeFilesSizeType < ActiveRecord::Migration[7.1]
|
327
327
|
def change
|
328
328
|
initialize_column_type_change :files, :size, :bigint
|
329
329
|
end
|
@@ -336,7 +336,7 @@ which will be passed to `add_column` when creating a new column, so you can over
|
|
336
336
|
2. Backfill data from the old column to the new column:
|
337
337
|
|
338
338
|
```ruby
|
339
|
-
class BackfillChangeFilesSizeType < ActiveRecord::Migration[7.
|
339
|
+
class BackfillChangeFilesSizeType < ActiveRecord::Migration[7.1]
|
340
340
|
disable_ddl_transaction!
|
341
341
|
|
342
342
|
def up
|
@@ -352,7 +352,7 @@ which will be passed to `add_column` when creating a new column, so you can over
|
|
352
352
|
3. Copy indexes, foreign keys, check constraints, NOT NULL constraint, swap new column in place:
|
353
353
|
|
354
354
|
```ruby
|
355
|
-
class FinalizeChangeFilesSizeType < ActiveRecord::Migration[7.
|
355
|
+
class FinalizeChangeFilesSizeType < ActiveRecord::Migration[7.1]
|
356
356
|
disable_ddl_transaction!
|
357
357
|
|
358
358
|
def change
|
@@ -365,7 +365,7 @@ which will be passed to `add_column` when creating a new column, so you can over
|
|
365
365
|
5. Finally, if everything is working as expected, remove copy trigger and old column:
|
366
366
|
|
367
367
|
```ruby
|
368
|
-
class CleanupChangeFilesSizeType < ActiveRecord::Migration[7.
|
368
|
+
class CleanupChangeFilesSizeType < ActiveRecord::Migration[7.1]
|
369
369
|
def up
|
370
370
|
cleanup_column_type_change :files, :size
|
371
371
|
end
|
@@ -385,7 +385,7 @@ which will be passed to `add_column` when creating a new column, so you can over
|
|
385
385
|
Renaming a column that's in use will cause errors in your application.
|
386
386
|
|
387
387
|
```ruby
|
388
|
-
class RenameUsersNameToFirstName < ActiveRecord::Migration[7.
|
388
|
+
class RenameUsersNameToFirstName < ActiveRecord::Migration[7.1]
|
389
389
|
def change
|
390
390
|
rename_column :users, :name, :first_name
|
391
391
|
end
|
@@ -454,7 +454,7 @@ nor any data/indexes/foreign keys copying will be made, so will be instantaneous
|
|
454
454
|
It will use a combination of a VIEW and column aliasing to work with both column names simultaneously
|
455
455
|
|
456
456
|
```ruby
|
457
|
-
class InitializeRenameUsersNameToFirstName < ActiveRecord::Migration[7.
|
457
|
+
class InitializeRenameUsersNameToFirstName < ActiveRecord::Migration[7.1]
|
458
458
|
def change
|
459
459
|
initialize_column_rename :users, :name, :first_name
|
460
460
|
end
|
@@ -485,7 +485,7 @@ end
|
|
485
485
|
9. Remove the VIEW created in step 3 and finally rename the column:
|
486
486
|
|
487
487
|
```ruby
|
488
|
-
class FinalizeRenameUsersNameToFirstName < ActiveRecord::Migration[7.
|
488
|
+
class FinalizeRenameUsersNameToFirstName < ActiveRecord::Migration[7.1]
|
489
489
|
def change
|
490
490
|
finalize_column_rename :users, :name, :first_name
|
491
491
|
end
|
@@ -501,7 +501,7 @@ end
|
|
501
501
|
Renaming a table that's in use will cause errors in your application.
|
502
502
|
|
503
503
|
```ruby
|
504
|
-
class RenameClientsToUsers < ActiveRecord::Migration[7.
|
504
|
+
class RenameClientsToUsers < ActiveRecord::Migration[7.1]
|
505
505
|
def change
|
506
506
|
rename_table :clients, :users
|
507
507
|
end
|
@@ -556,7 +556,7 @@ OnlineMigrations.config.table_renames = {
|
|
556
556
|
3. Create a VIEW:
|
557
557
|
|
558
558
|
```ruby
|
559
|
-
class InitializeRenameClientsToUsers < ActiveRecord::Migration[7.
|
559
|
+
class InitializeRenameClientsToUsers < ActiveRecord::Migration[7.1]
|
560
560
|
def change
|
561
561
|
initialize_table_rename :clients, :users
|
562
562
|
end
|
@@ -569,7 +569,7 @@ end
|
|
569
569
|
7. Remove the VIEW created in step 3:
|
570
570
|
|
571
571
|
```ruby
|
572
|
-
class FinalizeRenameClientsToUsers < ActiveRecord::Migration[7.
|
572
|
+
class FinalizeRenameClientsToUsers < ActiveRecord::Migration[7.1]
|
573
573
|
def change
|
574
574
|
finalize_table_rename :clients, :users
|
575
575
|
end
|
@@ -585,7 +585,7 @@ end
|
|
585
585
|
The `force` option can drop an existing table.
|
586
586
|
|
587
587
|
```ruby
|
588
|
-
class CreateUsers < ActiveRecord::Migration[7.
|
588
|
+
class CreateUsers < ActiveRecord::Migration[7.1]
|
589
589
|
def change
|
590
590
|
create_table :users, force: true do |t|
|
591
591
|
# ...
|
@@ -599,7 +599,7 @@ end
|
|
599
599
|
Create tables without the `force` option.
|
600
600
|
|
601
601
|
```ruby
|
602
|
-
class CreateUsers < ActiveRecord::Migration[7.
|
602
|
+
class CreateUsers < ActiveRecord::Migration[7.1]
|
603
603
|
def change
|
604
604
|
create_table :users do |t|
|
605
605
|
# ...
|
@@ -617,7 +617,7 @@ If you intend to drop an existing table, run `drop_table` first.
|
|
617
617
|
Adding a check constraint blocks reads and writes while every row is checked.
|
618
618
|
|
619
619
|
```ruby
|
620
|
-
class AddCheckConstraint < ActiveRecord::Migration[7.
|
620
|
+
class AddCheckConstraint < ActiveRecord::Migration[7.1]
|
621
621
|
def change
|
622
622
|
add_check_constraint :users, "char_length(name) >= 1", name: "name_check"
|
623
623
|
end
|
@@ -629,7 +629,7 @@ end
|
|
629
629
|
Add the check constraint without validating existing rows, and then validate them in a separate transaction:
|
630
630
|
|
631
631
|
```ruby
|
632
|
-
class AddCheckConstraint < ActiveRecord::Migration[7.
|
632
|
+
class AddCheckConstraint < ActiveRecord::Migration[7.1]
|
633
633
|
disable_ddl_transaction!
|
634
634
|
|
635
635
|
def change
|
@@ -648,7 +648,7 @@ end
|
|
648
648
|
Setting `NOT NULL` on an existing column blocks reads and writes while every row is checked.
|
649
649
|
|
650
650
|
```ruby
|
651
|
-
class ChangeUsersNameNull < ActiveRecord::Migration[7.
|
651
|
+
class ChangeUsersNameNull < ActiveRecord::Migration[7.1]
|
652
652
|
def change
|
653
653
|
change_column_null :users, :name, false
|
654
654
|
end
|
@@ -660,7 +660,7 @@ end
|
|
660
660
|
Instead, add a check constraint and validate it in a separate transaction:
|
661
661
|
|
662
662
|
```ruby
|
663
|
-
class ChangeUsersNameNull < ActiveRecord::Migration[7.
|
663
|
+
class ChangeUsersNameNull < ActiveRecord::Migration[7.1]
|
664
664
|
disable_ddl_transaction!
|
665
665
|
|
666
666
|
def change
|
@@ -675,7 +675,7 @@ end
|
|
675
675
|
A `NOT NULL` check constraint is functionally equivalent to setting `NOT NULL` on the column (but it won't show up in `schema.rb` in Rails < 6.1). In PostgreSQL 12+, once the check constraint is validated, you can safely set `NOT NULL` on the column and drop the check constraint.
|
676
676
|
|
677
677
|
```ruby
|
678
|
-
class ChangeUsersNameNullDropCheck < ActiveRecord::Migration[7.
|
678
|
+
class ChangeUsersNameNullDropCheck < ActiveRecord::Migration[7.1]
|
679
679
|
def change
|
680
680
|
# in PostgreSQL 12+, you can then safely set NOT NULL on the column
|
681
681
|
change_column_null :users, :name, false
|
@@ -689,7 +689,7 @@ end
|
|
689
689
|
Online Migrations does not support inspecting what happens inside an `execute` call, so cannot help you here. Make really sure that what you're doing is safe before proceeding, then wrap it in a `safety_assured { ... }` block:
|
690
690
|
|
691
691
|
```ruby
|
692
|
-
class ExecuteSQL < ActiveRecord::Migration[7.
|
692
|
+
class ExecuteSQL < ActiveRecord::Migration[7.1]
|
693
693
|
def change
|
694
694
|
safety_assured { execute "..." }
|
695
695
|
end
|
@@ -703,7 +703,7 @@ end
|
|
703
703
|
Adding an index non-concurrently blocks writes.
|
704
704
|
|
705
705
|
```ruby
|
706
|
-
class AddIndexOnUsersEmail < ActiveRecord::Migration[7.
|
706
|
+
class AddIndexOnUsersEmail < ActiveRecord::Migration[7.1]
|
707
707
|
def change
|
708
708
|
add_index :users, :email, unique: true
|
709
709
|
end
|
@@ -715,7 +715,7 @@ end
|
|
715
715
|
Add indexes concurrently.
|
716
716
|
|
717
717
|
```ruby
|
718
|
-
class AddIndexOnUsersEmail < ActiveRecord::Migration[7.
|
718
|
+
class AddIndexOnUsersEmail < ActiveRecord::Migration[7.1]
|
719
719
|
disable_ddl_transaction!
|
720
720
|
|
721
721
|
def change
|
@@ -733,7 +733,7 @@ end
|
|
733
733
|
While actual removing of an index is usually fast, removing it non-concurrently tries to obtain an `ACCESS EXCLUSIVE` lock on the table, waiting for all existing queries to complete and blocking all the subsequent queries (even `SELECT`s) on that table until the lock is obtained and index is removed.
|
734
734
|
|
735
735
|
```ruby
|
736
|
-
class RemoveIndexOnUsersEmail < ActiveRecord::Migration[7.
|
736
|
+
class RemoveIndexOnUsersEmail < ActiveRecord::Migration[7.1]
|
737
737
|
def change
|
738
738
|
remove_index :users, :email
|
739
739
|
end
|
@@ -745,7 +745,7 @@ end
|
|
745
745
|
Remove indexes concurrently.
|
746
746
|
|
747
747
|
```ruby
|
748
|
-
class RemoveIndexOnUsersEmail < ActiveRecord::Migration[7.
|
748
|
+
class RemoveIndexOnUsersEmail < ActiveRecord::Migration[7.1]
|
749
749
|
disable_ddl_transaction!
|
750
750
|
|
751
751
|
def change
|
@@ -763,7 +763,7 @@ end
|
|
763
763
|
Removing an old index before replacing it with the new one might result in slow queries while building the new index.
|
764
764
|
|
765
765
|
```ruby
|
766
|
-
class AddIndexOnCreationToProjects < ActiveRecord::Migration[7.
|
766
|
+
class AddIndexOnCreationToProjects < ActiveRecord::Migration[7.1]
|
767
767
|
disable_ddl_transaction!
|
768
768
|
|
769
769
|
def change
|
@@ -780,7 +780,7 @@ end
|
|
780
780
|
A safer approach is to create the new index and then delete the old one.
|
781
781
|
|
782
782
|
```ruby
|
783
|
-
class AddIndexOnCreationToProjects < ActiveRecord::Migration[7.
|
783
|
+
class AddIndexOnCreationToProjects < ActiveRecord::Migration[7.1]
|
784
784
|
disable_ddl_transaction!
|
785
785
|
|
786
786
|
def change
|
@@ -797,7 +797,7 @@ end
|
|
797
797
|
Rails adds an index non-concurrently to references by default, which blocks writes. Additionally, if `foreign_key` option (without `validate: false`) is provided, both tables are blocked while it is validated.
|
798
798
|
|
799
799
|
```ruby
|
800
|
-
class AddUserToProjects < ActiveRecord::Migration[7.
|
800
|
+
class AddUserToProjects < ActiveRecord::Migration[7.1]
|
801
801
|
def change
|
802
802
|
add_reference :projects, :user, foreign_key: true
|
803
803
|
end
|
@@ -810,7 +810,7 @@ Make sure the index is added concurrently and the foreign key is added in a sepa
|
|
810
810
|
Or you can use `add_reference_concurrently` helper. It will create a reference and take care of safely adding index and/or foreign key.
|
811
811
|
|
812
812
|
```ruby
|
813
|
-
class AddUserToProjects < ActiveRecord::Migration[7.
|
813
|
+
class AddUserToProjects < ActiveRecord::Migration[7.1]
|
814
814
|
disable_ddl_transaction!
|
815
815
|
|
816
816
|
def change
|
@@ -828,7 +828,7 @@ end
|
|
828
828
|
Adding a foreign key blocks writes on both tables.
|
829
829
|
|
830
830
|
```ruby
|
831
|
-
class AddForeignKeyToProjectsUser < ActiveRecord::Migration[7.
|
831
|
+
class AddForeignKeyToProjectsUser < ActiveRecord::Migration[7.1]
|
832
832
|
def change
|
833
833
|
add_foreign_key :projects, :users
|
834
834
|
end
|
@@ -838,7 +838,7 @@ end
|
|
838
838
|
or
|
839
839
|
|
840
840
|
```ruby
|
841
|
-
class AddReferenceToProjectsUser < ActiveRecord::Migration[7.
|
841
|
+
class AddReferenceToProjectsUser < ActiveRecord::Migration[7.1]
|
842
842
|
def change
|
843
843
|
add_reference :projects, :user, foreign_key: true
|
844
844
|
end
|
@@ -850,7 +850,7 @@ end
|
|
850
850
|
Add the foreign key without validating existing rows, and then validate them in a separate transaction.
|
851
851
|
|
852
852
|
```ruby
|
853
|
-
class AddForeignKeyToProjectsUser < ActiveRecord::Migration[7.
|
853
|
+
class AddForeignKeyToProjectsUser < ActiveRecord::Migration[7.1]
|
854
854
|
disable_ddl_transaction!
|
855
855
|
|
856
856
|
def change
|
@@ -880,16 +880,16 @@ end
|
|
880
880
|
|
881
881
|
[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`).
|
882
882
|
|
883
|
-
### Adding a unique
|
883
|
+
### Adding a unique constraint
|
884
884
|
|
885
885
|
:x: **Bad**
|
886
886
|
|
887
|
-
Adding a unique
|
887
|
+
Adding a unique constraint blocks reads and writes while the underlying index is being built.
|
888
888
|
|
889
889
|
```ruby
|
890
|
-
class
|
890
|
+
class AddUniqueConstraint < ActiveRecord::Migration[7.1]
|
891
891
|
def change
|
892
|
-
|
892
|
+
add_unique_constraint :sections, :position, deferrable: :deferred
|
893
893
|
end
|
894
894
|
end
|
895
895
|
```
|
@@ -899,7 +899,7 @@ end
|
|
899
899
|
A safer approach is to create a unique index first, and then create a unique key using that index.
|
900
900
|
|
901
901
|
```ruby
|
902
|
-
class
|
902
|
+
class AddUniqueConstraintAddIndex < ActiveRecord::Migration[7.1]
|
903
903
|
disable_ddl_transaction!
|
904
904
|
|
905
905
|
def change
|
@@ -909,13 +909,13 @@ end
|
|
909
909
|
```
|
910
910
|
|
911
911
|
```ruby
|
912
|
-
class
|
912
|
+
class AddUniqueConstraint < ActiveRecord::Migration[7.1]
|
913
913
|
def up
|
914
|
-
|
914
|
+
add_unique_constraint :sections, :position, deferrable: :deferred, using_index: "index_sections_on_position"
|
915
915
|
end
|
916
916
|
|
917
917
|
def down
|
918
|
-
|
918
|
+
remove_unique_constraint :sections, :position
|
919
919
|
end
|
920
920
|
end
|
921
921
|
```
|
@@ -927,7 +927,7 @@ end
|
|
927
927
|
There's no equality operator for the `json` column type, which can cause errors for existing `SELECT DISTINCT` queries in your application.
|
928
928
|
|
929
929
|
```ruby
|
930
|
-
class AddSettingsToProjects < ActiveRecord::Migration[7.
|
930
|
+
class AddSettingsToProjects < ActiveRecord::Migration[7.1]
|
931
931
|
def change
|
932
932
|
add_column :projects, :settings, :json
|
933
933
|
end
|
@@ -939,7 +939,7 @@ end
|
|
939
939
|
Use `jsonb` instead.
|
940
940
|
|
941
941
|
```ruby
|
942
|
-
class AddSettingsToProjects < ActiveRecord::Migration[7.
|
942
|
+
class AddSettingsToProjects < ActiveRecord::Migration[7.1]
|
943
943
|
def change
|
944
944
|
add_column :projects, :settings, :jsonb
|
945
945
|
end
|
@@ -953,7 +953,7 @@ end
|
|
953
953
|
Adding a stored generated column causes the entire table to be rewritten. During this time, reads and writes are blocked.
|
954
954
|
|
955
955
|
```ruby
|
956
|
-
class AddLowerEmailToUsers < ActiveRecord::Migration[7.
|
956
|
+
class AddLowerEmailToUsers < ActiveRecord::Migration[7.1]
|
957
957
|
def change
|
958
958
|
add_column :users, :lower_email, :virtual, type: :string, as: "LOWER(email)", stored: true
|
959
959
|
end
|
@@ -971,7 +971,7 @@ Add a non-generated column and use callbacks or triggers instead.
|
|
971
971
|
When using short integer types as primary key types, [there is a risk](https://m.signalvnoise.com/update-on-basecamp-3-being-stuck-in-read-only-as-of-nov-8-922am-cst/) of running out of IDs on inserts. The default type in Active Record < 5.1 for primary and foreign keys is `INTEGER`, which allows a little over of 2 billion records. Active Record 5.1 changed the default type to `BIGINT`.
|
972
972
|
|
973
973
|
```ruby
|
974
|
-
class CreateUsers < ActiveRecord::Migration[7.
|
974
|
+
class CreateUsers < ActiveRecord::Migration[7.1]
|
975
975
|
def change
|
976
976
|
create_table :users, id: :integer do |t|
|
977
977
|
# ...
|
@@ -985,7 +985,7 @@ end
|
|
985
985
|
Use one of `bigint`, `bigserial`, `uuid` instead.
|
986
986
|
|
987
987
|
```ruby
|
988
|
-
class CreateUsers < ActiveRecord::Migration[7.
|
988
|
+
class CreateUsers < ActiveRecord::Migration[7.1]
|
989
989
|
def change
|
990
990
|
create_table :users, id: :bigint do |t| # bigint is the default for Active Record >= 5.1
|
991
991
|
# ...
|
@@ -1001,7 +1001,7 @@ end
|
|
1001
1001
|
Hash index operations are not WAL-logged, so hash indexes might need to be rebuilt with `REINDEX` after a database crash if there were unwritten changes. Also, changes to hash indexes are not replicated over streaming or file-based replication after the initial base backup, so they give wrong answers to queries that subsequently use them. For these reasons, hash index use is discouraged.
|
1002
1002
|
|
1003
1003
|
```ruby
|
1004
|
-
class AddIndexToUsersOnEmail < ActiveRecord::Migration[7.
|
1004
|
+
class AddIndexToUsersOnEmail < ActiveRecord::Migration[7.1]
|
1005
1005
|
def change
|
1006
1006
|
add_index :users, :email, unique: true, using: :hash
|
1007
1007
|
end
|
@@ -1013,7 +1013,7 @@ end
|
|
1013
1013
|
Use B-tree indexes instead.
|
1014
1014
|
|
1015
1015
|
```ruby
|
1016
|
-
class AddIndexToUsersOnEmail < ActiveRecord::Migration[7.
|
1016
|
+
class AddIndexToUsersOnEmail < ActiveRecord::Migration[7.1]
|
1017
1017
|
def change
|
1018
1018
|
add_index :users, :email, unique: true # B-tree by default
|
1019
1019
|
end
|
@@ -1028,7 +1028,7 @@ Adding multiple foreign keys in a single migration blocks reads and writes on al
|
|
1028
1028
|
Avoid adding foreign key more than once per migration file, unless the source and target tables are identical.
|
1029
1029
|
|
1030
1030
|
```ruby
|
1031
|
-
class CreateUserProjects < ActiveRecord::Migration[7.
|
1031
|
+
class CreateUserProjects < ActiveRecord::Migration[7.1]
|
1032
1032
|
def change
|
1033
1033
|
create_table :user_projects do |t|
|
1034
1034
|
t.belongs_to :user, foreign_key: true
|
@@ -1043,7 +1043,7 @@ end
|
|
1043
1043
|
Add additional foreign keys in separate migration files. See [adding a foreign key](#adding-a-foreign-key) for how to properly add foreign keys.
|
1044
1044
|
|
1045
1045
|
```ruby
|
1046
|
-
class CreateUserProjects < ActiveRecord::Migration[7.
|
1046
|
+
class CreateUserProjects < ActiveRecord::Migration[7.1]
|
1047
1047
|
def change
|
1048
1048
|
create_table :user_projects do |t|
|
1049
1049
|
t.belongs_to :user, foreign_key: true
|
@@ -1052,7 +1052,7 @@ class CreateUserProjects < ActiveRecord::Migration[7.0]
|
|
1052
1052
|
end
|
1053
1053
|
end
|
1054
1054
|
|
1055
|
-
class AddForeignKeyFromUserProjectsToProject < ActiveRecord::Migration[7.
|
1055
|
+
class AddForeignKeyFromUserProjectsToProject < ActiveRecord::Migration[7.1]
|
1056
1056
|
def change
|
1057
1057
|
add_foreign_key :user_projects, :projects
|
1058
1058
|
end
|
@@ -1069,7 +1069,7 @@ Remove all the foreign keys first.
|
|
1069
1069
|
Assuming, `projects` has foreign keys on `users.id` and `repositories.id`:
|
1070
1070
|
|
1071
1071
|
```ruby
|
1072
|
-
class DropProjects < ActiveRecord::Migration[7.
|
1072
|
+
class DropProjects < ActiveRecord::Migration[7.1]
|
1073
1073
|
def change
|
1074
1074
|
drop_table :projects
|
1075
1075
|
end
|
@@ -1081,13 +1081,13 @@ end
|
|
1081
1081
|
Remove all the foreign keys first:
|
1082
1082
|
|
1083
1083
|
```ruby
|
1084
|
-
class RemoveProjectsUserFk < ActiveRecord::Migration[7.
|
1084
|
+
class RemoveProjectsUserFk < ActiveRecord::Migration[7.1]
|
1085
1085
|
def change
|
1086
1086
|
remove_foreign_key :projects, :users
|
1087
1087
|
end
|
1088
1088
|
end
|
1089
1089
|
|
1090
|
-
class RemoveProjectsRepositoryFk < ActiveRecord::Migration[7.
|
1090
|
+
class RemoveProjectsRepositoryFk < ActiveRecord::Migration[7.1]
|
1091
1091
|
def change
|
1092
1092
|
remove_foreign_key :projects, :repositories
|
1093
1093
|
end
|
@@ -1097,7 +1097,7 @@ end
|
|
1097
1097
|
Then remove the table:
|
1098
1098
|
|
1099
1099
|
```ruby
|
1100
|
-
class DropProjects < ActiveRecord::Migration[7.
|
1100
|
+
class DropProjects < ActiveRecord::Migration[7.1]
|
1101
1101
|
def change
|
1102
1102
|
drop_table :projects
|
1103
1103
|
end
|
@@ -1114,7 +1114,7 @@ Otherwise, there's a risk of bugs caused by IDs representable by one type but no
|
|
1114
1114
|
Assuming, there is a `users` table with `bigint` primary key type:
|
1115
1115
|
|
1116
1116
|
```ruby
|
1117
|
-
class AddUserIdToProjects < ActiveRecord::Migration[7.
|
1117
|
+
class AddUserIdToProjects < ActiveRecord::Migration[7.1]
|
1118
1118
|
def change
|
1119
1119
|
add_column :projects, :user_id, :integer
|
1120
1120
|
end
|
@@ -1128,7 +1128,7 @@ Add a reference column of the same type as a referenced primary key.
|
|
1128
1128
|
Assuming, there is a `users` table with `bigint` primary key type:
|
1129
1129
|
|
1130
1130
|
```ruby
|
1131
|
-
class AddUserIdToProjects < ActiveRecord::Migration[7.
|
1131
|
+
class AddUserIdToProjects < ActiveRecord::Migration[7.1]
|
1132
1132
|
def change
|
1133
1133
|
add_column :projects, :user_id, :bigint
|
1134
1134
|
end
|
@@ -1142,7 +1142,7 @@ end
|
|
1142
1142
|
Adding a single table inheritance column might cause errors in old instances of your application.
|
1143
1143
|
|
1144
1144
|
```ruby
|
1145
|
-
class AddTypeToUsers < ActiveRecord::Migration[7.
|
1145
|
+
class AddTypeToUsers < ActiveRecord::Migration[7.1]
|
1146
1146
|
def change
|
1147
1147
|
add_column :users, :string, :type, default: "Member"
|
1148
1148
|
end
|
@@ -1182,7 +1182,7 @@ A safer approach is to:
|
|
1182
1182
|
Active Record < 7 enables partial writes by default, which can cause incorrect values to be inserted when changing the default value of a column.
|
1183
1183
|
|
1184
1184
|
```ruby
|
1185
|
-
class ChangeSomeColumnDefault < ActiveRecord::Migration[7.
|
1185
|
+
class ChangeSomeColumnDefault < ActiveRecord::Migration[7.1]
|
1186
1186
|
def change
|
1187
1187
|
change_column_default :users, :some_column, from: "old", to: "new"
|
1188
1188
|
end
|
@@ -1210,7 +1210,7 @@ config.active_record.partial_inserts = false
|
|
1210
1210
|
To mark a step in the migration as safe, despite using a method that might otherwise be dangerous, wrap it in a `safety_assured` block.
|
1211
1211
|
|
1212
1212
|
```ruby
|
1213
|
-
class MySafeMigration < ActiveRecord::Migration[7.
|
1213
|
+
class MySafeMigration < ActiveRecord::Migration[7.1]
|
1214
1214
|
def change
|
1215
1215
|
safety_assured { remove_column :users, :some_column }
|
1216
1216
|
end
|
data/docs/configuring.md
CHANGED
@@ -154,7 +154,7 @@ This is useful to demystify `online_migrations` inner workings, and to better in
|
|
154
154
|
Consider migration, running on PostgreSQL < 11:
|
155
155
|
|
156
156
|
```ruby
|
157
|
-
class AddAdminToUsers < ActiveRecord::Migration[7.
|
157
|
+
class AddAdminToUsers < ActiveRecord::Migration[7.1]
|
158
158
|
disable_ddl_transaction!
|
159
159
|
|
160
160
|
def change
|
@@ -21,8 +21,8 @@ module OnlineMigrations
|
|
21
21
|
migration = "#{namespace}::#{name}".safe_constantize ||
|
22
22
|
"#{internal_namespace}::#{name}".safe_constantize
|
23
23
|
|
24
|
-
raise NotFoundError.new("Background Migration #{name} not found", name)
|
25
|
-
|
24
|
+
raise NotFoundError.new("Background Migration #{name} not found", name) if migration.nil?
|
25
|
+
if !(migration.is_a?(Class) && migration < self)
|
26
26
|
raise NotFoundError.new("#{name} is not a Background Migration", name)
|
27
27
|
end
|
28
28
|
|
@@ -8,7 +8,7 @@ module OnlineMigrations
|
|
8
8
|
relation = record.migration_relation
|
9
9
|
migration_name = record.migration_name
|
10
10
|
|
11
|
-
|
11
|
+
if !relation.is_a?(ActiveRecord::Relation)
|
12
12
|
record.errors.add(
|
13
13
|
:migration_name,
|
14
14
|
"#{migration_name}#relation must return an ActiveRecord::Relation object"
|
@@ -16,7 +16,7 @@ module OnlineMigrations
|
|
16
16
|
# https://github.com/rails/rails/pull/34727
|
17
17
|
associations.inject(model.unscoped) do |relation, association|
|
18
18
|
reflection = model.reflect_on_association(association)
|
19
|
-
|
19
|
+
if reflection.nil?
|
20
20
|
raise ArgumentError, "'#{model.name}' has no association called '#{association}'"
|
21
21
|
end
|
22
22
|
|
@@ -11,12 +11,12 @@ module OnlineMigrations
|
|
11
11
|
}
|
12
12
|
|
13
13
|
def validate(record)
|
14
|
-
return
|
14
|
+
return if !record.status_changed?
|
15
15
|
|
16
16
|
previous_status, new_status = record.status_change
|
17
17
|
valid_new_statuses = VALID_STATUS_TRANSITIONS.fetch(previous_status, [])
|
18
18
|
|
19
|
-
|
19
|
+
if !valid_new_statuses.include?(new_status)
|
20
20
|
record.errors.add(
|
21
21
|
:status,
|
22
22
|
"cannot transition background migration job from status #{previous_status} to #{new_status}"
|
@@ -29,12 +29,12 @@ module OnlineMigrations
|
|
29
29
|
}
|
30
30
|
|
31
31
|
def validate(record)
|
32
|
-
return
|
32
|
+
return if !record.status_changed?
|
33
33
|
|
34
34
|
previous_status, new_status = record.status_change
|
35
35
|
valid_new_statuses = VALID_STATUS_TRANSITIONS.fetch(previous_status, [])
|
36
36
|
|
37
|
-
|
37
|
+
if !valid_new_statuses.include?(new_status)
|
38
38
|
record.errors.add(
|
39
39
|
:status,
|
40
40
|
"cannot transition background migration from status #{previous_status} to #{new_status}"
|
@@ -59,7 +59,7 @@ module OnlineMigrations
|
|
59
59
|
def has_many_association(counter_association) # rubocop:disable Naming/PredicateName
|
60
60
|
has_many_association = model.reflect_on_association(counter_association)
|
61
61
|
|
62
|
-
|
62
|
+
if !has_many_association
|
63
63
|
has_many = model.reflect_on_all_associations(:has_many)
|
64
64
|
|
65
65
|
has_many_association = has_many.find do |association|
|
@@ -74,7 +74,7 @@ module OnlineMigrations
|
|
74
74
|
|
75
75
|
counter_association = has_many_association.plural_name if has_many_association
|
76
76
|
end
|
77
|
-
raise ArgumentError, "'#{model.name}' has no association called '#{counter_association}'"
|
77
|
+
raise ArgumentError, "'#{model.name}' has no association called '#{counter_association}'" if !has_many_association
|
78
78
|
|
79
79
|
if has_many_association.is_a?(ActiveRecord::Reflection::ThroughReflection)
|
80
80
|
has_many_association = has_many_association.through_reflection
|
@@ -14,7 +14,7 @@ module OnlineMigrations
|
|
14
14
|
end
|
15
15
|
|
16
16
|
def each_batch(of: 1000, column: relation.primary_key, start: nil, finish: nil, order: :asc)
|
17
|
-
|
17
|
+
if ![:asc, :desc].include?(order)
|
18
18
|
raise ArgumentError, ":order must be :asc or :desc, got #{order.inspect}"
|
19
19
|
end
|
20
20
|
|
@@ -26,7 +26,7 @@ module OnlineMigrations
|
|
26
26
|
|
27
27
|
start_row = base_relation.uncached { base_relation.first }
|
28
28
|
|
29
|
-
return
|
29
|
+
return if !start_row
|
30
30
|
|
31
31
|
start_id = start_row[column]
|
32
32
|
arel_table = relation.arel_table
|
@@ -67,7 +67,7 @@ module OnlineMigrations
|
|
67
67
|
# Retaining the results in the query cache would undermine the point of batching.
|
68
68
|
batch_relation.uncached { yield batch_relation, index }
|
69
69
|
|
70
|
-
break
|
70
|
+
break if !stop_row
|
71
71
|
end
|
72
72
|
end
|
73
73
|
|
@@ -113,21 +113,22 @@ module OnlineMigrations
|
|
113
113
|
|
114
114
|
if raw_connection.server_version >= 11_00_00
|
115
115
|
if primary_key(table_name) == column_name.to_s && old_col.type == :integer
|
116
|
-
#
|
117
|
-
#
|
118
|
-
#
|
119
|
-
#
|
116
|
+
# For PG < 11 and Primary Key conversions, setting a column as the PK
|
117
|
+
# converts even check constraints to NOT NULL column constraints
|
118
|
+
# and forces an inline re-verification of the whole table.
|
119
|
+
# To avoid this, we instead set it to `NOT NULL DEFAULT 0` and we'll
|
120
|
+
# copy the correct values when backfilling.
|
120
121
|
add_column(table_name, tmp_column_name, new_type,
|
121
122
|
**old_col_options.merge(column_options).merge(default: old_col.default || 0, null: false))
|
122
123
|
else
|
123
|
-
|
124
|
+
if !old_col.default.nil?
|
124
125
|
old_col_options = old_col_options.merge(default: old_col.default, null: old_col.null)
|
125
126
|
end
|
126
127
|
add_column(table_name, tmp_column_name, new_type, **old_col_options.merge(column_options))
|
127
128
|
end
|
128
129
|
else
|
129
130
|
add_column(table_name, tmp_column_name, new_type, **old_col_options.merge(column_options))
|
130
|
-
change_column_default(table_name, tmp_column_name, old_col.default)
|
131
|
+
change_column_default(table_name, tmp_column_name, old_col.default) if !old_col.default.nil?
|
131
132
|
end
|
132
133
|
end
|
133
134
|
|
@@ -175,6 +176,7 @@ module OnlineMigrations
|
|
175
176
|
#
|
176
177
|
# @example With type casting
|
177
178
|
# backfill_column_for_type_change(:users, :settings, type_cast_function: "jsonb")
|
179
|
+
# backfill_column_for_type_change(:users, :company_id, type_cast_function: Arel.sql("company_id::integer"))
|
178
180
|
#
|
179
181
|
# @example Additional batch options
|
180
182
|
# backfill_column_for_type_change(:files, :size, batch_size: 10_000)
|
@@ -201,7 +203,13 @@ module OnlineMigrations
|
|
201
203
|
|
202
204
|
old_value = Arel::Table.new(table_name)[column_name]
|
203
205
|
if (type_cast_function = type_cast_functions.with_indifferent_access[column_name])
|
204
|
-
old_value =
|
206
|
+
old_value =
|
207
|
+
case type_cast_function
|
208
|
+
when Arel::Nodes::SqlLiteral
|
209
|
+
type_cast_function
|
210
|
+
else
|
211
|
+
Arel::Nodes::NamedFunction.new(type_cast_function.to_s, [old_value])
|
212
|
+
end
|
205
213
|
end
|
206
214
|
|
207
215
|
[tmp_column, old_value]
|
@@ -247,22 +255,7 @@ module OnlineMigrations
|
|
247
255
|
|
248
256
|
# At this point we are sure there are no NULLs in this column
|
249
257
|
transaction do
|
250
|
-
|
251
|
-
# converts even check constraints to NOT NULL column constraints
|
252
|
-
# and forces an inline re-verification of the whole table.
|
253
|
-
#
|
254
|
-
# For PG >= 12 we can "promote" CHECK constraint to NOT NULL constraint,
|
255
|
-
# but for older versions we can set attribute as NOT NULL directly
|
256
|
-
# through PG internal tables.
|
257
|
-
# In-depth analysis of implications of this was made, so this approach
|
258
|
-
# is considered safe - https://habr.com/ru/company/haulmont/blog/493954/ (in russian).
|
259
|
-
execute(<<-SQL.strip_heredoc)
|
260
|
-
UPDATE pg_catalog.pg_attribute
|
261
|
-
SET attnotnull = true
|
262
|
-
WHERE attrelid = #{quote(table_name)}::regclass
|
263
|
-
AND attname = #{quote(tmp_column_name)}
|
264
|
-
SQL
|
265
|
-
|
258
|
+
__set_not_null(table_name, tmp_column_name)
|
266
259
|
remove_not_null_constraint(table_name, tmp_column_name)
|
267
260
|
end
|
268
261
|
end
|
@@ -389,7 +382,7 @@ module OnlineMigrations
|
|
389
382
|
options.each do |option|
|
390
383
|
if column.respond_to?(option)
|
391
384
|
value = column.public_send(option)
|
392
|
-
result[option] = value
|
385
|
+
result[option] = value if !value.nil?
|
393
386
|
end
|
394
387
|
end
|
395
388
|
result
|
@@ -417,7 +410,7 @@ module OnlineMigrations
|
|
417
410
|
end
|
418
411
|
|
419
412
|
# This is necessary as we can't properly rename indexes such as "taggings_idx".
|
420
|
-
|
413
|
+
if !index.name.include?(from_column)
|
421
414
|
raise "The index #{index.name} can not be copied as it does not " \
|
422
415
|
"mention the old column. You have to rename this index manually first."
|
423
416
|
end
|
@@ -482,6 +475,29 @@ module OnlineMigrations
|
|
482
475
|
end
|
483
476
|
end
|
484
477
|
|
478
|
+
def __set_not_null(table_name, column_name)
|
479
|
+
# For PG >= 12 we can "promote" CHECK constraint to NOT NULL constraint:
|
480
|
+
# https://github.com/postgres/postgres/commit/bbb96c3704c041d139181c6601e5bc770e045d26
|
481
|
+
if raw_connection.server_version >= 12_00_00
|
482
|
+
execute(<<-SQL.strip_heredoc)
|
483
|
+
ALTER TABLE #{quote_table_name(table_name)}
|
484
|
+
ALTER #{quote_column_name(column_name)}
|
485
|
+
SET NOT NULL
|
486
|
+
SQL
|
487
|
+
else
|
488
|
+
# For older versions we can set attribute as NOT NULL directly
|
489
|
+
# through PG internal tables.
|
490
|
+
# In-depth analysis of implications of this was made, so this approach
|
491
|
+
# is considered safe - https://habr.com/ru/company/haulmont/blog/493954/ (in russian).
|
492
|
+
execute(<<-SQL.strip_heredoc)
|
493
|
+
UPDATE pg_catalog.pg_attribute
|
494
|
+
SET attnotnull = true
|
495
|
+
WHERE attrelid = #{quote(table_name)}::regclass
|
496
|
+
AND attname = #{quote(column_name)}
|
497
|
+
SQL
|
498
|
+
end
|
499
|
+
end
|
500
|
+
|
485
501
|
def __check_constraints_for(table_name, column_name)
|
486
502
|
__check_constraints(table_name).select { |c| c["column_name"] == column_name }
|
487
503
|
end
|
@@ -7,11 +7,22 @@ require "set"
|
|
7
7
|
module OnlineMigrations
|
8
8
|
# @private
|
9
9
|
class CommandChecker
|
10
|
+
class << self
|
11
|
+
attr_accessor :safe
|
12
|
+
|
13
|
+
def safety_assured
|
14
|
+
prev_value = safe
|
15
|
+
self.safe = true
|
16
|
+
yield
|
17
|
+
ensure
|
18
|
+
self.safe = prev_value
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
10
22
|
attr_accessor :direction
|
11
23
|
|
12
24
|
def initialize(migration)
|
13
25
|
@migration = migration
|
14
|
-
@safe = false
|
15
26
|
@new_tables = []
|
16
27
|
@new_columns = []
|
17
28
|
@lock_timeout_checked = false
|
@@ -19,19 +30,11 @@ module OnlineMigrations
|
|
19
30
|
@removed_indexes = []
|
20
31
|
end
|
21
32
|
|
22
|
-
def safety_assured
|
23
|
-
prev_value = @safe
|
24
|
-
@safe = true
|
25
|
-
yield
|
26
|
-
ensure
|
27
|
-
@safe = prev_value
|
28
|
-
end
|
29
|
-
|
30
33
|
def check(command, *args, &block)
|
31
34
|
check_database_version
|
32
35
|
check_lock_timeout
|
33
36
|
|
34
|
-
|
37
|
+
if !safe?
|
35
38
|
do_check(command, *args, &block)
|
36
39
|
|
37
40
|
run_custom_checks(command, args)
|
@@ -45,6 +48,10 @@ module OnlineMigrations
|
|
45
48
|
end
|
46
49
|
ruby2_keywords(:check) if respond_to?(:ruby2_keywords, true)
|
47
50
|
|
51
|
+
def version_safe?
|
52
|
+
version && version <= OnlineMigrations.config.start_after
|
53
|
+
end
|
54
|
+
|
48
55
|
private
|
49
56
|
def check_database_version
|
50
57
|
return if defined?(@database_version_checked)
|
@@ -101,10 +108,11 @@ module OnlineMigrations
|
|
101
108
|
end
|
102
109
|
|
103
110
|
def safe?
|
104
|
-
|
111
|
+
self.class.safe ||
|
105
112
|
ENV["SAFETY_ASSURED"] ||
|
106
113
|
(direction == :down && !OnlineMigrations.config.check_down) ||
|
107
|
-
|
114
|
+
version_safe? ||
|
115
|
+
@migration.reverting?
|
108
116
|
end
|
109
117
|
|
110
118
|
def version
|
@@ -477,7 +485,7 @@ module OnlineMigrations
|
|
477
485
|
bad_foreign_key: bad_foreign_key
|
478
486
|
end
|
479
487
|
|
480
|
-
|
488
|
+
if !options[:polymorphic]
|
481
489
|
type = (options[:type] || (Utils.ar_version >= 5.1 ? :bigint : :integer)).to_sym
|
482
490
|
column_name = "#{ref_name}_id"
|
483
491
|
|
@@ -507,9 +515,9 @@ module OnlineMigrations
|
|
507
515
|
existing_indexes = connection.indexes(table_name)
|
508
516
|
|
509
517
|
@removed_indexes.each do |removed_index|
|
510
|
-
next
|
518
|
+
next if !removed_index.intersect?(index)
|
511
519
|
|
512
|
-
|
520
|
+
if existing_indexes.none? { |existing_index| removed_index.covered_by?(existing_index) }
|
513
521
|
raise_error :replace_index
|
514
522
|
end
|
515
523
|
end
|
@@ -560,7 +568,7 @@ module OnlineMigrations
|
|
560
568
|
end
|
561
569
|
|
562
570
|
def add_exclusion_constraint(table_name, _expression, **_options)
|
563
|
-
|
571
|
+
if !new_or_small_table?(table_name)
|
564
572
|
raise_error :add_exclusion_constraint
|
565
573
|
end
|
566
574
|
end
|
@@ -575,15 +583,15 @@ module OnlineMigrations
|
|
575
583
|
end
|
576
584
|
end
|
577
585
|
|
578
|
-
def
|
586
|
+
def add_unique_constraint(table_name, column_name = nil, **options)
|
579
587
|
return if new_or_small_table?(table_name) || options[:using_index] || !column_name
|
580
588
|
|
581
589
|
index_name = index_name(table_name, column_name)
|
582
590
|
|
583
|
-
raise_error :
|
591
|
+
raise_error :add_unique_constraint,
|
584
592
|
add_index_code: command_str(:add_index, table_name, column_name, unique: true, name: index_name, algorithm: :concurrently),
|
585
|
-
add_code: command_str(:
|
586
|
-
remove_code: command_str(:
|
593
|
+
add_code: command_str(:add_unique_constraint, table_name, **options.merge(using_index: index_name)),
|
594
|
+
remove_code: command_str(:remove_unique_constraint, table_name, column_name)
|
587
595
|
end
|
588
596
|
|
589
597
|
# Implementation is from Active Record
|
@@ -98,7 +98,7 @@ module OnlineMigrations
|
|
98
98
|
|
99
99
|
def invert_revert_initialize_columns_rename(args)
|
100
100
|
_table, old_new_column_hash = args
|
101
|
-
|
101
|
+
if !old_new_column_hash
|
102
102
|
raise ActiveRecord::IrreversibleMigration,
|
103
103
|
"revert_initialize_columns_rename is only reversible if given a hash of old and new columns."
|
104
104
|
end
|
@@ -107,7 +107,7 @@ module OnlineMigrations
|
|
107
107
|
|
108
108
|
def invert_finalize_table_rename(args)
|
109
109
|
_table_name, new_name = args
|
110
|
-
|
110
|
+
if !new_name
|
111
111
|
raise ActiveRecord::IrreversibleMigration,
|
112
112
|
"finalize_table_rename is only reversible if given a new_name."
|
113
113
|
end
|
@@ -115,7 +115,7 @@ module OnlineMigrations
|
|
115
115
|
end
|
116
116
|
|
117
117
|
def invert_revert_initialize_column_type_change(args)
|
118
|
-
|
118
|
+
if !args[2]
|
119
119
|
raise ActiveRecord::IrreversibleMigration,
|
120
120
|
"revert_initialize_column_type_change is only reversible if given a new_type."
|
121
121
|
end
|
@@ -141,7 +141,7 @@ module OnlineMigrations
|
|
141
141
|
end
|
142
142
|
|
143
143
|
def invert_remove_text_limit_constraint(args)
|
144
|
-
|
144
|
+
if !args[2]
|
145
145
|
raise ActiveRecord::IrreversibleMigration, "remove_text_limit_constraint is only reversible if given a limit."
|
146
146
|
end
|
147
147
|
|
@@ -261,7 +261,7 @@ module OnlineMigrations
|
|
261
261
|
|
262
262
|
private
|
263
263
|
def ensure_supports_multiple_dbs
|
264
|
-
|
264
|
+
if !Utils.supports_multiple_dbs?
|
265
265
|
raise "OnlineMigrations does not support multiple databases for Active Record < 6.1"
|
266
266
|
end
|
267
267
|
end
|
@@ -438,9 +438,9 @@ class <%= migration_name %> < <%= migration_parent %>
|
|
438
438
|
end
|
439
439
|
end",
|
440
440
|
|
441
|
-
|
442
|
-
"Adding a unique
|
443
|
-
A safer approach is to create a unique index first, and then create a unique
|
441
|
+
add_unique_constraint:
|
442
|
+
"Adding a unique constraint blocks reads and writes while the underlying index is being built.
|
443
|
+
A safer approach is to create a unique index first, and then create a unique constraint using that index.
|
444
444
|
|
445
445
|
class <%= migration_name %>AddIndex < <%= migration_parent %>
|
446
446
|
disable_ddl_transaction!
|
@@ -30,13 +30,22 @@ module OnlineMigrations
|
|
30
30
|
end
|
31
31
|
ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
|
32
32
|
|
33
|
+
# @private
|
34
|
+
def revert(*args)
|
35
|
+
if command_checker.version_safe?
|
36
|
+
safety_assured { super }
|
37
|
+
else
|
38
|
+
super
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
33
42
|
# Mark a command in the migration as safe, despite using a method that might otherwise be dangerous.
|
34
43
|
#
|
35
44
|
# @example
|
36
45
|
# safety_assured { remove_column(:users, :some_column) }
|
37
46
|
#
|
38
47
|
def safety_assured(&block)
|
39
|
-
command_checker.safety_assured(&block)
|
48
|
+
command_checker.class.safety_assured(&block)
|
40
49
|
end
|
41
50
|
|
42
51
|
# Stop running migrations.
|
@@ -87,7 +87,7 @@ module OnlineMigrations
|
|
87
87
|
value = Arel.sql(value.call.to_s) if value.is_a?(Proc)
|
88
88
|
|
89
89
|
# Ignore subqueries in conditions
|
90
|
-
|
90
|
+
if !value.is_a?(Arel::Nodes::SqlLiteral) || value.to_s !~ /select\s+/i
|
91
91
|
arel_column = model.arel_table[column_name]
|
92
92
|
if value.nil?
|
93
93
|
arel_column.not_eq(nil)
|
@@ -665,7 +665,7 @@ module OnlineMigrations
|
|
665
665
|
__ensure_not_in_transaction!
|
666
666
|
|
667
667
|
column_name = "#{ref_name}_id"
|
668
|
-
|
668
|
+
if !column_exists?(table_name, column_name)
|
669
669
|
type = options[:type] || (Utils.ar_version >= 5.1 ? :bigint : :integer)
|
670
670
|
allow_null = options.fetch(:null, true)
|
671
671
|
add_column(table_name, column_name, type, null: allow_null)
|
@@ -139,7 +139,7 @@ module OnlineMigrations
|
|
139
139
|
private_constant :FUNCTION_CALL_RE
|
140
140
|
|
141
141
|
def volatile_default?(connection, type, value)
|
142
|
-
return false
|
142
|
+
return false if !(value.is_a?(Proc) || (type.to_s == "uuid" && value.is_a?(String)))
|
143
143
|
|
144
144
|
value = value.call if value.is_a?(Proc)
|
145
145
|
return false if !value.is_a?(String)
|
@@ -13,7 +13,13 @@ module OnlineMigrations
|
|
13
13
|
stdout_logger.level = @activerecord_logger_was.level
|
14
14
|
stdout_logger = ActiveSupport::TaggedLogging.new(stdout_logger)
|
15
15
|
|
16
|
-
combined_logger =
|
16
|
+
combined_logger =
|
17
|
+
# Broadcasting logs API was changed in https://github.com/rails/rails/pull/48615.
|
18
|
+
if Utils.ar_version >= 7.1
|
19
|
+
ActiveSupport::BroadcastLogger.new(stdout_logger, @activerecord_logger_was)
|
20
|
+
else
|
21
|
+
stdout_logger.extend(ActiveSupport::Logger.broadcast(@activerecord_logger_was))
|
22
|
+
end
|
17
23
|
|
18
24
|
ActiveRecord::Base.logger = combined_logger
|
19
25
|
set_verbose_query_logs(false)
|
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.9.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- fatkodima
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-10-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -102,7 +102,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
102
102
|
- !ruby/object:Gem::Version
|
103
103
|
version: '0'
|
104
104
|
requirements: []
|
105
|
-
rubygems_version: 3.4.
|
105
|
+
rubygems_version: 3.4.10
|
106
106
|
signing_key:
|
107
107
|
specification_version: 4
|
108
108
|
summary: Catch unsafe PostgreSQL migrations in development and run them easier in
|