strong_migrations 0.6.0 → 2.3.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 +4 -4
- data/CHANGELOG.md +241 -1
- data/LICENSE.txt +1 -1
- data/README.md +551 -240
- data/lib/generators/strong_migrations/install_generator.rb +42 -0
- data/lib/generators/strong_migrations/templates/initializer.rb.tt +25 -0
- data/lib/strong_migrations/adapters/abstract_adapter.rb +76 -0
- data/lib/strong_migrations/adapters/mariadb_adapter.rb +32 -0
- data/lib/strong_migrations/adapters/mysql_adapter.rb +112 -0
- data/lib/strong_migrations/adapters/postgresql_adapter.rb +237 -0
- data/lib/strong_migrations/checker.rb +200 -291
- data/lib/strong_migrations/checks.rb +501 -0
- data/lib/strong_migrations/error_messages.rb +276 -0
- data/lib/strong_migrations/migration.rb +20 -2
- data/lib/strong_migrations/{database_tasks.rb → migration_context.rb} +20 -2
- data/lib/strong_migrations/migrator.rb +21 -0
- data/lib/strong_migrations/railtie.rb +0 -4
- data/lib/strong_migrations/safe_methods.rb +150 -0
- data/lib/strong_migrations/schema_dumper.rb +32 -0
- data/lib/strong_migrations/version.rb +1 -1
- data/lib/strong_migrations.rb +52 -216
- data/lib/tasks/strong_migrations.rake +2 -13
- metadata +19 -70
- data/lib/strong_migrations/alphabetize_columns.rb +0 -11
- data/lib/strong_migrations/migration_helpers.rb +0 -117
data/README.md
CHANGED
|
@@ -2,61 +2,103 @@
|
|
|
2
2
|
|
|
3
3
|
Catch unsafe migrations in development
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
✓ Detects potentially dangerous operations<br /> ✓ Prevents them from running by default<br /> ✓ Provides instructions on safer ways to do what you want
|
|
6
|
+
|
|
7
|
+
Supports PostgreSQL, MySQL, and MariaDB
|
|
6
8
|
|
|
7
9
|
:tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)
|
|
8
10
|
|
|
9
|
-
[](https://github.com/ankane/strong_migrations/actions)
|
|
10
12
|
|
|
11
13
|
## Installation
|
|
12
14
|
|
|
13
15
|
Add this line to your application’s Gemfile:
|
|
14
16
|
|
|
15
17
|
```ruby
|
|
16
|
-
gem
|
|
18
|
+
gem "strong_migrations"
|
|
17
19
|
```
|
|
18
20
|
|
|
21
|
+
And run:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
bundle install
|
|
25
|
+
rails generate strong_migrations:install
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Strong Migrations sets a long statement timeout for migrations so you can set a [short statement timeout](#app-timeouts) for your application.
|
|
29
|
+
|
|
19
30
|
## How It Works
|
|
20
31
|
|
|
21
|
-
|
|
32
|
+
When you run a migration that’s potentially dangerous, you’ll see an error message like:
|
|
22
33
|
|
|
23
|
-
|
|
34
|
+
```txt
|
|
35
|
+
=== Dangerous operation detected #strong_migrations ===
|
|
24
36
|
|
|
25
|
-
|
|
37
|
+
Active Record caches attributes, which causes problems
|
|
38
|
+
when removing columns. Be sure to ignore the column:
|
|
26
39
|
|
|
27
|
-
|
|
40
|
+
class User < ApplicationRecord
|
|
41
|
+
self.ignored_columns += ["name"]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
Deploy the code, then wrap this step in a safety_assured { ... } block.
|
|
28
45
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
- [[+]](#renaming-or-changing-the-type-of-a-column) changing the type of a column
|
|
36
|
-
- [[+]](#renaming-or-changing-the-type-of-a-column) renaming a column
|
|
37
|
-
- [[+]](#renaming-a-table) renaming a table
|
|
38
|
-
- [[+]](#creating-a-table-with-the-force-option) creating a table with the `force` option
|
|
39
|
-
- [[+]](#setting-not-null-on-an-existing-column) setting `NOT NULL` on an existing column
|
|
40
|
-
- [[+]](#adding-a-json-column) adding a `json` column
|
|
46
|
+
class RemoveColumn < ActiveRecord::Migration[8.0]
|
|
47
|
+
def change
|
|
48
|
+
safety_assured { remove_column :users, :name }
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
```
|
|
41
52
|
|
|
42
|
-
|
|
53
|
+
An operation is classified as dangerous if it either:
|
|
43
54
|
|
|
44
|
-
-
|
|
55
|
+
- Blocks reads or writes for more than a few seconds (after a lock is acquired)
|
|
56
|
+
- Has a good chance of causing application errors
|
|
57
|
+
|
|
58
|
+
## Checks
|
|
59
|
+
|
|
60
|
+
Potentially dangerous operations:
|
|
61
|
+
|
|
62
|
+
- [removing a column](#removing-a-column)
|
|
63
|
+
- [changing the type of a column](#changing-the-type-of-a-column)
|
|
64
|
+
- [renaming a column](#renaming-a-column)
|
|
65
|
+
- [renaming a table](#renaming-a-table)
|
|
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)
|
|
68
|
+
- [adding a stored generated column](#adding-a-stored-generated-column)
|
|
69
|
+
- [adding a check constraint](#adding-a-check-constraint)
|
|
70
|
+
- [executing SQL directly](#executing-SQL-directly)
|
|
71
|
+
- [backfilling data](#backfilling-data)
|
|
72
|
+
|
|
73
|
+
Postgres-specific checks:
|
|
74
|
+
|
|
75
|
+
- [adding an index non-concurrently](#adding-an-index-non-concurrently)
|
|
76
|
+
- [adding a reference](#adding-a-reference)
|
|
77
|
+
- [adding a foreign key](#adding-a-foreign-key)
|
|
78
|
+
- [adding a unique constraint](#adding-a-unique-constraint)
|
|
79
|
+
- [adding an exclusion constraint](#adding-an-exclusion-constraint)
|
|
80
|
+
- [adding a json column](#adding-a-json-column)
|
|
81
|
+
- [setting NOT NULL on an existing column](#setting-not-null-on-an-existing-column)
|
|
82
|
+
- [adding a column with a volatile default value](#adding-a-column-with-a-volatile-default-value)
|
|
83
|
+
|
|
84
|
+
Config-specific checks:
|
|
85
|
+
|
|
86
|
+
- [changing the default value of a column](#changing-the-default-value-of-a-column)
|
|
45
87
|
|
|
46
88
|
Best practices:
|
|
47
89
|
|
|
48
|
-
- [
|
|
90
|
+
- [keeping non-unique indexes to three columns or less](#keeping-non-unique-indexes-to-three-columns-or-less)
|
|
49
91
|
|
|
50
|
-
|
|
92
|
+
You can also add [custom checks](#custom-checks) or [disable specific checks](#disable-checks).
|
|
51
93
|
|
|
52
94
|
### Removing a column
|
|
53
95
|
|
|
54
96
|
#### Bad
|
|
55
97
|
|
|
56
|
-
|
|
98
|
+
Active Record caches database columns at runtime, so if you drop a column, it can cause exceptions until your app reboots.
|
|
57
99
|
|
|
58
100
|
```ruby
|
|
59
|
-
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[
|
|
101
|
+
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[8.0]
|
|
60
102
|
def change
|
|
61
103
|
remove_column :users, :some_column
|
|
62
104
|
end
|
|
@@ -65,388 +107,461 @@ end
|
|
|
65
107
|
|
|
66
108
|
#### Good
|
|
67
109
|
|
|
68
|
-
1. Tell
|
|
110
|
+
1. Tell Active Record to ignore the column from its cache
|
|
69
111
|
|
|
70
112
|
```ruby
|
|
71
113
|
class User < ApplicationRecord
|
|
72
|
-
self.ignored_columns
|
|
114
|
+
self.ignored_columns += ["some_column"]
|
|
73
115
|
end
|
|
74
116
|
```
|
|
75
117
|
|
|
76
|
-
2. Deploy code
|
|
118
|
+
2. Deploy the code
|
|
77
119
|
3. Write a migration to remove the column (wrap in `safety_assured` block)
|
|
78
120
|
|
|
79
121
|
```ruby
|
|
80
|
-
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[
|
|
122
|
+
class RemoveSomeColumnFromUsers < ActiveRecord::Migration[8.0]
|
|
81
123
|
def change
|
|
82
124
|
safety_assured { remove_column :users, :some_column }
|
|
83
125
|
end
|
|
84
126
|
end
|
|
85
127
|
```
|
|
86
128
|
|
|
87
|
-
4. Deploy and run migration
|
|
88
|
-
|
|
89
|
-
### Adding a column with a default value
|
|
129
|
+
4. Deploy and run the migration
|
|
130
|
+
5. Remove the line added in step 1
|
|
90
131
|
|
|
91
|
-
|
|
132
|
+
### Changing the type of a column
|
|
92
133
|
|
|
93
134
|
#### Bad
|
|
94
135
|
|
|
95
|
-
|
|
136
|
+
Changing the type of a 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.
|
|
96
137
|
|
|
97
138
|
```ruby
|
|
98
|
-
class
|
|
139
|
+
class ChangeSomeColumnType < ActiveRecord::Migration[8.0]
|
|
99
140
|
def change
|
|
100
|
-
|
|
141
|
+
change_column :users, :some_column, :new_type
|
|
101
142
|
end
|
|
102
143
|
end
|
|
103
144
|
```
|
|
104
145
|
|
|
105
|
-
|
|
146
|
+
Some changes don’t require a table rewrite and are safe in Postgres:
|
|
106
147
|
|
|
107
|
-
|
|
148
|
+
Type | Safe Changes
|
|
149
|
+
--- | ---
|
|
150
|
+
`cidr` | Changing to `inet`
|
|
151
|
+
`citext` | Changing to `text` if not indexed, changing to `string` with no `:limit` if not indexed
|
|
152
|
+
`datetime` | Increasing or removing `:precision`, changing to `timestamptz` when session time zone is UTC in Postgres 12+
|
|
153
|
+
`decimal` | Increasing `:precision` at same `:scale`, removing `:precision` and `:scale`
|
|
154
|
+
`interval` | Increasing or removing `:precision`
|
|
155
|
+
`numeric` | Increasing `:precision` at same `:scale`, removing `:precision` and `:scale`
|
|
156
|
+
`string` | Increasing or removing `:limit`, changing to `text`, changing `citext` if not indexed
|
|
157
|
+
`text` | Changing to `string` with no `:limit`, changing to `citext` if not indexed
|
|
158
|
+
`time` | Increasing or removing `:precision`
|
|
159
|
+
`timestamptz` | Increasing or removing `:limit`, changing to `datetime` when session time zone is UTC in Postgres 12+
|
|
108
160
|
|
|
109
|
-
|
|
110
|
-
class AddSomeColumnToUsers < ActiveRecord::Migration[6.0]
|
|
111
|
-
def up
|
|
112
|
-
add_column :users, :some_column, :text
|
|
113
|
-
change_column_default :users, :some_column, "default_value"
|
|
114
|
-
end
|
|
161
|
+
And some in MySQL and MariaDB:
|
|
115
162
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
163
|
+
Type | Safe Changes
|
|
164
|
+
--- | ---
|
|
165
|
+
`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`)
|
|
166
|
+
|
|
167
|
+
#### Good
|
|
121
168
|
|
|
122
|
-
|
|
169
|
+
A safer approach is to:
|
|
123
170
|
|
|
124
|
-
|
|
171
|
+
1. Create a new column
|
|
172
|
+
2. Write to both columns
|
|
173
|
+
3. Backfill data from the old column to the new column
|
|
174
|
+
4. Move reads from the old column to the new column
|
|
175
|
+
5. Stop writing to the old column
|
|
176
|
+
6. Drop the old column
|
|
177
|
+
|
|
178
|
+
### Renaming a column
|
|
125
179
|
|
|
126
180
|
#### Bad
|
|
127
181
|
|
|
128
|
-
|
|
182
|
+
Renaming a column that’s in use will cause errors in your application.
|
|
129
183
|
|
|
130
184
|
```ruby
|
|
131
|
-
class
|
|
185
|
+
class RenameSomeColumn < ActiveRecord::Migration[8.0]
|
|
132
186
|
def change
|
|
133
|
-
|
|
134
|
-
User.update_all some_column: "default_value"
|
|
187
|
+
rename_column :users, :some_column, :new_name
|
|
135
188
|
end
|
|
136
189
|
end
|
|
137
190
|
```
|
|
138
191
|
|
|
139
|
-
Also, running a single query to update data can cause issues for large tables.
|
|
140
|
-
|
|
141
192
|
#### Good
|
|
142
193
|
|
|
143
|
-
|
|
194
|
+
A safer approach is to:
|
|
144
195
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
196
|
+
1. Create a new column
|
|
197
|
+
2. Write to both columns
|
|
198
|
+
3. Backfill data from the old column to the new column
|
|
199
|
+
4. Move reads from the old column to the new column
|
|
200
|
+
5. Stop writing to the old column
|
|
201
|
+
6. Drop the old column
|
|
148
202
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
203
|
+
### Renaming a table
|
|
204
|
+
|
|
205
|
+
#### Bad
|
|
206
|
+
|
|
207
|
+
Renaming a table that’s in use will cause errors in your application.
|
|
208
|
+
|
|
209
|
+
```ruby
|
|
210
|
+
class RenameUsersToCustomers < ActiveRecord::Migration[8.0]
|
|
211
|
+
def change
|
|
212
|
+
rename_table :users, :customers
|
|
154
213
|
end
|
|
155
214
|
end
|
|
156
215
|
```
|
|
157
216
|
|
|
158
|
-
|
|
217
|
+
#### Good
|
|
218
|
+
|
|
219
|
+
A safer approach is to:
|
|
220
|
+
|
|
221
|
+
1. Create a new table
|
|
222
|
+
2. Write to both tables
|
|
223
|
+
3. Backfill data from the old table to the new table
|
|
224
|
+
4. Move reads from the old table to the new table
|
|
225
|
+
5. Stop writing to the old table
|
|
226
|
+
6. Drop the old table
|
|
227
|
+
|
|
228
|
+
### Creating a table with the force option
|
|
159
229
|
|
|
160
230
|
#### Bad
|
|
161
231
|
|
|
162
|
-
|
|
232
|
+
The `force` option can drop an existing table.
|
|
163
233
|
|
|
164
234
|
```ruby
|
|
165
|
-
class
|
|
235
|
+
class CreateUsers < ActiveRecord::Migration[8.0]
|
|
166
236
|
def change
|
|
167
|
-
|
|
237
|
+
create_table :users, force: true do |t|
|
|
238
|
+
# ...
|
|
239
|
+
end
|
|
168
240
|
end
|
|
169
241
|
end
|
|
170
242
|
```
|
|
171
243
|
|
|
172
244
|
#### Good
|
|
173
245
|
|
|
174
|
-
|
|
246
|
+
Create tables without the `force` option.
|
|
175
247
|
|
|
176
248
|
```ruby
|
|
177
|
-
class
|
|
178
|
-
disable_ddl_transaction!
|
|
179
|
-
|
|
249
|
+
class CreateUsers < ActiveRecord::Migration[8.0]
|
|
180
250
|
def change
|
|
181
|
-
|
|
251
|
+
create_table :users do |t|
|
|
252
|
+
# ...
|
|
253
|
+
end
|
|
182
254
|
end
|
|
183
255
|
end
|
|
184
256
|
```
|
|
185
257
|
|
|
186
|
-
If you
|
|
258
|
+
If you intend to drop an existing table, run `drop_table` first.
|
|
187
259
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
```sh
|
|
191
|
-
rails g index table column
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
### Adding a reference
|
|
260
|
+
### Adding an auto-incrementing column
|
|
195
261
|
|
|
196
262
|
#### Bad
|
|
197
263
|
|
|
198
|
-
|
|
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.
|
|
199
265
|
|
|
200
266
|
```ruby
|
|
201
|
-
class
|
|
267
|
+
class AddIdToCitiesUsers < ActiveRecord::Migration[8.0]
|
|
202
268
|
def change
|
|
203
|
-
|
|
269
|
+
add_column :cities_users, :id, :primary_key
|
|
204
270
|
end
|
|
205
271
|
end
|
|
206
272
|
```
|
|
207
273
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
Make sure the index is added concurrently.
|
|
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.
|
|
211
275
|
|
|
212
|
-
|
|
213
|
-
class AddReferenceToUsers < ActiveRecord::Migration[6.0]
|
|
214
|
-
disable_ddl_transaction!
|
|
276
|
+
#### Good
|
|
215
277
|
|
|
216
|
-
|
|
217
|
-
add_reference :users, :city, index: {algorithm: :concurrently}
|
|
218
|
-
end
|
|
219
|
-
end
|
|
220
|
-
```
|
|
278
|
+
Create a new table and migrate the data with the same steps as [renaming a table](#renaming-a-table).
|
|
221
279
|
|
|
222
|
-
### Adding a
|
|
280
|
+
### Adding a stored generated column
|
|
223
281
|
|
|
224
282
|
#### Bad
|
|
225
283
|
|
|
226
|
-
|
|
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.
|
|
227
285
|
|
|
228
286
|
```ruby
|
|
229
|
-
class
|
|
287
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[8.0]
|
|
230
288
|
def change
|
|
231
|
-
|
|
289
|
+
add_column :users, :some_column, :virtual, type: :string, as: "...", stored: true
|
|
232
290
|
end
|
|
233
291
|
end
|
|
234
292
|
```
|
|
235
293
|
|
|
236
294
|
#### Good
|
|
237
295
|
|
|
238
|
-
|
|
296
|
+
Add a non-generated column and use callbacks or triggers instead (or a virtual generated column with MySQL and MariaDB).
|
|
297
|
+
|
|
298
|
+
### Adding a check constraint
|
|
299
|
+
|
|
300
|
+
:turtle: Safe by default available
|
|
239
301
|
|
|
240
|
-
|
|
302
|
+
#### Bad
|
|
303
|
+
|
|
304
|
+
Adding a check constraint blocks reads and writes in Postgres and blocks writes in MySQL and MariaDB while every row is checked.
|
|
241
305
|
|
|
242
306
|
```ruby
|
|
243
|
-
class
|
|
307
|
+
class AddCheckConstraint < ActiveRecord::Migration[8.0]
|
|
244
308
|
def change
|
|
245
|
-
|
|
309
|
+
add_check_constraint :users, "price > 0", name: "price_check"
|
|
246
310
|
end
|
|
247
311
|
end
|
|
248
312
|
```
|
|
249
313
|
|
|
250
|
-
|
|
314
|
+
#### Good - Postgres
|
|
315
|
+
|
|
316
|
+
Add the check constraint without validating existing rows:
|
|
251
317
|
|
|
252
318
|
```ruby
|
|
253
|
-
class
|
|
319
|
+
class AddCheckConstraint < ActiveRecord::Migration[8.0]
|
|
254
320
|
def change
|
|
255
|
-
|
|
321
|
+
add_check_constraint :users, "price > 0", name: "price_check", validate: false
|
|
256
322
|
end
|
|
257
323
|
end
|
|
258
324
|
```
|
|
259
325
|
|
|
260
|
-
|
|
326
|
+
Then validate them in a separate migration.
|
|
261
327
|
|
|
262
328
|
```ruby
|
|
263
|
-
class
|
|
329
|
+
class ValidateCheckConstraint < ActiveRecord::Migration[8.0]
|
|
264
330
|
def change
|
|
265
|
-
|
|
266
|
-
execute 'ALTER TABLE "users" ADD CONSTRAINT "fk_rails_c1e9b98e31" FOREIGN KEY ("order_id") REFERENCES "orders" ("id") NOT VALID'
|
|
267
|
-
end
|
|
331
|
+
validate_check_constraint :users, name: "price_check"
|
|
268
332
|
end
|
|
269
333
|
end
|
|
270
334
|
```
|
|
271
335
|
|
|
272
|
-
|
|
336
|
+
#### Good - MySQL and MariaDB
|
|
337
|
+
|
|
338
|
+
[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).
|
|
339
|
+
|
|
340
|
+
### Executing SQL directly
|
|
341
|
+
|
|
342
|
+
Strong Migrations can’t ensure safety for raw SQL statements. Make really sure that what you’re doing is safe, then use:
|
|
273
343
|
|
|
274
344
|
```ruby
|
|
275
|
-
class
|
|
345
|
+
class ExecuteSQL < ActiveRecord::Migration[8.0]
|
|
276
346
|
def change
|
|
277
|
-
safety_assured
|
|
278
|
-
execute 'ALTER TABLE "users" VALIDATE CONSTRAINT "fk_rails_c1e9b98e31"'
|
|
279
|
-
end
|
|
347
|
+
safety_assured { execute "..." }
|
|
280
348
|
end
|
|
281
349
|
end
|
|
282
350
|
```
|
|
283
351
|
|
|
284
|
-
###
|
|
352
|
+
### Backfilling data
|
|
353
|
+
|
|
354
|
+
Note: Strong Migrations does not detect dangerous backfills.
|
|
285
355
|
|
|
286
356
|
#### Bad
|
|
287
357
|
|
|
358
|
+
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/).
|
|
359
|
+
|
|
288
360
|
```ruby
|
|
289
|
-
class
|
|
361
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[8.0]
|
|
290
362
|
def change
|
|
291
|
-
|
|
363
|
+
add_column :users, :some_column, :text
|
|
364
|
+
User.update_all some_column: "default_value"
|
|
292
365
|
end
|
|
293
366
|
end
|
|
294
367
|
```
|
|
295
368
|
|
|
296
|
-
|
|
369
|
+
Also, running a single query to update data can cause issues for large tables.
|
|
370
|
+
|
|
371
|
+
#### Good
|
|
372
|
+
|
|
373
|
+
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!`.
|
|
297
374
|
|
|
298
375
|
```ruby
|
|
299
|
-
class
|
|
300
|
-
|
|
301
|
-
|
|
376
|
+
class BackfillSomeColumn < ActiveRecord::Migration[8.0]
|
|
377
|
+
disable_ddl_transaction!
|
|
378
|
+
|
|
379
|
+
def up
|
|
380
|
+
User.unscoped.in_batches(of: 10000) do |relation|
|
|
381
|
+
relation.where(some_column: nil).update_all some_column: "default_value"
|
|
382
|
+
sleep(0.01) # throttle
|
|
383
|
+
end
|
|
302
384
|
end
|
|
303
385
|
end
|
|
304
386
|
```
|
|
305
387
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
#### Good
|
|
309
|
-
|
|
310
|
-
A safer approach is to:
|
|
388
|
+
Note: If backfilling with a method other than `update_all`, use `User.reset_column_information` to ensure the model has up-to-date column information.
|
|
311
389
|
|
|
312
|
-
|
|
313
|
-
2. Write to both columns
|
|
314
|
-
3. Backfill data from the old column to the new column
|
|
315
|
-
4. Move reads from the old column to the new column
|
|
316
|
-
5. Stop writing to the old column
|
|
317
|
-
6. Drop the old column
|
|
390
|
+
### Adding an index non-concurrently
|
|
318
391
|
|
|
319
|
-
|
|
392
|
+
:turtle: Safe by default available
|
|
320
393
|
|
|
321
394
|
#### Bad
|
|
322
395
|
|
|
396
|
+
In Postgres, adding an index non-concurrently blocks writes.
|
|
397
|
+
|
|
323
398
|
```ruby
|
|
324
|
-
class
|
|
399
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[8.0]
|
|
325
400
|
def change
|
|
326
|
-
|
|
401
|
+
add_index :users, :some_column
|
|
327
402
|
end
|
|
328
403
|
end
|
|
329
404
|
```
|
|
330
405
|
|
|
331
406
|
#### Good
|
|
332
407
|
|
|
333
|
-
|
|
408
|
+
Add indexes concurrently.
|
|
334
409
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
4. Move reads from the old table to the new table
|
|
339
|
-
5. Stop writing to the old table
|
|
340
|
-
6. Drop the old table
|
|
410
|
+
```ruby
|
|
411
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[8.0]
|
|
412
|
+
disable_ddl_transaction!
|
|
341
413
|
|
|
342
|
-
|
|
414
|
+
def change
|
|
415
|
+
add_index :users, :some_column, algorithm: :concurrently
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
If you forget `disable_ddl_transaction!`, the migration will fail. Also, note that indexes on new tables (those created in the same migration) don’t require this.
|
|
421
|
+
|
|
422
|
+
With [gindex](https://github.com/ankane/gindex), you can generate an index migration instantly with:
|
|
423
|
+
|
|
424
|
+
```sh
|
|
425
|
+
rails g index table column
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
### Adding a reference
|
|
429
|
+
|
|
430
|
+
:turtle: Safe by default available
|
|
343
431
|
|
|
344
432
|
#### Bad
|
|
345
433
|
|
|
346
|
-
|
|
434
|
+
Rails adds an index non-concurrently to references by default, which blocks writes in Postgres.
|
|
347
435
|
|
|
348
436
|
```ruby
|
|
349
|
-
class
|
|
437
|
+
class AddReferenceToUsers < ActiveRecord::Migration[8.0]
|
|
350
438
|
def change
|
|
351
|
-
|
|
352
|
-
# ...
|
|
353
|
-
end
|
|
439
|
+
add_reference :users, :city
|
|
354
440
|
end
|
|
355
441
|
end
|
|
356
442
|
```
|
|
357
443
|
|
|
358
444
|
#### Good
|
|
359
445
|
|
|
360
|
-
|
|
446
|
+
Make sure the index is added concurrently.
|
|
361
447
|
|
|
362
448
|
```ruby
|
|
363
|
-
class
|
|
449
|
+
class AddReferenceToUsers < ActiveRecord::Migration[8.0]
|
|
450
|
+
disable_ddl_transaction!
|
|
451
|
+
|
|
364
452
|
def change
|
|
365
|
-
|
|
366
|
-
# ...
|
|
367
|
-
end
|
|
453
|
+
add_reference :users, :city, index: {algorithm: :concurrently}
|
|
368
454
|
end
|
|
369
455
|
end
|
|
370
456
|
```
|
|
371
457
|
|
|
372
|
-
###
|
|
458
|
+
### Adding a foreign key
|
|
459
|
+
|
|
460
|
+
:turtle: Safe by default available
|
|
373
461
|
|
|
374
462
|
#### Bad
|
|
375
463
|
|
|
376
|
-
In Postgres,
|
|
464
|
+
In Postgres, adding a foreign key blocks writes on both tables.
|
|
377
465
|
|
|
378
466
|
```ruby
|
|
379
|
-
class
|
|
467
|
+
class AddForeignKeyOnUsers < ActiveRecord::Migration[8.0]
|
|
380
468
|
def change
|
|
381
|
-
|
|
469
|
+
add_foreign_key :users, :orders
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
or
|
|
475
|
+
|
|
476
|
+
```ruby
|
|
477
|
+
class AddReferenceToUsers < ActiveRecord::Migration[8.0]
|
|
478
|
+
def change
|
|
479
|
+
add_reference :users, :order, foreign_key: true
|
|
382
480
|
end
|
|
383
481
|
end
|
|
384
482
|
```
|
|
385
483
|
|
|
386
484
|
#### Good
|
|
387
485
|
|
|
388
|
-
|
|
486
|
+
Add the foreign key without validating existing rows:
|
|
389
487
|
|
|
390
488
|
```ruby
|
|
391
|
-
class
|
|
489
|
+
class AddForeignKeyOnUsers < ActiveRecord::Migration[8.0]
|
|
392
490
|
def change
|
|
393
|
-
|
|
394
|
-
execute 'ALTER TABLE "users" ADD CONSTRAINT "users_some_column_null" CHECK ("some_column" IS NOT NULL) NOT VALID'
|
|
395
|
-
end
|
|
491
|
+
add_foreign_key :users, :orders, validate: false
|
|
396
492
|
end
|
|
397
493
|
end
|
|
398
494
|
```
|
|
399
495
|
|
|
400
|
-
Then validate
|
|
496
|
+
Then validate them in a separate migration.
|
|
401
497
|
|
|
402
498
|
```ruby
|
|
403
|
-
class
|
|
499
|
+
class ValidateForeignKeyOnUsers < ActiveRecord::Migration[8.0]
|
|
404
500
|
def change
|
|
405
|
-
|
|
406
|
-
execute 'ALTER TABLE "users" VALIDATE CONSTRAINT "users_some_column_null"'
|
|
407
|
-
end
|
|
501
|
+
validate_foreign_key :users, :orders
|
|
408
502
|
end
|
|
409
503
|
end
|
|
410
504
|
```
|
|
411
505
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
### Using change_column_null with a default value
|
|
506
|
+
### Adding a unique constraint
|
|
415
507
|
|
|
416
508
|
#### Bad
|
|
417
509
|
|
|
418
|
-
|
|
510
|
+
In Postgres, adding a unique constraint creates a unique index, which blocks reads and writes.
|
|
419
511
|
|
|
420
512
|
```ruby
|
|
421
|
-
class
|
|
513
|
+
class AddUniqueConstraint < ActiveRecord::Migration[8.0]
|
|
422
514
|
def change
|
|
423
|
-
|
|
515
|
+
add_unique_constraint :users, :some_column
|
|
424
516
|
end
|
|
425
517
|
end
|
|
426
518
|
```
|
|
427
519
|
|
|
428
520
|
#### Good
|
|
429
521
|
|
|
430
|
-
|
|
522
|
+
Create a unique index concurrently, then use it for the constraint.
|
|
431
523
|
|
|
432
524
|
```ruby
|
|
433
|
-
class
|
|
525
|
+
class AddUniqueConstraint < ActiveRecord::Migration[8.0]
|
|
526
|
+
disable_ddl_transaction!
|
|
527
|
+
|
|
528
|
+
def up
|
|
529
|
+
add_index :users, :some_column, unique: true, algorithm: :concurrently
|
|
530
|
+
add_unique_constraint :users, using_index: "index_users_on_some_column"
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def down
|
|
534
|
+
remove_unique_constraint :users, :some_column
|
|
535
|
+
end
|
|
536
|
+
end
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
### Adding an exclusion constraint
|
|
540
|
+
|
|
541
|
+
#### Bad
|
|
542
|
+
|
|
543
|
+
In Postgres, adding an exclusion constraint blocks reads and writes while every row is checked.
|
|
544
|
+
|
|
545
|
+
```ruby
|
|
546
|
+
class AddExclusionConstraint < ActiveRecord::Migration[8.0]
|
|
434
547
|
def change
|
|
435
|
-
|
|
548
|
+
add_exclusion_constraint :users, "number WITH =", using: :gist
|
|
436
549
|
end
|
|
437
550
|
end
|
|
438
551
|
```
|
|
439
552
|
|
|
440
|
-
|
|
553
|
+
#### Good
|
|
554
|
+
|
|
555
|
+
[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`).
|
|
441
556
|
|
|
442
557
|
### Adding a json column
|
|
443
558
|
|
|
444
559
|
#### Bad
|
|
445
560
|
|
|
446
|
-
In Postgres, there’s no equality operator for the `json` column type, which can cause errors for existing `SELECT DISTINCT` queries.
|
|
561
|
+
In Postgres, there’s no equality operator for the `json` column type, which can cause errors for existing `SELECT DISTINCT` queries in your application.
|
|
447
562
|
|
|
448
563
|
```ruby
|
|
449
|
-
class AddPropertiesToUsers < ActiveRecord::Migration[
|
|
564
|
+
class AddPropertiesToUsers < ActiveRecord::Migration[8.0]
|
|
450
565
|
def change
|
|
451
566
|
add_column :users, :properties, :json
|
|
452
567
|
end
|
|
@@ -458,56 +573,120 @@ end
|
|
|
458
573
|
Use `jsonb` instead.
|
|
459
574
|
|
|
460
575
|
```ruby
|
|
461
|
-
class AddPropertiesToUsers < ActiveRecord::Migration[
|
|
576
|
+
class AddPropertiesToUsers < ActiveRecord::Migration[8.0]
|
|
462
577
|
def change
|
|
463
578
|
add_column :users, :properties, :jsonb
|
|
464
579
|
end
|
|
465
580
|
end
|
|
466
581
|
```
|
|
467
582
|
|
|
468
|
-
|
|
583
|
+
### Setting NOT NULL on an existing column
|
|
584
|
+
|
|
585
|
+
:turtle: Safe by default available
|
|
469
586
|
|
|
470
|
-
|
|
587
|
+
#### Bad
|
|
588
|
+
|
|
589
|
+
In Postgres, setting `NOT NULL` on an existing column blocks reads and writes while every row is checked.
|
|
471
590
|
|
|
472
591
|
```ruby
|
|
473
|
-
|
|
592
|
+
class SetSomeColumnNotNull < ActiveRecord::Migration[8.0]
|
|
593
|
+
def change
|
|
594
|
+
change_column_null :users, :some_column, false
|
|
595
|
+
end
|
|
596
|
+
end
|
|
597
|
+
```
|
|
598
|
+
|
|
599
|
+
#### Good
|
|
600
|
+
|
|
601
|
+
Instead, add a check constraint.
|
|
602
|
+
|
|
603
|
+
```ruby
|
|
604
|
+
class SetSomeColumnNotNull < ActiveRecord::Migration[8.0]
|
|
605
|
+
def change
|
|
606
|
+
add_check_constraint :users, "some_column IS NOT NULL", name: "users_some_column_null", validate: false
|
|
607
|
+
end
|
|
608
|
+
end
|
|
474
609
|
```
|
|
475
610
|
|
|
476
|
-
|
|
611
|
+
Then validate it in a separate migration. Once the check constraint is validated, you can safely set `NOT NULL` on the column and drop the check constraint.
|
|
477
612
|
|
|
478
613
|
```ruby
|
|
479
|
-
|
|
614
|
+
class ValidateSomeColumnNotNull < ActiveRecord::Migration[8.0]
|
|
615
|
+
def up
|
|
616
|
+
validate_check_constraint :users, name: "users_some_column_null"
|
|
617
|
+
change_column_null :users, :some_column, false
|
|
618
|
+
remove_check_constraint :users, name: "users_some_column_null"
|
|
619
|
+
end
|
|
620
|
+
|
|
621
|
+
def down
|
|
622
|
+
add_check_constraint :users, "some_column IS NOT NULL", name: "users_some_column_null", validate: false
|
|
623
|
+
change_column_null :users, :some_column, true
|
|
624
|
+
end
|
|
625
|
+
end
|
|
480
626
|
```
|
|
481
627
|
|
|
482
|
-
###
|
|
628
|
+
### Adding a column with a volatile default value
|
|
483
629
|
|
|
484
630
|
#### Bad
|
|
485
631
|
|
|
486
|
-
|
|
632
|
+
Adding a column with a volatile default value to an existing table causes the entire table to be rewritten. During this time, reads and writes are blocked.
|
|
487
633
|
|
|
488
634
|
```ruby
|
|
489
|
-
class
|
|
635
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[8.0]
|
|
490
636
|
def change
|
|
491
|
-
|
|
637
|
+
add_column :users, :some_column, :uuid, default: "gen_random_uuid()"
|
|
492
638
|
end
|
|
493
639
|
end
|
|
494
640
|
```
|
|
495
641
|
|
|
496
642
|
#### Good
|
|
497
643
|
|
|
498
|
-
|
|
644
|
+
Instead, add the column without a default value, then change the default.
|
|
499
645
|
|
|
500
646
|
```ruby
|
|
501
|
-
class
|
|
502
|
-
|
|
647
|
+
class AddSomeColumnToUsers < ActiveRecord::Migration[8.0]
|
|
648
|
+
def up
|
|
649
|
+
add_column :users, :some_column, :uuid
|
|
650
|
+
change_column_default :users, :some_column, from: nil, to: "gen_random_uuid()"
|
|
651
|
+
end
|
|
652
|
+
|
|
653
|
+
def down
|
|
654
|
+
remove_column :users, :some_column
|
|
655
|
+
end
|
|
656
|
+
end
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
Then [backfill the data](#backfilling-data).
|
|
660
|
+
|
|
661
|
+
### Changing the default value of a column
|
|
662
|
+
|
|
663
|
+
#### Bad
|
|
664
|
+
|
|
665
|
+
Rails < 7 enables partial writes by default, which can cause incorrect values to be inserted when changing the default value of a column.
|
|
503
666
|
|
|
667
|
+
```ruby
|
|
668
|
+
class ChangeSomeColumnDefault < ActiveRecord::Migration[6.1]
|
|
504
669
|
def change
|
|
505
|
-
|
|
670
|
+
change_column_default :users, :some_column, from: "old", to: "new"
|
|
506
671
|
end
|
|
507
672
|
end
|
|
673
|
+
|
|
674
|
+
User.create!(some_column: "old") # can insert "new"
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
#### Good
|
|
678
|
+
|
|
679
|
+
Disable partial writes in `config/application.rb`. For Rails < 7, use:
|
|
680
|
+
|
|
681
|
+
```ruby
|
|
682
|
+
config.active_record.partial_writes = false
|
|
508
683
|
```
|
|
509
684
|
|
|
510
|
-
|
|
685
|
+
For Rails 7+, use:
|
|
686
|
+
|
|
687
|
+
```ruby
|
|
688
|
+
config.active_record.partial_inserts = false
|
|
689
|
+
```
|
|
511
690
|
|
|
512
691
|
### Keeping non-unique indexes to three columns or less
|
|
513
692
|
|
|
@@ -516,7 +695,7 @@ end
|
|
|
516
695
|
Adding a non-unique index with more than three columns rarely improves performance.
|
|
517
696
|
|
|
518
697
|
```ruby
|
|
519
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[
|
|
698
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[8.0]
|
|
520
699
|
def change
|
|
521
700
|
add_index :users, [:a, :b, :c, :d]
|
|
522
701
|
end
|
|
@@ -528,9 +707,9 @@ end
|
|
|
528
707
|
Instead, start an index with columns that narrow down the results the most.
|
|
529
708
|
|
|
530
709
|
```ruby
|
|
531
|
-
class AddSomeIndexToUsers < ActiveRecord::Migration[
|
|
710
|
+
class AddSomeIndexToUsers < ActiveRecord::Migration[8.0]
|
|
532
711
|
def change
|
|
533
|
-
add_index :users, [:
|
|
712
|
+
add_index :users, [:d, :b]
|
|
534
713
|
end
|
|
535
714
|
end
|
|
536
715
|
```
|
|
@@ -542,7 +721,7 @@ For Postgres, be sure to add them concurrently.
|
|
|
542
721
|
To mark a step in the migration as safe, despite using a method that might otherwise be dangerous, wrap it in a `safety_assured` block.
|
|
543
722
|
|
|
544
723
|
```ruby
|
|
545
|
-
class MySafeMigration < ActiveRecord::Migration[
|
|
724
|
+
class MySafeMigration < ActiveRecord::Migration[8.0]
|
|
546
725
|
def change
|
|
547
726
|
safety_assured { remove_column :users, :some_column }
|
|
548
727
|
end
|
|
@@ -551,6 +730,21 @@ end
|
|
|
551
730
|
|
|
552
731
|
Certain methods like `execute` and `change_table` cannot be inspected and are prevented from running by default. Make sure what you’re doing is really safe and use this pattern.
|
|
553
732
|
|
|
733
|
+
## Safe by Default
|
|
734
|
+
|
|
735
|
+
Make certain operations safe by default. This allows you to write the code under the "Bad" section, but the migration will be performed as if you had written the "Good" version.
|
|
736
|
+
|
|
737
|
+
- adding and removing an index
|
|
738
|
+
- adding a foreign key
|
|
739
|
+
- adding a check constraint
|
|
740
|
+
- setting NOT NULL on an existing column
|
|
741
|
+
|
|
742
|
+
Add to `config/initializers/strong_migrations.rb`:
|
|
743
|
+
|
|
744
|
+
```ruby
|
|
745
|
+
StrongMigrations.safe_by_default = true
|
|
746
|
+
```
|
|
747
|
+
|
|
554
748
|
## Custom Checks
|
|
555
749
|
|
|
556
750
|
Add your own custom checks with:
|
|
@@ -567,6 +761,16 @@ Use the `stop!` method to stop migrations.
|
|
|
567
761
|
|
|
568
762
|
Note: Since `remove_column` always requires a `safety_assured` block, it’s not possible to add a custom check for `remove_column` operations.
|
|
569
763
|
|
|
764
|
+
## Opt-in Checks
|
|
765
|
+
|
|
766
|
+
### Removing an index non-concurrently
|
|
767
|
+
|
|
768
|
+
Postgres supports removing indexes concurrently, but removing them non-concurrently shouldn’t be an issue for most applications. You can enable this check with:
|
|
769
|
+
|
|
770
|
+
```ruby
|
|
771
|
+
StrongMigrations.enable_check(:remove_index)
|
|
772
|
+
```
|
|
773
|
+
|
|
570
774
|
## Disable Checks
|
|
571
775
|
|
|
572
776
|
Disable specific checks with:
|
|
@@ -575,106 +779,206 @@ Disable specific checks with:
|
|
|
575
779
|
StrongMigrations.disable_check(:add_index)
|
|
576
780
|
```
|
|
577
781
|
|
|
578
|
-
Check the [source code](https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations.rb) for the list of keys.
|
|
782
|
+
Check the [source code](https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations/error_messages.rb) for the list of keys.
|
|
579
783
|
|
|
580
|
-
##
|
|
784
|
+
## Skip Databases
|
|
581
785
|
|
|
582
|
-
|
|
786
|
+
Skip checks and other functionality for specific databases with:
|
|
583
787
|
|
|
584
788
|
```ruby
|
|
585
|
-
StrongMigrations.
|
|
789
|
+
StrongMigrations.skip_database(:catalog)
|
|
586
790
|
```
|
|
587
791
|
|
|
588
|
-
|
|
792
|
+
Note: This does not affect `alphabetize_schema`.
|
|
589
793
|
|
|
590
|
-
##
|
|
794
|
+
## Down Migrations / Rollbacks
|
|
591
795
|
|
|
592
|
-
|
|
796
|
+
By default, checks are disabled when migrating down. Enable them with:
|
|
593
797
|
|
|
594
|
-
```
|
|
595
|
-
|
|
798
|
+
```ruby
|
|
799
|
+
StrongMigrations.check_down = true
|
|
596
800
|
```
|
|
597
801
|
|
|
598
|
-
##
|
|
802
|
+
## Custom Messages
|
|
599
803
|
|
|
600
|
-
|
|
804
|
+
To customize specific messages, create an initializer with:
|
|
601
805
|
|
|
602
806
|
```ruby
|
|
603
|
-
|
|
604
|
-
`git status db/migrate/ --porcelain`.present?
|
|
807
|
+
StrongMigrations.error_messages[:add_column_default] = "Your custom instructions"
|
|
605
808
|
```
|
|
606
809
|
|
|
607
|
-
|
|
810
|
+
Check the [source code](https://github.com/ankane/strong_migrations/blob/master/lib/strong_migrations/error_messages.rb) for the list of keys.
|
|
811
|
+
|
|
812
|
+
## Migration Timeouts
|
|
813
|
+
|
|
814
|
+
It’s extremely important to set a short lock timeout for migrations. This way, if a migration can’t acquire a lock in a timely manner, other statements won’t be stuck behind it. We also recommend setting a long statement timeout so migrations can run for a while.
|
|
608
815
|
|
|
609
|
-
|
|
816
|
+
Create `config/initializers/strong_migrations.rb` with:
|
|
610
817
|
|
|
611
818
|
```ruby
|
|
612
|
-
|
|
819
|
+
StrongMigrations.lock_timeout = 10.seconds
|
|
820
|
+
StrongMigrations.statement_timeout = 1.hour
|
|
613
821
|
```
|
|
614
822
|
|
|
615
|
-
|
|
823
|
+
Or set the timeouts directly on the database user that runs migrations. For Postgres, use:
|
|
616
824
|
|
|
617
|
-
|
|
825
|
+
```sql
|
|
826
|
+
ALTER ROLE myuser SET lock_timeout = '10s';
|
|
827
|
+
ALTER ROLE myuser SET statement_timeout = '1h';
|
|
828
|
+
```
|
|
829
|
+
|
|
830
|
+
Note: If you use PgBouncer in transaction mode, you must set timeouts on the database user.
|
|
831
|
+
|
|
832
|
+
## App Timeouts
|
|
833
|
+
|
|
834
|
+
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.
|
|
835
|
+
|
|
836
|
+
For Postgres:
|
|
837
|
+
|
|
838
|
+
```yml
|
|
839
|
+
production:
|
|
840
|
+
connect_timeout: 5
|
|
841
|
+
variables:
|
|
842
|
+
statement_timeout: 15s
|
|
843
|
+
lock_timeout: 10s
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
Note: If you use PgBouncer in transaction mode, you must set the statement and lock timeouts on the database user as shown above.
|
|
847
|
+
|
|
848
|
+
For MySQL:
|
|
849
|
+
|
|
850
|
+
```yml
|
|
851
|
+
production:
|
|
852
|
+
connect_timeout: 5
|
|
853
|
+
read_timeout: 5
|
|
854
|
+
write_timeout: 5
|
|
855
|
+
variables:
|
|
856
|
+
max_execution_time: 15000 # ms
|
|
857
|
+
lock_wait_timeout: 10 # sec
|
|
858
|
+
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
For MariaDB:
|
|
862
|
+
|
|
863
|
+
```yml
|
|
864
|
+
production:
|
|
865
|
+
connect_timeout: 5
|
|
866
|
+
read_timeout: 5
|
|
867
|
+
write_timeout: 5
|
|
868
|
+
variables:
|
|
869
|
+
max_statement_time: 15 # sec
|
|
870
|
+
lock_wait_timeout: 10 # sec
|
|
871
|
+
```
|
|
872
|
+
|
|
873
|
+
For HTTP connections, Redis, and other services, check out [this guide](https://github.com/ankane/the-ultimate-guide-to-ruby-timeouts).
|
|
874
|
+
|
|
875
|
+
## Invalid Indexes
|
|
876
|
+
|
|
877
|
+
In Postgres, adding an index non-concurrently can leave behind an invalid index if the lock timeout is reached. Running the migration again can result in an error.
|
|
878
|
+
|
|
879
|
+
To automatically remove the invalid index when the migration runs again, use:
|
|
618
880
|
|
|
619
881
|
```ruby
|
|
620
|
-
StrongMigrations.
|
|
882
|
+
StrongMigrations.remove_invalid_indexes = true
|
|
621
883
|
```
|
|
622
884
|
|
|
623
|
-
|
|
885
|
+
Note: This feature is experimental.
|
|
624
886
|
|
|
625
|
-
##
|
|
887
|
+
## Lock Timeout Retries
|
|
626
888
|
|
|
627
|
-
|
|
889
|
+
Note: This feature is experimental.
|
|
890
|
+
|
|
891
|
+
There’s the option to automatically retry statements for migrations when the lock timeout is reached. Here’s how it works:
|
|
892
|
+
|
|
893
|
+
- If a lock timeout happens outside a transaction, the statement is retried
|
|
894
|
+
- If it happens inside the DDL transaction, the entire migration is retried (only applicable to Postgres)
|
|
895
|
+
|
|
896
|
+
Add to `config/initializers/strong_migrations.rb`:
|
|
628
897
|
|
|
629
898
|
```ruby
|
|
630
|
-
StrongMigrations.
|
|
899
|
+
StrongMigrations.lock_timeout_retries = 3
|
|
900
|
+
```
|
|
901
|
+
|
|
902
|
+
Set the delay between retries with:
|
|
903
|
+
|
|
904
|
+
```ruby
|
|
905
|
+
StrongMigrations.lock_timeout_retry_delay = 10.seconds
|
|
631
906
|
```
|
|
632
907
|
|
|
908
|
+
## Existing Migrations
|
|
909
|
+
|
|
910
|
+
To mark migrations as safe that were created before installing this gem, create an initializer with:
|
|
911
|
+
|
|
912
|
+
```ruby
|
|
913
|
+
StrongMigrations.start_after = 20250101000000
|
|
914
|
+
```
|
|
915
|
+
|
|
916
|
+
Use the version from your latest migration.
|
|
917
|
+
|
|
633
918
|
## Target Version
|
|
634
919
|
|
|
635
|
-
If your development database version is different from production, you can specify the production version so the right checks
|
|
920
|
+
If your development database version is different from production, you can specify the production version so the right checks run in development.
|
|
636
921
|
|
|
637
922
|
```ruby
|
|
638
|
-
StrongMigrations.
|
|
639
|
-
StrongMigrations.target_mysql_version = "8.0.12"
|
|
640
|
-
StrongMigrations.target_mariadb_version = "10.3.2"
|
|
923
|
+
StrongMigrations.target_version = 10 # or 8.0, 10.5, etc
|
|
641
924
|
```
|
|
642
925
|
|
|
926
|
+
The major version works well for Postgres, while the major and minor version is recommended for MySQL and MariaDB.
|
|
927
|
+
|
|
643
928
|
For safety, this option only affects development and test environments. In other environments, the actual server version is always used.
|
|
644
929
|
|
|
645
|
-
|
|
930
|
+
If your app has multiple databases with different versions, you can use:
|
|
931
|
+
|
|
932
|
+
```ruby
|
|
933
|
+
StrongMigrations.target_version = {primary: 13, catalog: 15}
|
|
934
|
+
```
|
|
646
935
|
|
|
647
|
-
|
|
936
|
+
## Analyze Tables
|
|
648
937
|
|
|
649
|
-
|
|
938
|
+
Analyze tables automatically (to update planner statistics) after an index is added. Create an initializer with:
|
|
650
939
|
|
|
651
940
|
```ruby
|
|
652
|
-
StrongMigrations.
|
|
653
|
-
StrongMigrations.lock_timeout = 10.seconds
|
|
941
|
+
StrongMigrations.auto_analyze = true
|
|
654
942
|
```
|
|
655
943
|
|
|
656
|
-
|
|
944
|
+
## Faster Migrations
|
|
657
945
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
946
|
+
Only dump the schema when adding a new migration. If you use Git, add to `config/environments/development.rb`:
|
|
947
|
+
|
|
948
|
+
```rb
|
|
949
|
+
config.active_record.dump_schema_after_migration = `git status db/migrate/ --porcelain`.present?
|
|
661
950
|
```
|
|
662
951
|
|
|
663
|
-
|
|
952
|
+
## Schema Sanity
|
|
953
|
+
|
|
954
|
+
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`:
|
|
955
|
+
|
|
956
|
+
```ruby
|
|
957
|
+
StrongMigrations.alphabetize_schema = true
|
|
958
|
+
```
|
|
664
959
|
|
|
665
960
|
## Permissions
|
|
666
961
|
|
|
667
962
|
We recommend using a [separate database user](https://ankane.org/postgres-users) for migrations when possible so you don’t need to grant your app user permission to alter tables.
|
|
668
963
|
|
|
964
|
+
## Smaller Projects
|
|
965
|
+
|
|
966
|
+
You probably don’t need this gem for smaller projects, as operations that are unsafe at scale can be perfectly safe on smaller, low-traffic tables.
|
|
967
|
+
|
|
669
968
|
## Additional Reading
|
|
670
969
|
|
|
671
|
-
- [Rails Migrations with No Downtime](https://pedro.herokuapp.com/past/2011/7/13/rails_migrations_with_no_downtime/)
|
|
672
970
|
- [PostgreSQL at Scale: Database Schema Changes Without Downtime](https://medium.com/braintree-product-technology/postgresql-at-scale-database-schema-changes-without-downtime-20d3749ed680)
|
|
971
|
+
- [MySQL InnoDB Online DDL Operations](https://dev.mysql.com/doc/refman/en/innodb-online-ddl-operations.html)
|
|
972
|
+
- [MariaDB InnoDB Online DDL Overview](https://mariadb.com/kb/en/innodb-online-ddl-overview/)
|
|
673
973
|
|
|
674
974
|
## Credits
|
|
675
975
|
|
|
676
976
|
Thanks to Bob Remeika and David Waller for the [original code](https://github.com/foobarfighter/safe-migrations) and [Sean Huber](https://github.com/LendingHome/zero_downtime_migrations) for the bad/good readme format.
|
|
677
977
|
|
|
978
|
+
## History
|
|
979
|
+
|
|
980
|
+
View the [changelog](https://github.com/ankane/strong_migrations/blob/master/CHANGELOG.md)
|
|
981
|
+
|
|
678
982
|
## Contributing
|
|
679
983
|
|
|
680
984
|
Everyone is encouraged to help improve this project. Here are a few ways you can help:
|
|
@@ -690,5 +994,12 @@ To get started with development:
|
|
|
690
994
|
git clone https://github.com/ankane/strong_migrations.git
|
|
691
995
|
cd strong_migrations
|
|
692
996
|
bundle install
|
|
997
|
+
|
|
998
|
+
# Postgres
|
|
999
|
+
createdb strong_migrations_test
|
|
693
1000
|
bundle exec rake test
|
|
1001
|
+
|
|
1002
|
+
# MySQL and MariaDB
|
|
1003
|
+
mysqladmin create strong_migrations_test
|
|
1004
|
+
ADAPTER=mysql2 bundle exec rake test
|
|
694
1005
|
```
|