online_migrations 0.1.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.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/test.yml +112 -0
  3. data/.gitignore +10 -0
  4. data/.rubocop.yml +113 -0
  5. data/.yardopts +1 -0
  6. data/BACKGROUND_MIGRATIONS.md +288 -0
  7. data/CHANGELOG.md +5 -0
  8. data/Gemfile +27 -0
  9. data/Gemfile.lock +108 -0
  10. data/LICENSE.txt +21 -0
  11. data/README.md +1067 -0
  12. data/Rakefile +23 -0
  13. data/gemfiles/activerecord_42.gemfile +6 -0
  14. data/gemfiles/activerecord_50.gemfile +5 -0
  15. data/gemfiles/activerecord_51.gemfile +5 -0
  16. data/gemfiles/activerecord_52.gemfile +5 -0
  17. data/gemfiles/activerecord_60.gemfile +5 -0
  18. data/gemfiles/activerecord_61.gemfile +5 -0
  19. data/gemfiles/activerecord_70.gemfile +5 -0
  20. data/gemfiles/activerecord_head.gemfile +5 -0
  21. data/lib/generators/online_migrations/background_migration_generator.rb +29 -0
  22. data/lib/generators/online_migrations/install_generator.rb +34 -0
  23. data/lib/generators/online_migrations/templates/background_migration.rb.tt +22 -0
  24. data/lib/generators/online_migrations/templates/initializer.rb.tt +94 -0
  25. data/lib/generators/online_migrations/templates/migration.rb.tt +46 -0
  26. data/lib/online_migrations/background_migration.rb +64 -0
  27. data/lib/online_migrations/background_migrations/advisory_lock.rb +62 -0
  28. data/lib/online_migrations/background_migrations/backfill_column.rb +52 -0
  29. data/lib/online_migrations/background_migrations/background_migration_class_validator.rb +36 -0
  30. data/lib/online_migrations/background_migrations/config.rb +98 -0
  31. data/lib/online_migrations/background_migrations/copy_column.rb +90 -0
  32. data/lib/online_migrations/background_migrations/migration.rb +210 -0
  33. data/lib/online_migrations/background_migrations/migration_helpers.rb +238 -0
  34. data/lib/online_migrations/background_migrations/migration_job.rb +92 -0
  35. data/lib/online_migrations/background_migrations/migration_job_runner.rb +63 -0
  36. data/lib/online_migrations/background_migrations/migration_job_status_validator.rb +27 -0
  37. data/lib/online_migrations/background_migrations/migration_runner.rb +97 -0
  38. data/lib/online_migrations/background_migrations/migration_status_validator.rb +45 -0
  39. data/lib/online_migrations/background_migrations/scheduler.rb +49 -0
  40. data/lib/online_migrations/batch_iterator.rb +87 -0
  41. data/lib/online_migrations/change_column_type_helpers.rb +587 -0
  42. data/lib/online_migrations/command_checker.rb +590 -0
  43. data/lib/online_migrations/command_recorder.rb +137 -0
  44. data/lib/online_migrations/config.rb +198 -0
  45. data/lib/online_migrations/copy_trigger.rb +91 -0
  46. data/lib/online_migrations/database_tasks.rb +19 -0
  47. data/lib/online_migrations/error_messages.rb +388 -0
  48. data/lib/online_migrations/foreign_key_definition.rb +17 -0
  49. data/lib/online_migrations/foreign_keys_collector.rb +33 -0
  50. data/lib/online_migrations/indexes_collector.rb +48 -0
  51. data/lib/online_migrations/lock_retrier.rb +250 -0
  52. data/lib/online_migrations/migration.rb +63 -0
  53. data/lib/online_migrations/migrator.rb +23 -0
  54. data/lib/online_migrations/schema_cache.rb +96 -0
  55. data/lib/online_migrations/schema_statements.rb +1042 -0
  56. data/lib/online_migrations/utils.rb +140 -0
  57. data/lib/online_migrations/version.rb +5 -0
  58. data/lib/online_migrations.rb +74 -0
  59. data/online_migrations.gemspec +28 -0
  60. metadata +119 -0
data/README.md ADDED
@@ -0,0 +1,1067 @@
1
+ # OnlineMigrations
2
+
3
+ Catch unsafe PostgreSQL migrations in development and run them easier in production.
4
+
5
+ :white_check_mark: Detects potentially dangerous operations\
6
+ :white_check_mark: Prevents them from running by default\
7
+ :white_check_mark: Provides instructions and helpers on safer ways to do what you want
8
+
9
+ **Note**: You probably don't need this gem for smaller projects, as operations that are unsafe at scale can be perfectly safe on smaller, low-traffic tables.
10
+
11
+ [![Build Status](https://github.com/fatkodima/online_migrations/actions/workflows/test.yml/badge.svg?branch=master)](https://github.com/fatkodima/online_migrations/actions/workflows/test.yml)
12
+
13
+ ## Requirements
14
+
15
+ - Ruby 2.1+
16
+ - Rails 4.2+
17
+ - PostgreSQL 9.6+
18
+
19
+ ## Installation
20
+
21
+ Add this line to your application's Gemfile:
22
+
23
+ ```ruby
24
+ gem 'online_migrations'
25
+ ```
26
+
27
+ And then run:
28
+
29
+ ```sh
30
+ $ bundle install
31
+ $ bin/rails generate online_migrations:install
32
+ ```
33
+
34
+ **Note**: If you do not have plans on using [background migrations](BACKGROUND_MIGRATIONS.md) feature, then you can delete the generated migration and regenerate it later, if needed.
35
+
36
+ ## Motivation
37
+
38
+ Writing a safe migration can be daunting. Numerous articles have been written on the topic and a few gems are trying to address the problem. Even for someone who has a pretty good command of PostgreSQL, remembering all the subtleties of explicit locking can be problematic.
39
+
40
+ **Online Migrations** was created to catch dangerous operations and provide a guidance and code helpers to run them safely.
41
+
42
+ An operation is classified as dangerous if it either:
43
+
44
+ - Blocks reads or writes for more than a few seconds (after a lock is acquired)
45
+ - Has a good chance of causing application errors
46
+
47
+ ## Example
48
+
49
+ Consider the following migration:
50
+
51
+ ```ruby
52
+ class AddAdminToUsers < ActiveRecord::Migration[7.0]
53
+ def change
54
+ add_column :users, :admin, :boolean, default: false, null: false
55
+ end
56
+ end
57
+ ```
58
+
59
+ If the `users` table is large, running this migration on a live PostgreSQL < 11 database will likely cause downtime.
60
+
61
+ A safer approach would be to run something like the following:
62
+
63
+ ```ruby
64
+ class AddAdminToUsers < ActiveRecord::Migration[7.0]
65
+ # Do not wrap the migration in a transaction so that locks are held for a shorter time.
66
+ disable_ddl_transaction!
67
+
68
+ def change
69
+ # Lower PostgreSQL's lock timeout to avoid statement queueing.
70
+ execute "SET lock_timeout TO '5s'" # The lock_timeout duration is customizable.
71
+
72
+ # Add the column without the default value and the not-null constraint.
73
+ add_column :users, :admin, :boolean
74
+
75
+ # Set the column's default value.
76
+ change_column_default :users, :admin, false
77
+
78
+ # Backfill the column in batches.
79
+ User.in_batches.update_all(admin: false)
80
+
81
+ # Add the not-null constraint. Beforehand, set a short statement timeout so that
82
+ # Postgres does not spend too much time performing the full table scan to verify
83
+ # the column contains no nulls.
84
+ execute "SET statement_timeout TO '5s'"
85
+ change_column_null :users, :admin, false
86
+ end
87
+ end
88
+ ```
89
+
90
+ When you actually run the original migration, you will get an error message:
91
+
92
+ ```txt
93
+ ⚠️ [online_migrations] Dangerous operation detected ⚠️
94
+
95
+ Adding a column with a non-null default blocks reads and writes while the entire table is rewritten.
96
+
97
+ A safer approach is to:
98
+ 1. add the column without a default value
99
+ 2. change the column default
100
+ 3. backfill existing rows with the new value
101
+ 4. add the NOT NULL constraint
102
+
103
+ add_column_with_default takes care of all this steps:
104
+
105
+ class AddAdminToUsers < ActiveRecord::Migration[7.0]
106
+ disable_ddl_transaction!
107
+
108
+ def change
109
+ add_column_with_default :users, :admin, :boolean, default: false, null: false
110
+ end
111
+ end
112
+ ```
113
+
114
+ It suggests how to safely implement a migration, which essentially runs the steps similar to described in the previous example.
115
+
116
+ ## Checks
117
+
118
+ Potentially dangerous operations:
119
+
120
+ - [removing a column](#removing-a-column)
121
+ - [adding a column with a default value](#adding-a-column-with-a-default-value)
122
+ - [backfilling data](#backfilling-data)
123
+ - [changing the type of a column](#changing-the-type-of-a-column)
124
+ - [renaming a column](#renaming-a-column)
125
+ - [renaming a table](#renaming-a-table)
126
+ - [creating a table with the force option](#creating-a-table-with-the-force-option)
127
+ - [adding a check constraint](#adding-a-check-constraint)
128
+ - [setting NOT NULL on an existing column](#setting-not-null-on-an-existing-column)
129
+ - [executing SQL directly](#executing-SQL-directly)
130
+ - [adding an index non-concurrently](#adding-an-index-non-concurrently)
131
+ - [removing an index non-concurrently](#removing-an-index-non-concurrently)
132
+ - [adding a reference](#adding-a-reference)
133
+ - [adding a foreign key](#adding-a-foreign-key)
134
+ - [adding a json column](#adding-a-json-column)
135
+ - [using primary key with short integer type](#using-primary-key-with-short-integer-type)
136
+ - [hash indexes](#hash-indexes)
137
+ - [adding multiple foreign keys](#adding-multiple-foreign-keys)
138
+
139
+ You can also add [custom checks](#custom-checks) or [disable specific checks](#disable-checks).
140
+
141
+ ### Removing a column
142
+
143
+ #### Bad
144
+
145
+ ActiveRecord caches database columns at runtime, so if you drop a column, it can cause exceptions until your app reboots.
146
+
147
+ ```ruby
148
+ class RemoveNameFromUsers < ActiveRecord::Migration[7.0]
149
+ def change
150
+ remove_column :users, :name
151
+ end
152
+ end
153
+ ```
154
+
155
+ #### Good
156
+
157
+ 1. Ignore the column:
158
+
159
+ ```ruby
160
+ class User < ApplicationRecord
161
+ self.ignored_columns = ["name"]
162
+ end
163
+ ```
164
+
165
+ 2. Deploy
166
+ 3. Wrap column removing in a `safety_assured` block:
167
+
168
+ ```ruby
169
+ class RemoveNameFromUsers < ActiveRecord::Migration[7.0]
170
+ def change
171
+ safety_assured { remove_column :users, :name }
172
+ end
173
+ end
174
+ ```
175
+
176
+ 4. Remove column ignoring from `User` model
177
+ 5. Deploy
178
+
179
+ ### Adding a column with a default value
180
+
181
+ #### Bad
182
+
183
+ 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
+
185
+ ```ruby
186
+ class AddAdminToUsers < ActiveRecord::Migration[7.0]
187
+ def change
188
+ add_column :users, :admin, :boolean, default: false
189
+ end
190
+ end
191
+ ```
192
+
193
+ 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
+
195
+ #### Good
196
+
197
+ A safer approach is to:
198
+
199
+ 1. add the column without a default value
200
+ 2. change the column default
201
+ 3. backfill existing rows with the new value
202
+
203
+ `add_column_with_default` helper takes care of all this steps:
204
+
205
+ ```ruby
206
+ class AddAdminToUsers < ActiveRecord::Migration[7.0]
207
+ disable_ddl_transaction!
208
+
209
+ def change
210
+ add_column_with_default :users, :admin, :boolean, default: false
211
+ end
212
+ end
213
+ ```
214
+
215
+ **Note**: If you forget `disable_ddl_transaction!`, the migration will fail.
216
+
217
+ ### Backfilling data
218
+
219
+ #### Bad
220
+
221
+ 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
+
223
+ ```ruby
224
+ class AddAdminToUsers < ActiveRecord::Migration[7.0]
225
+ def change
226
+ add_column :users, :admin, :boolean
227
+ User.update_all(admin: false)
228
+ end
229
+ end
230
+ ```
231
+
232
+ Also, running a single query to update data can cause issues for large tables.
233
+
234
+ #### Good
235
+
236
+ 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
+
238
+ ```ruby
239
+ class AddAdminToUsers < ActiveRecord::Migration[7.0]
240
+ def change
241
+ add_column :users, :admin, :boolean
242
+ end
243
+ end
244
+
245
+ class BackfillUsersAdminColumn < ActiveRecord::Migration[7.0]
246
+ disable_ddl_transaction!
247
+
248
+ def up
249
+ update_column_in_batches(:users, :admin, false, pause_ms: 10)
250
+ end
251
+ end
252
+ ```
253
+
254
+ **Note**: If you forget `disable_ddl_transaction!`, the migration will fail.
255
+ **Note**: You may consider [background migrations](#background-migrations) to run data changes on large tables.
256
+
257
+ ### Changing the type of a column
258
+
259
+ #### Bad
260
+
261
+ Changing the type of an existing column blocks reads and writes while the entire table is rewritten.
262
+
263
+ ```ruby
264
+ class ChangeFilesSizeType < ActiveRecord::Migration[7.0]
265
+ def change
266
+ change_column :files, :size, :bigint
267
+ end
268
+ end
269
+ ```
270
+
271
+ A few changes don't require a table rewrite (and are safe) in PostgreSQL:
272
+
273
+ - Increasing the length limit of a `varchar` column (or removing the limit)
274
+ - Changing a `varchar` column to a `text` column
275
+ - Changing a `text` column to a `varchar` column with no length limit
276
+ - Increasing the precision of a `decimal` or `numeric` column
277
+ - Making a `decimal` or `numeric` column unconstrained
278
+ - Changing between `timestamp` and `timestamptz` columns when session time zone is UTC in PostgreSQL 12+
279
+
280
+ #### Good
281
+
282
+ **Note**: The following steps can also be used to change the primary key's type (e.g., from `integer` to `bigint`).
283
+
284
+ A safer approach can be accomplished in several steps:
285
+
286
+ 1. Create a new column and keep column's data in sync:
287
+
288
+ ```ruby
289
+ class InitializeChangeFilesSizeType < ActiveRecord::Migration[7.0]
290
+ def change
291
+ initialize_column_type_change :files, :size, :bigint
292
+ end
293
+ end
294
+ ```
295
+
296
+ 2. Backfill data from the old column to the new column:
297
+
298
+ ```ruby
299
+ class BackfillChangeFilesSizeType < ActiveRecord::Migration[7.0]
300
+ disable_ddl_transaction!
301
+
302
+ def up
303
+ backfill_column_for_type_change :files, :size
304
+ end
305
+
306
+ def down
307
+ # no op
308
+ end
309
+ end
310
+ ```
311
+
312
+ 3. Copy indexes, foreign keys, check constraints, NOT NULL constraint, swap new column in place:
313
+
314
+ ```ruby
315
+ class FinalizeChangeFilesSizeType < ActiveRecord::Migration[7.0]
316
+ disable_ddl_transaction!
317
+
318
+ def change
319
+ finalize_column_type_change :files, :size
320
+ end
321
+ end
322
+ ```
323
+
324
+ 4. Deploy
325
+ 5. Finally, if everything is working as expected, remove copy trigger and old column:
326
+
327
+ ```ruby
328
+ class CleanupChangeFilesSizeType < ActiveRecord::Migration[7.0]
329
+ def up
330
+ cleanup_change_column_type_concurrently :files, :size
331
+ end
332
+
333
+ def down
334
+ initialize_column_type_change :files, :size, :integer
335
+ end
336
+ end
337
+ ```
338
+
339
+ 6. Deploy
340
+
341
+ ### Renaming a column
342
+
343
+ #### Bad
344
+
345
+ Renaming a column that's in use will cause errors in your application.
346
+
347
+ ```ruby
348
+ class RenameUsersNameToFirstName < ActiveRecord::Migration[7.0]
349
+ def change
350
+ rename_column :users, :name, :first_name
351
+ end
352
+ end
353
+ ```
354
+
355
+ #### Good
356
+
357
+ 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
+
359
+ The technique is built on top of database views, using the following steps:
360
+
361
+ 1. Rename the table to some temporary name
362
+ 2. Create a VIEW using the old table name with addition of a new column as an alias of the old one
363
+ 3. Add a workaround for ActiveRecord's schema cache
364
+
365
+ For the previous example, to rename `name` column to `first_name` of the `users` table, we can run:
366
+
367
+ ```sql
368
+ BEGIN;
369
+ ALTER TABLE users RENAME TO users_column_rename;
370
+ CREATE VIEW users AS SELECT *, first_name AS name FROM users;
371
+ COMMIT;
372
+ ```
373
+
374
+ As database views do not expose the underlying table schema (default values, not null constraints, indexes, etc), further steps are needed to update the application to use the new table name. ActiveRecord heavily relies on this data, for example, to initialize new models.
375
+
376
+ To work around this limitation, we need to tell ActiveRecord to acquire this information from original table using the new table name.
377
+
378
+ **Online Migrations** provides several helpers to implement column renaming:
379
+
380
+ 1. Instruct Rails that you are going to rename a column:
381
+
382
+ ```ruby
383
+ OnlineMigrations.config.column_renames = {
384
+ "users" => {
385
+ "name" => "first_name"
386
+ }
387
+ }
388
+ ```
389
+
390
+ 2. Deploy
391
+ 3. Create a VIEW with aliased column:
392
+
393
+ ```ruby
394
+ class InitializeRenameUsersNameToFirstName < ActiveRecord::Migration[7.0]
395
+ def change
396
+ initialize_column_rename :users, :name, :first_name
397
+ end
398
+ end
399
+ ```
400
+
401
+ 4. Replace usages of the old column with a new column in the codebase
402
+ 5. Deploy
403
+ 6. Remove the column rename config from step 1
404
+ 7. Remove the VIEW created in step 3:
405
+
406
+ ```ruby
407
+ class FinalizeRenameUsersNameToFirstName < ActiveRecord::Migration[7.0]
408
+ def change
409
+ finalize_column_rename :users, :name, :first_name
410
+ end
411
+ end
412
+ ```
413
+
414
+ 8. Deploy
415
+
416
+ ### Renaming a table
417
+
418
+ #### Bad
419
+
420
+ Renaming a table that's in use will cause errors in your application.
421
+
422
+ ```ruby
423
+ class RenameClientsToUsers < ActiveRecord::Migration[7.0]
424
+ def change
425
+ rename_table :clients, :users
426
+ end
427
+ end
428
+ ```
429
+
430
+ #### Good
431
+
432
+ 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
+
434
+ The technique is built on top of database views, using the following steps:
435
+
436
+ 1. Rename the database table
437
+ 2. Create a VIEW using the old table name by pointing to the new table name
438
+ 3. Add a workaround for ActiveRecord's schema cache
439
+
440
+ For the previous example, to rename `name` column to `first_name` of the `users` table, we can run:
441
+
442
+ ```sql
443
+ BEGIN;
444
+ ALTER TABLE clients RENAME TO users;
445
+ CREATE VIEW clients AS SELECT * FROM users;
446
+ COMMIT;
447
+ ```
448
+
449
+ As database views do not expose the underlying table schema (default values, not null constraints, indexes, etc), further steps are needed to update the application to use the new table name. ActiveRecord heavily relies on this data, for example, to initialize new models.
450
+
451
+ To work around this limitation, we need to tell ActiveRecord to acquire this information from original table using the new table name.
452
+
453
+ **Online Migrations** provides several helpers to implement table renaming:
454
+
455
+ 1. Instruct Rails that you are going to rename a table:
456
+
457
+ ```ruby
458
+ OnlineMigrations.config.table_renames = {
459
+ "clients" => "users"
460
+ }
461
+ ```
462
+
463
+ 2. Deploy
464
+ 3. Create a VIEW:
465
+
466
+ ```ruby
467
+ class InitializeRenameClientsToUsers < ActiveRecord::Migration[7.0]
468
+ def change
469
+ initialize_table_rename :clients, :users
470
+ end
471
+ end
472
+ ```
473
+
474
+ 4. Replace usages of the old table with a new table in the codebase
475
+ 5. Remove the table rename config from step 1
476
+ 6. Deploy
477
+ 7. Remove the VIEW created in step 3:
478
+
479
+ ```ruby
480
+ class FinalizeRenameClientsToUsers < ActiveRecord::Migration[7.0]
481
+ def change
482
+ finalize_table_rename :clients, :users
483
+ end
484
+ end
485
+ ```
486
+
487
+ 8. Deploy
488
+
489
+ ### Creating a table with the force option
490
+
491
+ #### Bad
492
+
493
+ The `force` option can drop an existing table.
494
+
495
+ ```ruby
496
+ class CreateUsers < ActiveRecord::Migration[7.0]
497
+ def change
498
+ create_table :users, force: true do |t|
499
+ # ...
500
+ end
501
+ end
502
+ end
503
+ ```
504
+
505
+ #### Good
506
+
507
+ Create tables without the `force` option.
508
+
509
+ ```ruby
510
+ class CreateUsers < ActiveRecord::Migration[7.0]
511
+ def change
512
+ create_table :users do |t|
513
+ # ...
514
+ end
515
+ end
516
+ end
517
+ ```
518
+
519
+ If you intend to drop an existing table, run `drop_table` first.
520
+
521
+ ### Adding a check constraint
522
+
523
+ #### Bad
524
+
525
+ Adding a check constraint blocks reads and writes while every row is checked.
526
+
527
+ ```ruby
528
+ class AddCheckConstraint < ActiveRecord::Migration[7.0]
529
+ def change
530
+ add_check_constraint :users, "char_length(name) >= 1", name: "name_check"
531
+ end
532
+ end
533
+ ```
534
+
535
+ #### Good
536
+
537
+ Add the check constraint without validating existing rows, and then validate them in a separate transaction:
538
+
539
+ ```ruby
540
+ class AddCheckConstraint < ActiveRecord::Migration[7.0]
541
+ disable_ddl_transaction!
542
+
543
+ def change
544
+ add_check_constraint :users, "char_length(name) >= 1", name: "name_check", validate: false
545
+ validate_check_constraint :users, name: "name_check"
546
+ end
547
+ end
548
+ ```
549
+
550
+ **Note**: If you forget `disable_ddl_transaction!`, the migration will fail.
551
+
552
+ ### Setting NOT NULL on an existing column
553
+
554
+ #### Bad
555
+
556
+ Setting `NOT NULL` on an existing column blocks reads and writes while every row is checked.
557
+
558
+ ```ruby
559
+ class ChangeUsersNameNull < ActiveRecord::Migration[7.0]
560
+ def change
561
+ change_column_null :users, :name, false
562
+ end
563
+ end
564
+ ```
565
+
566
+ #### Good
567
+
568
+ Instead, add a check constraint and validate it in a separate transaction:
569
+
570
+ ```ruby
571
+ class ChangeUsersNameNull < ActiveRecord::Migration[7.0]
572
+ disable_ddl_transaction!
573
+
574
+ def change
575
+ add_not_null_constraint :users, :name, name: "users_name_null", validate: false
576
+ validate_not_null_constraint :users, :name, name: "users_name_null"
577
+ end
578
+ end
579
+ ```
580
+
581
+ **Note**: If you forget `disable_ddl_transaction!`, the migration will fail.
582
+
583
+ A `NOT NULL` check constraint is functionally equivalent to setting `NOT NULL` on the column (but it won't show up in `schema.rb` in Rails < 6.1). In PostgreSQL 12+, once the check constraint is validated, you can safely set `NOT NULL` on the column and drop the check constraint.
584
+
585
+ ```ruby
586
+ class ChangeUsersNameNullDropCheck < ActiveRecord::Migration[7.0]
587
+ def change
588
+ # in PostgreSQL 12+, you can then safely set NOT NULL on the column
589
+ change_column_null :users, :name, false
590
+ remove_check_constraint :users, name: "users_name_null"
591
+ end
592
+ end
593
+ ```
594
+
595
+ ### Executing SQL directly
596
+
597
+ Online Migrations does not support inspecting what happens inside an `execute` call, so cannot help you here. Make really sure that what you're doing is safe before proceeding, then wrap it in a `safety_assured { ... }` block:
598
+
599
+ ```ruby
600
+ class ExecuteSQL < ActiveRecord::Migration[7.0]
601
+ def change
602
+ safety_assured { execute "..." }
603
+ end
604
+ end
605
+ ```
606
+
607
+ ### Adding an index non-concurrently
608
+
609
+ #### Bad
610
+
611
+ Adding an index non-concurrently blocks writes.
612
+
613
+ ```ruby
614
+ class AddIndexOnUsersEmail < ActiveRecord::Migration[7.0]
615
+ def change
616
+ add_index :users, :email, unique: true
617
+ end
618
+ end
619
+ ```
620
+
621
+ #### Good
622
+
623
+ Add indexes concurrently.
624
+
625
+ ```ruby
626
+ class AddIndexOnUsersEmail < ActiveRecord::Migration[7.0]
627
+ disable_ddl_transaction!
628
+
629
+ def change
630
+ add_index :users, :email, unique: true, algorithm: :concurrently
631
+ end
632
+ end
633
+ ```
634
+
635
+ **Note**: If you forget `disable_ddl_transaction!`, the migration will fail. Also, note that indexes on new tables (those created in the same migration) don't require this.
636
+
637
+ ### Removing an index non-concurrently
638
+
639
+ #### Bad
640
+
641
+ 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
+
643
+ ```ruby
644
+ class RemoveIndexOnUsersEmail < ActiveRecord::Migration[7.0]
645
+ def change
646
+ remove_index :users, :email
647
+ end
648
+ end
649
+ ```
650
+
651
+ #### Good
652
+
653
+ Remove indexes concurrently.
654
+
655
+ ```ruby
656
+ class RemoveIndexOnUsersEmail < ActiveRecord::Migration[7.0]
657
+ disable_ddl_transaction!
658
+
659
+ def change
660
+ remove_index :users, :email, algorithm: :concurrently
661
+ end
662
+ end
663
+ ```
664
+
665
+ **Note**: If you forget `disable_ddl_transaction!`, the migration will fail.
666
+
667
+ ### Adding a reference
668
+
669
+ #### Bad
670
+
671
+ 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
+
673
+ ```ruby
674
+ class AddUserToProjects < ActiveRecord::Migration[7.0]
675
+ def change
676
+ add_reference :projects, :user, foreign_key: true
677
+ end
678
+ end
679
+ ```
680
+
681
+ #### Good
682
+
683
+ Make sure the index is added concurrently and the foreign key is added in a separate migration.
684
+ Or you can use `add_reference_concurrently` helper. It will create a reference and take care of safely adding index and/or foreign key.
685
+
686
+ ```ruby
687
+ class AddUserToProjects < ActiveRecord::Migration[7.0]
688
+ disable_ddl_transaction!
689
+
690
+ def change
691
+ add_reference_concurrently :projects, :user
692
+ end
693
+ end
694
+ ```
695
+
696
+ **Note**: If you forget `disable_ddl_transaction!`, the migration will fail.
697
+
698
+ ### Adding a foreign key
699
+
700
+ #### Bad
701
+
702
+ Adding a foreign key blocks writes on both tables.
703
+
704
+ ```ruby
705
+ class AddForeignKeyToProjectsUser < ActiveRecord::Migration[7.0]
706
+ def change
707
+ add_foreign_key :projects, :users
708
+ end
709
+ end
710
+ ```
711
+
712
+ or
713
+
714
+ ```ruby
715
+ class AddReferenceToProjectsUser < ActiveRecord::Migration[7.0]
716
+ def change
717
+ add_reference :projects, :user, foreign_key: true
718
+ end
719
+ end
720
+ ```
721
+
722
+ #### Good
723
+
724
+ Add the foreign key without validating existing rows, and then validate them in a separate transaction.
725
+
726
+ ```ruby
727
+ class AddForeignKeyToProjectsUser < ActiveRecord::Migration[7.0]
728
+ disable_ddl_transaction!
729
+
730
+ def change
731
+ add_foreign_key :projects, :users, validate: false
732
+ validate_foreign_key :projects, :users
733
+ end
734
+ end
735
+ ```
736
+
737
+ **Note**: If you forget `disable_ddl_transaction!`, the migration will fail.
738
+
739
+ ### Adding a json column
740
+
741
+ #### Bad
742
+
743
+ There's no equality operator for the `json` column type, which can cause errors for existing `SELECT DISTINCT` queries in your application.
744
+
745
+ ```ruby
746
+ class AddSettingsToProjects < ActiveRecord::Migration[7.0]
747
+ def change
748
+ add_column :projects, :settings, :json
749
+ end
750
+ end
751
+ ```
752
+
753
+ #### Good
754
+
755
+ Use `jsonb` instead.
756
+
757
+ ```ruby
758
+ class AddSettingsToProjects < ActiveRecord::Migration[7.0]
759
+ def change
760
+ add_column :projects, :settings, :jsonb
761
+ end
762
+ end
763
+ ```
764
+
765
+ ### Using primary key with short integer type
766
+
767
+ #### Bad
768
+
769
+ 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
+
771
+ ```ruby
772
+ class CreateUsers < ActiveRecord::Migration[7.0]
773
+ def change
774
+ create_table :users, id: :integer do |t|
775
+ # ...
776
+ end
777
+ end
778
+ end
779
+ ```
780
+
781
+ #### Good
782
+
783
+ Use one of `bigint`, `bigserial`, `uuid` instead.
784
+
785
+ ```ruby
786
+ class CreateUsers < ActiveRecord::Migration[7.0]
787
+ def change
788
+ create_table :users, id: :bigint do |t| # bigint is the default for Active Record >= 5.1
789
+ # ...
790
+ end
791
+ end
792
+ end
793
+ ```
794
+
795
+ ### Hash indexes
796
+
797
+ #### Bad - PostgreSQL < 10
798
+
799
+ 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
+
801
+ ```ruby
802
+ class AddIndexToUsersOnEmail < ActiveRecord::Migration[7.0]
803
+ def change
804
+ add_index :users, :email, unique: true, using: :hash
805
+ end
806
+ end
807
+ ```
808
+
809
+ #### Good - PostgreSQL < 10
810
+
811
+ Use B-tree indexes instead.
812
+
813
+ ```ruby
814
+ class AddIndexToUsersOnEmail < ActiveRecord::Migration[7.0]
815
+ def change
816
+ add_index :users, :email, unique: true # B-tree by default
817
+ end
818
+ end
819
+ ```
820
+
821
+ ### Adding multiple foreign keys
822
+
823
+ #### Bad
824
+
825
+ Adding multiple foreign keys in a single migration blocks writes on all involved tables until migration is completed.
826
+ Avoid adding foreign key more than once per migration file, unless the source and target tables are identical.
827
+
828
+ ```ruby
829
+ class CreateUserProjects < ActiveRecord::Migration[7.0]
830
+ def change
831
+ create_table :user_projects do |t|
832
+ t.belongs_to :user, foreign_key: true
833
+ t.belongs_to :project, foreign_key: true
834
+ end
835
+ end
836
+ end
837
+ ```
838
+
839
+ #### Good
840
+
841
+ 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
+
843
+ ```ruby
844
+ class CreateUserProjects < ActiveRecord::Migration[7.0]
845
+ def change
846
+ create_table :user_projects do |t|
847
+ t.belongs_to :user, foreign_key: true
848
+ t.belongs_to :project, foreign_key: false
849
+ end
850
+ end
851
+ end
852
+
853
+ class AddForeignKeyFromUserProjectsToProject < ActiveRecord::Migration[7.0]
854
+ def change
855
+ add_foreign_key :user_projects, :projects
856
+ end
857
+ end
858
+ ```
859
+
860
+ ## Assuring Safety
861
+
862
+ 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.
863
+
864
+ ```ruby
865
+ class MySafeMigration < ActiveRecord::Migration[7.0]
866
+ def change
867
+ safety_assured { remove_column :users, :some_column }
868
+ end
869
+ end
870
+ ```
871
+
872
+ Certain methods like `execute` and `change_table` cannot be inspected and are prevented from running by default. Make sure what you're doing is really safe and use this pattern.
873
+
874
+ ## Configuring the gem
875
+
876
+ There are a few configurable options for the gem. Custom configurations should be placed in a `online_migrations.rb` initializer.
877
+
878
+ ```ruby
879
+ OnlineMigrations.configure do |config|
880
+ # ...
881
+ end
882
+ ```
883
+
884
+ **Note**: Check the [source code](https://github.com/fatkodima/online_migrations/blob/master/lib/online_migrations/config.rb) for the list of all available configuration options.
885
+
886
+ ### Custom checks
887
+
888
+ Add your own custom checks with:
889
+
890
+ ```ruby
891
+ # config/initializers/online_migrations.rb
892
+
893
+ config.add_check do |method, args|
894
+ if method == :add_column && args[0].to_s == "users"
895
+ stop!("No more columns on the users table")
896
+ end
897
+ end
898
+ ```
899
+
900
+ Use the `stop!` method to stop migrations.
901
+
902
+ **Note**: Since `remove_column`, `execute` and `change_table` always require a `safety_assured` block, it's not possible to add a custom check for these operations.
903
+
904
+ ### Disable Checks
905
+
906
+ Disable specific checks with:
907
+
908
+ ```ruby
909
+ # config/initializers/online_migrations.rb
910
+
911
+ config.disable_check(:remove_index)
912
+ ```
913
+
914
+ Check the [source code](https://github.com/fatkodima/online_migrations/blob/master/lib/online_migrations/error_messages.rb) for the list of keys.
915
+
916
+ ### Down Migrations / Rollbacks
917
+
918
+ By default, checks are disabled when migrating down. Enable them with:
919
+
920
+ ```ruby
921
+ # config/initializers/online_migrations.rb
922
+
923
+ config.check_down = true
924
+ ```
925
+
926
+ ### Custom Messages
927
+
928
+ You can customize specific error messages:
929
+
930
+ ```ruby
931
+ # config/initializers/online_migrations.rb
932
+
933
+ config.error_messages[:add_column_default] = "Your custom instructions"
934
+ ```
935
+
936
+ Check the [source code](https://github.com/fatkodima/online_migrations/blob/master/lib/online_migrations/error_messages.rb) for the list of keys.
937
+
938
+ ### Migration Timeouts
939
+
940
+ It’s extremely important to set a short lock timeout for migrations. This way, if a migration can't acquire a lock in a timely manner, other statements won't be stuck behind it.
941
+
942
+ Add timeouts to `config/database.yml`:
943
+
944
+ ```yml
945
+ production:
946
+ connect_timeout: 5
947
+ variables:
948
+ lock_timeout: 10s
949
+ statement_timeout: 15s
950
+ ```
951
+
952
+ Or set the timeouts directly on the database user that runs migrations:
953
+
954
+ ```sql
955
+ ALTER ROLE myuser SET lock_timeout = '10s';
956
+ ALTER ROLE myuser SET statement_timeout = '15s';
957
+ ```
958
+
959
+ ### Lock Timeout Retries
960
+
961
+ You can configure this gem to automatically retry statements that exceed the lock timeout:
962
+
963
+ ```ruby
964
+ # config/initializers/online_migrations.rb
965
+
966
+ config.lock_retrier = OnlineMigrations::ExponentialLockRetrier.new(
967
+ attempts: 30, # attempt 30 retries
968
+ base_delay: 0.01.seconds, # starting with delay of 10ms between each unsuccessful try, increasing exponentially
969
+ max_delay: 1.minute, # maximum delay is 1 minute
970
+ lock_timeout: 0.05.seconds # and 50ms set as lock timeout for each try
971
+ )
972
+ ```
973
+
974
+ When statement within transaction fails - the whole transaction is retried.
975
+
976
+ To permanently disable lock retries, you can set `lock_retrier` to `nil`.
977
+ To temporarily disable lock retries while running migrations, set `DISABLE_LOCK_RETRIES` env variable.
978
+
979
+ **Note**: Statements are retried by default, unless lock retries are disabled. It is possible to implement more sophisticated lock retriers. See [source code](https://github.com/fatkodima/online_migrations/blob/master/lib/online_migrations/lock_retrier.rb) for the examples.
980
+
981
+ ### Existing Migrations
982
+
983
+ To mark migrations as safe that were created before installing this gem, configure the migration version starting after which checks are performed:
984
+
985
+ ```ruby
986
+ # config/initializers/online_migrations.rb
987
+
988
+ config.start_after = 20220101000000
989
+ ```
990
+
991
+ Use the version from your latest migration.
992
+
993
+ ### Target Version
994
+
995
+ If your development database version is different from production, you can specify the production version so the right checks run in development.
996
+
997
+ ```ruby
998
+ # config/initializers/online_migrations.rb
999
+
1000
+ config.target_version = 10 # or "12.9" etc
1001
+ ```
1002
+
1003
+ For safety, this option only affects development and test environments. In other environments, the actual server version is always used.
1004
+
1005
+ ### Small Tables
1006
+
1007
+ Most projects have tables that are known to be small in size. These are usually "settings", "prices", "plans" etc. It is considered safe to perform most of the dangerous operations on them, like adding indexes, columns etc.
1008
+
1009
+ To mark tables as small:
1010
+
1011
+ ```ruby
1012
+ config.small_tables = [:settings, :prices]
1013
+ ```
1014
+
1015
+ ## Background Migrations
1016
+
1017
+ Read [BACKGROUND_MIGRATIONS.md](BACKGROUND_MIGRATIONS.md) on how to perform data migrations on large tables.
1018
+
1019
+ ## Credits
1020
+
1021
+ Thanks to [strong_migrations gem](https://github.com/ankane/strong_migrations), [GitLab](https://gitlab.com/gitlab-org/gitlab) and [maintenance_tasks gem](https://github.com/Shopify/maintenance_tasks) for the original ideas.
1022
+
1023
+ ## Contributing
1024
+
1025
+ Bug reports and pull requests are welcome on GitHub at https://github.com/fatkodima/online_migrations.
1026
+
1027
+ ## Development
1028
+
1029
+ After checking out the repo, run `bundle install` to install dependencies. Run `createdb online_migrations_test` to create a test database. Then, run `bundle exec rake test` to run the tests. This project uses multiple Gemfiles to test against multiple versions of ActiveRecord; you can run the tests against the specific version with `BUNDLE_GEMFILE=gemfiles/activerecord_61.gemfile bundle exec rake test`.
1030
+
1031
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
1032
+
1033
+ ## Additional resources
1034
+
1035
+ Alternatives:
1036
+
1037
+ - https://github.com/ankane/strong_migrations
1038
+ - https://github.com/LendingHome/zero_downtime_migrations
1039
+ - https://github.com/braintree/pg_ha_migrations
1040
+ - https://github.com/doctolib/safe-pg-migrations
1041
+
1042
+ Interesting reads:
1043
+
1044
+ - [Explicit Locking](https://www.postgresql.org/docs/current/explicit-locking.html)
1045
+ - [When Postgres blocks: 7 tips for dealing with locks](https://www.citusdata.com/blog/2018/02/22/seven-tips-for-dealing-with-postgres-locks/)
1046
+ - [PostgreSQL rocks, except when it blocks: Understanding locks](https://www.citusdata.com/blog/2018/02/15/when-postgresql-blocks/)
1047
+ - [PostgreSQL at Scale: Database Schema Changes Without Downtime](https://medium.com/paypal-tech/postgresql-at-scale-database-schema-changes-without-downtime-20d3749ed680)
1048
+ - [Adding a NOT NULL CONSTRAINT on PG Faster with Minimal Locking](https://medium.com/doctolib-engineering/adding-a-not-null-constraint-on-pg-faster-with-minimal-locking-38b2c00c4d1c)
1049
+ - [Adding columns with default values to really large tables in Postgres + Rails](https://wework.github.io/data/2015/11/05/add-columns-with-default-values-to-large-tables-in-rails-postgres/)
1050
+ - [Safe Operations For High Volume PostgreSQL](https://www.braintreepayments.com/blog/safe-operations-for-high-volume-postgresql/)
1051
+ - [Stop worrying about PostgreSQL locks in your Rails migrations](https://medium.com/doctolib/stop-worrying-about-postgresql-locks-in-your-rails-migrations-3426027e9cc9)
1052
+ - [Avoiding integer overflows with zero downtime](https://buildkite.com/blog/avoiding-integer-overflows-with-zero-downtime)
1053
+
1054
+ ## Maybe TODO
1055
+
1056
+ - support MySQL
1057
+ - support other ORMs
1058
+
1059
+ Background migrations:
1060
+
1061
+ - extract as a separate gem
1062
+ - add UI
1063
+ - support batching over non-integer and multiple columns
1064
+
1065
+ ## License
1066
+
1067
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).