strong_migrations 0.8.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1f885cc0a8b0fb30e3bc4d000e82584fdf3850943563b4b426d3cfc39d70015c
4
- data.tar.gz: d0dd29ea45ccc3b7c571e8bf03fa98c401d5489d1f5459488a0eff411cf6880e
3
+ metadata.gz: 4c0ea5c8cf3e904b89b8c4ab2f28ff60031515e40edf5b0c617ca4b8f8249650
4
+ data.tar.gz: 5cb0fd63058bf618595cd929ef90de3cf90f02963f0adca21182c578f41c3320
5
5
  SHA512:
6
- metadata.gz: c03a023d4c593d2868e524ca0f3e7ebd93e07e087e02f0d7090210b00fdb1fdc50d2abb74ab7df4131bddc6052fc5b9c10ce902ede8c708927476e40ad49d07f
7
- data.tar.gz: 479e34db0101a15e1db1a593f01773da5665baa5cda3403ad32ea58dca00a0d0faee0ef813cf1975b7e9f99f1591e7f4f2be5c456c96fd6ae4633b48b6a0b3b4
6
+ metadata.gz: 50db9d240d49b60b75d97c76765b8f5e3b252c2cc826bd5e50e9b57311082dfea2c9cc5790fd905a2eac45a3eb6afb2784c7ed7962dcaf0172157548e1c8d233
7
+ data.tar.gz: 2f2e5311cad061b712ee9dfb69a41b5e81f31be8db257bd095cbd8124bc461551afebb305d95b0bb090d6f7ef85c61618d61e64e9b73f266338a0aa7f5101b53
data/CHANGELOG.md CHANGED
@@ -1,3 +1,43 @@
1
+ ## 1.2.0 (2022-06-10)
2
+
3
+ - Added check for index corruption with Postgres 14.0 to 14.3
4
+
5
+ ## 1.1.0 (2022-06-08)
6
+
7
+ - Added check for `force` option with `create_join_table`
8
+ - Improved errors for extra arguments
9
+ - Fixed ignoring extra arguments with `safe_by_default`
10
+ - Fixed missing options with `remove_index` and `safe_by_default`
11
+
12
+ ## 1.0.0 (2022-03-21)
13
+
14
+ New safe operations with MySQL and MariaDB
15
+
16
+ - Setting `NOT NULL` on an existing column with strict mode enabled
17
+
18
+ New safe operations with Postgres
19
+
20
+ - Changing between `text` and `citext` when not indexed
21
+ - Changing a `string` column to a `citext` column when not indexed
22
+ - Changing a `citext` column to a `string` column with no `:limit` when not indexed
23
+ - Changing a `cidr` column to an `inet` column
24
+ - Increasing `:precision` of an `interval` or `time` column
25
+
26
+ New unsafe operations with Postgres
27
+
28
+ - Adding a column with a callable default value
29
+ - Decreasing `:precision` of a `datetime` column
30
+ - Decreasing `:limit` of a `timestamptz` column
31
+ - Passing a default value to `change_column_null`
32
+
33
+ Other
34
+
35
+ - Added experimental support for lock timeout retries
36
+ - Added `target_sql_mode` option
37
+ - Added error for `change_column_null` with default value with `safe_by_default` option
38
+ - Fixed instructions for `remove_columns` with options
39
+ - Dropped support for Postgres < 10, MySQL < 5.7, and MariaDB < 10.2
40
+
1
41
  ## 0.8.0 (2022-02-09)
2
42
 
3
43
  - 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