online_migrations 0.1.0 → 0.2.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 +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
|