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.
- checksums.yaml +7 -0
- data/.github/workflows/test.yml +112 -0
- data/.gitignore +10 -0
- data/.rubocop.yml +113 -0
- data/.yardopts +1 -0
- data/BACKGROUND_MIGRATIONS.md +288 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +27 -0
- data/Gemfile.lock +108 -0
- data/LICENSE.txt +21 -0
- data/README.md +1067 -0
- data/Rakefile +23 -0
- data/gemfiles/activerecord_42.gemfile +6 -0
- data/gemfiles/activerecord_50.gemfile +5 -0
- data/gemfiles/activerecord_51.gemfile +5 -0
- data/gemfiles/activerecord_52.gemfile +5 -0
- data/gemfiles/activerecord_60.gemfile +5 -0
- data/gemfiles/activerecord_61.gemfile +5 -0
- data/gemfiles/activerecord_70.gemfile +5 -0
- data/gemfiles/activerecord_head.gemfile +5 -0
- data/lib/generators/online_migrations/background_migration_generator.rb +29 -0
- data/lib/generators/online_migrations/install_generator.rb +34 -0
- data/lib/generators/online_migrations/templates/background_migration.rb.tt +22 -0
- data/lib/generators/online_migrations/templates/initializer.rb.tt +94 -0
- data/lib/generators/online_migrations/templates/migration.rb.tt +46 -0
- data/lib/online_migrations/background_migration.rb +64 -0
- data/lib/online_migrations/background_migrations/advisory_lock.rb +62 -0
- data/lib/online_migrations/background_migrations/backfill_column.rb +52 -0
- data/lib/online_migrations/background_migrations/background_migration_class_validator.rb +36 -0
- data/lib/online_migrations/background_migrations/config.rb +98 -0
- data/lib/online_migrations/background_migrations/copy_column.rb +90 -0
- data/lib/online_migrations/background_migrations/migration.rb +210 -0
- data/lib/online_migrations/background_migrations/migration_helpers.rb +238 -0
- data/lib/online_migrations/background_migrations/migration_job.rb +92 -0
- data/lib/online_migrations/background_migrations/migration_job_runner.rb +63 -0
- data/lib/online_migrations/background_migrations/migration_job_status_validator.rb +27 -0
- data/lib/online_migrations/background_migrations/migration_runner.rb +97 -0
- data/lib/online_migrations/background_migrations/migration_status_validator.rb +45 -0
- data/lib/online_migrations/background_migrations/scheduler.rb +49 -0
- data/lib/online_migrations/batch_iterator.rb +87 -0
- data/lib/online_migrations/change_column_type_helpers.rb +587 -0
- data/lib/online_migrations/command_checker.rb +590 -0
- data/lib/online_migrations/command_recorder.rb +137 -0
- data/lib/online_migrations/config.rb +198 -0
- data/lib/online_migrations/copy_trigger.rb +91 -0
- data/lib/online_migrations/database_tasks.rb +19 -0
- data/lib/online_migrations/error_messages.rb +388 -0
- data/lib/online_migrations/foreign_key_definition.rb +17 -0
- data/lib/online_migrations/foreign_keys_collector.rb +33 -0
- data/lib/online_migrations/indexes_collector.rb +48 -0
- data/lib/online_migrations/lock_retrier.rb +250 -0
- data/lib/online_migrations/migration.rb +63 -0
- data/lib/online_migrations/migrator.rb +23 -0
- data/lib/online_migrations/schema_cache.rb +96 -0
- data/lib/online_migrations/schema_statements.rb +1042 -0
- data/lib/online_migrations/utils.rb +140 -0
- data/lib/online_migrations/version.rb +5 -0
- data/lib/online_migrations.rb +74 -0
- data/online_migrations.gemspec +28 -0
- 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
|
+
[](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).
|