online_migrations 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +47 -0
- data/Gemfile.lock +1 -1
- data/README.md +162 -35
- data/lib/online_migrations/background_migrations/advisory_lock.rb +1 -1
- data/lib/online_migrations/background_migrations/backfill_column.rb +3 -1
- data/lib/online_migrations/background_migrations/migration_job.rb +1 -1
- data/lib/online_migrations/background_migrations/migration_job_status_validator.rb +2 -1
- data/lib/online_migrations/background_migrations/migration_runner.rb +3 -1
- data/lib/online_migrations/background_migrations/migration_status_validator.rb +2 -1
- data/lib/online_migrations/change_column_type_helpers.rb +4 -4
- data/lib/online_migrations/command_checker.rb +90 -18
- data/lib/online_migrations/config.rb +71 -5
- data/lib/online_migrations/copy_trigger.rb +3 -3
- data/lib/online_migrations/error_messages.rb +25 -2
- data/lib/online_migrations/index_definition.rb +52 -0
- data/lib/online_migrations/indexes_collector.rb +3 -5
- data/lib/online_migrations/schema_statements.rb +13 -13
- data/lib/online_migrations/utils.rb +7 -3
- data/lib/online_migrations/version.rb +1 -1
- data/lib/online_migrations.rb +1 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 3bac7f903ca89cda8d82ee421cae4c1f351a973c88c565f4cecd92ec98b7533c
|
4
|
+
data.tar.gz: e2e2fe22a1d1c70cb753a16fde3ce67890a73f17a058b72fa9383a460837926d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bd297d34d39f7fc1e488f962cb7f11eb0973b29a6b6b3e8330b11db6904430318f85ce60af2fda57adf751294310103c153be5a8511e2fd037477edc17f16dca
|
7
|
+
data.tar.gz: 7fafd129661fd5d63e372a7cb54e55d2715a16a3b4acc6a2d57d5bae69b5f5648723276c244a2ba6918b2330454845901cf34ea393c13e132bb9481ceb7d26a9
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,52 @@
|
|
1
1
|
## master (unreleased)
|
2
2
|
|
3
|
+
## 0.2.0 (2022-01-31)
|
4
|
+
|
5
|
+
- Check removing a table with multiple foreign keys
|
6
|
+
|
7
|
+
- Check for mismatched reference column types
|
8
|
+
|
9
|
+
For example, it detects cases like:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
class AddUserIdToProjects < ActiveRecord::Migration[7.0]
|
13
|
+
def change
|
14
|
+
add_column :projects, :user_id, :integer
|
15
|
+
end
|
16
|
+
end
|
17
|
+
```
|
18
|
+
|
19
|
+
where `users.id` is of type `bigint`.
|
20
|
+
|
21
|
+
- Add support for multiple databases to `start_after` and `target_version` configuration options
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
OnlineMigrations.configure do |config|
|
25
|
+
config.start_after = { primary: 20211112000000, animals: 20220101000000 }
|
26
|
+
config.target_version = { primary: 10, animals: 14.1 }
|
27
|
+
end
|
28
|
+
```
|
29
|
+
|
30
|
+
- Do not suggest `ignored_columns` when removing columns for Active Record 4.2 (`ignored_columns` was introduced in 5.0)
|
31
|
+
|
32
|
+
- Check replacing indexes
|
33
|
+
|
34
|
+
For example, you have an index on `projects.creator_id`. But decide, it is better to have a multicolumn index on `[creator_id, created_at]`:
|
35
|
+
|
36
|
+
```ruby
|
37
|
+
class AddIndexOnCreationToProjects < ActiveRecord::Migration[7.0]
|
38
|
+
disable_ddl_transaction!
|
39
|
+
|
40
|
+
def change
|
41
|
+
remove_index :projects, :creator_id, algorithm: :concurrently # (1)
|
42
|
+
add_index :projects, [:creator_id, :created_at], algorithm: :concurrently # (2)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
```
|
46
|
+
|
47
|
+
If there is no existing indexes covering `creator_id`, removing an old index (1) before replacing it with the new one (2) might result in slow queries while building the new index.
|
48
|
+
A safer approach is to swap removing the old and creation of the new index operations.
|
49
|
+
|
3
50
|
## 0.1.0 (2022-01-17)
|
4
51
|
|
5
52
|
- First release
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -129,18 +129,21 @@ Potentially dangerous operations:
|
|
129
129
|
- [executing SQL directly](#executing-SQL-directly)
|
130
130
|
- [adding an index non-concurrently](#adding-an-index-non-concurrently)
|
131
131
|
- [removing an index non-concurrently](#removing-an-index-non-concurrently)
|
132
|
+
- [replacing an index](#replacing-an-index)
|
132
133
|
- [adding a reference](#adding-a-reference)
|
133
134
|
- [adding a foreign key](#adding-a-foreign-key)
|
134
135
|
- [adding a json column](#adding-a-json-column)
|
135
136
|
- [using primary key with short integer type](#using-primary-key-with-short-integer-type)
|
136
137
|
- [hash indexes](#hash-indexes)
|
137
138
|
- [adding multiple foreign keys](#adding-multiple-foreign-keys)
|
139
|
+
- [removing a table with multiple foreign keys](#removing-a-table-with-multiple-foreign-keys)
|
140
|
+
- [mismatched reference column types](#mismatched-reference-column-types)
|
138
141
|
|
139
142
|
You can also add [custom checks](#custom-checks) or [disable specific checks](#disable-checks).
|
140
143
|
|
141
144
|
### Removing a column
|
142
145
|
|
143
|
-
|
146
|
+
:x: **Bad**
|
144
147
|
|
145
148
|
ActiveRecord caches database columns at runtime, so if you drop a column, it can cause exceptions until your app reboots.
|
146
149
|
|
@@ -152,14 +155,22 @@ class RemoveNameFromUsers < ActiveRecord::Migration[7.0]
|
|
152
155
|
end
|
153
156
|
```
|
154
157
|
|
155
|
-
|
158
|
+
:white_check_mark: **Good**
|
156
159
|
|
157
160
|
1. Ignore the column:
|
158
161
|
|
159
162
|
```ruby
|
163
|
+
# For Active Record 5+
|
160
164
|
class User < ApplicationRecord
|
161
165
|
self.ignored_columns = ["name"]
|
162
166
|
end
|
167
|
+
|
168
|
+
# For Active Record < 5
|
169
|
+
class User < ActiveRecord::Base
|
170
|
+
def self.columns
|
171
|
+
super.reject { |c| c.name == "name" }
|
172
|
+
end
|
173
|
+
end
|
163
174
|
```
|
164
175
|
|
165
176
|
2. Deploy
|
@@ -178,7 +189,7 @@ end
|
|
178
189
|
|
179
190
|
### Adding a column with a default value
|
180
191
|
|
181
|
-
|
192
|
+
:x: **Bad**
|
182
193
|
|
183
194
|
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.
|
184
195
|
|
@@ -192,7 +203,7 @@ end
|
|
192
203
|
|
193
204
|
In PostgreSQL 11+ this no longer requires a table rewrite and is safe. Volatile expressions, however, such as `random()`, will still result in table rewrites.
|
194
205
|
|
195
|
-
|
206
|
+
:white_check_mark: **Good**
|
196
207
|
|
197
208
|
A safer approach is to:
|
198
209
|
|
@@ -216,7 +227,7 @@ end
|
|
216
227
|
|
217
228
|
### Backfilling data
|
218
229
|
|
219
|
-
|
230
|
+
:x: **Bad**
|
220
231
|
|
221
232
|
ActiveRecord 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/).
|
222
233
|
|
@@ -231,7 +242,7 @@ end
|
|
231
242
|
|
232
243
|
Also, running a single query to update data can cause issues for large tables.
|
233
244
|
|
234
|
-
|
245
|
+
:white_check_mark: **Good**
|
235
246
|
|
236
247
|
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!`.
|
237
248
|
|
@@ -256,7 +267,7 @@ end
|
|
256
267
|
|
257
268
|
### Changing the type of a column
|
258
269
|
|
259
|
-
|
270
|
+
:x: **Bad**
|
260
271
|
|
261
272
|
Changing the type of an existing column blocks reads and writes while the entire table is rewritten.
|
262
273
|
|
@@ -277,7 +288,7 @@ A few changes don't require a table rewrite (and are safe) in PostgreSQL:
|
|
277
288
|
- Making a `decimal` or `numeric` column unconstrained
|
278
289
|
- Changing between `timestamp` and `timestamptz` columns when session time zone is UTC in PostgreSQL 12+
|
279
290
|
|
280
|
-
|
291
|
+
:white_check_mark: **Good**
|
281
292
|
|
282
293
|
**Note**: The following steps can also be used to change the primary key's type (e.g., from `integer` to `bigint`).
|
283
294
|
|
@@ -340,7 +351,7 @@ A safer approach can be accomplished in several steps:
|
|
340
351
|
|
341
352
|
### Renaming a column
|
342
353
|
|
343
|
-
|
354
|
+
:x: **Bad**
|
344
355
|
|
345
356
|
Renaming a column that's in use will cause errors in your application.
|
346
357
|
|
@@ -352,7 +363,7 @@ class RenameUsersNameToFirstName < ActiveRecord::Migration[7.0]
|
|
352
363
|
end
|
353
364
|
```
|
354
365
|
|
355
|
-
|
366
|
+
:white_check_mark: **Good**
|
356
367
|
|
357
368
|
The "classic" approach suggests creating a new column and copy data/indexes/etc to it from the old column. This can be costly for very large tables. There is a trick that helps to avoid such heavy operations.
|
358
369
|
|
@@ -415,7 +426,7 @@ end
|
|
415
426
|
|
416
427
|
### Renaming a table
|
417
428
|
|
418
|
-
|
429
|
+
:x: **Bad**
|
419
430
|
|
420
431
|
Renaming a table that's in use will cause errors in your application.
|
421
432
|
|
@@ -427,7 +438,7 @@ class RenameClientsToUsers < ActiveRecord::Migration[7.0]
|
|
427
438
|
end
|
428
439
|
```
|
429
440
|
|
430
|
-
|
441
|
+
:white_check_mark: **Good**
|
431
442
|
|
432
443
|
The "classic" approach suggests creating a new table and copy data/indexes/etc to it from the old table. This can be costly for very large tables. There is a trick that helps to avoid such heavy operations.
|
433
444
|
|
@@ -488,7 +499,7 @@ end
|
|
488
499
|
|
489
500
|
### Creating a table with the force option
|
490
501
|
|
491
|
-
|
502
|
+
:x: **Bad**
|
492
503
|
|
493
504
|
The `force` option can drop an existing table.
|
494
505
|
|
@@ -502,7 +513,7 @@ class CreateUsers < ActiveRecord::Migration[7.0]
|
|
502
513
|
end
|
503
514
|
```
|
504
515
|
|
505
|
-
|
516
|
+
:white_check_mark: **Good**
|
506
517
|
|
507
518
|
Create tables without the `force` option.
|
508
519
|
|
@@ -520,7 +531,7 @@ If you intend to drop an existing table, run `drop_table` first.
|
|
520
531
|
|
521
532
|
### Adding a check constraint
|
522
533
|
|
523
|
-
|
534
|
+
:x: **Bad**
|
524
535
|
|
525
536
|
Adding a check constraint blocks reads and writes while every row is checked.
|
526
537
|
|
@@ -532,7 +543,7 @@ class AddCheckConstraint < ActiveRecord::Migration[7.0]
|
|
532
543
|
end
|
533
544
|
```
|
534
545
|
|
535
|
-
|
546
|
+
:white_check_mark: **Good**
|
536
547
|
|
537
548
|
Add the check constraint without validating existing rows, and then validate them in a separate transaction:
|
538
549
|
|
@@ -551,7 +562,7 @@ end
|
|
551
562
|
|
552
563
|
### Setting NOT NULL on an existing column
|
553
564
|
|
554
|
-
|
565
|
+
:x: **Bad**
|
555
566
|
|
556
567
|
Setting `NOT NULL` on an existing column blocks reads and writes while every row is checked.
|
557
568
|
|
@@ -563,7 +574,7 @@ class ChangeUsersNameNull < ActiveRecord::Migration[7.0]
|
|
563
574
|
end
|
564
575
|
```
|
565
576
|
|
566
|
-
|
577
|
+
:white_check_mark: **Good**
|
567
578
|
|
568
579
|
Instead, add a check constraint and validate it in a separate transaction:
|
569
580
|
|
@@ -606,7 +617,7 @@ end
|
|
606
617
|
|
607
618
|
### Adding an index non-concurrently
|
608
619
|
|
609
|
-
|
620
|
+
:x: **Bad**
|
610
621
|
|
611
622
|
Adding an index non-concurrently blocks writes.
|
612
623
|
|
@@ -618,7 +629,7 @@ class AddIndexOnUsersEmail < ActiveRecord::Migration[7.0]
|
|
618
629
|
end
|
619
630
|
```
|
620
631
|
|
621
|
-
|
632
|
+
:white_check_mark: **Good**
|
622
633
|
|
623
634
|
Add indexes concurrently.
|
624
635
|
|
@@ -636,7 +647,7 @@ end
|
|
636
647
|
|
637
648
|
### Removing an index non-concurrently
|
638
649
|
|
639
|
-
|
650
|
+
:x: **Bad**
|
640
651
|
|
641
652
|
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.
|
642
653
|
|
@@ -648,7 +659,7 @@ class RemoveIndexOnUsersEmail < ActiveRecord::Migration[7.0]
|
|
648
659
|
end
|
649
660
|
```
|
650
661
|
|
651
|
-
|
662
|
+
:white_check_mark: **Good**
|
652
663
|
|
653
664
|
Remove indexes concurrently.
|
654
665
|
|
@@ -664,9 +675,43 @@ end
|
|
664
675
|
|
665
676
|
**Note**: If you forget `disable_ddl_transaction!`, the migration will fail.
|
666
677
|
|
678
|
+
### Replacing an index
|
679
|
+
|
680
|
+
:x: **Bad**
|
681
|
+
|
682
|
+
Removing an old index before replacing it with the new one might result in slow queries while building the new index.
|
683
|
+
|
684
|
+
```ruby
|
685
|
+
class AddIndexOnCreationToProjects < ActiveRecord::Migration[7.0]
|
686
|
+
disable_ddl_transaction!
|
687
|
+
|
688
|
+
def change
|
689
|
+
remove_index :projects, :creator_id, algorithm: :concurrently
|
690
|
+
add_index :projects, [:creator_id, :created_at], algorithm: :concurrently
|
691
|
+
end
|
692
|
+
end
|
693
|
+
```
|
694
|
+
|
695
|
+
**Note**: If removed index is covered by any existing index, then it is safe to remove the index before replacing it with the new one.
|
696
|
+
|
697
|
+
:white_check_mark: **Good**
|
698
|
+
|
699
|
+
A safer approach is to create the new index and then delete the old one.
|
700
|
+
|
701
|
+
```ruby
|
702
|
+
class AddIndexOnCreationToProjects < ActiveRecord::Migration[7.0]
|
703
|
+
disable_ddl_transaction!
|
704
|
+
|
705
|
+
def change
|
706
|
+
add_index :projects, [:creator_id, :created_at], algorithm: :concurrently
|
707
|
+
remove_index :projects, :creator_id, algorithm: :concurrently
|
708
|
+
end
|
709
|
+
end
|
710
|
+
```
|
711
|
+
|
667
712
|
### Adding a reference
|
668
713
|
|
669
|
-
|
714
|
+
:x: **Bad**
|
670
715
|
|
671
716
|
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.
|
672
717
|
|
@@ -678,7 +723,7 @@ class AddUserToProjects < ActiveRecord::Migration[7.0]
|
|
678
723
|
end
|
679
724
|
```
|
680
725
|
|
681
|
-
|
726
|
+
:white_check_mark: **Good**
|
682
727
|
|
683
728
|
Make sure the index is added concurrently and the foreign key is added in a separate migration.
|
684
729
|
Or you can use `add_reference_concurrently` helper. It will create a reference and take care of safely adding index and/or foreign key.
|
@@ -697,7 +742,7 @@ end
|
|
697
742
|
|
698
743
|
### Adding a foreign key
|
699
744
|
|
700
|
-
|
745
|
+
:x: **Bad**
|
701
746
|
|
702
747
|
Adding a foreign key blocks writes on both tables.
|
703
748
|
|
@@ -719,7 +764,7 @@ class AddReferenceToProjectsUser < ActiveRecord::Migration[7.0]
|
|
719
764
|
end
|
720
765
|
```
|
721
766
|
|
722
|
-
|
767
|
+
:white_check_mark: **Good**
|
723
768
|
|
724
769
|
Add the foreign key without validating existing rows, and then validate them in a separate transaction.
|
725
770
|
|
@@ -738,7 +783,7 @@ end
|
|
738
783
|
|
739
784
|
### Adding a json column
|
740
785
|
|
741
|
-
|
786
|
+
:x: **Bad**
|
742
787
|
|
743
788
|
There's no equality operator for the `json` column type, which can cause errors for existing `SELECT DISTINCT` queries in your application.
|
744
789
|
|
@@ -750,7 +795,7 @@ class AddSettingsToProjects < ActiveRecord::Migration[7.0]
|
|
750
795
|
end
|
751
796
|
```
|
752
797
|
|
753
|
-
|
798
|
+
:white_check_mark: **Good**
|
754
799
|
|
755
800
|
Use `jsonb` instead.
|
756
801
|
|
@@ -764,7 +809,7 @@ end
|
|
764
809
|
|
765
810
|
### Using primary key with short integer type
|
766
811
|
|
767
|
-
|
812
|
+
:x: **Bad**
|
768
813
|
|
769
814
|
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 ActiveRecord < 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`.
|
770
815
|
|
@@ -778,7 +823,7 @@ class CreateUsers < ActiveRecord::Migration[7.0]
|
|
778
823
|
end
|
779
824
|
```
|
780
825
|
|
781
|
-
|
826
|
+
:white_check_mark: **Good**
|
782
827
|
|
783
828
|
Use one of `bigint`, `bigserial`, `uuid` instead.
|
784
829
|
|
@@ -794,7 +839,7 @@ end
|
|
794
839
|
|
795
840
|
### Hash indexes
|
796
841
|
|
797
|
-
|
842
|
+
:x: **Bad - PostgreSQL < 10**
|
798
843
|
|
799
844
|
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.
|
800
845
|
|
@@ -806,7 +851,7 @@ class AddIndexToUsersOnEmail < ActiveRecord::Migration[7.0]
|
|
806
851
|
end
|
807
852
|
```
|
808
853
|
|
809
|
-
|
854
|
+
:white_check_mark: **Good - PostgreSQL < 10**
|
810
855
|
|
811
856
|
Use B-tree indexes instead.
|
812
857
|
|
@@ -820,9 +865,9 @@ end
|
|
820
865
|
|
821
866
|
### Adding multiple foreign keys
|
822
867
|
|
823
|
-
|
868
|
+
:x: **Bad**
|
824
869
|
|
825
|
-
Adding multiple foreign keys in a single migration blocks writes on all involved tables until migration is completed.
|
870
|
+
Adding multiple foreign keys in a single migration blocks reads and writes on all involved tables until migration is completed.
|
826
871
|
Avoid adding foreign key more than once per migration file, unless the source and target tables are identical.
|
827
872
|
|
828
873
|
```ruby
|
@@ -836,7 +881,7 @@ class CreateUserProjects < ActiveRecord::Migration[7.0]
|
|
836
881
|
end
|
837
882
|
```
|
838
883
|
|
839
|
-
|
884
|
+
:white_check_mark: **Good**
|
840
885
|
|
841
886
|
Add additional foreign keys in separate migration files. See [adding a foreign key](#adding-a-foreign-key) for how to properly add foreign keys.
|
842
887
|
|
@@ -857,6 +902,82 @@ class AddForeignKeyFromUserProjectsToProject < ActiveRecord::Migration[7.0]
|
|
857
902
|
end
|
858
903
|
```
|
859
904
|
|
905
|
+
### Removing a table with multiple foreign keys
|
906
|
+
|
907
|
+
:x: **Bad**
|
908
|
+
|
909
|
+
Removing a table with multiple foreign keys blocks reads and writes on all involved tables until migration is completed.
|
910
|
+
Remove all the foreign keys first.
|
911
|
+
|
912
|
+
Assuming, `projects` has foreign keys on `users.id` and `repositories.id`:
|
913
|
+
|
914
|
+
```ruby
|
915
|
+
class DropProjects < ActiveRecord::Migration[7.0]
|
916
|
+
def change
|
917
|
+
drop_table :projects
|
918
|
+
end
|
919
|
+
end
|
920
|
+
```
|
921
|
+
|
922
|
+
:white_check_mark: **Good**
|
923
|
+
|
924
|
+
Remove all the foreign keys first:
|
925
|
+
|
926
|
+
```ruby
|
927
|
+
class RemoveProjectsUserFk < ActiveRecord::Migration[7.0]
|
928
|
+
def change
|
929
|
+
remove_foreign_key :projects, :users
|
930
|
+
end
|
931
|
+
end
|
932
|
+
|
933
|
+
class RemoveProjectsRepositoryFk < ActiveRecord::Migration[7.0]
|
934
|
+
def change
|
935
|
+
remove_foreign_key :projects, :repositories
|
936
|
+
end
|
937
|
+
end
|
938
|
+
```
|
939
|
+
|
940
|
+
Then remove the table:
|
941
|
+
|
942
|
+
```ruby
|
943
|
+
class DropProjects < ActiveRecord::Migration[7.0]
|
944
|
+
def change
|
945
|
+
drop_table :projects
|
946
|
+
end
|
947
|
+
end
|
948
|
+
```
|
949
|
+
|
950
|
+
### Mismatched reference column types
|
951
|
+
|
952
|
+
:x: **Bad**
|
953
|
+
|
954
|
+
Reference columns should be of the same type as the referenced primary key.
|
955
|
+
Otherwise, there's a risk of bugs caused by IDs representable by one type but not the other.
|
956
|
+
|
957
|
+
Assuming, there is a `users` table with `bigint` primary key type:
|
958
|
+
|
959
|
+
```ruby
|
960
|
+
class AddUserIdToProjects < ActiveRecord::Migration[7.0]
|
961
|
+
def change
|
962
|
+
add_column :projects, :user_id, :integer
|
963
|
+
end
|
964
|
+
end
|
965
|
+
```
|
966
|
+
|
967
|
+
:white_check_mark: **Good**
|
968
|
+
|
969
|
+
Add a reference column of the same type as a referenced primary key.
|
970
|
+
|
971
|
+
Assuming, there is a `users` table with `bigint` primary key type:
|
972
|
+
|
973
|
+
```ruby
|
974
|
+
class AddUserIdToProjects < ActiveRecord::Migration[7.0]
|
975
|
+
def change
|
976
|
+
add_column :projects, :user_id, :bigint
|
977
|
+
end
|
978
|
+
end
|
979
|
+
```
|
980
|
+
|
860
981
|
## Assuring Safety
|
861
982
|
|
862
983
|
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.
|
@@ -986,6 +1107,9 @@ To mark migrations as safe that were created before installing this gem, configu
|
|
986
1107
|
# config/initializers/online_migrations.rb
|
987
1108
|
|
988
1109
|
config.start_after = 20220101000000
|
1110
|
+
|
1111
|
+
# or if you use multiple databases (ActiveRecord 6+)
|
1112
|
+
config.start_after = { primary: 20211112000000, animals: 20220101000000 }
|
989
1113
|
```
|
990
1114
|
|
991
1115
|
Use the version from your latest migration.
|
@@ -998,6 +1122,9 @@ If your development database version is different from production, you can speci
|
|
998
1122
|
# config/initializers/online_migrations.rb
|
999
1123
|
|
1000
1124
|
config.target_version = 10 # or "12.9" etc
|
1125
|
+
|
1126
|
+
# or if you use multiple databases (ActiveRecord 6+)
|
1127
|
+
config.target_version = { primary: 10, animals: 14.1 }
|
1001
1128
|
```
|
1002
1129
|
|
1003
1130
|
For safety, this option only affects development and test environments. In other environments, the actual server version is always used.
|
@@ -37,7 +37,7 @@ module OnlineMigrations
|
|
37
37
|
objid = lock_key & 0xffffffff
|
38
38
|
classid = (lock_key & (0xffffffff << 32)) >> 32
|
39
39
|
|
40
|
-
active = connection.select_value(
|
40
|
+
active = connection.select_value(<<-SQL.strip_heredoc)
|
41
41
|
SELECT granted
|
42
42
|
FROM pg_locks
|
43
43
|
WHERE locktype = 'advisory'
|
@@ -13,7 +13,9 @@ module OnlineMigrations
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def relation
|
16
|
-
|
16
|
+
column, value = updates.first
|
17
|
+
|
18
|
+
if updates.size == 1 && !value.nil?
|
17
19
|
# If value is nil, the generated SQL is correct (`WHERE column IS NOT NULL`).
|
18
20
|
# Otherwise, the SQL is `WHERE column != value`. This condition ignores column
|
19
21
|
# with NULLs in it, so we need to also manually check for NULLs.
|
@@ -26,7 +26,7 @@ module OnlineMigrations
|
|
26
26
|
stuck_sql = connection.unprepared_statement { stuck.to_sql }
|
27
27
|
failed_retriable_sql = connection.unprepared_statement { failed_retriable.to_sql }
|
28
28
|
|
29
|
-
from(Arel.sql(
|
29
|
+
from(Arel.sql(<<-SQL.strip_heredoc))
|
30
30
|
(
|
31
31
|
(#{failed_retriable_sql})
|
32
32
|
UNION
|
@@ -11,8 +11,9 @@ module OnlineMigrations
|
|
11
11
|
}
|
12
12
|
|
13
13
|
def validate(record)
|
14
|
-
return unless
|
14
|
+
return unless record.status_changed?
|
15
15
|
|
16
|
+
previous_status, new_status = record.status_change
|
16
17
|
valid_new_statuses = VALID_STATUS_TRANSITIONS.fetch(previous_status, [])
|
17
18
|
|
18
19
|
unless valid_new_statuses.include?(new_status)
|
@@ -79,7 +79,9 @@ module OnlineMigrations
|
|
79
79
|
end
|
80
80
|
|
81
81
|
def find_or_create_next_migration_job
|
82
|
-
|
82
|
+
min_value, max_value = migration.next_batch_range
|
83
|
+
|
84
|
+
if min_value && max_value
|
83
85
|
create_migration_job!(min_value, max_value)
|
84
86
|
else
|
85
87
|
migration.migration_jobs.retriable.first
|
@@ -29,8 +29,9 @@ module OnlineMigrations
|
|
29
29
|
}
|
30
30
|
|
31
31
|
def validate(record)
|
32
|
-
return unless
|
32
|
+
return unless record.status_changed?
|
33
33
|
|
34
|
+
previous_status, new_status = record.status_change
|
34
35
|
valid_new_statuses = VALID_STATUS_TRANSITIONS.fetch(previous_status, [])
|
35
36
|
|
36
37
|
unless valid_new_statuses.include?(new_status)
|
@@ -249,7 +249,7 @@ module OnlineMigrations
|
|
249
249
|
# through PG internal tables.
|
250
250
|
# In-depth analysis of implications of this was made, so this approach
|
251
251
|
# is considered safe - https://habr.com/ru/company/haulmont/blog/493954/ (in russian).
|
252
|
-
execute(
|
252
|
+
execute(<<-SQL.strip_heredoc)
|
253
253
|
UPDATE pg_catalog.pg_attribute
|
254
254
|
SET attnotnull = true
|
255
255
|
WHERE attrelid = #{quote(table_name)}::regclass
|
@@ -471,7 +471,7 @@ module OnlineMigrations
|
|
471
471
|
def __check_constraints(table_name)
|
472
472
|
schema = __schema_for_table(table_name)
|
473
473
|
|
474
|
-
check_sql =
|
474
|
+
check_sql = <<-SQL.strip_heredoc
|
475
475
|
SELECT
|
476
476
|
ccu.column_name as column_name,
|
477
477
|
con.conname as constraint_name,
|
@@ -494,7 +494,7 @@ module OnlineMigrations
|
|
494
494
|
end
|
495
495
|
|
496
496
|
def __rename_constraint(table_name, old_name, new_name)
|
497
|
-
execute(
|
497
|
+
execute(<<-SQL.strip_heredoc)
|
498
498
|
ALTER TABLE #{quote_table_name(table_name)}
|
499
499
|
RENAME CONSTRAINT #{quote_column_name(old_name)} TO #{quote_column_name(new_name)}
|
500
500
|
SQL
|
@@ -566,7 +566,7 @@ module OnlineMigrations
|
|
566
566
|
def __referencing_table_names(table_name)
|
567
567
|
schema = __schema_for_table(table_name)
|
568
568
|
|
569
|
-
select_values(
|
569
|
+
select_values(<<-SQL.strip_heredoc)
|
570
570
|
SELECT DISTINCT con.conrelid::regclass::text AS conrelname
|
571
571
|
FROM pg_catalog.pg_constraint con
|
572
572
|
INNER JOIN pg_catalog.pg_namespace nsp
|
@@ -15,6 +15,7 @@ module OnlineMigrations
|
|
15
15
|
@new_tables = []
|
16
16
|
@lock_timeout_checked = false
|
17
17
|
@foreign_key_tables = Set.new
|
18
|
+
@removed_indexes = []
|
18
19
|
end
|
19
20
|
|
20
21
|
def safety_assured
|
@@ -122,16 +123,23 @@ module OnlineMigrations
|
|
122
123
|
end
|
123
124
|
|
124
125
|
def create_join_table(table1, table2, **options, &block)
|
125
|
-
|
126
|
-
|
126
|
+
table_name = options[:table_name] || derive_join_table_name(table1, table2)
|
127
|
+
create_table(table_name, **options, &block)
|
128
|
+
end
|
127
129
|
|
128
|
-
|
129
|
-
|
130
|
-
|
130
|
+
def drop_table(table_name, **_options)
|
131
|
+
foreign_keys = connection.foreign_keys(table_name)
|
132
|
+
referenced_tables = foreign_keys.map(&:to_table).uniq
|
133
|
+
referenced_tables.delete(table_name.to_s) # ignore self references
|
134
|
+
|
135
|
+
if referenced_tables.size > 1
|
136
|
+
raise_error :drop_table_multiple_foreign_keys
|
131
137
|
end
|
138
|
+
end
|
132
139
|
|
140
|
+
def drop_join_table(table1, table2, **options)
|
133
141
|
table_name = options[:table_name] || derive_join_table_name(table1, table2)
|
134
|
-
|
142
|
+
drop_table(table_name, **options)
|
135
143
|
end
|
136
144
|
|
137
145
|
def change_table(*)
|
@@ -157,10 +165,13 @@ module OnlineMigrations
|
|
157
165
|
volatile_default: volatile_default
|
158
166
|
end
|
159
167
|
|
160
|
-
if type
|
168
|
+
if type == :json
|
161
169
|
raise_error :add_column_json,
|
162
170
|
code: command_str(:add_column, table_name, column_name, :jsonb, options)
|
163
171
|
end
|
172
|
+
|
173
|
+
type = :bigint if type == :integer && options[:limit] == 8
|
174
|
+
check_mismatched_foreign_key_type(table_name, column_name, type)
|
164
175
|
end
|
165
176
|
|
166
177
|
def rename_column(table_name, column_name, new_column, **)
|
@@ -266,7 +277,7 @@ module OnlineMigrations
|
|
266
277
|
|
267
278
|
if postgresql_version >= Gem::Version.new("12")
|
268
279
|
vars[:remove_constraint_code] = command_str(:remove_check_constraint, table_name, name: constraint_name)
|
269
|
-
vars[:change_column_null_code] = command_str(:change_column_null, table_name, column_name,
|
280
|
+
vars[:change_column_null_code] = command_str(:change_column_null, table_name, column_name, false)
|
270
281
|
end
|
271
282
|
|
272
283
|
raise_error :change_column_null, **vars
|
@@ -283,16 +294,18 @@ module OnlineMigrations
|
|
283
294
|
table_name, *columns = args
|
284
295
|
when :remove_timestamps
|
285
296
|
table_name = args[0]
|
286
|
-
columns = [
|
297
|
+
columns = ["created_at", "updated_at"]
|
287
298
|
else
|
288
299
|
table_name, reference = args
|
289
|
-
columns = [
|
290
|
-
columns <<
|
300
|
+
columns = ["#{reference}_id"]
|
301
|
+
columns << "#{reference}_type" if options[:polymorphic]
|
291
302
|
end
|
292
303
|
|
304
|
+
columns = columns.map(&:to_s)
|
305
|
+
|
293
306
|
if !new_table?(table_name)
|
294
307
|
indexes = connection.indexes(table_name).select do |index|
|
295
|
-
(index.columns & columns
|
308
|
+
(index.columns & columns).any?
|
296
309
|
end
|
297
310
|
|
298
311
|
raise_error :remove_column,
|
@@ -345,15 +358,40 @@ module OnlineMigrations
|
|
345
358
|
bad_index: bad_index,
|
346
359
|
bad_foreign_key: bad_foreign_key
|
347
360
|
end
|
361
|
+
|
362
|
+
unless options[:polymorphic]
|
363
|
+
type = options[:type] || (Utils.ar_version >= 5.1 ? :bigint : :integer)
|
364
|
+
column_name = "#{ref_name}_id"
|
365
|
+
|
366
|
+
foreign_key_options = foreign_key.is_a?(Hash) ? foreign_key : {}
|
367
|
+
check_mismatched_foreign_key_type(table_name, column_name, type, **foreign_key_options)
|
368
|
+
end
|
348
369
|
end
|
349
370
|
alias add_belongs_to add_reference
|
350
371
|
|
351
372
|
def add_index(table_name, column_name, **options)
|
352
373
|
if options[:using].to_s == "hash" && postgresql_version < Gem::Version.new("10")
|
353
374
|
raise_error :add_hash_index
|
354
|
-
|
355
|
-
|
356
|
-
|
375
|
+
end
|
376
|
+
|
377
|
+
if !new_or_small_table?(table_name)
|
378
|
+
if options[:algorithm] != :concurrently
|
379
|
+
raise_error :add_index,
|
380
|
+
command: command_str(:add_index, table_name, column_name, **options.merge(algorithm: :concurrently))
|
381
|
+
end
|
382
|
+
|
383
|
+
if @removed_indexes.any?
|
384
|
+
index = IndexDefinition.new(table: table_name, columns: column_name, **options)
|
385
|
+
existing_indexes = connection.indexes(table_name)
|
386
|
+
|
387
|
+
@removed_indexes.each do |removed_index|
|
388
|
+
next unless removed_index.intersect?(index)
|
389
|
+
|
390
|
+
unless existing_indexes.any? { |existing_index| removed_index.covered_by?(existing_index) }
|
391
|
+
raise_error :replace_index
|
392
|
+
end
|
393
|
+
end
|
394
|
+
end
|
357
395
|
end
|
358
396
|
end
|
359
397
|
|
@@ -364,6 +402,11 @@ module OnlineMigrations
|
|
364
402
|
raise_error :remove_index,
|
365
403
|
command: command_str(:remove_index, table_name, **options.merge(algorithm: :concurrently))
|
366
404
|
end
|
405
|
+
|
406
|
+
if options[:column] || options[:name]
|
407
|
+
options[:column] ||= connection.indexes(table_name).find { |index| index.name == options[:name].to_s }
|
408
|
+
@removed_indexes << IndexDefinition.new(table: table_name, columns: options.delete(:column), **options)
|
409
|
+
end
|
367
410
|
end
|
368
411
|
|
369
412
|
def add_foreign_key(from_table, to_table, **options)
|
@@ -433,7 +476,7 @@ module OnlineMigrations
|
|
433
476
|
nil
|
434
477
|
when Hash
|
435
478
|
options[:id][:type]
|
436
|
-
when nil
|
479
|
+
when :primary_key, nil
|
437
480
|
# default type is used
|
438
481
|
connection.native_database_types[:primary_key].split.first
|
439
482
|
else
|
@@ -504,6 +547,7 @@ module OnlineMigrations
|
|
504
547
|
vars[:migration_name] = @migration.name
|
505
548
|
vars[:migration_parent] = Utils.migration_parent_string
|
506
549
|
vars[:model_parent] = Utils.model_parent_string
|
550
|
+
vars[:ar_version] = Utils.ar_version
|
507
551
|
|
508
552
|
if RUBY_VERSION >= "2.6"
|
509
553
|
message = ERB.new(template, trim_mode: "<>").result_with_hash(vars)
|
@@ -545,7 +589,7 @@ module OnlineMigrations
|
|
545
589
|
end
|
546
590
|
|
547
591
|
def crud_blocked?
|
548
|
-
locks_query =
|
592
|
+
locks_query = <<-SQL.strip_heredoc
|
549
593
|
SELECT relation::regclass::text
|
550
594
|
FROM pg_locks
|
551
595
|
WHERE mode IN ('ShareLock', 'ShareRowExclusiveLock', 'ExclusiveLock', 'AccessExclusiveLock')
|
@@ -563,7 +607,7 @@ module OnlineMigrations
|
|
563
607
|
end
|
564
608
|
|
565
609
|
def check_constraints(table_name)
|
566
|
-
constraints_query =
|
610
|
+
constraints_query = <<-SQL.strip_heredoc
|
567
611
|
SELECT pg_get_constraintdef(oid) AS def
|
568
612
|
FROM pg_constraint
|
569
613
|
WHERE contype = 'c'
|
@@ -574,6 +618,34 @@ module OnlineMigrations
|
|
574
618
|
connection.select_all(constraints_query).to_a
|
575
619
|
end
|
576
620
|
|
621
|
+
def check_mismatched_foreign_key_type(table_name, column_name, type, **options)
|
622
|
+
column_name = column_name.to_s
|
623
|
+
ref_name = column_name.sub(/_id\z/, "")
|
624
|
+
|
625
|
+
if like_foreign_key?(column_name, type)
|
626
|
+
foreign_table_name = Utils.foreign_table_name(ref_name, options)
|
627
|
+
|
628
|
+
if connection.table_exists?(foreign_table_name)
|
629
|
+
primary_key = options[:primary_key] || connection.primary_key(foreign_table_name)
|
630
|
+
primary_key_column = column_for(foreign_table_name, primary_key)
|
631
|
+
|
632
|
+
if primary_key_column && type != primary_key_column.sql_type.to_sym
|
633
|
+
raise_error :mismatched_foreign_key_type,
|
634
|
+
table_name: table_name, column_name: column_name
|
635
|
+
end
|
636
|
+
end
|
637
|
+
end
|
638
|
+
end
|
639
|
+
|
640
|
+
def like_foreign_key?(column_name, type)
|
641
|
+
column_name.end_with?("_id") &&
|
642
|
+
[:integer, :bigint, :serial, :bigserial, :uuid].include?(type)
|
643
|
+
end
|
644
|
+
|
645
|
+
def column_for(table_name, column_name)
|
646
|
+
connection.columns(table_name).find { |column| column.name == column_name.to_s }
|
647
|
+
end
|
648
|
+
|
577
649
|
# From ActiveRecord
|
578
650
|
def derive_join_table_name(table1, table2)
|
579
651
|
[table1.to_s, table2.to_s].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').tr("\0", "_")
|
@@ -5,20 +5,69 @@ module OnlineMigrations
|
|
5
5
|
class Config
|
6
6
|
include ErrorMessages
|
7
7
|
|
8
|
-
#
|
8
|
+
# Set the migration version starting after which checks are performed
|
9
|
+
# @example
|
10
|
+
# OnlineMigrations.config.start_after = 20220101000000
|
11
|
+
#
|
12
|
+
# @example Multiple databases
|
13
|
+
# OnlineMigrations.config.start_after = { primary: 20211112000000, animals: 20220101000000 }
|
14
|
+
#
|
15
|
+
# @note Use the version from your latest migration.
|
16
|
+
#
|
17
|
+
def start_after=(value)
|
18
|
+
if value.is_a?(Hash)
|
19
|
+
ensure_supports_multiple_dbs
|
20
|
+
@start_after = value.stringify_keys
|
21
|
+
else
|
22
|
+
@start_after = value
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# The migration version starting after which checks are performed
|
9
27
|
# @return [Integer]
|
10
28
|
#
|
11
|
-
|
29
|
+
def start_after
|
30
|
+
if @start_after.is_a?(Hash)
|
31
|
+
@start_after.fetch(db_config_name) do
|
32
|
+
raise "OnlineMigrations.config.start_after is not configured for :#{db_config_name}"
|
33
|
+
end
|
34
|
+
else
|
35
|
+
@start_after
|
36
|
+
end
|
37
|
+
end
|
12
38
|
|
13
|
-
#
|
39
|
+
# Set the database version against which the checks will be performed
|
14
40
|
#
|
15
41
|
# If your development database version is different from production, you can specify
|
16
42
|
# the production version so the right checks run in development.
|
17
43
|
#
|
18
|
-
# @example
|
44
|
+
# @example
|
19
45
|
# OnlineMigrations.config.target_version = 10
|
20
46
|
#
|
21
|
-
|
47
|
+
# @example Multiple databases
|
48
|
+
# OnlineMigrations.config.target_version = { primary: 10, animals: 14.1 }
|
49
|
+
#
|
50
|
+
def target_version=(value)
|
51
|
+
if value.is_a?(Hash)
|
52
|
+
ensure_supports_multiple_dbs
|
53
|
+
@target_version = value.stringify_keys
|
54
|
+
else
|
55
|
+
@target_version = value
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# The database version against which the checks will be performed
|
60
|
+
# @return [Numeric, String, nil]
|
61
|
+
#
|
62
|
+
def target_version
|
63
|
+
if @target_version.is_a?(Hash)
|
64
|
+
@target_version.fetch(db_config_name) do
|
65
|
+
raise "OnlineMigrations.config.target_version is not configured for :#{db_config_name}"
|
66
|
+
end
|
67
|
+
else
|
68
|
+
@target_version
|
69
|
+
end
|
70
|
+
end
|
22
71
|
|
23
72
|
# Whether to perform checks when migrating down
|
24
73
|
#
|
@@ -119,6 +168,7 @@ module OnlineMigrations
|
|
119
168
|
|
120
169
|
@checks = []
|
121
170
|
@start_after = 0
|
171
|
+
@target_version = nil
|
122
172
|
@small_tables = []
|
123
173
|
@check_down = false
|
124
174
|
@enabled_checks = @error_messages.keys.map { |k| [k, {}] }.to_h
|
@@ -194,5 +244,21 @@ module OnlineMigrations
|
|
194
244
|
def add_check(start_after: nil, &block)
|
195
245
|
@checks << [{ start_after: start_after }, block]
|
196
246
|
end
|
247
|
+
|
248
|
+
private
|
249
|
+
def ensure_supports_multiple_dbs
|
250
|
+
unless Utils.supports_multiple_dbs?
|
251
|
+
raise "Multiple databases are not supported by this ActiveRecord version"
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
def db_config_name
|
256
|
+
connection = OnlineMigrations.current_migration.connection
|
257
|
+
if Utils.ar_version < 6.1
|
258
|
+
connection.pool.spec.name
|
259
|
+
else
|
260
|
+
connection.pool.db_config.name
|
261
|
+
end
|
262
|
+
end
|
197
263
|
end
|
198
264
|
end
|
@@ -23,7 +23,7 @@ module OnlineMigrations
|
|
23
23
|
trigger_name = name(from_columns, to_columns)
|
24
24
|
assignment_clauses = assignment_clauses_for_columns(from_columns, to_columns)
|
25
25
|
|
26
|
-
connection.execute(
|
26
|
+
connection.execute(<<-SQL.strip_heredoc)
|
27
27
|
CREATE OR REPLACE FUNCTION #{trigger_name}() RETURNS TRIGGER AS $$
|
28
28
|
BEGIN
|
29
29
|
#{assignment_clauses};
|
@@ -32,11 +32,11 @@ module OnlineMigrations
|
|
32
32
|
$$ LANGUAGE plpgsql;
|
33
33
|
SQL
|
34
34
|
|
35
|
-
connection.execute(
|
35
|
+
connection.execute(<<-SQL.strip_heredoc)
|
36
36
|
DROP TRIGGER IF EXISTS #{trigger_name} ON #{quoted_table_name}
|
37
37
|
SQL
|
38
38
|
|
39
|
-
connection.execute(
|
39
|
+
connection.execute(<<-SQL.strip_heredoc)
|
40
40
|
CREATE TRIGGER #{trigger_name}
|
41
41
|
BEFORE INSERT OR UPDATE
|
42
42
|
ON #{quoted_table_name}
|
@@ -190,12 +190,17 @@ class <%= migration_name %> < <%= migration_parent %>
|
|
190
190
|
def change
|
191
191
|
<%= add_constraint_code %>
|
192
192
|
<% if backfill_code %>
|
193
|
+
|
194
|
+
# Passing a default value to change_column_null runs a single UPDATE query,
|
195
|
+
# which can cause downtime. Instead, backfill the existing rows in batches.
|
193
196
|
<%= backfill_code %>
|
197
|
+
|
194
198
|
<% end %>
|
195
199
|
<%= validate_constraint_code %>
|
196
200
|
<% if remove_constraint_code %>
|
197
|
-
|
201
|
+
|
198
202
|
<%= change_column_null_code %>
|
203
|
+
<%= remove_constraint_code %>
|
199
204
|
<% end %>
|
200
205
|
end
|
201
206
|
end",
|
@@ -221,7 +226,13 @@ A safer approach is to:
|
|
221
226
|
1. Ignore the column(s):
|
222
227
|
|
223
228
|
class <%= model %> < <%= model_parent %>
|
229
|
+
<% if ar_version >= 5 %>
|
224
230
|
self.ignored_columns = <%= columns %>
|
231
|
+
<% else %>
|
232
|
+
def self.columns
|
233
|
+
super.reject { |c| <%= columns %>.include?(c.name) }
|
234
|
+
end
|
235
|
+
<% end %>
|
225
236
|
end
|
226
237
|
|
227
238
|
2. Deploy
|
@@ -306,6 +317,10 @@ class <%= migration_name %> < <%= migration_parent %>
|
|
306
317
|
end
|
307
318
|
end",
|
308
319
|
|
320
|
+
replace_index:
|
321
|
+
"Removing an old index before replacing it with the new one might result in slow queries while building the new index.
|
322
|
+
A safer approach is to create the new index and then delete the old one.",
|
323
|
+
|
309
324
|
add_foreign_key:
|
310
325
|
"Adding a foreign key blocks writes on both tables. Add the foreign key without validating existing rows,
|
311
326
|
and then validate them in a separate transaction.
|
@@ -381,8 +396,16 @@ execute call, so cannot help you here. Make really sure that what
|
|
381
396
|
you're doing is safe before proceeding, then wrap it in a safety_assured { ... } block.",
|
382
397
|
|
383
398
|
multiple_foreign_keys:
|
384
|
-
"Adding multiple foreign keys in a single migration blocks writes on all involved tables until migration is completed.
|
399
|
+
"Adding multiple foreign keys in a single migration blocks reads and writes on all involved tables until migration is completed.
|
385
400
|
Avoid adding foreign key more than once per migration file, unless the source and target tables are identical.",
|
401
|
+
|
402
|
+
drop_table_multiple_foreign_keys:
|
403
|
+
"Dropping a table with multiple foreign keys blocks reads and writes on all involved tables until migration is completed.
|
404
|
+
Remove all the foreign keys first.",
|
405
|
+
|
406
|
+
mismatched_foreign_key_type:
|
407
|
+
"<%= table_name %>.<%= column_name %> references a column of different type - foreign keys should be of the same type as the referenced primary key.
|
408
|
+
Otherwise, there's a risk of errors caused by IDs representable by one type but not the other.",
|
386
409
|
}
|
387
410
|
end
|
388
411
|
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module OnlineMigrations
|
4
|
+
# @private
|
5
|
+
class IndexDefinition
|
6
|
+
attr_reader :table, :columns, :unique, :opclasses, :where, :type, :using
|
7
|
+
|
8
|
+
def initialize(**options)
|
9
|
+
@table = options[:table]
|
10
|
+
@columns = Array(options[:columns]).map(&:to_s)
|
11
|
+
@unique = options[:unique]
|
12
|
+
@opclasses = options[:opclass] || {}
|
13
|
+
@where = options[:where]
|
14
|
+
@type = options[:type]
|
15
|
+
@using = options[:using] || :btree
|
16
|
+
end
|
17
|
+
|
18
|
+
# @param other [OnlineMigrations::IndexDefinition, ActiveRecord::ConnectionAdapters::IndexDefinition]
|
19
|
+
def intersect?(other)
|
20
|
+
# For ActiveRecord::ConnectionAdapters::IndexDefinition is for expression indexes,
|
21
|
+
# `columns` is a string
|
22
|
+
table == other.table &&
|
23
|
+
(columns & Array(other.columns)).any?
|
24
|
+
end
|
25
|
+
|
26
|
+
# @param other [OnlineMigrations::IndexDefinition, ActiveRecord::ConnectionAdapters::IndexDefinition]
|
27
|
+
def covered_by?(other)
|
28
|
+
return false if type != other.type
|
29
|
+
return false if using != other.using
|
30
|
+
return false if where != other.where
|
31
|
+
return false if other.respond_to?(:opclasses) && opclasses != other.opclasses
|
32
|
+
|
33
|
+
case [unique, other.unique]
|
34
|
+
when [true, true]
|
35
|
+
columns == other.columns
|
36
|
+
when [true, false]
|
37
|
+
false
|
38
|
+
else
|
39
|
+
prefix?(self, other)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
def prefix?(lhs, rhs)
|
45
|
+
lhs_columns = Array(lhs.columns)
|
46
|
+
rhs_columns = Array(rhs.columns)
|
47
|
+
|
48
|
+
lhs_columns.count <= rhs_columns.count &&
|
49
|
+
rhs_columns[0...lhs_columns.count] == lhs_columns
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -3,8 +3,6 @@
|
|
3
3
|
module OnlineMigrations
|
4
4
|
# @private
|
5
5
|
class IndexesCollector
|
6
|
-
IndexDefinition = Struct.new(:using)
|
7
|
-
|
8
6
|
COLUMN_TYPES = [:bigint, :binary, :boolean, :date, :datetime, :decimal,
|
9
7
|
:float, :integer, :json, :string, :text, :time, :timestamp, :virtual]
|
10
8
|
|
@@ -19,7 +17,7 @@ module OnlineMigrations
|
|
19
17
|
end
|
20
18
|
|
21
19
|
def index(_column_name, **options)
|
22
|
-
@indexes << IndexDefinition.new(options[:using].to_s)
|
20
|
+
@indexes << IndexDefinition.new(using: options[:using].to_s)
|
23
21
|
end
|
24
22
|
|
25
23
|
def references(*_ref_names, **options)
|
@@ -27,7 +25,7 @@ module OnlineMigrations
|
|
27
25
|
|
28
26
|
if index
|
29
27
|
using = index.is_a?(Hash) ? index[:using].to_s : nil
|
30
|
-
@indexes << IndexDefinition.new(using)
|
28
|
+
@indexes << IndexDefinition.new(using: using)
|
31
29
|
end
|
32
30
|
end
|
33
31
|
alias belongs_to references
|
@@ -40,7 +38,7 @@ module OnlineMigrations
|
|
40
38
|
|
41
39
|
if index
|
42
40
|
using = index.is_a?(Hash) ? index[:using].to_s : nil
|
43
|
-
@indexes << IndexDefinition.new(using)
|
41
|
+
@indexes << IndexDefinition.new(using: using)
|
44
42
|
end
|
45
43
|
end
|
46
44
|
end
|
@@ -677,7 +677,7 @@ module OnlineMigrations
|
|
677
677
|
index_name = options[:name]
|
678
678
|
index_name ||= index_name(table_name, column_names)
|
679
679
|
|
680
|
-
if index_exists?(table_name,
|
680
|
+
if index_exists?(table_name, column_names, **options)
|
681
681
|
disable_statement_timeout do
|
682
682
|
# "DROP INDEX CONCURRENTLY" requires a "SHARE UPDATE EXCLUSIVE" lock.
|
683
683
|
# It only conflicts with constraint validations, other creating/removing indexes,
|
@@ -692,7 +692,7 @@ module OnlineMigrations
|
|
692
692
|
end
|
693
693
|
else
|
694
694
|
Utils.say("Index was not removed because it does not exist (this may be due to an aborted migration "\
|
695
|
-
"or similar): table_name: #{table_name}, column_name: #{
|
695
|
+
"or similar): table_name: #{table_name}, column_name: #{column_names}")
|
696
696
|
end
|
697
697
|
end
|
698
698
|
|
@@ -701,9 +701,9 @@ module OnlineMigrations
|
|
701
701
|
# @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_foreign_key
|
702
702
|
#
|
703
703
|
def add_foreign_key(from_table, to_table, validate: true, **options)
|
704
|
-
if foreign_key_exists?(from_table, **options
|
705
|
-
message =
|
706
|
-
"(this can be due to an aborted migration or similar): from_table: #{from_table}, to_table: #{to_table}"
|
704
|
+
if foreign_key_exists?(from_table, to_table, **options)
|
705
|
+
message = "Foreign key was not created because it already exists " \
|
706
|
+
"(this can be due to an aborted migration or similar): from_table: #{from_table}, to_table: #{to_table}".dup
|
707
707
|
message << ", #{options.inspect}" if options.any?
|
708
708
|
|
709
709
|
Utils.say(message)
|
@@ -714,7 +714,7 @@ module OnlineMigrations
|
|
714
714
|
options[:primary_key] ||= "id"
|
715
715
|
options[:name] ||= __foreign_key_name(to_table, options[:column])
|
716
716
|
|
717
|
-
query =
|
717
|
+
query = <<-SQL.strip_heredoc.dup
|
718
718
|
ALTER TABLE #{from_table}
|
719
719
|
ADD CONSTRAINT #{options[:name]}
|
720
720
|
FOREIGN KEY (#{options[:column]})
|
@@ -853,7 +853,7 @@ module OnlineMigrations
|
|
853
853
|
# ActiveRecord methods
|
854
854
|
def __ensure_not_in_transaction!(method_name = caller[0])
|
855
855
|
if transaction_open?
|
856
|
-
raise
|
856
|
+
raise <<-MSG.strip_heredoc
|
857
857
|
`#{method_name}` cannot run inside a transaction block.
|
858
858
|
|
859
859
|
You can remove transaction block by calling `disable_ddl_transaction!` in the body of
|
@@ -865,7 +865,7 @@ module OnlineMigrations
|
|
865
865
|
def __column_not_nullable?(table_name, column_name)
|
866
866
|
schema = __schema_for_table(table_name)
|
867
867
|
|
868
|
-
query =
|
868
|
+
query = <<-SQL.strip_heredoc
|
869
869
|
SELECT is_nullable
|
870
870
|
FROM information_schema.columns
|
871
871
|
WHERE table_schema = #{schema}
|
@@ -895,7 +895,7 @@ module OnlineMigrations
|
|
895
895
|
end
|
896
896
|
|
897
897
|
def __index_column_names(column_names)
|
898
|
-
if column_names.is_a?(String) && /\W/.match
|
898
|
+
if column_names.is_a?(String) && /\W/.match(column_names)
|
899
899
|
column_names
|
900
900
|
else
|
901
901
|
Array(column_names)
|
@@ -904,7 +904,7 @@ module OnlineMigrations
|
|
904
904
|
|
905
905
|
def __index_valid?(index_name, schema:)
|
906
906
|
# ActiveRecord <= 4.2 returns a string, instead of automatically casting to boolean
|
907
|
-
valid = select_value
|
907
|
+
valid = select_value <<-SQL.strip_heredoc
|
908
908
|
SELECT indisvalid
|
909
909
|
FROM pg_index i
|
910
910
|
JOIN pg_class c
|
@@ -931,7 +931,7 @@ module OnlineMigrations
|
|
931
931
|
when :cascade then "ON #{action} CASCADE"
|
932
932
|
when :restrict then "ON #{action} RESTRICT"
|
933
933
|
else
|
934
|
-
raise ArgumentError,
|
934
|
+
raise ArgumentError, <<-MSG.strip_heredoc
|
935
935
|
'#{dependency}' is not supported for :on_update or :on_delete.
|
936
936
|
Supported values are: :nullify, :cascade, :restrict
|
937
937
|
MSG
|
@@ -981,7 +981,7 @@ module OnlineMigrations
|
|
981
981
|
schema = __schema_for_table(table_name)
|
982
982
|
contype = type == :check ? "c" : "f"
|
983
983
|
|
984
|
-
validated = select_value(
|
984
|
+
validated = select_value(<<-SQL.strip_heredoc)
|
985
985
|
SELECT convalidated
|
986
986
|
FROM pg_catalog.pg_constraint con
|
987
987
|
INNER JOIN pg_catalog.pg_namespace nsp
|
@@ -1018,7 +1018,7 @@ module OnlineMigrations
|
|
1018
1018
|
def __check_constraint_exists?(table_name, constraint_name)
|
1019
1019
|
schema = __schema_for_table(table_name)
|
1020
1020
|
|
1021
|
-
check_sql =
|
1021
|
+
check_sql = <<-SQL.strip_heredoc
|
1022
1022
|
SELECT COUNT(*)
|
1023
1023
|
FROM pg_catalog.pg_constraint con
|
1024
1024
|
INNER JOIN pg_catalog.pg_class cl
|
@@ -25,6 +25,10 @@ module OnlineMigrations
|
|
25
25
|
Kernel.warn("[online_migrations] #{message}")
|
26
26
|
end
|
27
27
|
|
28
|
+
def supports_multiple_dbs?
|
29
|
+
ar_version >= 6.0
|
30
|
+
end
|
31
|
+
|
28
32
|
def migration_parent
|
29
33
|
if ar_version <= 4.2
|
30
34
|
ActiveRecord::Migration
|
@@ -63,7 +67,7 @@ module OnlineMigrations
|
|
63
67
|
end
|
64
68
|
|
65
69
|
def to_bool(value)
|
66
|
-
value.to_s.match
|
70
|
+
!value.to_s.match(/^(true|t|yes|y|1|on)$/i).nil?
|
67
71
|
end
|
68
72
|
|
69
73
|
def foreign_table_name(ref_name, options)
|
@@ -89,7 +93,7 @@ module OnlineMigrations
|
|
89
93
|
def estimated_count(connection, table_name)
|
90
94
|
quoted_table = connection.quote(table_name)
|
91
95
|
|
92
|
-
count = connection.select_value(
|
96
|
+
count = connection.select_value(<<-SQL.strip_heredoc)
|
93
97
|
SELECT
|
94
98
|
(reltuples / COALESCE(NULLIF(relpages, 0), 1)) *
|
95
99
|
(pg_relation_size(#{quoted_table}) / (current_setting('block_size')::integer))
|
@@ -127,7 +131,7 @@ module OnlineMigrations
|
|
127
131
|
end
|
128
132
|
|
129
133
|
def volatile_function?(connection, function_name)
|
130
|
-
query =
|
134
|
+
query = <<-SQL.strip_heredoc
|
131
135
|
SELECT provolatile
|
132
136
|
FROM pg_catalog.pg_proc
|
133
137
|
WHERE proname = #{connection.quote(function_name)}
|
data/lib/online_migrations.rb
CHANGED
@@ -11,6 +11,7 @@ require "online_migrations/migrator"
|
|
11
11
|
require "online_migrations/database_tasks"
|
12
12
|
require "online_migrations/foreign_key_definition"
|
13
13
|
require "online_migrations/foreign_keys_collector"
|
14
|
+
require "online_migrations/index_definition"
|
14
15
|
require "online_migrations/indexes_collector"
|
15
16
|
require "online_migrations/command_checker"
|
16
17
|
require "online_migrations/schema_cache"
|
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.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- fatkodima
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2022-01-
|
11
|
+
date: 2022-01-31 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -80,6 +80,7 @@ files:
|
|
80
80
|
- lib/online_migrations/error_messages.rb
|
81
81
|
- lib/online_migrations/foreign_key_definition.rb
|
82
82
|
- lib/online_migrations/foreign_keys_collector.rb
|
83
|
+
- lib/online_migrations/index_definition.rb
|
83
84
|
- lib/online_migrations/indexes_collector.rb
|
84
85
|
- lib/online_migrations/lock_retrier.rb
|
85
86
|
- lib/online_migrations/migration.rb
|