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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2ef314694846eec868e7e01b74fcfc9a29d361e01e3bc9102914a0bd3ba0facc
4
- data.tar.gz: b27a43c73b0c394bd30579db15a412ade64d53fcc72a5d9a9b8fc35695d3aedd
3
+ metadata.gz: 38cd87929d4889113bc2cc13aa786965c8b86fadf05d1d0439e2c927becfef61
4
+ data.tar.gz: 0f17d511058c0bd1852adcddf9a2e978b8e407002ddf201be7aedb6c797dd294
5
5
  SHA512:
6
- metadata.gz: 793918328d63451ef6a6ec3b90b3d2a4082ef2c57e018fa0f84e223defe506c3722f7e2168ef97854fcade562dc115943d9655954fdd9a319a13dca6ec50648b
7
- data.tar.gz: c353256d7854ea1240bfc1e3f6e0ce7b8c76b72404f728c37695f923c4189777463481d57abc55efdce7a0326621a6a61ca9fcb98638e2f2aa279496d126bd20
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
@@ -1,4 +1,4 @@
1
- Copyright (c) 2013 Bob Remeika and David Waller, 2015-2020 Andrew Kane
1
+ Copyright (c) 2013 Bob Remeika and David Waller, 2015-2021 Andrew Kane
2
2
 
3
3
  MIT License
4
4
 
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://travis-ci.org/ankane/strong_migrations.svg?branch=master)](https://travis-ci.org/ankane/strong_migrations)
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.0]
46
+ class RemoveColumn < ActiveRecord::Migration[6.1]
47
47
  def change
48
- safety_assured { remove_column :users, :name, :string }
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.0]
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.0]
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.0]
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.0]
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.0]
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.0]
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.0]
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.0]
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.0]
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.0]
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.0]
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 the every row is checked.
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.0]
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, but it won’t show up in `schema.rb`. In Postgres 12+, once the check constraint is validated, you can safely set `NOT NULL` on the column and drop the check constraint.
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.0]
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.0]
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.0]
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.0]
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.0]
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.0]
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.0]
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.0]
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.0]
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.0]
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.0]
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.0]
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.0]
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.0]
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 [unreleased]
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.target_postgresql_version = "10"
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, create an initializer with:
821
+ Only dump the schema when adding a new migration. If you use Git, add to the end of your `Rakefile`:
728
822
 
729
- ```ruby
730
- ActiveRecord::Base.dump_schema_after_migration = Rails.env.development? &&
731
- `git status db/migrate/ --porcelain`.present?
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
- attr_accessor :direction
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
- unless safe?
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
- # TODO likely need to quote the column in some situations
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
- validate_constraint_code = String.new(constraint_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [table, constraint_name]))
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
- validate_constraint_code << "\n #{command_str(:change_column_null, [table, column, null])}"
229
- validate_constraint_code << "\n #{constraint_str("ALTER TABLE %s DROP CONSTRAINT %s", [table, constraint_name])}"
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: constraint_str("ALTER TABLE %s ADD CONSTRAINT %s CHECK (%s IS NOT NULL) NOT VALID", [table, constraint_name, column]),
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) || ActiveRecord::VERSION::STRING < "5.2"
279
+ validate = options.fetch(:validate, true) || ar_version < 5.2
249
280
 
250
281
  if postgresql? && validate
251
- if ActiveRecord::VERSION::STRING < "5.2"
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: 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]),
260
- validate_foreign_key_code: constraint_str("ALTER TABLE %s VALIDATE CONSTRAINT %s", [from_table, fk_name])
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
- if postgresql?
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
- "SET max_execution_time = #{connection.quote(StrongMigrations.statement_timeout.to_i * 1000)}"
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"] || @migration.is_a?(ActiveRecord::Schema) ||
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.to_i * 1000
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
- code = statement % identifiers.map { |v| connection.quote_table_name(v) }
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
@@ -1,3 +1,3 @@
1
1
  module StrongMigrations
2
- VERSION = "0.7.0"
2
+ VERSION = "0.7.8"
3
3
  end
@@ -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.0
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: 2020-07-23 00:00:00.000000000 Z
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.1.2
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: []