strong_migrations 0.7.2 → 0.7.7

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: f3410e3379861436cbec2b46796069db388fffea222fa50ff46b054a153fbdaf
4
- data.tar.gz: f328bee4412388114e888557d9b691ed3dad10bd0a5bf1ffb93c09f0b48d6696
3
+ metadata.gz: de58409c64c5050d99432576f5d38f615de75832de7214960f9f686248bfa31c
4
+ data.tar.gz: d6113e7875c2b0f75e4548117692af874ffa12d7ebea731ed94344069661399d
5
5
  SHA512:
6
- metadata.gz: 7e588f76685aa486f38949629b2f0aebe5f1acf92a5b53a29b4e4e1f5f334439eec3ef915240736caf55cff1f21197369305ec073c6adbbedaf6477aabb282cb
7
- data.tar.gz: 3e350c8a68ec076295d36aac330ff9eae5ad75329b9775fb8839319f7fbc99c62b8ea58ca5fc57ec36c750a510f9ae743a76fd6fedc7fd755d1e07b7ab7230b5
6
+ metadata.gz: 23655309809f41dbe53842ee01ac8f573153ed49936f003bef0076591ec26f2bff7464c32fbf35ca8e2037e2eb8227c0e6c12b2e8c609e9cd05ccd6127474620
7
+ data.tar.gz: 501307d37d8577552735553b0e4b6f96f9afa415d3ed5737a06f16c44227ad7996daea70a9b2a480303678135c45806c410eacce03e125a92c2929b531bbdb34
data/CHANGELOG.md CHANGED
@@ -1,3 +1,25 @@
1
+ ## 0.7.7 (2021-06-07)
2
+
3
+ - Removed timeouts and `auto_analyze` from schema load
4
+
5
+ ## 0.7.6 (2021-01-17)
6
+
7
+ - Fixed `NOT NULL` constraint check for quoted columns
8
+ - Fixed deprecation warning with Active Record 6.1
9
+
10
+ ## 0.7.5 (2021-01-12)
11
+
12
+ - Added checks for `add_check_constraint` and `validate_check_constraint`
13
+
14
+ ## 0.7.4 (2020-12-16)
15
+
16
+ - Added `safe_by_default` option to install generator
17
+ - Fixed warnings with Active Record 6.1
18
+
19
+ ## 0.7.3 (2020-11-24)
20
+
21
+ - Added `safe_by_default` option
22
+
1
23
  ## 0.7.2 (2020-10-25)
2
24
 
3
25
  - Added support for float timeouts
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,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.0]
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.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
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:
@@ -13,8 +13,14 @@ module StrongMigrations
13
13
  Time.now.utc.strftime("%Y%m%d%H%M%S")
14
14
  end
15
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
+
16
22
  def target_version
17
- case ActiveRecord::Base.connection_config[:adapter].to_s
23
+ case adapter
18
24
  when /mysql/
19
25
  # could try to connect to database and check for MariaDB
20
26
  # but this should be fine
@@ -23,6 +29,18 @@ module StrongMigrations
23
29
  "10"
24
30
  end
25
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
26
44
  end
27
45
  end
28
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
 
@@ -19,4 +18,8 @@ StrongMigrations.auto_analyze = true
19
18
  # if method == :add_index && args[0].to_s == "users"
20
19
  # stop! "No more indexes on the users table"
21
20
  # end
22
- # 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 %>
@@ -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, :target_version
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
@@ -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
@@ -335,8 +392,7 @@ Then add the foreign key in separate migrations."
335
392
  end
336
393
 
337
394
  def safe?
338
- @safe || ENV["SAFETY_ASSURED"] || @migration.is_a?(ActiveRecord::Schema) ||
339
- (direction == :down && !StrongMigrations.check_down) || version_safe?
395
+ @safe || ENV["SAFETY_ASSURED"] || (direction == :down && !StrongMigrations.check_down) || version_safe?
340
396
  end
341
397
 
342
398
  def version_safe?
@@ -391,6 +447,10 @@ Then add the foreign key in separate migrations."
391
447
  Gem::Version.new(version)
392
448
  end
393
449
 
450
+ def ar_version
451
+ ActiveRecord::VERSION::STRING.to_f
452
+ end
453
+
394
454
  def check_lock_timeout
395
455
  limit = StrongMigrations.lock_timeout_limit
396
456
 
@@ -443,6 +503,14 @@ Then add the foreign key in separate migrations."
443
503
  end
444
504
  end
445
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
+
446
514
  def constraints(table_name)
447
515
  query = <<~SQL
448
516
  SELECT
@@ -475,7 +543,10 @@ Then add the foreign key in separate migrations."
475
543
 
476
544
  def constraint_str(statement, identifiers)
477
545
  # not all identifiers are tables, but this method of quoting should be fine
478
- 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)
479
550
  "safety_assured do\n execute '#{code}' \n end"
480
551
  end
481
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,125 @@
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
+ @migration.add_foreign_key(table, name)
34
+ end
35
+ end
36
+ dir.down do
37
+ @migration.remove_reference(table, reference)
38
+ end
39
+ end
40
+ end
41
+
42
+ def safe_add_foreign_key(from_table, to_table, options)
43
+ @migration.reversible do |dir|
44
+ dir.up do
45
+ @migration.add_foreign_key(from_table, to_table, **options.merge(validate: false))
46
+ disable_transaction
47
+ @migration.validate_foreign_key(from_table, to_table)
48
+ end
49
+ dir.down do
50
+ @migration.remove_foreign_key(from_table, to_table)
51
+ end
52
+ end
53
+ end
54
+
55
+ def safe_add_foreign_key_code(from_table, to_table, add_code, validate_code)
56
+ @migration.reversible do |dir|
57
+ dir.up do
58
+ @migration.safety_assured do
59
+ @migration.execute(add_code)
60
+ disable_transaction
61
+ @migration.execute(validate_code)
62
+ end
63
+ end
64
+ dir.down do
65
+ @migration.remove_foreign_key(from_table, to_table)
66
+ end
67
+ end
68
+ end
69
+
70
+ def safe_add_check_constraint(table, expression, add_options, validate_options)
71
+ @migration.reversible do |dir|
72
+ dir.up do
73
+ @migration.add_check_constraint(table, expression, **add_options)
74
+ disable_transaction
75
+ @migration.validate_check_constraint(table, **validate_options)
76
+ end
77
+ dir.down do
78
+ @migration.remove_check_constraint(table, expression, **add_options)
79
+ end
80
+ end
81
+ end
82
+
83
+ def safe_change_column_null(add_code, validate_code, change_args, remove_code)
84
+ @migration.reversible do |dir|
85
+ dir.up do
86
+ @migration.safety_assured do
87
+ @migration.execute(add_code)
88
+ disable_transaction
89
+ @migration.execute(validate_code)
90
+ end
91
+ if change_args
92
+ @migration.change_column_null(*change_args)
93
+ @migration.safety_assured do
94
+ @migration.execute(remove_code)
95
+ end
96
+ end
97
+ end
98
+ dir.down do
99
+ if change_args
100
+ down_args = change_args.dup
101
+ down_args[2] = true
102
+ @migration.change_column_null(*down_args)
103
+ else
104
+ @migration.safety_assured do
105
+ @migration.execute(remove_code)
106
+ end
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ # hard to commit at right time when reverting
113
+ # so just commit at start
114
+ def disable_transaction
115
+ if in_transaction? && !transaction_disabled
116
+ @migration.connection.commit_db_transaction
117
+ self.transaction_disabled = true
118
+ end
119
+ end
120
+
121
+ def in_transaction?
122
+ @migration.connection.open_transactions > 0
123
+ end
124
+ end
125
+ end
@@ -1,3 +1,3 @@
1
1
  module StrongMigrations
2
- VERSION = "0.7.2"
2
+ VERSION = "0.7.7"
3
3
  end
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.2
4
+ version: 0.7.7
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: 2020-10-25 00:00:00.000000000 Z
13
+ date: 2021-06-07 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -116,6 +116,7 @@ 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
@@ -137,7 +138,7 @@ 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.4
141
+ rubygems_version: 3.2.3
141
142
  signing_key:
142
143
  specification_version: 4
143
144
  summary: Catch unsafe migrations in development