strong_migrations 0.7.0 → 0.7.8
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/LICENSE.txt +1 -1
- data/README.md +138 -40
- data/lib/generators/strong_migrations/install_generator.rb +29 -0
- data/lib/generators/strong_migrations/templates/initializer.rb.tt +10 -3
- data/lib/strong_migrations/checker.rb +99 -24
- data/lib/strong_migrations/migration.rb +3 -0
- data/lib/strong_migrations/safe_methods.rb +129 -0
- data/lib/strong_migrations/version.rb +1 -1
- data/lib/strong_migrations.rb +28 -1
- metadata +8 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 38cd87929d4889113bc2cc13aa786965c8b86fadf05d1d0439e2c927becfef61
|
4
|
+
data.tar.gz: 0f17d511058c0bd1852adcddf9a2e978b8e407002ddf201be7aedb6c797dd294
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d28ef8e2d9cd837fac0a5e72743e1b88e7ef268ce9660e5119c27b50cc0e5215ae20e36a8be570a987e967f4a8bdf47ccd68eb6ee66f7188b787d3d64596c514
|
7
|
+
data.tar.gz: f1e012eed6af3ebcc0ffbf5a1f4b90438ee054597f41a9420a3645af8535fff4cb3ef64b69abdc547fbf20a14658b03f0492395ad46c6bfc79b26726424c81c4
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,37 @@
|
|
1
|
+
## 0.7.8 (2021-08-03)
|
2
|
+
|
3
|
+
- Fixed issue with `add_reference ..., foreign_key: {to_table: ...}` with `safe_by_default`
|
4
|
+
|
5
|
+
## 0.7.7 (2021-06-07)
|
6
|
+
|
7
|
+
- Removed timeouts and `auto_analyze` from schema load
|
8
|
+
|
9
|
+
## 0.7.6 (2021-01-17)
|
10
|
+
|
11
|
+
- Fixed `NOT NULL` constraint check for quoted columns
|
12
|
+
- Fixed deprecation warning with Active Record 6.1
|
13
|
+
|
14
|
+
## 0.7.5 (2021-01-12)
|
15
|
+
|
16
|
+
- Added checks for `add_check_constraint` and `validate_check_constraint`
|
17
|
+
|
18
|
+
## 0.7.4 (2020-12-16)
|
19
|
+
|
20
|
+
- Added `safe_by_default` option to install generator
|
21
|
+
- Fixed warnings with Active Record 6.1
|
22
|
+
|
23
|
+
## 0.7.3 (2020-11-24)
|
24
|
+
|
25
|
+
- Added `safe_by_default` option
|
26
|
+
|
27
|
+
## 0.7.2 (2020-10-25)
|
28
|
+
|
29
|
+
- Added support for float timeouts
|
30
|
+
|
31
|
+
## 0.7.1 (2020-07-27)
|
32
|
+
|
33
|
+
- Added `target_version` option to replace database-specific options
|
34
|
+
|
1
35
|
## 0.7.0 (2020-07-22)
|
2
36
|
|
3
37
|
- Added `check_down` option
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -8,7 +8,7 @@ Supports for PostgreSQL, MySQL, and MariaDB
|
|
8
8
|
|
9
9
|
:tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)
|
10
10
|
|
11
|
-
[![Build Status](https://
|
11
|
+
[![Build Status](https://github.com/ankane/strong_migrations/workflows/build/badge.svg?branch=master)](https://github.com/ankane/strong_migrations/actions)
|
12
12
|
|
13
13
|
## Installation
|
14
14
|
|
@@ -43,9 +43,9 @@ end
|
|
43
43
|
|
44
44
|
Deploy the code, then wrap this step in a safety_assured { ... } block.
|
45
45
|
|
46
|
-
class RemoveColumn < ActiveRecord::Migration[6.
|
46
|
+
class RemoveColumn < ActiveRecord::Migration[6.1]
|
47
47
|
def change
|
48
|
-
safety_assured { remove_column :users, :name
|
48
|
+
safety_assured { remove_column :users, :name }
|
49
49
|
end
|
50
50
|
end
|
51
51
|
```
|
@@ -66,6 +66,7 @@ Potentially dangerous operations:
|
|
66
66
|
- [renaming a column](#renaming-a-column)
|
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
|
+
- [adding a check constraint](#adding-a-check-constraint)
|
69
70
|
- [setting NOT NULL on an existing column](#setting-not-null-on-an-existing-column)
|
70
71
|
- [executing SQL directly](#executing-SQL-directly)
|
71
72
|
|
@@ -89,7 +90,7 @@ You can also add [custom checks](#custom-checks) or [disable specific checks](#d
|
|
89
90
|
Active Record caches database columns at runtime, so if you drop a column, it can cause exceptions until your app reboots.
|
90
91
|
|
91
92
|
```ruby
|
92
|
-
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[6.
|
93
|
+
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[6.1]
|
93
94
|
def change
|
94
95
|
remove_column :users, :some_column
|
95
96
|
end
|
@@ -110,7 +111,7 @@ end
|
|
110
111
|
3. Write a migration to remove the column (wrap in `safety_assured` block)
|
111
112
|
|
112
113
|
```ruby
|
113
|
-
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[6.
|
114
|
+
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[6.1]
|
114
115
|
def change
|
115
116
|
safety_assured { remove_column :users, :some_column }
|
116
117
|
end
|
@@ -126,7 +127,7 @@ end
|
|
126
127
|
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.
|
127
128
|
|
128
129
|
```ruby
|
129
|
-
class AddSomeColumnToUsers < ActiveRecord::Migration[6.
|
130
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[6.1]
|
130
131
|
def change
|
131
132
|
add_column :users, :some_column, :text, default: "default_value"
|
132
133
|
end
|
@@ -140,7 +141,7 @@ In Postgres 11+, MySQL 8.0.12+, and MariaDB 10.3.2+, this no longer requires a t
|
|
140
141
|
Instead, add the column without a default value, then change the default.
|
141
142
|
|
142
143
|
```ruby
|
143
|
-
class AddSomeColumnToUsers < ActiveRecord::Migration[6.
|
144
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[6.1]
|
144
145
|
def up
|
145
146
|
add_column :users, :some_column, :text
|
146
147
|
change_column_default :users, :some_column, "default_value"
|
@@ -161,7 +162,7 @@ See the next section for how to backfill.
|
|
161
162
|
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/).
|
162
163
|
|
163
164
|
```ruby
|
164
|
-
class AddSomeColumnToUsers < ActiveRecord::Migration[6.
|
165
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[6.1]
|
165
166
|
def change
|
166
167
|
add_column :users, :some_column, :text
|
167
168
|
User.update_all some_column: "default_value"
|
@@ -176,7 +177,7 @@ Also, running a single query to update data can cause issues for large tables.
|
|
176
177
|
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!`.
|
177
178
|
|
178
179
|
```ruby
|
179
|
-
class BackfillSomeColumn < ActiveRecord::Migration[6.
|
180
|
+
class BackfillSomeColumn < ActiveRecord::Migration[6.1]
|
180
181
|
disable_ddl_transaction!
|
181
182
|
|
182
183
|
def up
|
@@ -195,7 +196,7 @@ end
|
|
195
196
|
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.
|
196
197
|
|
197
198
|
```ruby
|
198
|
-
class ChangeSomeColumnType < ActiveRecord::Migration[6.
|
199
|
+
class ChangeSomeColumnType < ActiveRecord::Migration[6.1]
|
199
200
|
def change
|
200
201
|
change_column :users, :some_column, :new_type
|
201
202
|
end
|
@@ -234,7 +235,7 @@ A safer approach is to:
|
|
234
235
|
Renaming a column that’s in use will cause errors in your application.
|
235
236
|
|
236
237
|
```ruby
|
237
|
-
class RenameSomeColumn < ActiveRecord::Migration[6.
|
238
|
+
class RenameSomeColumn < ActiveRecord::Migration[6.1]
|
238
239
|
def change
|
239
240
|
rename_column :users, :some_column, :new_name
|
240
241
|
end
|
@@ -259,7 +260,7 @@ A safer approach is to:
|
|
259
260
|
Renaming a table that’s in use will cause errors in your application.
|
260
261
|
|
261
262
|
```ruby
|
262
|
-
class RenameUsersToCustomers < ActiveRecord::Migration[6.
|
263
|
+
class RenameUsersToCustomers < ActiveRecord::Migration[6.1]
|
263
264
|
def change
|
264
265
|
rename_table :users, :customers
|
265
266
|
end
|
@@ -284,7 +285,7 @@ A safer approach is to:
|
|
284
285
|
The `force` option can drop an existing table.
|
285
286
|
|
286
287
|
```ruby
|
287
|
-
class CreateUsers < ActiveRecord::Migration[6.
|
288
|
+
class CreateUsers < ActiveRecord::Migration[6.1]
|
288
289
|
def change
|
289
290
|
create_table :users, force: true do |t|
|
290
291
|
# ...
|
@@ -298,7 +299,7 @@ end
|
|
298
299
|
Create tables without the `force` option.
|
299
300
|
|
300
301
|
```ruby
|
301
|
-
class CreateUsers < ActiveRecord::Migration[6.
|
302
|
+
class CreateUsers < ActiveRecord::Migration[6.1]
|
302
303
|
def change
|
303
304
|
create_table :users do |t|
|
304
305
|
# ...
|
@@ -309,14 +310,58 @@ end
|
|
309
310
|
|
310
311
|
If you intend to drop an existing table, run `drop_table` first.
|
311
312
|
|
313
|
+
### Adding a check constraint
|
314
|
+
|
315
|
+
:turtle: Safe by default available
|
316
|
+
|
317
|
+
#### Bad
|
318
|
+
|
319
|
+
Adding a check constraint blocks reads and writes in Postgres and blocks writes in MySQL and MariaDB while every row is checked.
|
320
|
+
|
321
|
+
```ruby
|
322
|
+
class AddCheckConstraint < ActiveRecord::Migration[6.1]
|
323
|
+
def change
|
324
|
+
add_check_constraint :users, "price > 0", name: "price_check"
|
325
|
+
end
|
326
|
+
end
|
327
|
+
```
|
328
|
+
|
329
|
+
#### Good - Postgres
|
330
|
+
|
331
|
+
Add the check constraint without validating existing rows:
|
332
|
+
|
333
|
+
```ruby
|
334
|
+
class AddCheckConstraint < ActiveRecord::Migration[6.1]
|
335
|
+
def change
|
336
|
+
add_check_constraint :users, "price > 0", name: "price_check", validate: false
|
337
|
+
end
|
338
|
+
end
|
339
|
+
```
|
340
|
+
|
341
|
+
Then validate them in a separate migration.
|
342
|
+
|
343
|
+
```ruby
|
344
|
+
class ValidateCheckConstraint < ActiveRecord::Migration[6.1]
|
345
|
+
def change
|
346
|
+
validate_check_constraint :users, name: "price_check"
|
347
|
+
end
|
348
|
+
end
|
349
|
+
```
|
350
|
+
|
351
|
+
#### Good - MySQL and MariaDB
|
352
|
+
|
353
|
+
[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
|
+
|
312
355
|
### Setting NOT NULL on an existing column
|
313
356
|
|
357
|
+
:turtle: Safe by default available
|
358
|
+
|
314
359
|
#### Bad
|
315
360
|
|
316
|
-
Setting `NOT NULL` on an existing column blocks reads and writes while
|
361
|
+
Setting `NOT NULL` on an existing column blocks reads and writes while every row is checked.
|
317
362
|
|
318
363
|
```ruby
|
319
|
-
class SetSomeColumnNotNull < ActiveRecord::Migration[6.
|
364
|
+
class SetSomeColumnNotNull < ActiveRecord::Migration[6.1]
|
320
365
|
def change
|
321
366
|
change_column_null :users, :some_column, false
|
322
367
|
end
|
@@ -325,7 +370,19 @@ end
|
|
325
370
|
|
326
371
|
#### Good - Postgres
|
327
372
|
|
328
|
-
Instead, add a check constraint
|
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:
|
329
386
|
|
330
387
|
```ruby
|
331
388
|
class SetSomeColumnNotNull < ActiveRecord::Migration[6.0]
|
@@ -337,7 +394,23 @@ class SetSomeColumnNotNull < ActiveRecord::Migration[6.0]
|
|
337
394
|
end
|
338
395
|
```
|
339
396
|
|
340
|
-
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
|
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:
|
341
414
|
|
342
415
|
```ruby
|
343
416
|
class ValidateSomeColumnNotNull < ActiveRecord::Migration[6.0]
|
@@ -364,7 +437,7 @@ end
|
|
364
437
|
Strong Migrations can’t ensure safety for raw SQL statements. Make really sure that what you’re doing is safe, then use:
|
365
438
|
|
366
439
|
```ruby
|
367
|
-
class ExecuteSQL < ActiveRecord::Migration[6.
|
440
|
+
class ExecuteSQL < ActiveRecord::Migration[6.1]
|
368
441
|
def change
|
369
442
|
safety_assured { execute "..." }
|
370
443
|
end
|
@@ -373,12 +446,14 @@ end
|
|
373
446
|
|
374
447
|
### Adding an index non-concurrently
|
375
448
|
|
449
|
+
:turtle: Safe by default available
|
450
|
+
|
376
451
|
#### Bad
|
377
452
|
|
378
453
|
In Postgres, adding an index non-concurrently blocks writes.
|
379
454
|
|
380
455
|
```ruby
|
381
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[6.
|
456
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[6.1]
|
382
457
|
def change
|
383
458
|
add_index :users, :some_column
|
384
459
|
end
|
@@ -390,7 +465,7 @@ end
|
|
390
465
|
Add indexes concurrently.
|
391
466
|
|
392
467
|
```ruby
|
393
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[6.
|
468
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[6.1]
|
394
469
|
disable_ddl_transaction!
|
395
470
|
|
396
471
|
def change
|
@@ -409,12 +484,14 @@ rails g index table column
|
|
409
484
|
|
410
485
|
### Adding a reference
|
411
486
|
|
487
|
+
:turtle: Safe by default available
|
488
|
+
|
412
489
|
#### Bad
|
413
490
|
|
414
491
|
Rails adds an index non-concurrently to references by default, which blocks writes in Postgres.
|
415
492
|
|
416
493
|
```ruby
|
417
|
-
class AddReferenceToUsers < ActiveRecord::Migration[6.
|
494
|
+
class AddReferenceToUsers < ActiveRecord::Migration[6.1]
|
418
495
|
def change
|
419
496
|
add_reference :users, :city
|
420
497
|
end
|
@@ -426,7 +503,7 @@ end
|
|
426
503
|
Make sure the index is added concurrently.
|
427
504
|
|
428
505
|
```ruby
|
429
|
-
class AddReferenceToUsers < ActiveRecord::Migration[6.
|
506
|
+
class AddReferenceToUsers < ActiveRecord::Migration[6.1]
|
430
507
|
disable_ddl_transaction!
|
431
508
|
|
432
509
|
def change
|
@@ -437,12 +514,14 @@ end
|
|
437
514
|
|
438
515
|
### Adding a foreign key
|
439
516
|
|
517
|
+
:turtle: Safe by default available
|
518
|
+
|
440
519
|
#### Bad
|
441
520
|
|
442
521
|
In Postgres, adding a foreign key blocks writes on both tables.
|
443
522
|
|
444
523
|
```ruby
|
445
|
-
class AddForeignKeyOnUsers < ActiveRecord::Migration[6.
|
524
|
+
class AddForeignKeyOnUsers < ActiveRecord::Migration[6.1]
|
446
525
|
def change
|
447
526
|
add_foreign_key :users, :orders
|
448
527
|
end
|
@@ -452,7 +531,7 @@ end
|
|
452
531
|
or
|
453
532
|
|
454
533
|
```ruby
|
455
|
-
class AddReferenceToUsers < ActiveRecord::Migration[6.
|
534
|
+
class AddReferenceToUsers < ActiveRecord::Migration[6.1]
|
456
535
|
def change
|
457
536
|
add_reference :users, :order, foreign_key: true
|
458
537
|
end
|
@@ -466,7 +545,7 @@ Add the foreign key without validating existing rows, then validate them in a se
|
|
466
545
|
For Rails 5.2+, use:
|
467
546
|
|
468
547
|
```ruby
|
469
|
-
class AddForeignKeyOnUsers < ActiveRecord::Migration[6.
|
548
|
+
class AddForeignKeyOnUsers < ActiveRecord::Migration[6.1]
|
470
549
|
def change
|
471
550
|
add_foreign_key :users, :orders, validate: false
|
472
551
|
end
|
@@ -476,7 +555,7 @@ end
|
|
476
555
|
Then:
|
477
556
|
|
478
557
|
```ruby
|
479
|
-
class ValidateForeignKeyOnUsers < ActiveRecord::Migration[6.
|
558
|
+
class ValidateForeignKeyOnUsers < ActiveRecord::Migration[6.1]
|
480
559
|
def change
|
481
560
|
validate_foreign_key :users, :orders
|
482
561
|
end
|
@@ -514,7 +593,7 @@ end
|
|
514
593
|
In Postgres, there’s no equality operator for the `json` column type, which can cause errors for existing `SELECT DISTINCT` queries in your application.
|
515
594
|
|
516
595
|
```ruby
|
517
|
-
class AddPropertiesToUsers < ActiveRecord::Migration[6.
|
596
|
+
class AddPropertiesToUsers < ActiveRecord::Migration[6.1]
|
518
597
|
def change
|
519
598
|
add_column :users, :properties, :json
|
520
599
|
end
|
@@ -526,7 +605,7 @@ end
|
|
526
605
|
Use `jsonb` instead.
|
527
606
|
|
528
607
|
```ruby
|
529
|
-
class AddPropertiesToUsers < ActiveRecord::Migration[6.
|
608
|
+
class AddPropertiesToUsers < ActiveRecord::Migration[6.1]
|
530
609
|
def change
|
531
610
|
add_column :users, :properties, :jsonb
|
532
611
|
end
|
@@ -540,7 +619,7 @@ end
|
|
540
619
|
Adding a non-unique index with more than three columns rarely improves performance.
|
541
620
|
|
542
621
|
```ruby
|
543
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[6.
|
622
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[6.1]
|
544
623
|
def change
|
545
624
|
add_index :users, [:a, :b, :c, :d]
|
546
625
|
end
|
@@ -552,7 +631,7 @@ end
|
|
552
631
|
Instead, start an index with columns that narrow down the results the most.
|
553
632
|
|
554
633
|
```ruby
|
555
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[6.
|
634
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[6.1]
|
556
635
|
def change
|
557
636
|
add_index :users, [:b, :d]
|
558
637
|
end
|
@@ -566,7 +645,7 @@ For Postgres, be sure to add them concurrently.
|
|
566
645
|
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.
|
567
646
|
|
568
647
|
```ruby
|
569
|
-
class MySafeMigration < ActiveRecord::Migration[6.
|
648
|
+
class MySafeMigration < ActiveRecord::Migration[6.1]
|
570
649
|
def change
|
571
650
|
safety_assured { remove_column :users, :some_column }
|
572
651
|
end
|
@@ -575,6 +654,21 @@ end
|
|
575
654
|
|
576
655
|
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.
|
577
656
|
|
657
|
+
## Safe by Default
|
658
|
+
|
659
|
+
Make operations safe by default.
|
660
|
+
|
661
|
+
- adding and removing an index
|
662
|
+
- adding a foreign key
|
663
|
+
- adding a check constraint
|
664
|
+
- setting NOT NULL on an existing column
|
665
|
+
|
666
|
+
Add to `config/initializers/strong_migrations.rb`:
|
667
|
+
|
668
|
+
```ruby
|
669
|
+
StrongMigrations.safe_by_default = true
|
670
|
+
```
|
671
|
+
|
578
672
|
## Custom Checks
|
579
673
|
|
580
674
|
Add your own custom checks with:
|
@@ -611,7 +705,7 @@ StrongMigrations.disable_check(:add_index)
|
|
611
705
|
|
612
706
|
Check the [source code](https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations.rb) for the list of keys.
|
613
707
|
|
614
|
-
## Down Migrations / Rollbacks
|
708
|
+
## Down Migrations / Rollbacks
|
615
709
|
|
616
710
|
By default, checks are disabled when migrating down. Enable them with:
|
617
711
|
|
@@ -707,11 +801,11 @@ Use the version from your latest migration.
|
|
707
801
|
If your development database version is different from production, you can specify the production version so the right checks run in development.
|
708
802
|
|
709
803
|
```ruby
|
710
|
-
StrongMigrations.
|
711
|
-
StrongMigrations.target_mysql_version = "8.0.12"
|
712
|
-
StrongMigrations.target_mariadb_version = "10.3.2"
|
804
|
+
StrongMigrations.target_version = 10 # or "8.0.12", "10.3.2", etc
|
713
805
|
```
|
714
806
|
|
807
|
+
The major version works well for Postgres, while the full version is recommended for MySQL and MariaDB.
|
808
|
+
|
715
809
|
For safety, this option only affects development and test environments. In other environments, the actual server version is always used.
|
716
810
|
|
717
811
|
## Analyze Tables
|
@@ -724,11 +818,15 @@ StrongMigrations.auto_analyze = true
|
|
724
818
|
|
725
819
|
## Faster Migrations
|
726
820
|
|
727
|
-
Only dump the schema when adding a new migration. If you use Git,
|
821
|
+
Only dump the schema when adding a new migration. If you use Git, add to the end of your `Rakefile`:
|
728
822
|
|
729
|
-
```
|
730
|
-
|
731
|
-
|
823
|
+
```rb
|
824
|
+
task :faster_migrations do
|
825
|
+
ActiveRecord::Base.dump_schema_after_migration = Rails.env.development? &&
|
826
|
+
`git status db/migrate/ --porcelain`.present?
|
827
|
+
end
|
828
|
+
|
829
|
+
task "db:migrate": "faster_migrations"
|
732
830
|
```
|
733
831
|
|
734
832
|
## Schema Sanity
|
@@ -12,6 +12,35 @@ module StrongMigrations
|
|
12
12
|
def start_after
|
13
13
|
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
14
14
|
end
|
15
|
+
|
16
|
+
def pgbouncer_message
|
17
|
+
if postgresql?
|
18
|
+
"\n# If you use PgBouncer in transaction mode, delete these lines and set timeouts on the database user"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def target_version
|
23
|
+
case adapter
|
24
|
+
when /mysql/
|
25
|
+
# could try to connect to database and check for MariaDB
|
26
|
+
# but this should be fine
|
27
|
+
'"8.0.12"'
|
28
|
+
else
|
29
|
+
"10"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def adapter
|
34
|
+
if ActiveRecord::VERSION::STRING.to_f >= 6.1
|
35
|
+
ActiveRecord::Base.connection_db_config.adapter.to_s
|
36
|
+
else
|
37
|
+
ActiveRecord::Base.connection_config[:adapter].to_s
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def postgresql?
|
42
|
+
adapter =~ /postg/
|
43
|
+
end
|
15
44
|
end
|
16
45
|
end
|
17
46
|
end
|
@@ -1,8 +1,7 @@
|
|
1
1
|
# Mark existing migrations as safe
|
2
2
|
StrongMigrations.start_after = <%= start_after %>
|
3
3
|
|
4
|
-
# Set timeouts for migrations
|
5
|
-
# If you use PgBouncer in transaction mode, delete these lines and set timeouts on the database user
|
4
|
+
# Set timeouts for migrations<%= pgbouncer_message %>
|
6
5
|
StrongMigrations.lock_timeout = 10.seconds
|
7
6
|
StrongMigrations.statement_timeout = 1.hour
|
8
7
|
|
@@ -10,9 +9,17 @@ StrongMigrations.statement_timeout = 1.hour
|
|
10
9
|
# Outdated statistics can sometimes hurt performance
|
11
10
|
StrongMigrations.auto_analyze = true
|
12
11
|
|
12
|
+
# Set the version of the production database
|
13
|
+
# so the right checks are run in development
|
14
|
+
# StrongMigrations.target_version = <%= target_version %>
|
15
|
+
|
13
16
|
# Add custom checks
|
14
17
|
# StrongMigrations.add_check do |method, args|
|
15
18
|
# if method == :add_index && args[0].to_s == "users"
|
16
19
|
# stop! "No more indexes on the users table"
|
17
20
|
# end
|
18
|
-
# end
|
21
|
+
# end<% if postgresql? %>
|
22
|
+
|
23
|
+
# Make some operations safe by default
|
24
|
+
# See https://github.com/ankane/strong_migrations#safe-by-default
|
25
|
+
# StrongMigrations.safe_by_default = true<% end %>
|
@@ -1,6 +1,8 @@
|
|
1
1
|
module StrongMigrations
|
2
2
|
class Checker
|
3
|
-
|
3
|
+
include SafeMethods
|
4
|
+
|
5
|
+
attr_accessor :direction, :transaction_disabled
|
4
6
|
|
5
7
|
def initialize(migration)
|
6
8
|
@migration = migration
|
@@ -24,7 +26,7 @@ module StrongMigrations
|
|
24
26
|
set_timeouts
|
25
27
|
check_lock_timeout
|
26
28
|
|
27
|
-
|
29
|
+
if !safe? || safe_by_default_method?(method)
|
28
30
|
case method
|
29
31
|
when :remove_column, :remove_columns, :remove_timestamps, :remove_reference, :remove_belongs_to
|
30
32
|
columns =
|
@@ -65,6 +67,7 @@ module StrongMigrations
|
|
65
67
|
raise_error :add_index_columns, header: "Best practice"
|
66
68
|
end
|
67
69
|
if postgresql? && options[:algorithm] != :concurrently && !new_table?(table)
|
70
|
+
return safe_add_index(table, columns, options) if StrongMigrations.safe_by_default
|
68
71
|
raise_error :add_index, command: command_str("add_index", [table, columns, options.merge(algorithm: :concurrently)])
|
69
72
|
end
|
70
73
|
when :remove_index
|
@@ -75,6 +78,7 @@ module StrongMigrations
|
|
75
78
|
options ||= {}
|
76
79
|
|
77
80
|
if postgresql? && options[:algorithm] != :concurrently && !new_table?(table)
|
81
|
+
return safe_remove_index(table, options) if StrongMigrations.safe_by_default
|
78
82
|
raise_error :remove_index, command: command_str("remove_index", [table, options.merge(algorithm: :concurrently)])
|
79
83
|
end
|
80
84
|
when :add_column
|
@@ -184,14 +188,14 @@ Then add the NOT NULL constraint in separate migrations."
|
|
184
188
|
bad_index = index_value && !concurrently_set
|
185
189
|
|
186
190
|
if bad_index || options[:foreign_key]
|
187
|
-
columns = options[:polymorphic] ? [:"#{reference}_type", :"#{reference}_id"] : :"#{reference}_id"
|
188
|
-
|
189
191
|
if index_value.is_a?(Hash)
|
190
192
|
options[:index] = options[:index].merge(algorithm: :concurrently)
|
191
193
|
else
|
192
194
|
options = options.merge(index: {algorithm: :concurrently})
|
193
195
|
end
|
194
196
|
|
197
|
+
return safe_add_reference(table, reference, options) if StrongMigrations.safe_by_default
|
198
|
+
|
195
199
|
if options.delete(:foreign_key)
|
196
200
|
headline = "Adding a foreign key blocks writes on both tables."
|
197
201
|
append = "
|
@@ -215,22 +219,49 @@ Then add the foreign key in separate migrations."
|
|
215
219
|
if postgresql?
|
216
220
|
safe = false
|
217
221
|
if postgresql_version >= Gem::Version.new("12")
|
218
|
-
#
|
219
|
-
safe = constraints(table).any? { |c| c["def"] == "CHECK ((#{column} IS NOT NULL))" }
|
222
|
+
safe = constraints(table).any? { |c| c["def"] == "CHECK ((#{column} IS NOT NULL))" || c["def"] == "CHECK ((#{connection.quote_column_name(column)} IS NOT NULL))" }
|
220
223
|
end
|
221
224
|
|
222
225
|
unless safe
|
223
226
|
# match https://github.com/nullobject/rein
|
224
227
|
constraint_name = "#{table}_#{column}_null"
|
225
228
|
|
226
|
-
|
229
|
+
add_code = constraint_str("ALTER TABLE %s ADD CONSTRAINT %s CHECK (%s IS NOT NULL) NOT VALID", [table, constraint_name, column])
|
230
|
+
validate_code = constraint_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [table, constraint_name])
|
231
|
+
remove_code = constraint_str("ALTER TABLE %s DROP CONSTRAINT %s", [table, constraint_name])
|
232
|
+
|
233
|
+
validate_constraint_code =
|
234
|
+
if ar_version >= 6.1
|
235
|
+
String.new(command_str(:validate_check_constraint, [table, {name: constraint_name}]))
|
236
|
+
else
|
237
|
+
String.new(safety_assured_str(validate_code))
|
238
|
+
end
|
239
|
+
|
227
240
|
if postgresql_version >= Gem::Version.new("12")
|
228
|
-
|
229
|
-
|
241
|
+
change_args = [table, column, null]
|
242
|
+
|
243
|
+
validate_constraint_code << "\n #{command_str(:change_column_null, change_args)}"
|
244
|
+
|
245
|
+
if ar_version >= 6.1
|
246
|
+
validate_constraint_code << "\n #{command_str(:remove_check_constraint, [table, {name: constraint_name}])}"
|
247
|
+
else
|
248
|
+
validate_constraint_code << "\n #{safety_assured_str(remove_code)}"
|
249
|
+
end
|
230
250
|
end
|
231
251
|
|
252
|
+
return safe_change_column_null(add_code, validate_code, change_args, remove_code) if StrongMigrations.safe_by_default
|
253
|
+
|
254
|
+
add_constraint_code =
|
255
|
+
if ar_version >= 6.1
|
256
|
+
# only quote when needed
|
257
|
+
expr_column = column.to_s =~ /\A[a-z0-9_]+\z/ ? column : connection.quote_column_name(column)
|
258
|
+
command_str(:add_check_constraint, [table, "#{expr_column} IS NOT NULL", {name: constraint_name, validate: false}])
|
259
|
+
else
|
260
|
+
safety_assured_str(add_code)
|
261
|
+
end
|
262
|
+
|
232
263
|
raise_error :change_column_null_postgresql,
|
233
|
-
add_constraint_code:
|
264
|
+
add_constraint_code: add_constraint_code,
|
234
265
|
validate_constraint_code: validate_constraint_code
|
235
266
|
end
|
236
267
|
elsif mysql? || mariadb?
|
@@ -245,20 +276,27 @@ Then add the foreign key in separate migrations."
|
|
245
276
|
options ||= {}
|
246
277
|
|
247
278
|
# always validated before 5.2
|
248
|
-
validate = options.fetch(:validate, true) ||
|
279
|
+
validate = options.fetch(:validate, true) || ar_version < 5.2
|
249
280
|
|
250
281
|
if postgresql? && validate
|
251
|
-
if
|
282
|
+
if ar_version < 5.2
|
252
283
|
# fk name logic from rails
|
253
284
|
primary_key = options[:primary_key] || "id"
|
254
285
|
column = options[:column] || "#{to_table.to_s.singularize}_id"
|
255
286
|
hashed_identifier = Digest::SHA256.hexdigest("#{from_table}_#{column}_fk").first(10)
|
256
287
|
fk_name = options[:name] || "fk_rails_#{hashed_identifier}"
|
257
288
|
|
289
|
+
add_code = constraint_str("ALTER TABLE %s ADD CONSTRAINT %s FOREIGN KEY (%s) REFERENCES %s (%s) NOT VALID", [from_table, fk_name, column, to_table, primary_key])
|
290
|
+
validate_code = constraint_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [from_table, fk_name])
|
291
|
+
|
292
|
+
return safe_add_foreign_key_code(from_table, to_table, add_code, validate_code) if StrongMigrations.safe_by_default
|
293
|
+
|
258
294
|
raise_error :add_foreign_key,
|
259
|
-
add_foreign_key_code:
|
260
|
-
validate_foreign_key_code:
|
295
|
+
add_foreign_key_code: safety_assured_str(add_code),
|
296
|
+
validate_foreign_key_code: safety_assured_str(validate_code)
|
261
297
|
else
|
298
|
+
return safe_add_foreign_key(from_table, to_table, options) if StrongMigrations.safe_by_default
|
299
|
+
|
262
300
|
raise_error :add_foreign_key,
|
263
301
|
add_foreign_key_code: command_str("add_foreign_key", [from_table, to_table, options.merge(validate: false)]),
|
264
302
|
validate_foreign_key_code: command_str("validate_foreign_key", [from_table, to_table])
|
@@ -268,6 +306,29 @@ Then add the foreign key in separate migrations."
|
|
268
306
|
if postgresql? && writes_blocked?
|
269
307
|
raise_error :validate_foreign_key
|
270
308
|
end
|
309
|
+
when :add_check_constraint
|
310
|
+
table, expression, options = args
|
311
|
+
options ||= {}
|
312
|
+
|
313
|
+
if !new_table?(table)
|
314
|
+
if postgresql? && options[:validate] != false
|
315
|
+
add_options = options.merge(validate: false)
|
316
|
+
name = options[:name] || @migration.check_constraint_options(table, expression, options)[:name]
|
317
|
+
validate_options = {name: name}
|
318
|
+
|
319
|
+
return safe_add_check_constraint(table, expression, add_options, validate_options) if StrongMigrations.safe_by_default
|
320
|
+
|
321
|
+
raise_error :add_check_constraint,
|
322
|
+
add_check_constraint_code: command_str("add_check_constraint", [table, expression, add_options]),
|
323
|
+
validate_check_constraint_code: command_str("validate_check_constraint", [table, validate_options])
|
324
|
+
elsif mysql? || mariadb?
|
325
|
+
raise_error :add_check_constraint_mysql
|
326
|
+
end
|
327
|
+
end
|
328
|
+
when :validate_check_constraint
|
329
|
+
if postgresql? && writes_blocked?
|
330
|
+
raise_error :validate_check_constraint
|
331
|
+
end
|
271
332
|
end
|
272
333
|
|
273
334
|
StrongMigrations.checks.each do |check|
|
@@ -279,11 +340,7 @@ Then add the foreign key in separate migrations."
|
|
279
340
|
|
280
341
|
# outdated statistics + a new index can hurt performance of existing queries
|
281
342
|
if StrongMigrations.auto_analyze && direction == :up && method == :add_index
|
282
|
-
|
283
|
-
connection.execute "ANALYZE #{connection.quote_table_name(args[0].to_s)}"
|
284
|
-
elsif mariadb? || mysql?
|
285
|
-
connection.execute "ANALYZE TABLE #{connection.quote_table_name(args[0].to_s)}"
|
286
|
-
end
|
343
|
+
analyze_table(args[0])
|
287
344
|
end
|
288
345
|
|
289
346
|
result
|
@@ -298,7 +355,8 @@ Then add the foreign key in separate migrations."
|
|
298
355
|
if postgresql?
|
299
356
|
"SET statement_timeout TO #{connection.quote(postgresql_timeout(StrongMigrations.statement_timeout))}"
|
300
357
|
elsif mysql?
|
301
|
-
|
358
|
+
# use ceil to prevent no timeout for values under 1 ms
|
359
|
+
"SET max_execution_time = #{connection.quote((StrongMigrations.statement_timeout.to_f * 1000).ceil)}"
|
302
360
|
elsif mariadb?
|
303
361
|
"SET max_statement_time = #{connection.quote(StrongMigrations.statement_timeout)}"
|
304
362
|
else
|
@@ -334,8 +392,7 @@ Then add the foreign key in separate migrations."
|
|
334
392
|
end
|
335
393
|
|
336
394
|
def safe?
|
337
|
-
@safe || ENV["SAFETY_ASSURED"] ||
|
338
|
-
(direction == :down && !StrongMigrations.check_down) || version_safe?
|
395
|
+
@safe || ENV["SAFETY_ASSURED"] || (direction == :down && !StrongMigrations.check_down) || version_safe?
|
339
396
|
end
|
340
397
|
|
341
398
|
def version_safe?
|
@@ -380,6 +437,7 @@ Then add the foreign key in separate migrations."
|
|
380
437
|
end
|
381
438
|
|
382
439
|
def target_version(target_version)
|
440
|
+
target_version ||= StrongMigrations.target_version
|
383
441
|
version =
|
384
442
|
if target_version && StrongMigrations.developer_env?
|
385
443
|
target_version.to_s
|
@@ -389,6 +447,10 @@ Then add the foreign key in separate migrations."
|
|
389
447
|
Gem::Version.new(version)
|
390
448
|
end
|
391
449
|
|
450
|
+
def ar_version
|
451
|
+
ActiveRecord::VERSION::STRING.to_f
|
452
|
+
end
|
453
|
+
|
392
454
|
def check_lock_timeout
|
393
455
|
limit = StrongMigrations.lock_timeout_limit
|
394
456
|
|
@@ -403,6 +465,7 @@ Then add the foreign key in separate migrations."
|
|
403
465
|
end
|
404
466
|
elsif mysql? || mariadb?
|
405
467
|
lock_timeout = connection.select_all("SHOW VARIABLES LIKE 'lock_wait_timeout'").first["Value"]
|
468
|
+
# lock timeout is an integer
|
406
469
|
if lock_timeout.to_i > limit
|
407
470
|
warn "[strong_migrations] DANGER: Lock timeout is longer than #{limit} seconds: #{lock_timeout}"
|
408
471
|
end
|
@@ -435,7 +498,16 @@ Then add the foreign key in separate migrations."
|
|
435
498
|
if timeout.is_a?(String)
|
436
499
|
timeout
|
437
500
|
else
|
438
|
-
timeout
|
501
|
+
# use ceil to prevent no timeout for values under 1 ms
|
502
|
+
(timeout.to_f * 1000).ceil
|
503
|
+
end
|
504
|
+
end
|
505
|
+
|
506
|
+
def analyze_table(table)
|
507
|
+
if postgresql?
|
508
|
+
connection.execute "ANALYZE #{connection.quote_table_name(table.to_s)}"
|
509
|
+
elsif mariadb? || mysql?
|
510
|
+
connection.execute "ANALYZE TABLE #{connection.quote_table_name(table.to_s)}"
|
439
511
|
end
|
440
512
|
end
|
441
513
|
|
@@ -471,7 +543,10 @@ Then add the foreign key in separate migrations."
|
|
471
543
|
|
472
544
|
def constraint_str(statement, identifiers)
|
473
545
|
# not all identifiers are tables, but this method of quoting should be fine
|
474
|
-
|
546
|
+
statement % identifiers.map { |v| connection.quote_table_name(v) }
|
547
|
+
end
|
548
|
+
|
549
|
+
def safety_assured_str(code)
|
475
550
|
"safety_assured do\n execute '#{code}' \n end"
|
476
551
|
end
|
477
552
|
|
@@ -3,9 +3,12 @@ module StrongMigrations
|
|
3
3
|
def migrate(direction)
|
4
4
|
strong_migrations_checker.direction = direction
|
5
5
|
super
|
6
|
+
connection.begin_db_transaction if strong_migrations_checker.transaction_disabled
|
6
7
|
end
|
7
8
|
|
8
9
|
def method_missing(method, *args)
|
10
|
+
return super if is_a?(ActiveRecord::Schema)
|
11
|
+
|
9
12
|
strong_migrations_checker.perform(method, *args) do
|
10
13
|
super
|
11
14
|
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
module StrongMigrations
|
2
|
+
module SafeMethods
|
3
|
+
def safe_by_default_method?(method)
|
4
|
+
StrongMigrations.safe_by_default && [:add_index, :add_belongs_to, :add_reference, :remove_index, :add_foreign_key, :add_check_constraint, :change_column_null].include?(method)
|
5
|
+
end
|
6
|
+
|
7
|
+
# TODO check if invalid index with expected name exists and remove if needed
|
8
|
+
def safe_add_index(table, columns, options)
|
9
|
+
disable_transaction
|
10
|
+
@migration.add_index(table, columns, **options.merge(algorithm: :concurrently))
|
11
|
+
end
|
12
|
+
|
13
|
+
def safe_remove_index(table, options)
|
14
|
+
disable_transaction
|
15
|
+
@migration.remove_index(table, **options.merge(algorithm: :concurrently))
|
16
|
+
end
|
17
|
+
|
18
|
+
def safe_add_reference(table, reference, options)
|
19
|
+
@migration.reversible do |dir|
|
20
|
+
dir.up do
|
21
|
+
disable_transaction
|
22
|
+
foreign_key = options.delete(:foreign_key)
|
23
|
+
@migration.add_reference(table, reference, **options)
|
24
|
+
if foreign_key
|
25
|
+
# same as Active Record
|
26
|
+
name =
|
27
|
+
if foreign_key.is_a?(Hash) && foreign_key[:to_table]
|
28
|
+
foreign_key[:to_table]
|
29
|
+
else
|
30
|
+
(ActiveRecord::Base.pluralize_table_names ? reference.to_s.pluralize : reference).to_sym
|
31
|
+
end
|
32
|
+
|
33
|
+
if reference
|
34
|
+
@migration.add_foreign_key(table, name, column: "#{reference}_id")
|
35
|
+
else
|
36
|
+
@migration.add_foreign_key(table, name)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
dir.down do
|
41
|
+
@migration.remove_reference(table, reference)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def safe_add_foreign_key(from_table, to_table, options)
|
47
|
+
@migration.reversible do |dir|
|
48
|
+
dir.up do
|
49
|
+
@migration.add_foreign_key(from_table, to_table, **options.merge(validate: false))
|
50
|
+
disable_transaction
|
51
|
+
@migration.validate_foreign_key(from_table, to_table)
|
52
|
+
end
|
53
|
+
dir.down do
|
54
|
+
@migration.remove_foreign_key(from_table, to_table)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def safe_add_foreign_key_code(from_table, to_table, add_code, validate_code)
|
60
|
+
@migration.reversible do |dir|
|
61
|
+
dir.up do
|
62
|
+
@migration.safety_assured do
|
63
|
+
@migration.execute(add_code)
|
64
|
+
disable_transaction
|
65
|
+
@migration.execute(validate_code)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
dir.down do
|
69
|
+
@migration.remove_foreign_key(from_table, to_table)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def safe_add_check_constraint(table, expression, add_options, validate_options)
|
75
|
+
@migration.reversible do |dir|
|
76
|
+
dir.up do
|
77
|
+
@migration.add_check_constraint(table, expression, **add_options)
|
78
|
+
disable_transaction
|
79
|
+
@migration.validate_check_constraint(table, **validate_options)
|
80
|
+
end
|
81
|
+
dir.down do
|
82
|
+
@migration.remove_check_constraint(table, expression, **add_options)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def safe_change_column_null(add_code, validate_code, change_args, remove_code)
|
88
|
+
@migration.reversible do |dir|
|
89
|
+
dir.up do
|
90
|
+
@migration.safety_assured do
|
91
|
+
@migration.execute(add_code)
|
92
|
+
disable_transaction
|
93
|
+
@migration.execute(validate_code)
|
94
|
+
end
|
95
|
+
if change_args
|
96
|
+
@migration.change_column_null(*change_args)
|
97
|
+
@migration.safety_assured do
|
98
|
+
@migration.execute(remove_code)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
102
|
+
dir.down do
|
103
|
+
if change_args
|
104
|
+
down_args = change_args.dup
|
105
|
+
down_args[2] = true
|
106
|
+
@migration.change_column_null(*down_args)
|
107
|
+
else
|
108
|
+
@migration.safety_assured do
|
109
|
+
@migration.execute(remove_code)
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
# hard to commit at right time when reverting
|
117
|
+
# so just commit at start
|
118
|
+
def disable_transaction
|
119
|
+
if in_transaction? && !transaction_disabled
|
120
|
+
@migration.connection.commit_db_transaction
|
121
|
+
self.transaction_disabled = true
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def in_transaction?
|
126
|
+
@migration.connection.open_transactions > 0
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
data/lib/strong_migrations.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
require "active_support"
|
3
3
|
|
4
4
|
# modules
|
5
|
+
require "strong_migrations/safe_methods"
|
5
6
|
require "strong_migrations/checker"
|
6
7
|
require "strong_migrations/database_tasks"
|
7
8
|
require "strong_migrations/migration"
|
@@ -17,12 +18,14 @@ module StrongMigrations
|
|
17
18
|
class << self
|
18
19
|
attr_accessor :auto_analyze, :start_after, :checks, :error_messages,
|
19
20
|
:target_postgresql_version, :target_mysql_version, :target_mariadb_version,
|
20
|
-
:enabled_checks, :lock_timeout, :statement_timeout, :check_down
|
21
|
+
:enabled_checks, :lock_timeout, :statement_timeout, :check_down, :target_version,
|
22
|
+
:safe_by_default
|
21
23
|
attr_writer :lock_timeout_limit
|
22
24
|
end
|
23
25
|
self.auto_analyze = false
|
24
26
|
self.start_after = 0
|
25
27
|
self.checks = []
|
28
|
+
self.safe_by_default = false
|
26
29
|
self.error_messages = {
|
27
30
|
add_column_default:
|
28
31
|
"Adding a column with a non-null default blocks %{rewrite_blocks} while the entire table is rewritten.
|
@@ -213,6 +216,30 @@ end",
|
|
213
216
|
|
214
217
|
validate_foreign_key:
|
215
218
|
"Validating a foreign key while writes are blocked is dangerous.
|
219
|
+
Use disable_ddl_transaction! or a separate migration.",
|
220
|
+
|
221
|
+
add_check_constraint:
|
222
|
+
"Adding a check constraint key blocks reads and writes while every row is checked.
|
223
|
+
Instead, add the check constraint without validating existing rows,
|
224
|
+
then validate them in a separate migration.
|
225
|
+
|
226
|
+
class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
227
|
+
def change
|
228
|
+
%{add_check_constraint_code}
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
class Validate%{migration_name} < ActiveRecord::Migration%{migration_suffix}
|
233
|
+
def change
|
234
|
+
%{validate_check_constraint_code}
|
235
|
+
end
|
236
|
+
end",
|
237
|
+
|
238
|
+
add_check_constraint_mysql:
|
239
|
+
"Adding a check constraint to an existing table is not safe with your database engine.",
|
240
|
+
|
241
|
+
validate_check_constraint:
|
242
|
+
"Validating a check constraint while writes are blocked is dangerous.
|
216
243
|
Use disable_ddl_transaction! or a separate migration."
|
217
244
|
}
|
218
245
|
self.enabled_checks = (error_messages.keys - [:remove_index]).map { |k| [k, {}] }.to_h
|
metadata
CHANGED
@@ -1,16 +1,16 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: strong_migrations
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.7.
|
4
|
+
version: 0.7.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Andrew Kane
|
8
8
|
- Bob Remeika
|
9
9
|
- David Waller
|
10
|
-
autorequire:
|
10
|
+
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date:
|
13
|
+
date: 2021-08-03 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: activerecord
|
@@ -96,7 +96,7 @@ dependencies:
|
|
96
96
|
- - ">="
|
97
97
|
- !ruby/object:Gem::Version
|
98
98
|
version: '0'
|
99
|
-
description:
|
99
|
+
description:
|
100
100
|
email:
|
101
101
|
- andrew@chartkick.com
|
102
102
|
- bob.remeika@gmail.com
|
@@ -116,13 +116,14 @@ files:
|
|
116
116
|
- lib/strong_migrations/database_tasks.rb
|
117
117
|
- lib/strong_migrations/migration.rb
|
118
118
|
- lib/strong_migrations/railtie.rb
|
119
|
+
- lib/strong_migrations/safe_methods.rb
|
119
120
|
- lib/strong_migrations/version.rb
|
120
121
|
- lib/tasks/strong_migrations.rake
|
121
122
|
homepage: https://github.com/ankane/strong_migrations
|
122
123
|
licenses:
|
123
124
|
- MIT
|
124
125
|
metadata: {}
|
125
|
-
post_install_message:
|
126
|
+
post_install_message:
|
126
127
|
rdoc_options: []
|
127
128
|
require_paths:
|
128
129
|
- lib
|
@@ -137,8 +138,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
137
138
|
- !ruby/object:Gem::Version
|
138
139
|
version: '0'
|
139
140
|
requirements: []
|
140
|
-
rubygems_version: 3.
|
141
|
-
signing_key:
|
141
|
+
rubygems_version: 3.2.22
|
142
|
+
signing_key:
|
142
143
|
specification_version: 4
|
143
144
|
summary: Catch unsafe migrations in development
|
144
145
|
test_files: []
|