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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 10e5140728aecd56d8f79b52eda79f3244c50212a731db898f3fe36673082bdd
4
- data.tar.gz: fa086fff375514e8779ef1d143af68037013373f1954ee4af2596ac3090f1ce9
3
+ metadata.gz: 3bac7f903ca89cda8d82ee421cae4c1f351a973c88c565f4cecd92ec98b7533c
4
+ data.tar.gz: e2e2fe22a1d1c70cb753a16fde3ce67890a73f17a058b72fa9383a460837926d
5
5
  SHA512:
6
- metadata.gz: c18e7f727443671759894c0320c050fbb789ce4015e49eeea751c07f9ec7232e4746c62554493e2118a08b2cc97a519c3645882d3a20c1091ed853a607039d54
7
- data.tar.gz: e44ccdb25d5ea1da727e4c6828a55f7426322262388a7a1eb5f022bbd976116550dfbcd04efda289bc7340a5662343d0a283cb4f6fdd756aa7d07313e125dba5
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
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- online_migrations (0.1.0)
4
+ online_migrations (0.2.0)
5
5
  activerecord (>= 4.2)
6
6
 
7
7
  GEM
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
- #### Bad
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
- #### Good
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
- #### Bad
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
- #### Good
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
- #### Bad
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
- #### Good
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
- #### Bad
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
- #### Good
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
- #### Bad
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
- #### Good
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
- #### Bad
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
- #### Good
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
- #### Bad
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
- #### Good
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
- #### Bad
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
- #### Good
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
- #### Bad
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
- #### Good
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
- #### Bad
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
- #### Good
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
- #### Bad
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
- #### Good
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
- #### Bad
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
- #### Good
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
- #### Bad
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
- #### Good
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
- #### Bad
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
- #### Good
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
- #### Bad
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
- #### Good
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
- #### Bad - PostgreSQL < 10
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
- #### Good - PostgreSQL < 10
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
- #### Bad
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
- #### Good
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(<<~SQL)
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
- if updates.size == 1 && (column, value = updates.first) && !value.nil?
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(<<~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 (previous_status, new_status = record.status_change)
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
- if (min_value, max_value = migration.next_batch_range)
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 (previous_status, new_status = record.status_change)
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(<<~SQL)
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 = <<~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(<<~SQL)
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(<<~SQL)
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
- raise_error :create_table if options[:force]
126
- raise_error :short_primary_key_type if short_primary_key_type?(options)
126
+ table_name = options[:table_name] || derive_join_table_name(table1, table2)
127
+ create_table(table_name, **options, &block)
128
+ end
127
129
 
128
- if block
129
- collect_foreign_keys(&block)
130
- check_for_hash_indexes(&block) if postgresql_version < Gem::Version.new("10")
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
- @new_tables << table_name.to_s
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.to_s == "json"
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, true)
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 = [:created_at, :updated_at]
297
+ columns = ["created_at", "updated_at"]
287
298
  else
288
299
  table_name, reference = args
289
- columns = [:"#{reference}_id"]
290
- columns << :"#{reference}_type" if options[:polymorphic]
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.map(&:to_s)).any?
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
- elsif options[:algorithm] != :concurrently && !new_or_small_table?(table_name)
355
- raise_error :add_index,
356
- command: command_str(:add_index, table_name, column_name, **options.merge(algorithm: :concurrently))
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 = <<~SQL
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 = <<~SQL
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
- # The migration version starting from which checks are performed
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
- attr_accessor :start_after
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
- # The database version against which the checks will be performed
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 Set specific target version
44
+ # @example
19
45
  # OnlineMigrations.config.target_version = 10
20
46
  #
21
- attr_accessor :target_version
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(<<~SQL)
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(<<~SQL)
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(<<~SQL)
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
- <%= remove_constraint_code %>
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, column_name, **options)
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: #{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.merge(to_table: to_table))
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}"
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 = +<<~SQL
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 <<~MSG
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 = <<~SQL
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?(column_names)
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 <<~SQL
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, <<~MSG
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(<<~SQL)
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 = <<~SQL.squish
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?(/^(true|t|yes|y|1|on)$/i)
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(<<~SQL)
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 = <<~SQL
134
+ query = <<-SQL.strip_heredoc
131
135
  SELECT provolatile
132
136
  FROM pg_catalog.pg_proc
133
137
  WHERE proname = #{connection.quote(function_name)}
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OnlineMigrations
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -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.1.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-16 00:00:00.000000000 Z
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