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 +4 -4
- data/CHANGELOG.md +6 -0
- data/LICENSE.txt +1 -1
- data/README.md +113 -92
- data/lib/strong_migrations/adapters/abstract_adapter.rb +4 -0
- data/lib/strong_migrations/adapters/mariadb_adapter.rb +1 -1
- data/lib/strong_migrations/adapters/mysql_adapter.rb +1 -1
- data/lib/strong_migrations/adapters/postgresql_adapter.rb +4 -0
- data/lib/strong_migrations/checks.rb +10 -7
- data/lib/strong_migrations/error_messages.rb +3 -0
- data/lib/strong_migrations/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f6462f35144a092664bb7456db2a1ad402695c6b5b1ba7001c1f1f977ea88d3e
|
4
|
+
data.tar.gz: fc71c84ae237b8dbbd3413af7b47ab4fa9962dcbbdd6b3e99b9edd816d3ae009
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
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
|
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
|
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
|
@@ -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
|
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:
|
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.
|
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-
|
13
|
+
date: 2024-03-12 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: activerecord
|