strong_migrations 0.8.0 → 1.0.0

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: 1f885cc0a8b0fb30e3bc4d000e82584fdf3850943563b4b426d3cfc39d70015c
4
- data.tar.gz: d0dd29ea45ccc3b7c571e8bf03fa98c401d5489d1f5459488a0eff411cf6880e
3
+ metadata.gz: a22e260ec0e3e09954c65535725a3ddc4439b9cf94182a21cf5963e12d8485fe
4
+ data.tar.gz: 865633da561d77e615df16a9a00f8524a95bf73edc5590e3932855aaf544d4f7
5
5
  SHA512:
6
- metadata.gz: c03a023d4c593d2868e524ca0f3e7ebd93e07e087e02f0d7090210b00fdb1fdc50d2abb74ab7df4131bddc6052fc5b9c10ce902ede8c708927476e40ad49d07f
7
- data.tar.gz: 479e34db0101a15e1db1a593f01773da5665baa5cda3403ad32ea58dca00a0d0faee0ef813cf1975b7e9f99f1591e7f4f2be5c456c96fd6ae4633b48b6a0b3b4
6
+ metadata.gz: f9b4df7a67aae2c8e1f7c58327b0e7e298261a33d9810dff4b5634bee3e000f1b26e64ad7b3997c8fc6c1cfef3a5cbeabe35ecbe508ed902679e3c9a720ecfb8
7
+ data.tar.gz: f4a7ecb6e64c40d1be1025a978387b94b9c2e47897cc25568c32dc32043292bc8fe14c9e6471108a4d84a1b1704459707f4fcf8a25227b7a86d8bcf2237dc19d
data/CHANGELOG.md CHANGED
@@ -1,3 +1,32 @@
1
+ ## 1.0.0 (2022-03-21)
2
+
3
+ New safe operations with MySQL and MariaDB
4
+
5
+ - Setting `NOT NULL` on an existing column with strict mode enabled
6
+
7
+ New safe operations with Postgres
8
+
9
+ - Changing between `text` and `citext` when not indexed
10
+ - Changing a `string` column to a `citext` column when not indexed
11
+ - Changing a `citext` column to a `string` column with no `:limit` when not indexed
12
+ - Changing a `cidr` column to an `inet` column
13
+ - Increasing `:precision` of an `interval` or `time` column
14
+
15
+ New unsafe operations with Postgres
16
+
17
+ - Adding a column with a callable default value
18
+ - Decreasing `:precision` of a `datetime` column
19
+ - Decreasing `:limit` of a `timestamptz` column
20
+ - Passing a default value to `change_column_null`
21
+
22
+ Other
23
+
24
+ - Added experimental support for lock timeout retries
25
+ - Added `target_sql_mode` option
26
+ - Added error for `change_column_null` with default value with `safe_by_default` option
27
+ - Fixed instructions for `remove_columns` with options
28
+ - Dropped support for Postgres < 10, MySQL < 5.7, and MariaDB < 10.2
29
+
1
30
  ## 0.8.0 (2022-02-09)
2
31
 
3
32
  - Fixed error with versioned schema with Active Record 7.0.2+
data/README.md CHANGED
@@ -4,7 +4,7 @@ Catch unsafe migrations in development
4
4
 
5
5
  &nbsp;&nbsp;✓&nbsp;&nbsp;Detects potentially dangerous operations<br />&nbsp;&nbsp;✓&nbsp;&nbsp;Prevents them from running by default<br />&nbsp;&nbsp;✓&nbsp;&nbsp;Provides instructions on safer ways to do what you want
6
6
 
7
- Supports for PostgreSQL, MySQL, and MariaDB
7
+ Supports PostgreSQL, MySQL, and MariaDB
8
8
 
9
9
  :tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)
10
10
 
@@ -67,7 +67,6 @@ Potentially dangerous operations:
67
67
  - [renaming a table](#renaming-a-table)
68
68
  - [creating a table with the force option](#creating-a-table-with-the-force-option)
69
69
  - [adding a check constraint](#adding-a-check-constraint)
70
- - [setting NOT NULL on an existing column](#setting-not-null-on-an-existing-column)
71
70
  - [executing SQL directly](#executing-SQL-directly)
72
71
 
73
72
  Postgres-specific checks:
@@ -76,6 +75,7 @@ Postgres-specific checks:
76
75
  - [adding a reference](#adding-a-reference)
77
76
  - [adding a foreign key](#adding-a-foreign-key)
78
77
  - [adding a json column](#adding-a-json-column)
78
+ - [setting NOT NULL on an existing column](#setting-not-null-on-an-existing-column)
79
79
 
80
80
  Best practices:
81
81
 
@@ -204,19 +204,26 @@ class ChangeSomeColumnType < ActiveRecord::Migration[7.0]
204
204
  end
205
205
  ```
206
206
 
207
- A few changes don’t require a table rewrite (and are safe) in Postgres:
207
+ Some changes don’t require a table rewrite and are safe in Postgres:
208
208
 
209
- - Increasing the length limit of a `varchar` column (or removing the limit)
210
- - Changing a `varchar` column to a `text` column
211
- - Changing a `text` column to a `varchar` column with no length limit
212
- - Increasing the precision of a `decimal` or `numeric` column
213
- - Making a `decimal` or `numeric` column unconstrained
214
- - Changing between `timestamp` and `timestamptz` columns when session time zone is UTC in Postgres 12+
209
+ Type | Safe Changes
210
+ --- | ---
211
+ `cidr` | Changing to `inet`
212
+ `citext` | Changing to `text` if not indexed, changing to `string` with no `:limit` if not indexed
213
+ `datetime` | Increasing or removing `:precision`, changing to `timestamptz` when session time zone is UTC in Postgres 12+
214
+ `decimal` | Increasing `:precision` at same `:scale`, removing `:precision` and `:scale`
215
+ `interval` | Increasing or removing `:precision`
216
+ `numeric` | Increasing `:precision` at same `:scale`, removing `:precision` and `:scale`
217
+ `string` | Increasing or removing `:limit`, changing to `text`, changing `citext` if not indexed
218
+ `text` | Changing to `string` with no `:limit`, changing to `citext` if not indexed
219
+ `time` | Increasing or removing `:precision`
220
+ `timestamptz` | Increasing or removing `:limit`, changing to `datetime` when session time zone is UTC in Postgres 12+
215
221
 
216
- And a few in MySQL and MariaDB:
222
+ And some in MySQL and MariaDB:
217
223
 
218
- - Increasing the length limit of a `varchar` column from under 255 up to 255
219
- - Increasing the length limit of a `varchar` column from over 255 to the max limit
224
+ Type | Safe Changes
225
+ --- | ---
226
+ `string` | Increasing `:limit` from under 255 up to 255, increasing `:limit` from over 255 to the max
220
227
 
221
228
  #### Good
222
229
 
@@ -353,86 +360,6 @@ end
353
360
 
354
361
  [Let us know](https://github.com/ankane/strong_migrations/issues/new) if you have a safe way to do this (check constraints can be added with `NOT ENFORCED`, but enforcing blocks writes).
355
362
 
356
- ### Setting NOT NULL on an existing column
357
-
358
- :turtle: Safe by default available
359
-
360
- #### Bad
361
-
362
- Setting `NOT NULL` on an existing column blocks reads and writes while every row is checked.
363
-
364
- ```ruby
365
- class SetSomeColumnNotNull < ActiveRecord::Migration[7.0]
366
- def change
367
- change_column_null :users, :some_column, false
368
- end
369
- end
370
- ```
371
-
372
- #### Good - Postgres
373
-
374
- Instead, add a check constraint.
375
-
376
- For Rails 6.1, use:
377
-
378
- ```ruby
379
- class SetSomeColumnNotNull < ActiveRecord::Migration[7.0]
380
- def change
381
- add_check_constraint :users, "some_column IS NOT NULL", name: "users_some_column_null", validate: false
382
- end
383
- end
384
- ```
385
-
386
- For Rails < 6.1, use:
387
-
388
- ```ruby
389
- class SetSomeColumnNotNull < ActiveRecord::Migration[6.0]
390
- def change
391
- safety_assured do
392
- execute 'ALTER TABLE "users" ADD CONSTRAINT "users_some_column_null" CHECK ("some_column" IS NOT NULL) NOT VALID'
393
- end
394
- end
395
- end
396
- ```
397
-
398
- 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.
399
-
400
- For Rails 6.1, use:
401
-
402
- ```ruby
403
- class ValidateSomeColumnNotNull < ActiveRecord::Migration[7.0]
404
- def change
405
- validate_check_constraint :users, name: "users_some_column_null"
406
-
407
- # in Postgres 12+, you can then safely set NOT NULL on the column
408
- change_column_null :users, :some_column, false
409
- remove_check_constraint :users, name: "users_some_column_null"
410
- end
411
- end
412
- ```
413
-
414
- For Rails < 6.1, use:
415
-
416
- ```ruby
417
- class ValidateSomeColumnNotNull < ActiveRecord::Migration[6.0]
418
- def change
419
- safety_assured do
420
- execute 'ALTER TABLE "users" VALIDATE CONSTRAINT "users_some_column_null"'
421
- end
422
-
423
- # in Postgres 12+, you can then safely set NOT NULL on the column
424
- change_column_null :users, :some_column, false
425
- safety_assured do
426
- execute 'ALTER TABLE "users" DROP CONSTRAINT "users_some_column_null"'
427
- end
428
- end
429
- end
430
- ```
431
-
432
- #### Good - MySQL and MariaDB
433
-
434
- [Let us know](https://github.com/ankane/strong_migrations/issues/new) if you have a safe way to do this.
435
-
436
363
  ### Executing SQL directly
437
364
 
438
365
  Strong Migrations can’t ensure safety for raw SQL statements. Make really sure that what you’re doing is safe, then use:
@@ -587,6 +514,82 @@ class AddPropertiesToUsers < ActiveRecord::Migration[7.0]
587
514
  end
588
515
  ```
589
516
 
517
+ ### Setting NOT NULL on an existing column
518
+
519
+ :turtle: Safe by default available
520
+
521
+ #### Bad
522
+
523
+ In Postgres, setting `NOT NULL` on an existing column blocks reads and writes while every row is checked.
524
+
525
+ ```ruby
526
+ class SetSomeColumnNotNull < ActiveRecord::Migration[7.0]
527
+ def change
528
+ change_column_null :users, :some_column, false
529
+ end
530
+ end
531
+ ```
532
+
533
+ #### Good
534
+
535
+ Instead, add a check constraint.
536
+
537
+ For Rails 6.1, use:
538
+
539
+ ```ruby
540
+ class SetSomeColumnNotNull < ActiveRecord::Migration[7.0]
541
+ def change
542
+ add_check_constraint :users, "some_column IS NOT NULL", name: "users_some_column_null", validate: false
543
+ end
544
+ end
545
+ ```
546
+
547
+ For Rails < 6.1, use:
548
+
549
+ ```ruby
550
+ class SetSomeColumnNotNull < ActiveRecord::Migration[6.0]
551
+ def change
552
+ safety_assured do
553
+ execute 'ALTER TABLE "users" ADD CONSTRAINT "users_some_column_null" CHECK ("some_column" IS NOT NULL) NOT VALID'
554
+ end
555
+ end
556
+ end
557
+ ```
558
+
559
+ Then validate it in a separate migration. A `NOT NULL` check constraint is [functionally equivalent](https://medium.com/doctolib/adding-a-not-null-constraint-on-pg-faster-with-minimal-locking-38b2c00c4d1c) to setting `NOT NULL` on the column (but it won’t show up in `schema.rb` in Rails < 6.1). In Postgres 12+, once the check constraint is validated, you can safely set `NOT NULL` on the column and drop the check constraint.
560
+
561
+ For Rails 6.1, use:
562
+
563
+ ```ruby
564
+ class ValidateSomeColumnNotNull < ActiveRecord::Migration[7.0]
565
+ def change
566
+ validate_check_constraint :users, name: "users_some_column_null"
567
+
568
+ # in Postgres 12+, you can then safely set NOT NULL on the column
569
+ change_column_null :users, :some_column, false
570
+ remove_check_constraint :users, name: "users_some_column_null"
571
+ end
572
+ end
573
+ ```
574
+
575
+ For Rails < 6.1, use:
576
+
577
+ ```ruby
578
+ class ValidateSomeColumnNotNull < ActiveRecord::Migration[6.0]
579
+ def change
580
+ safety_assured do
581
+ execute 'ALTER TABLE "users" VALIDATE CONSTRAINT "users_some_column_null"'
582
+ end
583
+
584
+ # in Postgres 12+, you can then safely set NOT NULL on the column
585
+ change_column_null :users, :some_column, false
586
+ safety_assured do
587
+ execute 'ALTER TABLE "users" DROP CONSTRAINT "users_some_column_null"'
588
+ end
589
+ end
590
+ end
591
+ ```
592
+
590
593
  ### Keeping non-unique indexes to three columns or less
591
594
 
592
595
  #### Bad
@@ -696,7 +699,7 @@ To customize specific messages, create an initializer with:
696
699
  StrongMigrations.error_messages[:add_column_default] = "Your custom instructions"
697
700
  ```
698
701
 
699
- Check the [source code](https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations.rb) for the list of keys.
702
+ Check the [source code](https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations/error_messages.rb) for the list of keys.
700
703
 
701
704
  ## Migration Timeouts
702
705
 
@@ -718,6 +721,25 @@ ALTER ROLE myuser SET statement_timeout = '1h';
718
721
 
719
722
  Note: If you use PgBouncer in transaction mode, you must set timeouts on the database user.
720
723
 
724
+ ## Lock Timeout Retries [experimental]
725
+
726
+ There’s the option to automatically retry statements when the lock timeout is reached. Here’s how it works:
727
+
728
+ - If a lock timeout happens outside a transaction, the statement is retried
729
+ - If it happens inside the DDL transaction, the entire migration is retried (only applicable to Postgres)
730
+
731
+ Add to `config/initializers/strong_migrations.rb`:
732
+
733
+ ```ruby
734
+ StrongMigrations.lock_timeout_retries = 3
735
+ ```
736
+
737
+ Set the delay between retries with:
738
+
739
+ ```ruby
740
+ StrongMigrations.lock_timeout_retry_delay = 10.seconds
741
+ ```
742
+
721
743
  ## App Timeouts
722
744
 
723
745
  We recommend adding timeouts to `config/database.yml` to prevent connections from hanging and individual queries from taking up too many resources in controllers, jobs, the Rails console, and other places.
@@ -0,0 +1,61 @@
1
+ module StrongMigrations
2
+ module Adapters
3
+ class AbstractAdapter
4
+ def initialize(checker)
5
+ @checker = checker
6
+ end
7
+
8
+ def name
9
+ "Unknown"
10
+ end
11
+
12
+ def min_version
13
+ end
14
+
15
+ def set_statement_timeout(timeout)
16
+ raise StrongMigrations::Error, "Statement timeout not supported for this database"
17
+ end
18
+
19
+ def set_lock_timeout(timeout)
20
+ raise StrongMigrations::Error, "Lock timeout not supported for this database"
21
+ end
22
+
23
+ def check_lock_timeout(limit)
24
+ # do nothing
25
+ end
26
+
27
+ def add_column_default_safe?
28
+ false
29
+ end
30
+
31
+ def change_type_safe?(table, column, type, options, existing_column, existing_type)
32
+ false
33
+ end
34
+
35
+ def rewrite_blocks
36
+ "reads and writes"
37
+ end
38
+
39
+ private
40
+
41
+ def connection
42
+ @checker.send(:connection)
43
+ end
44
+
45
+ def select_all(statement)
46
+ connection.select_all(statement)
47
+ end
48
+
49
+ def target_version(target_version)
50
+ target_version ||= StrongMigrations.target_version
51
+ version =
52
+ if target_version && StrongMigrations.developer_env?
53
+ target_version.to_s
54
+ else
55
+ yield
56
+ end
57
+ Gem::Version.new(version)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,29 @@
1
+ module StrongMigrations
2
+ module Adapters
3
+ class MariaDBAdapter < MySQLAdapter
4
+ def name
5
+ "MariaDB"
6
+ end
7
+
8
+ def min_version
9
+ "10.2"
10
+ end
11
+
12
+ def server_version
13
+ @server_version ||= begin
14
+ target_version(StrongMigrations.target_mariadb_version) do
15
+ select_all("SELECT VERSION()").first["VERSION()"].split("-").first
16
+ end
17
+ end
18
+ end
19
+
20
+ def set_statement_timeout(timeout)
21
+ select_all("SET max_statement_time = #{connection.quote(timeout)}")
22
+ end
23
+
24
+ def add_column_default_safe?
25
+ server_version >= Gem::Version.new("10.3.2")
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,87 @@
1
+ # note: MariaDB inherits from this adapter
2
+ # when making changes, be sure to see how it affects it
3
+ module StrongMigrations
4
+ module Adapters
5
+ class MySQLAdapter < AbstractAdapter
6
+ def name
7
+ "MySQL"
8
+ end
9
+
10
+ def min_version
11
+ "5.7"
12
+ end
13
+
14
+ def server_version
15
+ @server_version ||= begin
16
+ target_version(StrongMigrations.target_mysql_version) do
17
+ select_all("SELECT VERSION()").first["VERSION()"].split("-").first
18
+ end
19
+ end
20
+ end
21
+
22
+ def set_statement_timeout(timeout)
23
+ # use ceil to prevent no timeout for values under 1 ms
24
+ select_all("SET max_execution_time = #{connection.quote((timeout.to_f * 1000).ceil)}")
25
+ end
26
+
27
+ def set_lock_timeout(timeout)
28
+ select_all("SET lock_wait_timeout = #{connection.quote(timeout)}")
29
+ end
30
+
31
+ def check_lock_timeout(limit)
32
+ lock_timeout = connection.select_all("SHOW VARIABLES LIKE 'lock_wait_timeout'").first["Value"]
33
+ # lock timeout is an integer
34
+ if lock_timeout.to_i > limit
35
+ warn "[strong_migrations] DANGER: Lock timeout is longer than #{limit} seconds: #{lock_timeout}"
36
+ end
37
+ end
38
+
39
+ def analyze_table(table)
40
+ connection.execute "ANALYZE TABLE #{connection.quote_table_name(table.to_s)}"
41
+ end
42
+
43
+ def add_column_default_safe?
44
+ server_version >= Gem::Version.new("8.0.12")
45
+ end
46
+
47
+ def change_type_safe?(table, column, type, options, existing_column, existing_type)
48
+ safe = false
49
+
50
+ case type.to_s
51
+ when "string"
52
+ # https://dev.mysql.com/doc/refman/5.7/en/innodb-online-ddl-operations.html
53
+ # https://mariadb.com/kb/en/innodb-online-ddl-operations-with-the-instant-alter-algorithm/#changing-the-data-type-of-a-column
54
+ # increased limit, but doesn't change number of length bytes
55
+ # 1-255 = 1 byte, 256-65532 = 2 bytes, 65533+ = too big for varchar
56
+ limit = options[:limit] || 255
57
+ safe = ["varchar"].include?(existing_type) &&
58
+ limit >= existing_column.limit &&
59
+ (limit <= 255 || existing_column.limit > 255)
60
+ end
61
+
62
+ safe
63
+ end
64
+
65
+ def strict_mode?
66
+ sql_modes = sql_modes()
67
+ sql_modes.include?("STRICT_ALL_TABLES") || sql_modes.include?("STRICT_TRANS_TABLES")
68
+ end
69
+
70
+ def rewrite_blocks
71
+ "writes"
72
+ end
73
+
74
+ private
75
+
76
+ # do not memoize
77
+ # want latest value
78
+ def sql_modes
79
+ if StrongMigrations.target_sql_mode && StrongMigrations.developer_env?
80
+ StrongMigrations.target_sql_mode.split(",")
81
+ else
82
+ select_all("SELECT @@SESSION.sql_mode").first["@@SESSION.sql_mode"].split(",")
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end