strong_migrations 1.7.0 → 1.8.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: 0b621e800497f9e9c77e0208a899f7e8d17a944caa61dffdea80911e8ddd5ead
4
- data.tar.gz: 3f7d4275c416db6b954c1c2e1109ad5118a1c0a7cc6eb25a81203f82a4e47b35
3
+ metadata.gz: f6462f35144a092664bb7456db2a1ad402695c6b5b1ba7001c1f1f977ea88d3e
4
+ data.tar.gz: fc71c84ae237b8dbbd3413af7b47ab4fa9962dcbbdd6b3e99b9edd816d3ae009
5
5
  SHA512:
6
- metadata.gz: '0243834c1f5b12bc637a00b49f8b7eb773dcdd5b8592a354b705d72b6e5b6a728babfb12fc3d058093086529ebfe7de34abd9f1c834bc95323b01f8a48417445'
7
- data.tar.gz: a24deb56f0adbe0206791b9155707489d03c02f021842f9284eaadcf5a2b99c30fe6f3ae2c51fef15fc5fef9bf7066b1a72f84c446ed8c12919c43f29eee9f95
6
+ metadata.gz: 2c4e5bf21e4bb42046fc2d4a530e40d9fc5f843db7c630779789934ec1a47965398e6b637707dc6a7106eb339ef26231ecfea73c312fd9a04f66f96504fcdc6a
7
+ data.tar.gz: 542be70454fa38a8ad0c29e6bedcf670e54323ff8d9ae8f5eb59756476bbbaca6092a15f09c54163714e3db83261a8554e1fe51b00da8ad05ca7667a4612c3c8
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## 1.8.0 (2024-03-11)
2
+
3
+ - Added check for `add_column` with auto-incrementing columns
4
+ - Updated instructions for removing a column to append to `ignored_columns`
5
+ - Fixed check for adding a column with a default value for MySQL and MariaDB
6
+
1
7
  ## 1.7.0 (2024-01-05)
2
8
 
3
9
  - Added check for `add_unique_constraint`
data/LICENSE.txt CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2013 Bob Remeika and David Waller, 2015-2022 Andrew Kane
1
+ Copyright (c) 2013 Bob Remeika and David Waller, 2015-2024 Andrew Kane
2
2
 
3
3
  MIT License
4
4
 
data/README.md CHANGED
@@ -38,7 +38,7 @@ Active Record caches attributes, which causes problems
38
38
  when removing columns. Be sure to ignore the column:
39
39
 
40
40
  class User < ApplicationRecord
41
- self.ignored_columns = ["name"]
41
+ self.ignored_columns += ["name"]
42
42
  end
43
43
 
44
44
  Deploy the code, then wrap this step in a safety_assured { ... } block.
@@ -60,15 +60,15 @@ An operation is classified as dangerous if it either:
60
60
  Potentially dangerous operations:
61
61
 
62
62
  - [removing a column](#removing-a-column)
63
- - [adding a column with a default value](#adding-a-column-with-a-default-value)
64
- - [backfilling data](#backfilling-data)
65
- - [adding a stored generated column](#adding-a-stored-generated-column)
66
63
  - [changing the type of a column](#changing-the-type-of-a-column)
67
64
  - [renaming a column](#renaming-a-column)
68
65
  - [renaming a table](#renaming-a-table)
69
66
  - [creating a table with the force option](#creating-a-table-with-the-force-option)
67
+ - [adding an auto-incrementing column](#adding-an-auto-incrementing-column) [unreleased]
68
+ - [adding a stored generated column](#adding-a-stored-generated-column)
70
69
  - [adding a check constraint](#adding-a-check-constraint)
71
70
  - [executing SQL directly](#executing-SQL-directly)
71
+ - [backfilling data](#backfilling-data)
72
72
 
73
73
  Postgres-specific checks:
74
74
 
@@ -79,6 +79,7 @@ Postgres-specific checks:
79
79
  - [adding an exclusion constraint](#adding-an-exclusion-constraint)
80
80
  - [adding a json column](#adding-a-json-column)
81
81
  - [setting NOT NULL on an existing column](#setting-not-null-on-an-existing-column)
82
+ - [adding a column with a default value](#adding-a-column-with-a-default-value)
82
83
 
83
84
  Config-specific checks:
84
85
 
@@ -110,7 +111,7 @@ end
110
111
 
111
112
  ```ruby
112
113
  class User < ApplicationRecord
113
- self.ignored_columns = ["some_column"]
114
+ self.ignored_columns += ["some_column"]
114
115
  end
115
116
  ```
116
117
 
@@ -128,93 +129,6 @@ end
128
129
  4. Deploy and run the migration
129
130
  5. Remove the line added in step 1
130
131
 
131
- ### Adding a column with a default value
132
-
133
- #### Bad
134
-
135
- 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.
136
-
137
- ```ruby
138
- class AddSomeColumnToUsers < ActiveRecord::Migration[7.1]
139
- def change
140
- add_column :users, :some_column, :text, default: "default_value"
141
- end
142
- end
143
- ```
144
-
145
- 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()`).
146
-
147
- #### Good
148
-
149
- Instead, add the column without a default value, then change the default.
150
-
151
- ```ruby
152
- class AddSomeColumnToUsers < ActiveRecord::Migration[7.1]
153
- def up
154
- add_column :users, :some_column, :text
155
- change_column_default :users, :some_column, "default_value"
156
- end
157
-
158
- def down
159
- remove_column :users, :some_column
160
- end
161
- end
162
- ```
163
-
164
- See the next section for how to backfill.
165
-
166
- ### Backfilling data
167
-
168
- #### Bad
169
-
170
- 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/).
171
-
172
- ```ruby
173
- class AddSomeColumnToUsers < ActiveRecord::Migration[7.1]
174
- def change
175
- add_column :users, :some_column, :text
176
- User.update_all some_column: "default_value"
177
- end
178
- end
179
- ```
180
-
181
- Also, running a single query to update data can cause issues for large tables.
182
-
183
- #### Good
184
-
185
- 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!`.
186
-
187
- ```ruby
188
- class BackfillSomeColumn < ActiveRecord::Migration[7.1]
189
- disable_ddl_transaction!
190
-
191
- def up
192
- User.unscoped.in_batches do |relation|
193
- relation.update_all some_column: "default_value"
194
- sleep(0.01) # throttle
195
- end
196
- end
197
- end
198
- ```
199
-
200
- ### Adding a stored generated column
201
-
202
- #### Bad
203
-
204
- 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.
205
-
206
- ```ruby
207
- class AddSomeColumnToUsers < ActiveRecord::Migration[7.1]
208
- def change
209
- add_column :users, :some_column, :virtual, type: :string, as: "...", stored: true
210
- end
211
- end
212
- ```
213
-
214
- #### Good
215
-
216
- Add a non-generated column and use callbacks or triggers instead (or a virtual generated column with MySQL and MariaDB).
217
-
218
132
  ### Changing the type of a column
219
133
 
220
134
  #### Bad
@@ -343,6 +257,44 @@ end
343
257
 
344
258
  If you intend to drop an existing table, run `drop_table` first.
345
259
 
260
+ ### Adding an auto-incrementing column
261
+
262
+ #### Bad
263
+
264
+ Adding an auto-incrementing column (`serial`/`bigserial` in Postgres and `AUTO_INCREMENT` in MySQL and MariaDB) 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.
265
+
266
+ ```ruby
267
+ class AddIdToCitiesUsers < ActiveRecord::Migration[7.1]
268
+ def change
269
+ add_column :cities_users, :id, :primary_key
270
+ end
271
+ end
272
+ ```
273
+
274
+ With MySQL and MariaDB, this can also [generate different values on replicas](https://dev.mysql.com/doc/mysql-replication-excerpt/8.0/en/replication-features-auto-increment.html) if using statement-based replication.
275
+
276
+ #### Good
277
+
278
+ Create a new table and migrate the data with the same steps as [renaming a table](#renaming-a-table).
279
+
280
+ ### Adding a stored generated column
281
+
282
+ #### Bad
283
+
284
+ 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.
285
+
286
+ ```ruby
287
+ class AddSomeColumnToUsers < ActiveRecord::Migration[7.1]
288
+ def change
289
+ add_column :users, :some_column, :virtual, type: :string, as: "...", stored: true
290
+ end
291
+ end
292
+ ```
293
+
294
+ #### Good
295
+
296
+ Add a non-generated column and use callbacks or triggers instead (or a virtual generated column with MySQL and MariaDB).
297
+
346
298
  ### Adding a check constraint
347
299
 
348
300
  :turtle: Safe by default available
@@ -397,6 +349,40 @@ class ExecuteSQL < ActiveRecord::Migration[7.1]
397
349
  end
398
350
  ```
399
351
 
352
+ ### Backfilling data
353
+
354
+ #### Bad
355
+
356
+ 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/).
357
+
358
+ ```ruby
359
+ class AddSomeColumnToUsers < ActiveRecord::Migration[7.1]
360
+ def change
361
+ add_column :users, :some_column, :text
362
+ User.update_all some_column: "default_value"
363
+ end
364
+ end
365
+ ```
366
+
367
+ Also, running a single query to update data can cause issues for large tables.
368
+
369
+ #### Good
370
+
371
+ 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!`.
372
+
373
+ ```ruby
374
+ class BackfillSomeColumn < ActiveRecord::Migration[7.1]
375
+ disable_ddl_transaction!
376
+
377
+ def up
378
+ User.unscoped.in_batches do |relation|
379
+ relation.update_all some_column: "default_value"
380
+ sleep(0.01) # throttle
381
+ end
382
+ end
383
+ end
384
+ ```
385
+
400
386
  ### Adding an index non-concurrently
401
387
 
402
388
  :turtle: Safe by default available
@@ -666,6 +652,41 @@ class ValidateSomeColumnNotNull < ActiveRecord::Migration[6.0]
666
652
  end
667
653
  ```
668
654
 
655
+ ### Adding a column with a default value
656
+
657
+ #### Bad
658
+
659
+ In earlier versions of Postgres, 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.
660
+
661
+ ```ruby
662
+ class AddSomeColumnToUsers < ActiveRecord::Migration[7.1]
663
+ def change
664
+ add_column :users, :some_column, :text, default: "default_value"
665
+ end
666
+ end
667
+ ```
668
+
669
+ In Postgres 11+, this no longer requires a table rewrite and is safe (except for volatile functions like `gen_random_uuid()`).
670
+
671
+ #### Good
672
+
673
+ Instead, add the column without a default value, then change the default.
674
+
675
+ ```ruby
676
+ class AddSomeColumnToUsers < ActiveRecord::Migration[7.1]
677
+ def up
678
+ add_column :users, :some_column, :text
679
+ change_column_default :users, :some_column, "default_value"
680
+ end
681
+
682
+ def down
683
+ remove_column :users, :some_column
684
+ end
685
+ end
686
+ ```
687
+
688
+ Then [backfill the data](#backfilling-data).
689
+
669
690
  ### Changing the default value of a column
670
691
 
671
692
  #### Bad
@@ -36,6 +36,10 @@ module StrongMigrations
36
36
  "reads and writes"
37
37
  end
38
38
 
39
+ def auto_incrementing_types
40
+ ["primary_key"]
41
+ end
42
+
39
43
  private
40
44
 
41
45
  def connection
@@ -25,7 +25,7 @@ module StrongMigrations
25
25
  end
26
26
 
27
27
  def add_column_default_safe?
28
- server_version >= Gem::Version.new("10.3.2")
28
+ true
29
29
  end
30
30
  end
31
31
  end
@@ -44,7 +44,7 @@ module StrongMigrations
44
44
  end
45
45
 
46
46
  def add_column_default_safe?
47
- server_version >= Gem::Version.new("8.0.12")
47
+ true
48
48
  end
49
49
 
50
50
  def change_type_safe?(table, column, type, options, existing_column, existing_type)
@@ -169,6 +169,10 @@ module StrongMigrations
169
169
  rows.empty? || rows.any? { |r| r["provolatile"] == "v" }
170
170
  end
171
171
 
172
+ def auto_incrementing_types
173
+ ["primary_key", "serial", "bigserial"]
174
+ end
175
+
172
176
  private
173
177
 
174
178
  def set_timeout(setting, timeout)
@@ -42,9 +42,7 @@ module StrongMigrations
42
42
  if options.key?(:default) && (!adapter.add_column_default_safe? || (volatile = (postgresql? && type.to_s == "uuid" && default.to_s.include?("()") && adapter.default_volatile?(default))))
43
43
  if options[:null] == false
44
44
  options = options.except(:null)
45
- append = "
46
-
47
- Then add the NOT NULL constraint in separate migrations."
45
+ append = "\n\nThen add the NOT NULL constraint in separate migrations."
48
46
  end
49
47
 
50
48
  if default.nil?
@@ -78,6 +76,13 @@ Then add the NOT NULL constraint in separate migrations."
78
76
  if type.to_s == "virtual" && options[:stored]
79
77
  raise_error :add_column_generated_stored, rewrite_blocks: adapter.rewrite_blocks
80
78
  end
79
+
80
+ if adapter.auto_incrementing_types.include?(type.to_s)
81
+ append = (mysql? || mariadb?) ? "\n\nIf using statement-based replication, this can also generate different values on replicas." : ""
82
+ raise_error :add_column_auto_incrementing,
83
+ rewrite_blocks: adapter.rewrite_blocks,
84
+ append: append
85
+ end
81
86
  end
82
87
 
83
88
  def check_add_exclusion_constraint(*args)
@@ -165,9 +170,7 @@ Then add the NOT NULL constraint in separate migrations."
165
170
 
166
171
  if options.delete(:foreign_key)
167
172
  headline = "Adding a foreign key blocks writes on both tables."
168
- append = "
169
-
170
- Then add the foreign key in separate migrations."
173
+ append = "\n\nThen add the foreign key in separate migrations."
171
174
  else
172
175
  headline = "Adding an index non-concurrently locks the table."
173
176
  end
@@ -353,7 +356,7 @@ Then add the foreign key in separate migrations."
353
356
  cols
354
357
  end
355
358
 
356
- code = "self.ignored_columns = #{columns.inspect}"
359
+ code = "self.ignored_columns += #{columns.inspect}"
357
360
 
358
361
  raise_error :remove_column,
359
362
  model: args[0].to_s.classify,
@@ -53,6 +53,9 @@ end",
53
53
  add_column_generated_stored:
54
54
  "Adding a stored generated column blocks %{rewrite_blocks} while the entire table is rewritten.",
55
55
 
56
+ add_column_auto_incrementing:
57
+ "Adding an auto-incrementing column blocks %{rewrite_blocks} while the entire table is rewritten.",
58
+
56
59
  change_column:
57
60
  "Changing the type of an existing column blocks %{rewrite_blocks}
58
61
  while the entire table is rewritten. A safer approach is to:
@@ -1,3 +1,3 @@
1
1
  module StrongMigrations
2
- VERSION = "1.7.0"
2
+ VERSION = "1.8.0"
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: 1.7.0
4
+ version: 1.8.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: 2024-01-05 00:00:00.000000000 Z
13
+ date: 2024-03-12 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activerecord