strong_migrations 0.7.8 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -0
- data/LICENSE.txt +1 -1
- data/README.md +139 -142
- data/lib/strong_migrations/adapters/abstract_adapter.rb +61 -0
- data/lib/strong_migrations/adapters/mariadb_adapter.rb +29 -0
- data/lib/strong_migrations/adapters/mysql_adapter.rb +87 -0
- data/lib/strong_migrations/adapters/postgresql_adapter.rb +221 -0
- data/lib/strong_migrations/checker.rb +111 -516
- data/lib/strong_migrations/checks.rb +402 -0
- data/lib/strong_migrations/database_tasks.rb +2 -1
- data/lib/strong_migrations/error_messages.rb +222 -0
- data/lib/strong_migrations/migration.rb +7 -2
- data/lib/strong_migrations/migrator.rb +19 -0
- data/lib/strong_migrations/safe_methods.rb +5 -16
- data/lib/strong_migrations/version.rb +1 -1
- data/lib/strong_migrations.rb +16 -218
- metadata +14 -77
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a22e260ec0e3e09954c65535725a3ddc4439b9cf94182a21cf5963e12d8485fe
|
4
|
+
data.tar.gz: 865633da561d77e615df16a9a00f8524a95bf73edc5590e3932855aaf544d4f7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f9b4df7a67aae2c8e1f7c58327b0e7e298261a33d9810dff4b5634bee3e000f1b26e64ad7b3997c8fc6c1cfef3a5cbeabe35ecbe508ed902679e3c9a720ecfb8
|
7
|
+
data.tar.gz: f4a7ecb6e64c40d1be1025a978387b94b9c2e47897cc25568c32dc32043292bc8fe14c9e6471108a4d84a1b1704459707f4fcf8a25227b7a86d8bcf2237dc19d
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,41 @@
|
|
1
|
+
## 1.0.0 (2022-03-21)
|
2
|
+
|
3
|
+
New safe operations with MySQL and MariaDB
|
4
|
+
|
5
|
+
- Setting `NOT NULL` on an existing column with strict mode enabled
|
6
|
+
|
7
|
+
New safe operations with Postgres
|
8
|
+
|
9
|
+
- Changing between `text` and `citext` when not indexed
|
10
|
+
- Changing a `string` column to a `citext` column when not indexed
|
11
|
+
- Changing a `citext` column to a `string` column with no `:limit` when not indexed
|
12
|
+
- Changing a `cidr` column to an `inet` column
|
13
|
+
- Increasing `:precision` of an `interval` or `time` column
|
14
|
+
|
15
|
+
New unsafe operations with Postgres
|
16
|
+
|
17
|
+
- Adding a column with a callable default value
|
18
|
+
- Decreasing `:precision` of a `datetime` column
|
19
|
+
- Decreasing `:limit` of a `timestamptz` column
|
20
|
+
- Passing a default value to `change_column_null`
|
21
|
+
|
22
|
+
Other
|
23
|
+
|
24
|
+
- Added experimental support for lock timeout retries
|
25
|
+
- Added `target_sql_mode` option
|
26
|
+
- Added error for `change_column_null` with default value with `safe_by_default` option
|
27
|
+
- Fixed instructions for `remove_columns` with options
|
28
|
+
- Dropped support for Postgres < 10, MySQL < 5.7, and MariaDB < 10.2
|
29
|
+
|
30
|
+
## 0.8.0 (2022-02-09)
|
31
|
+
|
32
|
+
- Fixed error with versioned schema with Active Record 7.0.2+
|
33
|
+
- Dropped support for Ruby < 2.6 and Active Record < 5.2
|
34
|
+
|
35
|
+
## 0.7.9 (2021-12-15)
|
36
|
+
|
37
|
+
- Fixed error with multiple databases with Active Record 7
|
38
|
+
|
1
39
|
## 0.7.8 (2021-08-03)
|
2
40
|
|
3
41
|
- Fixed issue with `add_reference ..., foreign_key: {to_table: ...}` with `safe_by_default`
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -4,7 +4,7 @@ Catch unsafe migrations in development
|
|
4
4
|
|
5
5
|
✓ Detects potentially dangerous operations<br /> ✓ Prevents them from running by default<br /> ✓ Provides instructions on safer ways to do what you want
|
6
6
|
|
7
|
-
Supports
|
7
|
+
Supports PostgreSQL, MySQL, and MariaDB
|
8
8
|
|
9
9
|
:tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)
|
10
10
|
|
@@ -15,7 +15,7 @@ Supports for PostgreSQL, MySQL, and MariaDB
|
|
15
15
|
Add this line to your application’s Gemfile:
|
16
16
|
|
17
17
|
```ruby
|
18
|
-
gem
|
18
|
+
gem "strong_migrations"
|
19
19
|
```
|
20
20
|
|
21
21
|
And run:
|
@@ -43,7 +43,7 @@ end
|
|
43
43
|
|
44
44
|
Deploy the code, then wrap this step in a safety_assured { ... } block.
|
45
45
|
|
46
|
-
class RemoveColumn < ActiveRecord::Migration[
|
46
|
+
class RemoveColumn < ActiveRecord::Migration[7.0]
|
47
47
|
def change
|
48
48
|
safety_assured { remove_column :users, :name }
|
49
49
|
end
|
@@ -67,7 +67,6 @@ Potentially dangerous operations:
|
|
67
67
|
- [renaming a table](#renaming-a-table)
|
68
68
|
- [creating a table with the force option](#creating-a-table-with-the-force-option)
|
69
69
|
- [adding a check constraint](#adding-a-check-constraint)
|
70
|
-
- [setting NOT NULL on an existing column](#setting-not-null-on-an-existing-column)
|
71
70
|
- [executing SQL directly](#executing-SQL-directly)
|
72
71
|
|
73
72
|
Postgres-specific checks:
|
@@ -76,6 +75,7 @@ Postgres-specific checks:
|
|
76
75
|
- [adding a reference](#adding-a-reference)
|
77
76
|
- [adding a foreign key](#adding-a-foreign-key)
|
78
77
|
- [adding a json column](#adding-a-json-column)
|
78
|
+
- [setting NOT NULL on an existing column](#setting-not-null-on-an-existing-column)
|
79
79
|
|
80
80
|
Best practices:
|
81
81
|
|
@@ -90,7 +90,7 @@ You can also add [custom checks](#custom-checks) or [disable specific checks](#d
|
|
90
90
|
Active Record caches database columns at runtime, so if you drop a column, it can cause exceptions until your app reboots.
|
91
91
|
|
92
92
|
```ruby
|
93
|
-
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[
|
93
|
+
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[7.0]
|
94
94
|
def change
|
95
95
|
remove_column :users, :some_column
|
96
96
|
end
|
@@ -111,7 +111,7 @@ end
|
|
111
111
|
3. Write a migration to remove the column (wrap in `safety_assured` block)
|
112
112
|
|
113
113
|
```ruby
|
114
|
-
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[
|
114
|
+
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[7.0]
|
115
115
|
def change
|
116
116
|
safety_assured { remove_column :users, :some_column }
|
117
117
|
end
|
@@ -119,6 +119,7 @@ end
|
|
119
119
|
```
|
120
120
|
|
121
121
|
4. Deploy and run migration
|
122
|
+
5. Remove the line added in step 1
|
122
123
|
|
123
124
|
### Adding a column with a default value
|
124
125
|
|
@@ -127,7 +128,7 @@ end
|
|
127
128
|
In earlier versions of Postgres, MySQL, and MariaDB, adding a column with a default value to an existing table causes the entire table to be rewritten. During this time, reads and writes are blocked in Postgres, and writes are blocked in MySQL and MariaDB.
|
128
129
|
|
129
130
|
```ruby
|
130
|
-
class AddSomeColumnToUsers < ActiveRecord::Migration[
|
131
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[7.0]
|
131
132
|
def change
|
132
133
|
add_column :users, :some_column, :text, default: "default_value"
|
133
134
|
end
|
@@ -141,7 +142,7 @@ In Postgres 11+, MySQL 8.0.12+, and MariaDB 10.3.2+, this no longer requires a t
|
|
141
142
|
Instead, add the column without a default value, then change the default.
|
142
143
|
|
143
144
|
```ruby
|
144
|
-
class AddSomeColumnToUsers < ActiveRecord::Migration[
|
145
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[7.0]
|
145
146
|
def up
|
146
147
|
add_column :users, :some_column, :text
|
147
148
|
change_column_default :users, :some_column, "default_value"
|
@@ -162,7 +163,7 @@ See the next section for how to backfill.
|
|
162
163
|
Active Record creates a transaction around each migration, 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/).
|
163
164
|
|
164
165
|
```ruby
|
165
|
-
class AddSomeColumnToUsers < ActiveRecord::Migration[
|
166
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[7.0]
|
166
167
|
def change
|
167
168
|
add_column :users, :some_column, :text
|
168
169
|
User.update_all some_column: "default_value"
|
@@ -177,7 +178,7 @@ Also, running a single query to update data can cause issues for large tables.
|
|
177
178
|
There are three keys to backfilling safely: batching, throttling, and running it outside a transaction. Use the Rails console or a separate migration with `disable_ddl_transaction!`.
|
178
179
|
|
179
180
|
```ruby
|
180
|
-
class BackfillSomeColumn < ActiveRecord::Migration[
|
181
|
+
class BackfillSomeColumn < ActiveRecord::Migration[7.0]
|
181
182
|
disable_ddl_transaction!
|
182
183
|
|
183
184
|
def up
|
@@ -196,26 +197,33 @@ end
|
|
196
197
|
Changing the type of a column causes the entire table to be rewritten. During this time, reads and writes are blocked in Postgres, and writes are blocked in MySQL and MariaDB.
|
197
198
|
|
198
199
|
```ruby
|
199
|
-
class ChangeSomeColumnType < ActiveRecord::Migration[
|
200
|
+
class ChangeSomeColumnType < ActiveRecord::Migration[7.0]
|
200
201
|
def change
|
201
202
|
change_column :users, :some_column, :new_type
|
202
203
|
end
|
203
204
|
end
|
204
205
|
```
|
205
206
|
|
206
|
-
|
207
|
+
Some changes don’t require a table rewrite and are safe in Postgres:
|
207
208
|
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
209
|
+
Type | Safe Changes
|
210
|
+
--- | ---
|
211
|
+
`cidr` | Changing to `inet`
|
212
|
+
`citext` | Changing to `text` if not indexed, changing to `string` with no `:limit` if not indexed
|
213
|
+
`datetime` | Increasing or removing `:precision`, changing to `timestamptz` when session time zone is UTC in Postgres 12+
|
214
|
+
`decimal` | Increasing `:precision` at same `:scale`, removing `:precision` and `:scale`
|
215
|
+
`interval` | Increasing or removing `:precision`
|
216
|
+
`numeric` | Increasing `:precision` at same `:scale`, removing `:precision` and `:scale`
|
217
|
+
`string` | Increasing or removing `:limit`, changing to `text`, changing `citext` if not indexed
|
218
|
+
`text` | Changing to `string` with no `:limit`, changing to `citext` if not indexed
|
219
|
+
`time` | Increasing or removing `:precision`
|
220
|
+
`timestamptz` | Increasing or removing `:limit`, changing to `datetime` when session time zone is UTC in Postgres 12+
|
214
221
|
|
215
|
-
And
|
222
|
+
And some in MySQL and MariaDB:
|
216
223
|
|
217
|
-
|
218
|
-
|
224
|
+
Type | Safe Changes
|
225
|
+
--- | ---
|
226
|
+
`string` | Increasing `:limit` from under 255 up to 255, increasing `:limit` from over 255 to the max
|
219
227
|
|
220
228
|
#### Good
|
221
229
|
|
@@ -235,7 +243,7 @@ A safer approach is to:
|
|
235
243
|
Renaming a column that’s in use will cause errors in your application.
|
236
244
|
|
237
245
|
```ruby
|
238
|
-
class RenameSomeColumn < ActiveRecord::Migration[
|
246
|
+
class RenameSomeColumn < ActiveRecord::Migration[7.0]
|
239
247
|
def change
|
240
248
|
rename_column :users, :some_column, :new_name
|
241
249
|
end
|
@@ -260,7 +268,7 @@ A safer approach is to:
|
|
260
268
|
Renaming a table that’s in use will cause errors in your application.
|
261
269
|
|
262
270
|
```ruby
|
263
|
-
class RenameUsersToCustomers < ActiveRecord::Migration[
|
271
|
+
class RenameUsersToCustomers < ActiveRecord::Migration[7.0]
|
264
272
|
def change
|
265
273
|
rename_table :users, :customers
|
266
274
|
end
|
@@ -285,7 +293,7 @@ A safer approach is to:
|
|
285
293
|
The `force` option can drop an existing table.
|
286
294
|
|
287
295
|
```ruby
|
288
|
-
class CreateUsers < ActiveRecord::Migration[
|
296
|
+
class CreateUsers < ActiveRecord::Migration[7.0]
|
289
297
|
def change
|
290
298
|
create_table :users, force: true do |t|
|
291
299
|
# ...
|
@@ -299,7 +307,7 @@ end
|
|
299
307
|
Create tables without the `force` option.
|
300
308
|
|
301
309
|
```ruby
|
302
|
-
class CreateUsers < ActiveRecord::Migration[
|
310
|
+
class CreateUsers < ActiveRecord::Migration[7.0]
|
303
311
|
def change
|
304
312
|
create_table :users do |t|
|
305
313
|
# ...
|
@@ -319,7 +327,7 @@ If you intend to drop an existing table, run `drop_table` first.
|
|
319
327
|
Adding a check constraint blocks reads and writes in Postgres and blocks writes in MySQL and MariaDB while every row is checked.
|
320
328
|
|
321
329
|
```ruby
|
322
|
-
class AddCheckConstraint < ActiveRecord::Migration[
|
330
|
+
class AddCheckConstraint < ActiveRecord::Migration[7.0]
|
323
331
|
def change
|
324
332
|
add_check_constraint :users, "price > 0", name: "price_check"
|
325
333
|
end
|
@@ -331,7 +339,7 @@ end
|
|
331
339
|
Add the check constraint without validating existing rows:
|
332
340
|
|
333
341
|
```ruby
|
334
|
-
class AddCheckConstraint < ActiveRecord::Migration[
|
342
|
+
class AddCheckConstraint < ActiveRecord::Migration[7.0]
|
335
343
|
def change
|
336
344
|
add_check_constraint :users, "price > 0", name: "price_check", validate: false
|
337
345
|
end
|
@@ -341,7 +349,7 @@ end
|
|
341
349
|
Then validate them in a separate migration.
|
342
350
|
|
343
351
|
```ruby
|
344
|
-
class ValidateCheckConstraint < ActiveRecord::Migration[
|
352
|
+
class ValidateCheckConstraint < ActiveRecord::Migration[7.0]
|
345
353
|
def change
|
346
354
|
validate_check_constraint :users, name: "price_check"
|
347
355
|
end
|
@@ -352,92 +360,12 @@ end
|
|
352
360
|
|
353
361
|
[Let us know](https://github.com/ankane/strong_migrations/issues/new) if you have a safe way to do this (check constraints can be added with `NOT ENFORCED`, but enforcing blocks writes).
|
354
362
|
|
355
|
-
### Setting NOT NULL on an existing column
|
356
|
-
|
357
|
-
:turtle: Safe by default available
|
358
|
-
|
359
|
-
#### Bad
|
360
|
-
|
361
|
-
Setting `NOT NULL` on an existing column blocks reads and writes while every row is checked.
|
362
|
-
|
363
|
-
```ruby
|
364
|
-
class SetSomeColumnNotNull < ActiveRecord::Migration[6.1]
|
365
|
-
def change
|
366
|
-
change_column_null :users, :some_column, false
|
367
|
-
end
|
368
|
-
end
|
369
|
-
```
|
370
|
-
|
371
|
-
#### Good - Postgres
|
372
|
-
|
373
|
-
Instead, add a check constraint.
|
374
|
-
|
375
|
-
For Rails 6.1, use:
|
376
|
-
|
377
|
-
```ruby
|
378
|
-
class SetSomeColumnNotNull < ActiveRecord::Migration[6.1]
|
379
|
-
def change
|
380
|
-
add_check_constraint :users, "some_column IS NOT NULL", name: "users_some_column_null", validate: false
|
381
|
-
end
|
382
|
-
end
|
383
|
-
```
|
384
|
-
|
385
|
-
For Rails < 6.1, use:
|
386
|
-
|
387
|
-
```ruby
|
388
|
-
class SetSomeColumnNotNull < ActiveRecord::Migration[6.0]
|
389
|
-
def change
|
390
|
-
safety_assured do
|
391
|
-
execute 'ALTER TABLE "users" ADD CONSTRAINT "users_some_column_null" CHECK ("some_column" IS NOT NULL) NOT VALID'
|
392
|
-
end
|
393
|
-
end
|
394
|
-
end
|
395
|
-
```
|
396
|
-
|
397
|
-
Then validate it in a separate migration. A `NOT NULL` check constraint is [functionally equivalent](https://medium.com/doctolib/adding-a-not-null-constraint-on-pg-faster-with-minimal-locking-38b2c00c4d1c) to setting `NOT NULL` on the column (but it won’t show up in `schema.rb` in Rails < 6.1). In Postgres 12+, once the check constraint is validated, you can safely set `NOT NULL` on the column and drop the check constraint.
|
398
|
-
|
399
|
-
For Rails 6.1, use:
|
400
|
-
|
401
|
-
```ruby
|
402
|
-
class ValidateSomeColumnNotNull < ActiveRecord::Migration[6.1]
|
403
|
-
def change
|
404
|
-
validate_check_constraint :users, name: "users_some_column_null"
|
405
|
-
|
406
|
-
# in Postgres 12+, you can then safely set NOT NULL on the column
|
407
|
-
change_column_null :users, :some_column, false
|
408
|
-
remove_check_constraint :users, name: "users_some_column_null"
|
409
|
-
end
|
410
|
-
end
|
411
|
-
```
|
412
|
-
|
413
|
-
For Rails < 6.1, use:
|
414
|
-
|
415
|
-
```ruby
|
416
|
-
class ValidateSomeColumnNotNull < ActiveRecord::Migration[6.0]
|
417
|
-
def change
|
418
|
-
safety_assured do
|
419
|
-
execute 'ALTER TABLE "users" VALIDATE CONSTRAINT "users_some_column_null"'
|
420
|
-
end
|
421
|
-
|
422
|
-
# in Postgres 12+, you can then safely set NOT NULL on the column
|
423
|
-
change_column_null :users, :some_column, false
|
424
|
-
safety_assured do
|
425
|
-
execute 'ALTER TABLE "users" DROP CONSTRAINT "users_some_column_null"'
|
426
|
-
end
|
427
|
-
end
|
428
|
-
end
|
429
|
-
```
|
430
|
-
|
431
|
-
#### Good - MySQL and MariaDB
|
432
|
-
|
433
|
-
[Let us know](https://github.com/ankane/strong_migrations/issues/new) if you have a safe way to do this.
|
434
|
-
|
435
363
|
### Executing SQL directly
|
436
364
|
|
437
365
|
Strong Migrations can’t ensure safety for raw SQL statements. Make really sure that what you’re doing is safe, then use:
|
438
366
|
|
439
367
|
```ruby
|
440
|
-
class ExecuteSQL < ActiveRecord::Migration[
|
368
|
+
class ExecuteSQL < ActiveRecord::Migration[7.0]
|
441
369
|
def change
|
442
370
|
safety_assured { execute "..." }
|
443
371
|
end
|
@@ -453,7 +381,7 @@ end
|
|
453
381
|
In Postgres, adding an index non-concurrently blocks writes.
|
454
382
|
|
455
383
|
```ruby
|
456
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[
|
384
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[7.0]
|
457
385
|
def change
|
458
386
|
add_index :users, :some_column
|
459
387
|
end
|
@@ -465,7 +393,7 @@ end
|
|
465
393
|
Add indexes concurrently.
|
466
394
|
|
467
395
|
```ruby
|
468
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[
|
396
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[7.0]
|
469
397
|
disable_ddl_transaction!
|
470
398
|
|
471
399
|
def change
|
@@ -491,7 +419,7 @@ rails g index table column
|
|
491
419
|
Rails adds an index non-concurrently to references by default, which blocks writes in Postgres.
|
492
420
|
|
493
421
|
```ruby
|
494
|
-
class AddReferenceToUsers < ActiveRecord::Migration[
|
422
|
+
class AddReferenceToUsers < ActiveRecord::Migration[7.0]
|
495
423
|
def change
|
496
424
|
add_reference :users, :city
|
497
425
|
end
|
@@ -503,7 +431,7 @@ end
|
|
503
431
|
Make sure the index is added concurrently.
|
504
432
|
|
505
433
|
```ruby
|
506
|
-
class AddReferenceToUsers < ActiveRecord::Migration[
|
434
|
+
class AddReferenceToUsers < ActiveRecord::Migration[7.0]
|
507
435
|
disable_ddl_transaction!
|
508
436
|
|
509
437
|
def change
|
@@ -521,7 +449,7 @@ end
|
|
521
449
|
In Postgres, adding a foreign key blocks writes on both tables.
|
522
450
|
|
523
451
|
```ruby
|
524
|
-
class AddForeignKeyOnUsers < ActiveRecord::Migration[
|
452
|
+
class AddForeignKeyOnUsers < ActiveRecord::Migration[7.0]
|
525
453
|
def change
|
526
454
|
add_foreign_key :users, :orders
|
527
455
|
end
|
@@ -531,7 +459,7 @@ end
|
|
531
459
|
or
|
532
460
|
|
533
461
|
```ruby
|
534
|
-
class AddReferenceToUsers < ActiveRecord::Migration[
|
462
|
+
class AddReferenceToUsers < ActiveRecord::Migration[7.0]
|
535
463
|
def change
|
536
464
|
add_reference :users, :order, foreign_key: true
|
537
465
|
end
|
@@ -540,74 +468,124 @@ end
|
|
540
468
|
|
541
469
|
#### Good
|
542
470
|
|
543
|
-
Add the foreign key without validating existing rows
|
544
|
-
|
545
|
-
For Rails 5.2+, use:
|
471
|
+
Add the foreign key without validating existing rows:
|
546
472
|
|
547
473
|
```ruby
|
548
|
-
class AddForeignKeyOnUsers < ActiveRecord::Migration[
|
474
|
+
class AddForeignKeyOnUsers < ActiveRecord::Migration[7.0]
|
549
475
|
def change
|
550
476
|
add_foreign_key :users, :orders, validate: false
|
551
477
|
end
|
552
478
|
end
|
553
479
|
```
|
554
480
|
|
555
|
-
Then
|
481
|
+
Then validate them in a separate migration.
|
556
482
|
|
557
483
|
```ruby
|
558
|
-
class ValidateForeignKeyOnUsers < ActiveRecord::Migration[
|
484
|
+
class ValidateForeignKeyOnUsers < ActiveRecord::Migration[7.0]
|
559
485
|
def change
|
560
486
|
validate_foreign_key :users, :orders
|
561
487
|
end
|
562
488
|
end
|
563
489
|
```
|
564
490
|
|
565
|
-
|
491
|
+
### Adding a json column
|
492
|
+
|
493
|
+
#### Bad
|
494
|
+
|
495
|
+
In Postgres, there’s no equality operator for the `json` column type, which can cause errors for existing `SELECT DISTINCT` queries in your application.
|
566
496
|
|
567
497
|
```ruby
|
568
|
-
class
|
498
|
+
class AddPropertiesToUsers < ActiveRecord::Migration[7.0]
|
569
499
|
def change
|
570
|
-
|
571
|
-
execute 'ALTER TABLE "users" ADD CONSTRAINT "fk_rails_c1e9b98e31" FOREIGN KEY ("order_id") REFERENCES "orders" ("id") NOT VALID'
|
572
|
-
end
|
500
|
+
add_column :users, :properties, :json
|
573
501
|
end
|
574
502
|
end
|
575
503
|
```
|
576
504
|
|
577
|
-
|
505
|
+
#### Good
|
506
|
+
|
507
|
+
Use `jsonb` instead.
|
578
508
|
|
579
509
|
```ruby
|
580
|
-
class
|
510
|
+
class AddPropertiesToUsers < ActiveRecord::Migration[7.0]
|
581
511
|
def change
|
582
|
-
|
583
|
-
execute 'ALTER TABLE "users" VALIDATE CONSTRAINT "fk_rails_c1e9b98e31"'
|
584
|
-
end
|
512
|
+
add_column :users, :properties, :jsonb
|
585
513
|
end
|
586
514
|
end
|
587
515
|
```
|
588
516
|
|
589
|
-
###
|
517
|
+
### Setting NOT NULL on an existing column
|
518
|
+
|
519
|
+
:turtle: Safe by default available
|
590
520
|
|
591
521
|
#### Bad
|
592
522
|
|
593
|
-
In Postgres,
|
523
|
+
In Postgres, setting `NOT NULL` on an existing column blocks reads and writes while every row is checked.
|
594
524
|
|
595
525
|
```ruby
|
596
|
-
class
|
526
|
+
class SetSomeColumnNotNull < ActiveRecord::Migration[7.0]
|
597
527
|
def change
|
598
|
-
|
528
|
+
change_column_null :users, :some_column, false
|
599
529
|
end
|
600
530
|
end
|
601
531
|
```
|
602
532
|
|
603
533
|
#### Good
|
604
534
|
|
605
|
-
|
535
|
+
Instead, add a check constraint.
|
536
|
+
|
537
|
+
For Rails 6.1, use:
|
606
538
|
|
607
539
|
```ruby
|
608
|
-
class
|
540
|
+
class SetSomeColumnNotNull < ActiveRecord::Migration[7.0]
|
609
541
|
def change
|
610
|
-
|
542
|
+
add_check_constraint :users, "some_column IS NOT NULL", name: "users_some_column_null", validate: false
|
543
|
+
end
|
544
|
+
end
|
545
|
+
```
|
546
|
+
|
547
|
+
For Rails < 6.1, use:
|
548
|
+
|
549
|
+
```ruby
|
550
|
+
class SetSomeColumnNotNull < ActiveRecord::Migration[6.0]
|
551
|
+
def change
|
552
|
+
safety_assured do
|
553
|
+
execute 'ALTER TABLE "users" ADD CONSTRAINT "users_some_column_null" CHECK ("some_column" IS NOT NULL) NOT VALID'
|
554
|
+
end
|
555
|
+
end
|
556
|
+
end
|
557
|
+
```
|
558
|
+
|
559
|
+
Then validate it in a separate migration. A `NOT NULL` check constraint is [functionally equivalent](https://medium.com/doctolib/adding-a-not-null-constraint-on-pg-faster-with-minimal-locking-38b2c00c4d1c) to setting `NOT NULL` on the column (but it won’t show up in `schema.rb` in Rails < 6.1). In Postgres 12+, once the check constraint is validated, you can safely set `NOT NULL` on the column and drop the check constraint.
|
560
|
+
|
561
|
+
For Rails 6.1, use:
|
562
|
+
|
563
|
+
```ruby
|
564
|
+
class ValidateSomeColumnNotNull < ActiveRecord::Migration[7.0]
|
565
|
+
def change
|
566
|
+
validate_check_constraint :users, name: "users_some_column_null"
|
567
|
+
|
568
|
+
# in Postgres 12+, you can then safely set NOT NULL on the column
|
569
|
+
change_column_null :users, :some_column, false
|
570
|
+
remove_check_constraint :users, name: "users_some_column_null"
|
571
|
+
end
|
572
|
+
end
|
573
|
+
```
|
574
|
+
|
575
|
+
For Rails < 6.1, use:
|
576
|
+
|
577
|
+
```ruby
|
578
|
+
class ValidateSomeColumnNotNull < ActiveRecord::Migration[6.0]
|
579
|
+
def change
|
580
|
+
safety_assured do
|
581
|
+
execute 'ALTER TABLE "users" VALIDATE CONSTRAINT "users_some_column_null"'
|
582
|
+
end
|
583
|
+
|
584
|
+
# in Postgres 12+, you can then safely set NOT NULL on the column
|
585
|
+
change_column_null :users, :some_column, false
|
586
|
+
safety_assured do
|
587
|
+
execute 'ALTER TABLE "users" DROP CONSTRAINT "users_some_column_null"'
|
588
|
+
end
|
611
589
|
end
|
612
590
|
end
|
613
591
|
```
|
@@ -619,7 +597,7 @@ end
|
|
619
597
|
Adding a non-unique index with more than three columns rarely improves performance.
|
620
598
|
|
621
599
|
```ruby
|
622
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[
|
600
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[7.0]
|
623
601
|
def change
|
624
602
|
add_index :users, [:a, :b, :c, :d]
|
625
603
|
end
|
@@ -631,7 +609,7 @@ end
|
|
631
609
|
Instead, start an index with columns that narrow down the results the most.
|
632
610
|
|
633
611
|
```ruby
|
634
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[
|
612
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[7.0]
|
635
613
|
def change
|
636
614
|
add_index :users, [:b, :d]
|
637
615
|
end
|
@@ -645,7 +623,7 @@ For Postgres, be sure to add them concurrently.
|
|
645
623
|
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.
|
646
624
|
|
647
625
|
```ruby
|
648
|
-
class MySafeMigration < ActiveRecord::Migration[
|
626
|
+
class MySafeMigration < ActiveRecord::Migration[7.0]
|
649
627
|
def change
|
650
628
|
safety_assured { remove_column :users, :some_column }
|
651
629
|
end
|
@@ -721,7 +699,7 @@ To customize specific messages, create an initializer with:
|
|
721
699
|
StrongMigrations.error_messages[:add_column_default] = "Your custom instructions"
|
722
700
|
```
|
723
701
|
|
724
|
-
Check the [source code](https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations.rb) for the list of keys.
|
702
|
+
Check the [source code](https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations/error_messages.rb) for the list of keys.
|
725
703
|
|
726
704
|
## Migration Timeouts
|
727
705
|
|
@@ -743,6 +721,25 @@ ALTER ROLE myuser SET statement_timeout = '1h';
|
|
743
721
|
|
744
722
|
Note: If you use PgBouncer in transaction mode, you must set timeouts on the database user.
|
745
723
|
|
724
|
+
## Lock Timeout Retries [experimental]
|
725
|
+
|
726
|
+
There’s the option to automatically retry statements when the lock timeout is reached. Here’s how it works:
|
727
|
+
|
728
|
+
- If a lock timeout happens outside a transaction, the statement is retried
|
729
|
+
- If it happens inside the DDL transaction, the entire migration is retried (only applicable to Postgres)
|
730
|
+
|
731
|
+
Add to `config/initializers/strong_migrations.rb`:
|
732
|
+
|
733
|
+
```ruby
|
734
|
+
StrongMigrations.lock_timeout_retries = 3
|
735
|
+
```
|
736
|
+
|
737
|
+
Set the delay between retries with:
|
738
|
+
|
739
|
+
```ruby
|
740
|
+
StrongMigrations.lock_timeout_retry_delay = 10.seconds
|
741
|
+
```
|
742
|
+
|
746
743
|
## App Timeouts
|
747
744
|
|
748
745
|
We recommend adding timeouts to `config/database.yml` to prevent connections from hanging and individual queries from taking up too many resources in controllers, jobs, the Rails console, and other places.
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module StrongMigrations
|
2
|
+
module Adapters
|
3
|
+
class AbstractAdapter
|
4
|
+
def initialize(checker)
|
5
|
+
@checker = checker
|
6
|
+
end
|
7
|
+
|
8
|
+
def name
|
9
|
+
"Unknown"
|
10
|
+
end
|
11
|
+
|
12
|
+
def min_version
|
13
|
+
end
|
14
|
+
|
15
|
+
def set_statement_timeout(timeout)
|
16
|
+
raise StrongMigrations::Error, "Statement timeout not supported for this database"
|
17
|
+
end
|
18
|
+
|
19
|
+
def set_lock_timeout(timeout)
|
20
|
+
raise StrongMigrations::Error, "Lock timeout not supported for this database"
|
21
|
+
end
|
22
|
+
|
23
|
+
def check_lock_timeout(limit)
|
24
|
+
# do nothing
|
25
|
+
end
|
26
|
+
|
27
|
+
def add_column_default_safe?
|
28
|
+
false
|
29
|
+
end
|
30
|
+
|
31
|
+
def change_type_safe?(table, column, type, options, existing_column, existing_type)
|
32
|
+
false
|
33
|
+
end
|
34
|
+
|
35
|
+
def rewrite_blocks
|
36
|
+
"reads and writes"
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
def connection
|
42
|
+
@checker.send(:connection)
|
43
|
+
end
|
44
|
+
|
45
|
+
def select_all(statement)
|
46
|
+
connection.select_all(statement)
|
47
|
+
end
|
48
|
+
|
49
|
+
def target_version(target_version)
|
50
|
+
target_version ||= StrongMigrations.target_version
|
51
|
+
version =
|
52
|
+
if target_version && StrongMigrations.developer_env?
|
53
|
+
target_version.to_s
|
54
|
+
else
|
55
|
+
yield
|
56
|
+
end
|
57
|
+
Gem::Version.new(version)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|