strong_migrations 1.2.0 → 1.6.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: 4c0ea5c8cf3e904b89b8c4ab2f28ff60031515e40edf5b0c617ca4b8f8249650
4
- data.tar.gz: 5cb0fd63058bf618595cd929ef90de3cf90f02963f0adca21182c578f41c3320
3
+ metadata.gz: 23737504a1d3beb08bf2412b161b8e59a8a620b4f4d993f11fe931e7f1772452
4
+ data.tar.gz: 993dc5f9d78cba148540a22772c18ed115ea6ae8075dd209cc3a869cc2a647a4
5
5
  SHA512:
6
- metadata.gz: 50db9d240d49b60b75d97c76765b8f5e3b252c2cc826bd5e50e9b57311082dfea2c9cc5790fd905a2eac45a3eb6afb2784c7ed7962dcaf0172157548e1c8d233
7
- data.tar.gz: 2f2e5311cad061b712ee9dfb69a41b5e81f31be8db257bd095cbd8124bc461551afebb305d95b0bb090d6f7ef85c61618d61e64e9b73f266338a0aa7f5101b53
6
+ metadata.gz: e906dc1c79d12a72a7d0e6fb4ca87319095d3b0a39f2da1dbd08b453d19e3215892745c86f80bf9ea26bbf07173d87022aab21b424a881f24744b65396fcd9bc
7
+ data.tar.gz: 56a0dadcb236f09a1a7d5450456ae343ac8cb6662b65f983c28b4268edad6e7cbecfa47c86ebb0d1da72dcc1b305e7196521811a48838c25beb1119846a2b928
data/CHANGELOG.md CHANGED
@@ -1,3 +1,47 @@
1
+ ## 1.6.0 (2023-07-22)
2
+
3
+ - Added check for `change_column_default`
4
+
5
+ ## 1.5.0 (2023-07-02)
6
+
7
+ - Added check for `add_column` with stored generated columns
8
+ - Fixed `add_reference` with `foreign_key` and `index: false`
9
+
10
+ ## 1.4.4 (2023-03-08)
11
+
12
+ - Fixed `add_foreign_key` with `name` and `column` options with `safe_by_default`
13
+
14
+ ## 1.4.3 (2023-02-19)
15
+
16
+ - Fixed check for `change_column` to account for charset with MySQL and MariaDB
17
+
18
+ ## 1.4.2 (2023-01-29)
19
+
20
+ - Added `alphabetize_schema` option
21
+
22
+ ## 1.4.1 (2023-01-05)
23
+
24
+ - Added support for multiple databases to `target_version`
25
+
26
+ ## 1.4.0 (2022-10-31)
27
+
28
+ - Added check for `add_exclusion_constraint`
29
+ - Added support for `RACK_ENV`
30
+ - Fixed error when `Rails` defined without `Rails.env`
31
+ - Fixed error with `change_column_null` when table does not exist
32
+
33
+ ## 1.3.2 (2022-10-09)
34
+
35
+ - Improved error message for `add_column` with `default: nil` with Postgres 10
36
+
37
+ ## 1.3.1 (2022-09-21)
38
+
39
+ - Fixed check for `add_column` with `default: nil` with Postgres 10
40
+
41
+ ## 1.3.0 (2022-08-30)
42
+
43
+ - Added check for `add_column` with `uuid` type and volatile default value
44
+
1
45
  ## 1.2.0 (2022-06-10)
2
46
 
3
47
  - Added check for index corruption with Postgres 14.0 to 14.3
data/README.md CHANGED
@@ -62,6 +62,7 @@ Potentially dangerous operations:
62
62
  - [removing a column](#removing-a-column)
63
63
  - [adding a column with a default value](#adding-a-column-with-a-default-value)
64
64
  - [backfilling data](#backfilling-data)
65
+ - [adding a stored generated column](#adding-a-stored-generated-column)
65
66
  - [changing the type of a column](#changing-the-type-of-a-column)
66
67
  - [renaming a column](#renaming-a-column)
67
68
  - [renaming a table](#renaming-a-table)
@@ -74,9 +75,14 @@ Postgres-specific checks:
74
75
  - [adding an index non-concurrently](#adding-an-index-non-concurrently)
75
76
  - [adding a reference](#adding-a-reference)
76
77
  - [adding a foreign key](#adding-a-foreign-key)
78
+ - [adding an exclusion constraint](#adding-an-exclusion-constraint)
77
79
  - [adding a json column](#adding-a-json-column)
78
80
  - [setting NOT NULL on an existing column](#setting-not-null-on-an-existing-column)
79
81
 
82
+ Config-specific checks:
83
+
84
+ - [changing the default value of a column](#changing-the-default-value-of-a-column)
85
+
80
86
  Best practices:
81
87
 
82
88
  - [keeping non-unique indexes to three columns or less](#keeping-non-unique-indexes-to-three-columns-or-less)
@@ -107,7 +113,7 @@ end
107
113
  end
108
114
  ```
109
115
 
110
- 2. Deploy code
116
+ 2. Deploy the code
111
117
  3. Write a migration to remove the column (wrap in `safety_assured` block)
112
118
 
113
119
  ```ruby
@@ -118,7 +124,7 @@ end
118
124
  end
119
125
  ```
120
126
 
121
- 4. Deploy and run migration
127
+ 4. Deploy and run the migration
122
128
  5. Remove the line added in step 1
123
129
 
124
130
  ### Adding a column with a default value
@@ -135,7 +141,7 @@ class AddSomeColumnToUsers < ActiveRecord::Migration[7.0]
135
141
  end
136
142
  ```
137
143
 
138
- In Postgres 11+, MySQL 8.0.12+, and MariaDB 10.3.2+, this no longer requires a table rewrite and is safe.
144
+ In Postgres 11+, MySQL 8.0.12+, and MariaDB 10.3.2+, this no longer requires a table rewrite and is safe (except for volatile functions like `gen_random_uuid()`).
139
145
 
140
146
  #### Good
141
147
 
@@ -190,6 +196,24 @@ class BackfillSomeColumn < ActiveRecord::Migration[7.0]
190
196
  end
191
197
  ```
192
198
 
199
+ ### Adding a stored generated column
200
+
201
+ #### Bad
202
+
203
+ Adding a stored generated 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.
204
+
205
+ ```ruby
206
+ class AddSomeColumnToUsers < ActiveRecord::Migration[7.0]
207
+ def change
208
+ add_column :users, :some_column, :virtual, type: :string, as: "...", stored: true
209
+ end
210
+ end
211
+ ```
212
+
213
+ #### Good
214
+
215
+ Add a non-generated column and use callbacks or triggers instead (or a virtual generated column with MySQL and MariaDB).
216
+
193
217
  ### Changing the type of a column
194
218
 
195
219
  #### Bad
@@ -223,7 +247,7 @@ And some in MySQL and MariaDB:
223
247
 
224
248
  Type | Safe Changes
225
249
  --- | ---
226
- `string` | Increasing `:limit` from under 255 up to 255, increasing `:limit` from over 255 to the max
250
+ `string` | Increasing `:limit` from under 63 up to 63, increasing `:limit` from over 63 to the max (the threshold can be different if using an encoding other than `utf8mb4` - for instance, it’s 85 for `utf8mb3` and 255 for `latin1`)
227
251
 
228
252
  #### Good
229
253
 
@@ -488,6 +512,24 @@ class ValidateForeignKeyOnUsers < ActiveRecord::Migration[7.0]
488
512
  end
489
513
  ```
490
514
 
515
+ ### Adding an exclusion constraint
516
+
517
+ #### Bad
518
+
519
+ In Postgres, adding an exclusion constraint blocks reads and writes while every row is checked.
520
+
521
+ ```ruby
522
+ class AddExclusionContraint < ActiveRecord::Migration[7.1]
523
+ def change
524
+ add_exclusion_constraint :users, "number WITH =", using: :gist
525
+ end
526
+ end
527
+ ```
528
+
529
+ #### Good
530
+
531
+ [Let us know](https://github.com/ankane/strong_migrations/issues/new) if you have a safe way to do this (exclusion constraints cannot be marked `NOT VALID`).
532
+
491
533
  ### Adding a json column
492
534
 
493
535
  #### Bad
@@ -590,6 +632,36 @@ class ValidateSomeColumnNotNull < ActiveRecord::Migration[6.0]
590
632
  end
591
633
  ```
592
634
 
635
+ ### Changing the default value of a column
636
+
637
+ #### Bad
638
+
639
+ Rails < 7 enables partial writes by default, which can cause incorrect values to be inserted when changing the default value of a column.
640
+
641
+ ```ruby
642
+ class ChangeSomeColumnDefault < ActiveRecord::Migration[6.1]
643
+ def change
644
+ change_column_default :users, :some_column, from: "old", to: "new"
645
+ end
646
+ end
647
+
648
+ User.create!(some_column: "old") # can insert "new"
649
+ ```
650
+
651
+ #### Good
652
+
653
+ Disable partial writes in `config/application.rb`. For Rails < 7, use:
654
+
655
+ ```ruby
656
+ config.active_record.partial_writes = false
657
+ ```
658
+
659
+ For Rails 7, use:
660
+
661
+ ```ruby
662
+ config.active_record.partial_inserts = false
663
+ ```
664
+
593
665
  ### Keeping non-unique indexes to three columns or less
594
666
 
595
667
  #### Bad
@@ -681,7 +753,7 @@ Disable specific checks with:
681
753
  StrongMigrations.disable_check(:add_index)
682
754
  ```
683
755
 
684
- Check the [source code](https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations.rb) for the list of keys.
756
+ Check the [source code](https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations/error_messages.rb) for the list of keys.
685
757
 
686
758
  ## Down Migrations / Rollbacks
687
759
 
@@ -805,6 +877,12 @@ The major version works well for Postgres, while the full version is recommended
805
877
 
806
878
  For safety, this option only affects development and test environments. In other environments, the actual server version is always used.
807
879
 
880
+ If your app has multiple databases with different versions, with Rails 6.1+, you can use:
881
+
882
+ ```ruby
883
+ StrongMigrations.target_version = {primary: 13, catalog: 15}
884
+ ```
885
+
808
886
  ## Analyze Tables
809
887
 
810
888
  Analyze tables automatically (to update planner statistics) after an index is added. Create an initializer with:
@@ -815,23 +893,18 @@ StrongMigrations.auto_analyze = true
815
893
 
816
894
  ## Faster Migrations
817
895
 
818
- Only dump the schema when adding a new migration. If you use Git, add to the end of your `Rakefile`:
896
+ Only dump the schema when adding a new migration. If you use Git, add to `config/environments/development.rb`:
819
897
 
820
898
  ```rb
821
- task :faster_migrations do
822
- ActiveRecord::Base.dump_schema_after_migration = Rails.env.development? &&
823
- `git status db/migrate/ --porcelain`.present?
824
- end
825
-
826
- task "db:migrate": "faster_migrations"
899
+ config.active_record.dump_schema_after_migration = `git status db/migrate/ --porcelain`.present?
827
900
  ```
828
901
 
829
902
  ## Schema Sanity
830
903
 
831
- Columns can flip order in `db/schema.rb` when you have multiple developers. One way to prevent this is to [alphabetize them](https://www.pgrs.net/2008/03/12/alphabetize-schema-rb-columns/). Add to the end of your `Rakefile`:
904
+ Columns can flip order in `db/schema.rb` when you have multiple developers. One way to prevent this is to [alphabetize them](https://www.pgrs.net/2008/03/12/alphabetize-schema-rb-columns/). Add to `config/initializers/strong_migrations.rb`:
832
905
 
833
906
  ```ruby
834
- task "db:schema:dump": "strong_migrations:alphabetize_columns"
907
+ StrongMigrations.alphabetize_schema = true
835
908
  ```
836
909
 
837
910
  ## Permissions
@@ -50,7 +50,23 @@ module StrongMigrations
50
50
  target_version ||= StrongMigrations.target_version
51
51
  version =
52
52
  if target_version && StrongMigrations.developer_env?
53
- target_version.to_s
53
+ if target_version.is_a?(Hash)
54
+ # Active Record 6.0 supports multiple databases
55
+ # but connection.pool.spec.name always returns "primary"
56
+ # in migrations with rails db:migrate
57
+ if ActiveRecord::VERSION::STRING.to_f < 6.1
58
+ # error class is not shown in db:migrate output so ensure message is descriptive
59
+ raise StrongMigrations::Error, "StrongMigrations.target_version does not support multiple databases for Active Record < 6.1"
60
+ end
61
+
62
+ db_config_name = connection.pool.db_config.name
63
+ target_version.stringify_keys.fetch(db_config_name) do
64
+ # error class is not shown in db:migrate output so ensure message is descriptive
65
+ raise StrongMigrations::Error, "StrongMigrations.target_version is not configured for :#{db_config_name} database"
66
+ end.to_s
67
+ else
68
+ target_version.to_s
69
+ end
54
70
  else
55
71
  yield
56
72
  end
@@ -18,6 +18,9 @@ module StrongMigrations
18
18
  end
19
19
 
20
20
  def set_statement_timeout(timeout)
21
+ # fix deprecation warning with Active Record 7.1
22
+ timeout = timeout.value if timeout.is_a?(ActiveSupport::Duration)
23
+
21
24
  select_all("SET max_statement_time = #{connection.quote(timeout)}")
22
25
  end
23
26
 
@@ -25,6 +25,9 @@ module StrongMigrations
25
25
  end
26
26
 
27
27
  def set_lock_timeout(timeout)
28
+ # fix deprecation warning with Active Record 7.1
29
+ timeout = timeout.value if timeout.is_a?(ActiveSupport::Duration)
30
+
28
31
  select_all("SET lock_wait_timeout = #{connection.quote(timeout)}")
29
32
  end
30
33
 
@@ -49,14 +52,31 @@ module StrongMigrations
49
52
 
50
53
  case type.to_s
51
54
  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
55
  limit = options[:limit] || 255
57
- safe = ["varchar"].include?(existing_type) &&
58
- limit >= existing_column.limit &&
59
- (limit <= 255 || existing_column.limit > 255)
56
+ if ["varchar"].include?(existing_type) && limit >= existing_column.limit
57
+ # https://dev.mysql.com/doc/refman/5.7/en/innodb-online-ddl-operations.html
58
+ # https://mariadb.com/kb/en/innodb-online-ddl-operations-with-the-instant-alter-algorithm/#changing-the-data-type-of-a-column
59
+ # increased limit, but doesn't change number of length bytes
60
+ # 1-255 = 1 byte, 256-65532 = 2 bytes, 65533+ = too big for varchar
61
+
62
+ # account for charset
63
+ # https://dev.mysql.com/doc/refman/8.0/en/charset-mysql.html
64
+ # https://mariadb.com/kb/en/supported-character-sets-and-collations/
65
+ sql = <<~SQL
66
+ SELECT cs.MAXLEN
67
+ FROM INFORMATION_SCHEMA.CHARACTER_SETS cs
68
+ INNER JOIN INFORMATION_SCHEMA.COLLATIONS c ON c.CHARACTER_SET_NAME = cs.CHARACTER_SET_NAME
69
+ INNER JOIN INFORMATION_SCHEMA.TABLES t ON t.TABLE_COLLATION = c.COLLATION_NAME
70
+ WHERE t.TABLE_SCHEMA = database() AND t.TABLE_NAME = #{connection.quote(table)}
71
+ SQL
72
+ row = connection.select_all(sql).first
73
+ if row
74
+ threshold = 255 / row["MAXLEN"]
75
+ safe = limit <= threshold || existing_column.limit > threshold
76
+ else
77
+ warn "[strong_migrations] Could not determine charset"
78
+ end
79
+ end
60
80
  end
61
81
 
62
82
  safe
@@ -14,11 +14,7 @@ module StrongMigrations
14
14
  target_version(StrongMigrations.target_postgresql_version) do
15
15
  version = select_all("SHOW server_version_num").first["server_version_num"].to_i
16
16
  # major and minor version
17
- if version >= 100000
18
- "#{version / 10000}.#{(version % 10000)}"
19
- else
20
- "#{version / 10000}.#{(version % 10000) / 100}"
21
- end
17
+ "#{version / 10000}.#{(version % 10000)}"
22
18
  end
23
19
  end
24
20
  end
@@ -76,7 +72,7 @@ module StrongMigrations
76
72
  # but there doesn't seem to be a way to set/modify it
77
73
  # https://wiki.postgresql.org/wiki/What%27s_new_in_PostgreSQL_9.2#Reduce_ALTER_TABLE_rewrites
78
74
  when "numeric", "decimal"
79
- # numeric and decimal are equivalent and can be used interchangably
75
+ # numeric and decimal are equivalent and can be used interchangeably
80
76
  safe = ["numeric", "decimal"].include?(existing_type) &&
81
77
  (
82
78
  (
@@ -166,6 +162,13 @@ module StrongMigrations
166
162
  !StrongMigrations.developer_env?
167
163
  end
168
164
 
165
+ # default to true if unsure
166
+ def default_volatile?(default)
167
+ name = default.to_s.delete_suffix("()")
168
+ rows = select_all("SELECT provolatile FROM pg_proc WHERE proname = #{connection.quote(name)}").to_a
169
+ rows.empty? || rows.any? { |r| r["provolatile"] == "v" }
170
+ end
171
+
169
172
  private
170
173
 
171
174
  def set_timeout(setting, timeout)
@@ -8,6 +8,7 @@ module StrongMigrations
8
8
  def initialize(migration)
9
9
  @migration = migration
10
10
  @new_tables = []
11
+ @new_columns = []
11
12
  @safe = false
12
13
  @timeouts_set = false
13
14
  @committed = false
@@ -36,6 +37,8 @@ module StrongMigrations
36
37
  check_add_check_constraint(*args)
37
38
  when :add_column
38
39
  check_add_column(*args)
40
+ when :add_exclusion_constraint
41
+ check_add_exclusion_constraint(*args)
39
42
  when :add_foreign_key
40
43
  check_add_foreign_key(*args)
41
44
  when :add_index
@@ -44,6 +47,8 @@ module StrongMigrations
44
47
  check_add_reference(method, *args)
45
48
  when :change_column
46
49
  check_change_column(*args)
50
+ when :change_column_default
51
+ check_change_column_default(*args)
47
52
  when :change_column_null
48
53
  check_change_column_null(*args)
49
54
  when :change_table
@@ -32,7 +32,14 @@ module StrongMigrations
32
32
  table, column, type = args
33
33
  default = options[:default]
34
34
 
35
- if !default.nil? && !adapter.add_column_default_safe?
35
+ # keep track of new columns of change_column_default check
36
+ @new_columns << [table.to_s, column.to_s]
37
+
38
+ # Check key since DEFAULT NULL behaves differently from no default
39
+ #
40
+ # Also, Active Record has special case for uuid columns that allows function default values
41
+ # https://github.com/rails/rails/blob/v7.0.3.1/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb#L92-L93
42
+ if options.key?(:default) && (!adapter.add_column_default_safe? || (volatile = (postgresql? && type.to_s == "uuid" && default.to_s.include?("()") && adapter.default_volatile?(default))))
36
43
  if options[:null] == false
37
44
  options = options.except(:null)
38
45
  append = "
@@ -40,13 +47,21 @@ module StrongMigrations
40
47
  Then add the NOT NULL constraint in separate migrations."
41
48
  end
42
49
 
43
- raise_error :add_column_default,
44
- add_command: command_str("add_column", [table, column, type, options.except(:default)]),
45
- change_command: command_str("change_column_default", [table, column, default]),
46
- remove_command: command_str("remove_column", [table, column]),
47
- code: backfill_code(table, column, default),
48
- append: append,
49
- rewrite_blocks: adapter.rewrite_blocks
50
+ if default.nil?
51
+ raise_error :add_column_default_null,
52
+ command: command_str("add_column", [table, column, type, options.except(:default)]),
53
+ append: append,
54
+ rewrite_blocks: adapter.rewrite_blocks
55
+ else
56
+ raise_error :add_column_default,
57
+ add_command: command_str("add_column", [table, column, type, options.except(:default)]),
58
+ change_command: command_str("change_column_default", [table, column, default]),
59
+ remove_command: command_str("remove_column", [table, column]),
60
+ code: backfill_code(table, column, default, volatile),
61
+ append: append,
62
+ rewrite_blocks: adapter.rewrite_blocks,
63
+ default_type: (volatile ? "volatile" : "non-null")
64
+ end
50
65
  elsif default.is_a?(Proc) && postgresql?
51
66
  # adding a column with a VOLATILE default is not safe
52
67
  # https://www.postgresql.org/docs/current/sql-altertable.html#SQL-ALTERTABLE-NOTES
@@ -59,6 +74,18 @@ Then add the NOT NULL constraint in separate migrations."
59
74
  raise_error :add_column_json,
60
75
  command: command_str("add_column", [table, column, :jsonb, options])
61
76
  end
77
+
78
+ if type.to_s == "virtual" && options[:stored]
79
+ raise_error :add_column_generated_stored, rewrite_blocks: adapter.rewrite_blocks
80
+ end
81
+ end
82
+
83
+ def check_add_exclusion_constraint(*args)
84
+ table = args[0]
85
+
86
+ unless new_table?(table)
87
+ raise_error :add_exclusion_constraint
88
+ end
62
89
  end
63
90
 
64
91
  # unlike add_index, we don't make an exception here for new tables
@@ -127,7 +154,7 @@ Then add the NOT NULL constraint in separate migrations."
127
154
  if bad_index || options[:foreign_key]
128
155
  if index_value.is_a?(Hash)
129
156
  options[:index] = options[:index].merge(algorithm: :concurrently)
130
- else
157
+ elsif index_value
131
158
  options = options.merge(index: {algorithm: :concurrently})
132
159
  end
133
160
 
@@ -158,7 +185,8 @@ Then add the foreign key in separate migrations."
158
185
  table, column, type = args
159
186
 
160
187
  safe = false
161
- existing_column = connection.columns(table).find { |c| c.name.to_s == column.to_s }
188
+ table_columns = connection.columns(table) rescue []
189
+ existing_column = table_columns.find { |c| c.name.to_s == column.to_s }
162
190
  if existing_column
163
191
  existing_type = existing_column.sql_type.sub(/\(\d+(,\d+)?\)/, "")
164
192
  safe = adapter.change_type_safe?(table, column, type, options, existing_column, existing_type)
@@ -173,6 +201,18 @@ Then add the foreign key in separate migrations."
173
201
  raise_error :change_column, rewrite_blocks: adapter.rewrite_blocks unless safe
174
202
  end
175
203
 
204
+ def check_change_column_default(*args)
205
+ table, column, _default_or_changes = args
206
+
207
+ # just check ActiveRecord::Base, even though can override on model
208
+ partial_inserts = ar_version >= 7 ? ActiveRecord::Base.partial_inserts : ActiveRecord::Base.partial_writes
209
+
210
+ if partial_inserts && !new_column?(table, column)
211
+ raise_error :change_column_default,
212
+ config: ar_version >= 7 ? "partial_inserts" : "partial_writes"
213
+ end
214
+ end
215
+
176
216
  def check_change_column_null(*args)
177
217
  table, column, null, default = args
178
218
  if !null
@@ -219,16 +259,22 @@ Then add the foreign key in separate migrations."
219
259
 
220
260
  add_constraint_code =
221
261
  if constraint_methods
222
- # only quote when needed
223
- expr_column = column.to_s =~ /\A[a-z0-9_]+\z/ ? column : connection.quote_column_name(column)
224
- command_str(:add_check_constraint, [table, "#{expr_column} IS NOT NULL", {name: constraint_name, validate: false}])
262
+ command_str(:add_check_constraint, [table, "#{quote_column_if_needed(column)} IS NOT NULL", {name: constraint_name, validate: false}])
225
263
  else
226
264
  safety_assured_str(add_code)
227
265
  end
228
266
 
267
+ validate_constraint_code =
268
+ if safe_with_check_constraint
269
+ down_code = "#{add_constraint_code}\n #{command_str(:change_column_null, [table, column, true])}"
270
+ "def up\n #{validate_constraint_code}\n end\n\n def down\n #{down_code}\n end"
271
+ else
272
+ "def change\n #{validate_constraint_code}\n end"
273
+ end
274
+
229
275
  raise_error :change_column_null_postgresql,
230
276
  add_constraint_code: add_constraint_code,
231
- validate_constraint_code: "def change\n #{validate_constraint_code}\n end"
277
+ validate_constraint_code: validate_constraint_code
232
278
  end
233
279
  elsif mysql? || mariadb?
234
280
  unless adapter.strict_mode?
@@ -409,13 +455,29 @@ Then add the foreign key in separate migrations."
409
455
  "#{command} #{str_args.join(", ")}"
410
456
  end
411
457
 
412
- def backfill_code(table, column, default)
458
+ def backfill_code(table, column, default, function = false)
413
459
  model = table.to_s.classify
414
- "#{model}.unscoped.in_batches do |relation| \n relation.update_all #{column}: #{default.inspect}\n sleep(0.01)\n end"
460
+ if function
461
+ # update_all(column: Arel.sql(default)) also works in newer versions of Active Record
462
+ update_expr = "#{quote_column_if_needed(column)} = #{default}"
463
+ "#{model}.unscoped.in_batches do |relation| \n relation.where(#{column}: nil).update_all(#{update_expr.inspect})\n sleep(0.01)\n end"
464
+ else
465
+ "#{model}.unscoped.in_batches do |relation| \n relation.update_all #{column}: #{default.inspect}\n sleep(0.01)\n end"
466
+ end
467
+ end
468
+
469
+ # only quote when needed
470
+ # important! only use for display purposes
471
+ def quote_column_if_needed(column)
472
+ column.to_s =~ /\A[a-z0-9_]+\z/ ? column : connection.quote_column_name(column)
415
473
  end
416
474
 
417
475
  def new_table?(table)
418
476
  @new_tables.include?(table.to_s)
419
477
  end
478
+
479
+ def new_column?(table, column)
480
+ new_table?(table) || @new_columns.include?([table.to_s, column.to_s])
481
+ end
420
482
  end
421
483
  end
@@ -1,7 +1,7 @@
1
1
  module StrongMigrations
2
2
  self.error_messages = {
3
3
  add_column_default:
4
- "Adding a column with a non-null default blocks %{rewrite_blocks} while the entire table is rewritten.
4
+ "Adding a column with a %{default_type} default blocks %{rewrite_blocks} while the entire table is rewritten.
5
5
  Instead, add the column without a default value, then change the default.
6
6
 
7
7
  class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
@@ -25,6 +25,16 @@ class Backfill%{migration_name} < ActiveRecord::Migration%{migration_suffix}
25
25
  end
26
26
  end",
27
27
 
28
+ add_column_default_null:
29
+ "Adding a column with a null default blocks %{rewrite_blocks} while the entire table is rewritten.
30
+ Instead, add the column without a default value.
31
+
32
+ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
33
+ def change
34
+ %{command}
35
+ end
36
+ end",
37
+
28
38
  add_column_default_callable:
29
39
  "Strong Migrations does not support inspecting callable default values.
30
40
  Please make really sure you're not calling a VOLATILE function,
@@ -40,6 +50,9 @@ class %{migration_name} < ActiveRecord::Migration%{migration_suffix}
40
50
  end
41
51
  end",
42
52
 
53
+ add_column_generated_stored:
54
+ "Adding a stored generated column blocks %{rewrite_blocks} while the entire table is rewritten.",
55
+
43
56
  change_column:
44
57
  "Changing the type of an existing column blocks %{rewrite_blocks}
45
58
  while the entire table is rewritten. A safer approach is to:
@@ -148,6 +161,13 @@ Otherwise, remove the force option.",
148
161
  execute call, so cannot help you here. Please make really sure that what
149
162
  you're doing is safe before proceeding, then wrap it in a safety_assured { ... } block.",
150
163
 
164
+ change_column_default:
165
+ "Partial writes are enabled, which can cause incorrect values
166
+ to be inserted when changing the default value of a column.
167
+ Disable partial writes in config/application.rb:
168
+
169
+ config.active_record.%{config} = false",
170
+
151
171
  change_column_null:
152
172
  "Passing a default value to change_column_null runs a single UPDATE query,
153
173
  which can cause downtime. Instead, backfill the existing rows in the
@@ -221,7 +241,10 @@ end",
221
241
 
222
242
  validate_check_constraint:
223
243
  "Validating a check constraint while writes are blocked is dangerous.
224
- Use disable_ddl_transaction! or a separate migration."
244
+ Use disable_ddl_transaction! or a separate migration.",
245
+
246
+ add_exclusion_constraint:
247
+ "Adding an exclusion constraint blocks reads and writes while every row is checked."
225
248
  }
226
249
  self.enabled_checks = (error_messages.keys - [:remove_index]).map { |k| [k, {}] }.to_h
227
250
  end
@@ -48,10 +48,20 @@ module StrongMigrations
48
48
  dir.up do
49
49
  @migration.add_foreign_key(from_table, to_table, *args, **options.merge(validate: false))
50
50
  disable_transaction
51
- @migration.validate_foreign_key(from_table, to_table)
51
+ validate_options = options.slice(:column, :name)
52
+ if ActiveRecord::VERSION::MAJOR >= 6
53
+ @migration.validate_foreign_key(from_table, to_table, **validate_options)
54
+ else
55
+ @migration.validate_foreign_key(from_table, validate_options.any? ? validate_options : to_table)
56
+ end
52
57
  end
53
58
  dir.down do
54
- @migration.remove_foreign_key(from_table, to_table)
59
+ remove_options = options.slice(:column, :name)
60
+ if ActiveRecord::VERSION::MAJOR >= 6
61
+ @migration.remove_foreign_key(from_table, to_table, **remove_options)
62
+ else
63
+ @migration.remove_foreign_key(from_table, remove_options.any? ? remove_options : to_table)
64
+ end
55
65
  end
56
66
  end
57
67
  end
@@ -64,7 +74,7 @@ module StrongMigrations
64
74
  @migration.validate_check_constraint(table, **validate_options)
65
75
  end
66
76
  dir.down do
67
- @migration.remove_check_constraint(table, expression, **add_options)
77
+ @migration.remove_check_constraint(table, expression, **add_options.except(:validate))
68
78
  end
69
79
  end
70
80
  end
@@ -0,0 +1,21 @@
1
+ module StrongMigrations
2
+ module SchemaDumper
3
+ def initialize(connection, *args, **options)
4
+ return super unless StrongMigrations.alphabetize_schema
5
+
6
+ super(WrappedConnection.new(connection), *args, **options)
7
+ end
8
+ end
9
+
10
+ class WrappedConnection
11
+ delegate_missing_to :@connection
12
+
13
+ def initialize(connection)
14
+ @connection = connection
15
+ end
16
+
17
+ def columns(*args, **options)
18
+ @connection.columns(*args, **options).sort_by(&:name)
19
+ end
20
+ end
21
+ end
@@ -1,3 +1,3 @@
1
1
  module StrongMigrations
2
- VERSION = "1.2.0"
2
+ VERSION = "1.6.0"
3
3
  end
@@ -2,22 +2,22 @@
2
2
  require "active_support"
3
3
 
4
4
  # adapters
5
- require "strong_migrations/adapters/abstract_adapter"
6
- require "strong_migrations/adapters/mysql_adapter"
7
- require "strong_migrations/adapters/mariadb_adapter"
8
- require "strong_migrations/adapters/postgresql_adapter"
5
+ require_relative "strong_migrations/adapters/abstract_adapter"
6
+ require_relative "strong_migrations/adapters/mysql_adapter"
7
+ require_relative "strong_migrations/adapters/mariadb_adapter"
8
+ require_relative "strong_migrations/adapters/postgresql_adapter"
9
9
 
10
10
  # modules
11
- require "strong_migrations/checks"
12
- require "strong_migrations/safe_methods"
13
- require "strong_migrations/checker"
14
- require "strong_migrations/database_tasks"
15
- require "strong_migrations/migration"
16
- require "strong_migrations/migrator"
17
- require "strong_migrations/version"
11
+ require_relative "strong_migrations/checks"
12
+ require_relative "strong_migrations/safe_methods"
13
+ require_relative "strong_migrations/checker"
14
+ require_relative "strong_migrations/database_tasks"
15
+ require_relative "strong_migrations/migration"
16
+ require_relative "strong_migrations/migrator"
17
+ require_relative "strong_migrations/version"
18
18
 
19
19
  # integrations
20
- require "strong_migrations/railtie" if defined?(Rails)
20
+ require_relative "strong_migrations/railtie" if defined?(Rails)
21
21
 
22
22
  module StrongMigrations
23
23
  class Error < StandardError; end
@@ -28,7 +28,8 @@ module StrongMigrations
28
28
  attr_accessor :auto_analyze, :start_after, :checks, :error_messages,
29
29
  :target_postgresql_version, :target_mysql_version, :target_mariadb_version,
30
30
  :enabled_checks, :lock_timeout, :statement_timeout, :check_down, :target_version,
31
- :safe_by_default, :target_sql_mode, :lock_timeout_retries, :lock_timeout_retry_delay
31
+ :safe_by_default, :target_sql_mode, :lock_timeout_retries, :lock_timeout_retry_delay,
32
+ :alphabetize_schema
32
33
  attr_writer :lock_timeout_limit
33
34
  end
34
35
  self.auto_analyze = false
@@ -38,10 +39,21 @@ module StrongMigrations
38
39
  self.checks = []
39
40
  self.safe_by_default = false
40
41
  self.check_down = false
42
+ self.alphabetize_schema = false
41
43
 
42
44
  # private
43
45
  def self.developer_env?
44
- defined?(Rails) && (Rails.env.development? || Rails.env.test?)
46
+ env == "development" || env == "test"
47
+ end
48
+
49
+ # private
50
+ def self.env
51
+ if defined?(Rails.env)
52
+ Rails.env
53
+ else
54
+ # default to production for safety
55
+ ENV["RACK_ENV"] || "production"
56
+ end
45
57
  end
46
58
 
47
59
  def self.lock_timeout_limit
@@ -74,7 +86,7 @@ module StrongMigrations
74
86
  end
75
87
 
76
88
  # load error messages
77
- require "strong_migrations/error_messages"
89
+ require_relative "strong_migrations/error_messages"
78
90
 
79
91
  ActiveSupport.on_load(:active_record) do
80
92
  ActiveRecord::Migration.prepend(StrongMigrations::Migration)
@@ -83,4 +95,7 @@ ActiveSupport.on_load(:active_record) do
83
95
  if defined?(ActiveRecord::Tasks::DatabaseTasks)
84
96
  ActiveRecord::Tasks::DatabaseTasks.singleton_class.prepend(StrongMigrations::DatabaseTasks)
85
97
  end
98
+
99
+ require_relative "strong_migrations/schema_dumper"
100
+ ActiveRecord::SchemaDumper.prepend(StrongMigrations::SchemaDumper)
86
101
  end
@@ -1,14 +1,9 @@
1
1
  namespace :strong_migrations do
2
- # https://www.pgrs.net/2008/03/13/alphabetize-schema-rb-columns/
2
+ # https://www.pgrs.net/2008/03/12/alphabetize-schema-rb-columns/
3
3
  task :alphabetize_columns do
4
4
  $stderr.puts "Dumping schema"
5
5
  ActiveRecord::Base.logger.level = Logger::INFO
6
6
 
7
- require "strong_migrations/alphabetize_columns"
8
- ActiveRecord::Base.connection.class.prepend StrongMigrations::AlphabetizeColumns
9
- if ActiveRecord::ConnectionAdapters.const_defined?('PostGISAdapter')
10
- ActiveRecord::ConnectionAdapters::PostGISAdapter.prepend StrongMigrations::AlphabetizeColumns
11
- end
12
- ActiveRecord::ConnectionAdapters::AbstractAdapter.prepend StrongMigrations::AlphabetizeColumns
7
+ StrongMigrations.alphabetize_schema = true
13
8
  end
14
9
  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: 1.2.0
4
+ version: 1.6.0
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: 2022-06-10 00:00:00.000000000 Z
13
+ date: 2023-07-22 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord
@@ -45,7 +45,6 @@ files:
45
45
  - lib/strong_migrations/adapters/mariadb_adapter.rb
46
46
  - lib/strong_migrations/adapters/mysql_adapter.rb
47
47
  - lib/strong_migrations/adapters/postgresql_adapter.rb
48
- - lib/strong_migrations/alphabetize_columns.rb
49
48
  - lib/strong_migrations/checker.rb
50
49
  - lib/strong_migrations/checks.rb
51
50
  - lib/strong_migrations/database_tasks.rb
@@ -54,6 +53,7 @@ files:
54
53
  - lib/strong_migrations/migrator.rb
55
54
  - lib/strong_migrations/railtie.rb
56
55
  - lib/strong_migrations/safe_methods.rb
56
+ - lib/strong_migrations/schema_dumper.rb
57
57
  - lib/strong_migrations/version.rb
58
58
  - lib/tasks/strong_migrations.rake
59
59
  homepage: https://github.com/ankane/strong_migrations
@@ -75,7 +75,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
75
75
  - !ruby/object:Gem::Version
76
76
  version: '0'
77
77
  requirements: []
78
- rubygems_version: 3.3.7
78
+ rubygems_version: 3.4.10
79
79
  signing_key:
80
80
  specification_version: 4
81
81
  summary: Catch unsafe migrations in development
@@ -1,11 +0,0 @@
1
- module StrongMigrations
2
- module AlphabetizeColumns
3
- def columns(*args)
4
- super.sort_by(&:name)
5
- end
6
-
7
- def extensions(*args)
8
- super.sort
9
- end
10
- end
11
- end