strong_migrations 0.7.4 → 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 +17 -0
- data/LICENSE.txt +1 -1
- data/README.md +109 -35
- data/lib/generators/strong_migrations/install_generator.rb +5 -1
- data/lib/strong_migrations.rb +24 -0
- data/lib/strong_migrations/checker.rb +63 -14
- data/lib/strong_migrations/migration.rb +2 -0
- data/lib/strong_migrations/safe_methods.rb +19 -2
- data/lib/strong_migrations/version.rb +1 -1
- metadata +3 -3
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,20 @@
|
|
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
|
+
|
1
18
|
## 0.7.4 (2020-12-16)
|
2
19
|
|
3
20
|
- Added `safe_by_default` option to install generator
|
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -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[6.
|
46
|
+
class RemoveColumn < ActiveRecord::Migration[6.1]
|
47
47
|
def change
|
48
48
|
safety_assured { remove_column :users, :name }
|
49
49
|
end
|
@@ -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,6 +310,48 @@ 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
|
|
314
357
|
:turtle: Safe by default available
|
@@ -318,7 +361,7 @@ If you intend to drop an existing table, run `drop_table` first.
|
|
318
361
|
Setting `NOT NULL` on an existing column blocks reads and writes while every row is checked.
|
319
362
|
|
320
363
|
```ruby
|
321
|
-
class SetSomeColumnNotNull < ActiveRecord::Migration[6.
|
364
|
+
class SetSomeColumnNotNull < ActiveRecord::Migration[6.1]
|
322
365
|
def change
|
323
366
|
change_column_null :users, :some_column, false
|
324
367
|
end
|
@@ -327,7 +370,19 @@ end
|
|
327
370
|
|
328
371
|
#### Good - Postgres
|
329
372
|
|
330
|
-
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:
|
331
386
|
|
332
387
|
```ruby
|
333
388
|
class SetSomeColumnNotNull < ActiveRecord::Migration[6.0]
|
@@ -339,7 +394,23 @@ class SetSomeColumnNotNull < ActiveRecord::Migration[6.0]
|
|
339
394
|
end
|
340
395
|
```
|
341
396
|
|
342
|
-
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:
|
343
414
|
|
344
415
|
```ruby
|
345
416
|
class ValidateSomeColumnNotNull < ActiveRecord::Migration[6.0]
|
@@ -366,7 +437,7 @@ end
|
|
366
437
|
Strong Migrations can’t ensure safety for raw SQL statements. Make really sure that what you’re doing is safe, then use:
|
367
438
|
|
368
439
|
```ruby
|
369
|
-
class ExecuteSQL < ActiveRecord::Migration[6.
|
440
|
+
class ExecuteSQL < ActiveRecord::Migration[6.1]
|
370
441
|
def change
|
371
442
|
safety_assured { execute "..." }
|
372
443
|
end
|
@@ -382,7 +453,7 @@ end
|
|
382
453
|
In Postgres, adding an index non-concurrently blocks writes.
|
383
454
|
|
384
455
|
```ruby
|
385
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[6.
|
456
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[6.1]
|
386
457
|
def change
|
387
458
|
add_index :users, :some_column
|
388
459
|
end
|
@@ -394,7 +465,7 @@ end
|
|
394
465
|
Add indexes concurrently.
|
395
466
|
|
396
467
|
```ruby
|
397
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[6.
|
468
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[6.1]
|
398
469
|
disable_ddl_transaction!
|
399
470
|
|
400
471
|
def change
|
@@ -420,7 +491,7 @@ rails g index table column
|
|
420
491
|
Rails adds an index non-concurrently to references by default, which blocks writes in Postgres.
|
421
492
|
|
422
493
|
```ruby
|
423
|
-
class AddReferenceToUsers < ActiveRecord::Migration[6.
|
494
|
+
class AddReferenceToUsers < ActiveRecord::Migration[6.1]
|
424
495
|
def change
|
425
496
|
add_reference :users, :city
|
426
497
|
end
|
@@ -432,7 +503,7 @@ end
|
|
432
503
|
Make sure the index is added concurrently.
|
433
504
|
|
434
505
|
```ruby
|
435
|
-
class AddReferenceToUsers < ActiveRecord::Migration[6.
|
506
|
+
class AddReferenceToUsers < ActiveRecord::Migration[6.1]
|
436
507
|
disable_ddl_transaction!
|
437
508
|
|
438
509
|
def change
|
@@ -450,7 +521,7 @@ end
|
|
450
521
|
In Postgres, adding a foreign key blocks writes on both tables.
|
451
522
|
|
452
523
|
```ruby
|
453
|
-
class AddForeignKeyOnUsers < ActiveRecord::Migration[6.
|
524
|
+
class AddForeignKeyOnUsers < ActiveRecord::Migration[6.1]
|
454
525
|
def change
|
455
526
|
add_foreign_key :users, :orders
|
456
527
|
end
|
@@ -460,7 +531,7 @@ end
|
|
460
531
|
or
|
461
532
|
|
462
533
|
```ruby
|
463
|
-
class AddReferenceToUsers < ActiveRecord::Migration[6.
|
534
|
+
class AddReferenceToUsers < ActiveRecord::Migration[6.1]
|
464
535
|
def change
|
465
536
|
add_reference :users, :order, foreign_key: true
|
466
537
|
end
|
@@ -474,7 +545,7 @@ Add the foreign key without validating existing rows, then validate them in a se
|
|
474
545
|
For Rails 5.2+, use:
|
475
546
|
|
476
547
|
```ruby
|
477
|
-
class AddForeignKeyOnUsers < ActiveRecord::Migration[6.
|
548
|
+
class AddForeignKeyOnUsers < ActiveRecord::Migration[6.1]
|
478
549
|
def change
|
479
550
|
add_foreign_key :users, :orders, validate: false
|
480
551
|
end
|
@@ -484,7 +555,7 @@ end
|
|
484
555
|
Then:
|
485
556
|
|
486
557
|
```ruby
|
487
|
-
class ValidateForeignKeyOnUsers < ActiveRecord::Migration[6.
|
558
|
+
class ValidateForeignKeyOnUsers < ActiveRecord::Migration[6.1]
|
488
559
|
def change
|
489
560
|
validate_foreign_key :users, :orders
|
490
561
|
end
|
@@ -522,7 +593,7 @@ end
|
|
522
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.
|
523
594
|
|
524
595
|
```ruby
|
525
|
-
class AddPropertiesToUsers < ActiveRecord::Migration[6.
|
596
|
+
class AddPropertiesToUsers < ActiveRecord::Migration[6.1]
|
526
597
|
def change
|
527
598
|
add_column :users, :properties, :json
|
528
599
|
end
|
@@ -534,7 +605,7 @@ end
|
|
534
605
|
Use `jsonb` instead.
|
535
606
|
|
536
607
|
```ruby
|
537
|
-
class AddPropertiesToUsers < ActiveRecord::Migration[6.
|
608
|
+
class AddPropertiesToUsers < ActiveRecord::Migration[6.1]
|
538
609
|
def change
|
539
610
|
add_column :users, :properties, :jsonb
|
540
611
|
end
|
@@ -548,7 +619,7 @@ end
|
|
548
619
|
Adding a non-unique index with more than three columns rarely improves performance.
|
549
620
|
|
550
621
|
```ruby
|
551
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[6.
|
622
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[6.1]
|
552
623
|
def change
|
553
624
|
add_index :users, [:a, :b, :c, :d]
|
554
625
|
end
|
@@ -560,7 +631,7 @@ end
|
|
560
631
|
Instead, start an index with columns that narrow down the results the most.
|
561
632
|
|
562
633
|
```ruby
|
563
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[6.
|
634
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[6.1]
|
564
635
|
def change
|
565
636
|
add_index :users, [:b, :d]
|
566
637
|
end
|
@@ -574,7 +645,7 @@ For Postgres, be sure to add them concurrently.
|
|
574
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.
|
575
646
|
|
576
647
|
```ruby
|
577
|
-
class MySafeMigration < ActiveRecord::Migration[6.
|
648
|
+
class MySafeMigration < ActiveRecord::Migration[6.1]
|
578
649
|
def change
|
579
650
|
safety_assured { remove_column :users, :some_column }
|
580
651
|
end
|
@@ -585,12 +656,11 @@ Certain methods like `execute` and `change_table` cannot be inspected and are pr
|
|
585
656
|
|
586
657
|
## Safe by Default
|
587
658
|
|
588
|
-
*Experimental*
|
589
|
-
|
590
659
|
Make operations safe by default.
|
591
660
|
|
592
661
|
- adding and removing an index
|
593
662
|
- adding a foreign key
|
663
|
+
- adding a check constraint
|
594
664
|
- setting NOT NULL on an existing column
|
595
665
|
|
596
666
|
Add to `config/initializers/strong_migrations.rb`:
|
@@ -748,11 +818,15 @@ StrongMigrations.auto_analyze = true
|
|
748
818
|
|
749
819
|
## Faster Migrations
|
750
820
|
|
751
|
-
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`:
|
752
822
|
|
753
|
-
```
|
754
|
-
|
755
|
-
|
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"
|
756
830
|
```
|
757
831
|
|
758
832
|
## Schema Sanity
|
@@ -31,7 +31,11 @@ module StrongMigrations
|
|
31
31
|
end
|
32
32
|
|
33
33
|
def adapter
|
34
|
-
ActiveRecord::
|
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
|
35
39
|
end
|
36
40
|
|
37
41
|
def postgresql?
|
data/lib/strong_migrations.rb
CHANGED
@@ -216,6 +216,30 @@ end",
|
|
216
216
|
|
217
217
|
validate_foreign_key:
|
218
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.
|
219
243
|
Use disable_ddl_transaction! or a separate migration."
|
220
244
|
}
|
221
245
|
self.enabled_checks = (error_messages.keys - [:remove_index]).map { |k| [k, {}] }.to_h
|
@@ -219,8 +219,7 @@ Then add the foreign key in separate migrations."
|
|
219
219
|
if postgresql?
|
220
220
|
safe = false
|
221
221
|
if postgresql_version >= Gem::Version.new("12")
|
222
|
-
#
|
223
|
-
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))" }
|
224
223
|
end
|
225
224
|
|
226
225
|
unless safe
|
@@ -231,18 +230,38 @@ Then add the foreign key in separate migrations."
|
|
231
230
|
validate_code = constraint_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [table, constraint_name])
|
232
231
|
remove_code = constraint_str("ALTER TABLE %s DROP CONSTRAINT %s", [table, constraint_name])
|
233
232
|
|
234
|
-
validate_constraint_code =
|
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
|
+
|
235
240
|
if postgresql_version >= Gem::Version.new("12")
|
236
241
|
change_args = [table, column, null]
|
237
242
|
|
238
243
|
validate_constraint_code << "\n #{command_str(:change_column_null, change_args)}"
|
239
|
-
|
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
|
240
250
|
end
|
241
251
|
|
242
252
|
return safe_change_column_null(add_code, validate_code, change_args, remove_code) if StrongMigrations.safe_by_default
|
243
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
|
+
|
244
263
|
raise_error :change_column_null_postgresql,
|
245
|
-
add_constraint_code:
|
264
|
+
add_constraint_code: add_constraint_code,
|
246
265
|
validate_constraint_code: validate_constraint_code
|
247
266
|
end
|
248
267
|
elsif mysql? || mariadb?
|
@@ -257,10 +276,10 @@ Then add the foreign key in separate migrations."
|
|
257
276
|
options ||= {}
|
258
277
|
|
259
278
|
# always validated before 5.2
|
260
|
-
validate = options.fetch(:validate, true) ||
|
279
|
+
validate = options.fetch(:validate, true) || ar_version < 5.2
|
261
280
|
|
262
281
|
if postgresql? && validate
|
263
|
-
if
|
282
|
+
if ar_version < 5.2
|
264
283
|
# fk name logic from rails
|
265
284
|
primary_key = options[:primary_key] || "id"
|
266
285
|
column = options[:column] || "#{to_table.to_s.singularize}_id"
|
@@ -287,6 +306,29 @@ Then add the foreign key in separate migrations."
|
|
287
306
|
if postgresql? && writes_blocked?
|
288
307
|
raise_error :validate_foreign_key
|
289
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
|
290
332
|
end
|
291
333
|
|
292
334
|
StrongMigrations.checks.each do |check|
|
@@ -298,11 +340,7 @@ Then add the foreign key in separate migrations."
|
|
298
340
|
|
299
341
|
# outdated statistics + a new index can hurt performance of existing queries
|
300
342
|
if StrongMigrations.auto_analyze && direction == :up && method == :add_index
|
301
|
-
|
302
|
-
connection.execute "ANALYZE #{connection.quote_table_name(args[0].to_s)}"
|
303
|
-
elsif mariadb? || mysql?
|
304
|
-
connection.execute "ANALYZE TABLE #{connection.quote_table_name(args[0].to_s)}"
|
305
|
-
end
|
343
|
+
analyze_table(args[0])
|
306
344
|
end
|
307
345
|
|
308
346
|
result
|
@@ -354,8 +392,7 @@ Then add the foreign key in separate migrations."
|
|
354
392
|
end
|
355
393
|
|
356
394
|
def safe?
|
357
|
-
@safe || ENV["SAFETY_ASSURED"] ||
|
358
|
-
(direction == :down && !StrongMigrations.check_down) || version_safe?
|
395
|
+
@safe || ENV["SAFETY_ASSURED"] || (direction == :down && !StrongMigrations.check_down) || version_safe?
|
359
396
|
end
|
360
397
|
|
361
398
|
def version_safe?
|
@@ -410,6 +447,10 @@ Then add the foreign key in separate migrations."
|
|
410
447
|
Gem::Version.new(version)
|
411
448
|
end
|
412
449
|
|
450
|
+
def ar_version
|
451
|
+
ActiveRecord::VERSION::STRING.to_f
|
452
|
+
end
|
453
|
+
|
413
454
|
def check_lock_timeout
|
414
455
|
limit = StrongMigrations.lock_timeout_limit
|
415
456
|
|
@@ -462,6 +503,14 @@ Then add the foreign key in separate migrations."
|
|
462
503
|
end
|
463
504
|
end
|
464
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)}"
|
511
|
+
end
|
512
|
+
end
|
513
|
+
|
465
514
|
def constraints(table_name)
|
466
515
|
query = <<~SQL
|
467
516
|
SELECT
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module StrongMigrations
|
2
2
|
module SafeMethods
|
3
3
|
def safe_by_default_method?(method)
|
4
|
-
StrongMigrations.safe_by_default && [:add_index, :add_belongs_to, :add_reference, :remove_index, :add_foreign_key, :change_column_null].include?(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
5
|
end
|
6
6
|
|
7
7
|
# TODO check if invalid index with expected name exists and remove if needed
|
@@ -30,7 +30,11 @@ module StrongMigrations
|
|
30
30
|
(ActiveRecord::Base.pluralize_table_names ? reference.to_s.pluralize : reference).to_sym
|
31
31
|
end
|
32
32
|
|
33
|
-
|
33
|
+
if reference
|
34
|
+
@migration.add_foreign_key(table, name, column: "#{reference}_id")
|
35
|
+
else
|
36
|
+
@migration.add_foreign_key(table, name)
|
37
|
+
end
|
34
38
|
end
|
35
39
|
end
|
36
40
|
dir.down do
|
@@ -67,6 +71,19 @@ module StrongMigrations
|
|
67
71
|
end
|
68
72
|
end
|
69
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
|
+
|
70
87
|
def safe_change_column_null(add_code, validate_code, change_args, remove_code)
|
71
88
|
@migration.reversible do |dir|
|
72
89
|
dir.up do
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
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
|
@@ -10,7 +10,7 @@ authors:
|
|
10
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
|
@@ -138,7 +138,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
138
138
|
- !ruby/object:Gem::Version
|
139
139
|
version: '0'
|
140
140
|
requirements: []
|
141
|
-
rubygems_version: 3.2.
|
141
|
+
rubygems_version: 3.2.22
|
142
142
|
signing_key:
|
143
143
|
specification_version: 4
|
144
144
|
summary: Catch unsafe migrations in development
|