online_migrations 0.9.2 → 0.10.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 +8 -0
- data/README.md +144 -142
- data/docs/background_migrations.md +7 -6
- data/docs/configuring.md +20 -16
- data/lib/generators/online_migrations/templates/initializer.rb.tt +7 -0
- data/lib/online_migrations/background_migrations/copy_column.rb +12 -6
- data/lib/online_migrations/change_column_type_helpers.rb +5 -5
- data/lib/online_migrations/command_checker.rb +40 -18
- data/lib/online_migrations/config.rb +12 -0
- data/lib/online_migrations/schema_dumper.rb +21 -0
- data/lib/online_migrations/schema_statements.rb +16 -0
- data/lib/online_migrations/utils.rb +19 -0
- data/lib/online_migrations/version.rb +1 -1
- data/lib/online_migrations.rb +2 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 966629fafbc4d8c9de88240bc705799f66ab205754cd1acc99a1ae129d737de5
|
4
|
+
data.tar.gz: 61379d6eeec17e532314551d5a643ed465ba23bac69739be063f2fd704657451
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ae49cb8499bc7b086d9b1883106926d150a1227f495e1aff7e1f259197d68065a20b819797d5e7c9199d413be746e9deb94f7c9bc6005a53b9b94bbefd2d88cb
|
7
|
+
data.tar.gz: 4c36ad512328174d04600b7d10001721ed985ab73184cbfd7c2f2cde70788a32ef4a4e8de754134206510b29e17920da06820903f86a809766aa3c5aa4005408
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,13 @@
|
|
1
1
|
## master (unreleased)
|
2
2
|
|
3
|
+
## 0.10.0 (2023-12-12)
|
4
|
+
|
5
|
+
- Add `auto_analyze` configuration option
|
6
|
+
- Add `alphabetize_schema` configuration option
|
7
|
+
- Fix `backfill_column_for_type_change_in_background` for cast expressions
|
8
|
+
- Fix copying indexes with long names when changing column type
|
9
|
+
- Enhance error messages with the link to the detailed description
|
10
|
+
|
3
11
|
## 0.9.2 (2023-11-02)
|
4
12
|
|
5
13
|
- Fix checking which expression indexes to copy when changing column type
|
data/README.md
CHANGED
@@ -90,7 +90,7 @@ class AddAdminToUsers < ActiveRecord::Migration[7.1]
|
|
90
90
|
execute "SET statement_timeout TO '5s'"
|
91
91
|
change_column_null :users, :admin, false
|
92
92
|
end
|
93
|
-
|
93
|
+
|
94
94
|
def down
|
95
95
|
remove_column :users, :admin
|
96
96
|
end
|
@@ -177,30 +177,30 @@ end
|
|
177
177
|
|
178
178
|
1. Ignore the column:
|
179
179
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
180
|
+
```ruby
|
181
|
+
# For Active Record 5+
|
182
|
+
class User < ApplicationRecord
|
183
|
+
self.ignored_columns = ["name"]
|
184
|
+
end
|
185
185
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
186
|
+
# For Active Record < 5
|
187
|
+
class User < ActiveRecord::Base
|
188
|
+
def self.columns
|
189
|
+
super.reject { |c| c.name == "name" }
|
190
|
+
end
|
191
|
+
end
|
192
|
+
```
|
193
193
|
|
194
194
|
2. Deploy
|
195
195
|
3. Wrap column removing in a `safety_assured` block:
|
196
196
|
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
197
|
+
```ruby
|
198
|
+
class RemoveNameFromUsers < ActiveRecord::Migration[7.1]
|
199
|
+
def change
|
200
|
+
safety_assured { remove_column :users, :name }
|
201
|
+
end
|
202
|
+
end
|
203
|
+
```
|
204
204
|
|
205
205
|
4. Remove column ignoring from `User` model
|
206
206
|
5. Deploy
|
@@ -322,59 +322,59 @@ A safer approach can be accomplished in several steps:
|
|
322
322
|
|
323
323
|
1. Create a new column and keep column's data in sync:
|
324
324
|
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
325
|
+
```ruby
|
326
|
+
class InitializeChangeFilesSizeType < ActiveRecord::Migration[7.1]
|
327
|
+
def change
|
328
|
+
initialize_column_type_change :files, :size, :bigint
|
329
|
+
end
|
330
|
+
end
|
331
|
+
```
|
332
332
|
|
333
|
-
**Note**: `initialize_column_type_change` accepts additional options (like `:limit`, `:default` etc)
|
334
|
-
which will be passed to `add_column` when creating a new column, so you can override previous values.
|
333
|
+
**Note**: `initialize_column_type_change` accepts additional options (like `:limit`, `:default` etc)
|
334
|
+
which will be passed to `add_column` when creating a new column, so you can override previous values.
|
335
335
|
|
336
336
|
2. Backfill data from the old column to the new column:
|
337
337
|
|
338
|
-
|
339
|
-
|
340
|
-
|
338
|
+
```ruby
|
339
|
+
class BackfillChangeFilesSizeType < ActiveRecord::Migration[7.1]
|
340
|
+
disable_ddl_transaction!
|
341
341
|
|
342
|
-
|
343
|
-
|
344
|
-
|
342
|
+
def up
|
343
|
+
backfill_column_for_type_change :files, :size
|
344
|
+
end
|
345
345
|
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
346
|
+
def down
|
347
|
+
# no op
|
348
|
+
end
|
349
|
+
end
|
350
|
+
```
|
351
351
|
|
352
352
|
3. Copy indexes, foreign keys, check constraints, NOT NULL constraint, swap new column in place:
|
353
353
|
|
354
|
-
|
355
|
-
|
356
|
-
|
354
|
+
```ruby
|
355
|
+
class FinalizeChangeFilesSizeType < ActiveRecord::Migration[7.1]
|
356
|
+
disable_ddl_transaction!
|
357
357
|
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
358
|
+
def change
|
359
|
+
finalize_column_type_change :files, :size
|
360
|
+
end
|
361
|
+
end
|
362
|
+
```
|
363
363
|
|
364
364
|
4. Deploy
|
365
365
|
5. Finally, if everything is working as expected, remove copy trigger and old column:
|
366
366
|
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
|
371
|
-
|
367
|
+
```ruby
|
368
|
+
class CleanupChangeFilesSizeType < ActiveRecord::Migration[7.1]
|
369
|
+
def up
|
370
|
+
cleanup_column_type_change :files, :size
|
371
|
+
end
|
372
372
|
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
|
377
|
-
|
373
|
+
def down
|
374
|
+
initialize_column_type_change :files, :size, :integer
|
375
|
+
end
|
376
|
+
end
|
377
|
+
```
|
378
378
|
|
379
379
|
6. Deploy
|
380
380
|
|
@@ -430,67 +430,69 @@ To work around this limitation, we need to tell Active Record to acquire this in
|
|
430
430
|
|
431
431
|
1. Instruct Rails that you are going to rename a column:
|
432
432
|
|
433
|
-
```ruby
|
434
|
-
OnlineMigrations.config.column_renames = {
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
}
|
439
|
-
```
|
440
|
-
NOTE: You also need to temporarily enable partial writes (is disabled by default in Active Record >= 7)
|
441
|
-
until the process of column rename is fully done.
|
442
|
-
```ruby
|
443
|
-
# config/application.rb
|
444
|
-
# For Active Record >= 7
|
445
|
-
config.active_record.partial_inserts = true
|
433
|
+
```ruby
|
434
|
+
OnlineMigrations.config.column_renames = {
|
435
|
+
"users" => {
|
436
|
+
"name" => "first_name"
|
437
|
+
}
|
438
|
+
}
|
439
|
+
```
|
446
440
|
|
447
|
-
|
448
|
-
|
449
|
-
|
441
|
+
**Note**: You also need to temporarily enable partial writes (is disabled by default in Active Record >= 7)
|
442
|
+
until the process of column rename is fully done.
|
443
|
+
|
444
|
+
```ruby
|
445
|
+
# config/application.rb
|
446
|
+
# For Active Record >= 7
|
447
|
+
config.active_record.partial_inserts = true
|
448
|
+
|
449
|
+
# Or for Active Record < 7
|
450
|
+
config.active_record.partial_writes = true
|
451
|
+
```
|
450
452
|
|
451
453
|
2. Deploy
|
452
454
|
3. Tell the database that you are going to rename a column. This will not actually rename any columns,
|
453
455
|
nor any data/indexes/foreign keys copying will be made, so will be instantaneous.
|
454
456
|
It will use a combination of a VIEW and column aliasing to work with both column names simultaneously
|
455
457
|
|
456
|
-
```ruby
|
457
|
-
class InitializeRenameUsersNameToFirstName < ActiveRecord::Migration[7.1]
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
end
|
462
|
-
```
|
458
|
+
```ruby
|
459
|
+
class InitializeRenameUsersNameToFirstName < ActiveRecord::Migration[7.1]
|
460
|
+
def change
|
461
|
+
initialize_column_rename :users, :name, :first_name
|
462
|
+
end
|
463
|
+
end
|
464
|
+
```
|
463
465
|
|
464
466
|
4. Replace usages of the old column with a new column in the codebase
|
465
467
|
5. If you enabled Active Record `enumerate_columns_in_select_statements` setting in your application
|
466
|
-
|
467
|
-
|
468
|
-
|
469
|
-
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
468
|
+
(is disabled by default in Active Record >= 7), then you need to ignore old column:
|
469
|
+
|
470
|
+
```ruby
|
471
|
+
# For Active Record 5+
|
472
|
+
class User < ApplicationRecord
|
473
|
+
self.ignored_columns = ["name"]
|
474
|
+
end
|
475
|
+
|
476
|
+
# For Active Record < 5
|
477
|
+
class User < ActiveRecord::Base
|
478
|
+
def self.columns
|
479
|
+
super.reject { |c| c.name == "name" }
|
480
|
+
end
|
481
|
+
end
|
482
|
+
```
|
481
483
|
|
482
484
|
6. Deploy
|
483
485
|
7. Remove the column rename config from step 1
|
484
486
|
8. Remove the column ignore from step 5, if added
|
485
487
|
9. Remove the VIEW created in step 3 and finally rename the column:
|
486
488
|
|
487
|
-
```ruby
|
488
|
-
class FinalizeRenameUsersNameToFirstName < ActiveRecord::Migration[7.1]
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
end
|
493
|
-
```
|
489
|
+
```ruby
|
490
|
+
class FinalizeRenameUsersNameToFirstName < ActiveRecord::Migration[7.1]
|
491
|
+
def change
|
492
|
+
finalize_column_rename :users, :name, :first_name
|
493
|
+
end
|
494
|
+
end
|
495
|
+
```
|
494
496
|
|
495
497
|
10. Deploy
|
496
498
|
|
@@ -546,35 +548,35 @@ To work around this limitation, we need to tell Active Record to acquire this in
|
|
546
548
|
|
547
549
|
1. Instruct Rails that you are going to rename a table:
|
548
550
|
|
549
|
-
```ruby
|
550
|
-
OnlineMigrations.config.table_renames = {
|
551
|
-
|
552
|
-
}
|
553
|
-
```
|
551
|
+
```ruby
|
552
|
+
OnlineMigrations.config.table_renames = {
|
553
|
+
"clients" => "users"
|
554
|
+
}
|
555
|
+
```
|
554
556
|
|
555
557
|
2. Deploy
|
556
558
|
3. Create a VIEW:
|
557
559
|
|
558
|
-
```ruby
|
559
|
-
class InitializeRenameClientsToUsers < ActiveRecord::Migration[7.1]
|
560
|
-
|
561
|
-
|
562
|
-
|
563
|
-
end
|
564
|
-
```
|
560
|
+
```ruby
|
561
|
+
class InitializeRenameClientsToUsers < ActiveRecord::Migration[7.1]
|
562
|
+
def change
|
563
|
+
initialize_table_rename :clients, :users
|
564
|
+
end
|
565
|
+
end
|
566
|
+
```
|
565
567
|
|
566
568
|
4. Replace usages of the old table with a new table in the codebase
|
567
569
|
5. Remove the table rename config from step 1
|
568
570
|
6. Deploy
|
569
571
|
7. Remove the VIEW created in step 3:
|
570
572
|
|
571
|
-
```ruby
|
572
|
-
class FinalizeRenameClientsToUsers < ActiveRecord::Migration[7.1]
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
end
|
577
|
-
```
|
573
|
+
```ruby
|
574
|
+
class FinalizeRenameClientsToUsers < ActiveRecord::Migration[7.1]
|
575
|
+
def change
|
576
|
+
finalize_table_rename :clients, :users
|
577
|
+
end
|
578
|
+
end
|
579
|
+
```
|
578
580
|
|
579
581
|
8. Deploy
|
580
582
|
|
@@ -1157,19 +1159,19 @@ A safer approach is to:
|
|
1157
1159
|
|
1158
1160
|
1. ignore the column:
|
1159
1161
|
|
1160
|
-
|
1161
|
-
|
1162
|
-
|
1163
|
-
|
1164
|
-
|
1162
|
+
```ruby
|
1163
|
+
# For Active Record 5+
|
1164
|
+
class User < ApplicationRecord
|
1165
|
+
self.ignored_columns = ["type"]
|
1166
|
+
end
|
1165
1167
|
|
1166
|
-
|
1167
|
-
|
1168
|
-
|
1169
|
-
|
1170
|
-
|
1171
|
-
|
1172
|
-
|
1168
|
+
# For Active Record < 5
|
1169
|
+
class User < ActiveRecord::Base
|
1170
|
+
def self.columns
|
1171
|
+
super.reject { |c| c.name == "type" }
|
1172
|
+
end
|
1173
|
+
end
|
1174
|
+
```
|
1173
1175
|
|
1174
1176
|
2. deploy
|
1175
1177
|
3. remove the column ignoring from step 1 and apply initial code changes
|
@@ -1281,18 +1283,18 @@ The main differences are:
|
|
1281
1283
|
|
1282
1284
|
1. `strong_migrations` provides you **text guidance** on how to run migrations safer and you should implement them yourself. This new gem has actual [**code helpers**](https://github.com/fatkodima/online_migrations/blob/master/lib/online_migrations/schema_statements.rb) (and suggests them when fails on unsafe migrations) you can use to do what you want. See [example](#example) for an example.
|
1283
1285
|
|
1284
|
-
It has migrations helpers for:
|
1286
|
+
It has migrations helpers for:
|
1285
1287
|
|
1286
|
-
* renaming tables/columns
|
1287
|
-
* changing columns types (including changing primary/foreign keys from `integer` to `bigint`)
|
1288
|
-
* adding columns with default values
|
1289
|
-
* backfilling data
|
1290
|
-
* adding different types of constraints
|
1291
|
-
* and others
|
1288
|
+
* renaming tables/columns
|
1289
|
+
* changing columns types (including changing primary/foreign keys from `integer` to `bigint`)
|
1290
|
+
* adding columns with default values
|
1291
|
+
* backfilling data
|
1292
|
+
* adding different types of constraints
|
1293
|
+
* and others
|
1292
1294
|
|
1293
1295
|
2. This gem has a [powerful internal framework](https://github.com/fatkodima/online_migrations/blob/master/docs/background_migrations.md) for running data migrations on very large tables using background migrations.
|
1294
1296
|
|
1295
|
-
For example, you can use background migrations to migrate data that’s stored in a single JSON column to a separate table instead; backfill values from one column to another (as one of the steps when changing column type); or backfill some column’s value from an API.
|
1297
|
+
For example, you can use background migrations to migrate data that’s stored in a single JSON column to a separate table instead; backfill values from one column to another (as one of the steps when changing column type); or backfill some column’s value from an API.
|
1296
1298
|
|
1297
1299
|
3. Yet, it has more checks for unsafe changes (see [checks](#checks)).
|
1298
1300
|
|
@@ -137,17 +137,18 @@ require "test_helper"
|
|
137
137
|
module OnlineMigrations
|
138
138
|
module BackgroundMigrations
|
139
139
|
class BackfillProjectIssuesCountTest < ActiveSupport::TestCase
|
140
|
-
test "#process_batch performs
|
141
|
-
rails = Project.create!(name: "
|
140
|
+
test "#process_batch performs an iteration" do
|
141
|
+
rails = Project.create!(name: "Ruby on Rails")
|
142
142
|
postgres = Project.create!(name: "PostgreSQL")
|
143
143
|
|
144
144
|
2.times { rails.issues.create! }
|
145
|
-
|
145
|
+
postgres.issues.create!
|
146
146
|
|
147
|
-
BackfillProjectIssuesCount.new
|
147
|
+
migration = BackfillProjectIssuesCount.new
|
148
|
+
migration.process_batch(migration.relation)
|
148
149
|
|
149
|
-
assert_equal 2, rails.issues_count
|
150
|
-
assert_equal 1, postgres.issues_count
|
150
|
+
assert_equal 2, rails.reload.issues_count
|
151
|
+
assert_equal 1, postgres.reload.issues_count
|
151
152
|
end
|
152
153
|
end
|
153
154
|
end
|
data/docs/configuring.md
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
There are a few configurable options for the gem. Custom configurations should be placed in a `online_migrations.rb` initializer.
|
4
4
|
|
5
5
|
```ruby
|
6
|
+
# config/initializers/online_migrations.rb
|
7
|
+
|
6
8
|
OnlineMigrations.configure do |config|
|
7
9
|
# ...
|
8
10
|
end
|
@@ -15,8 +17,6 @@ end
|
|
15
17
|
Add your own custom checks with:
|
16
18
|
|
17
19
|
```ruby
|
18
|
-
# config/initializers/online_migrations.rb
|
19
|
-
|
20
20
|
config.add_check do |method, args|
|
21
21
|
if method == :add_column && args[0].to_s == "users"
|
22
22
|
stop!("No more columns on the users table")
|
@@ -33,8 +33,6 @@ Use the `stop!` method to stop migrations.
|
|
33
33
|
Disable specific checks with:
|
34
34
|
|
35
35
|
```ruby
|
36
|
-
# config/initializers/online_migrations.rb
|
37
|
-
|
38
36
|
config.disable_check(:remove_index)
|
39
37
|
```
|
40
38
|
|
@@ -45,8 +43,6 @@ Check the [source code](https://github.com/fatkodima/online_migrations/blob/mast
|
|
45
43
|
By default, checks are disabled when migrating down. Enable them with:
|
46
44
|
|
47
45
|
```ruby
|
48
|
-
# config/initializers/online_migrations.rb
|
49
|
-
|
50
46
|
config.check_down = true
|
51
47
|
```
|
52
48
|
|
@@ -55,8 +51,6 @@ config.check_down = true
|
|
55
51
|
You can customize specific error messages:
|
56
52
|
|
57
53
|
```ruby
|
58
|
-
# config/initializers/online_migrations.rb
|
59
|
-
|
60
54
|
config.error_messages[:add_column_default] = "Your custom instructions"
|
61
55
|
```
|
62
56
|
|
@@ -88,8 +82,6 @@ ALTER ROLE myuser SET statement_timeout = '15s';
|
|
88
82
|
You can configure this gem to automatically retry statements that exceed the lock timeout:
|
89
83
|
|
90
84
|
```ruby
|
91
|
-
# config/initializers/online_migrations.rb
|
92
|
-
|
93
85
|
config.lock_retrier = OnlineMigrations::ExponentialLockRetrier.new(
|
94
86
|
attempts: 30, # attempt 30 retries
|
95
87
|
base_delay: 0.01.seconds, # starting with delay of 10ms between each unsuccessful try, increasing exponentially
|
@@ -110,8 +102,6 @@ To temporarily disable lock retries while running migrations, set `DISABLE_LOCK_
|
|
110
102
|
To mark migrations as safe that were created before installing this gem, configure the migration version starting after which checks are performed:
|
111
103
|
|
112
104
|
```ruby
|
113
|
-
# config/initializers/online_migrations.rb
|
114
|
-
|
115
105
|
config.start_after = 20220101000000
|
116
106
|
|
117
107
|
# or if you use multiple databases (Active Record 6+)
|
@@ -125,8 +115,6 @@ Use the version from your latest migration.
|
|
125
115
|
If your development database version is different from production, you can specify the production version so the right checks run in development.
|
126
116
|
|
127
117
|
```ruby
|
128
|
-
# config/initializers/online_migrations.rb
|
129
|
-
|
130
118
|
config.target_version = 10 # or "12.9" etc
|
131
119
|
|
132
120
|
# or if you use multiple databases (Active Record 6+)
|
@@ -200,9 +188,25 @@ So you can actually check which steps are performed.
|
|
200
188
|
To enable verbose sql logs:
|
201
189
|
|
202
190
|
```ruby
|
203
|
-
# config/initializers/online_migrations.rb
|
204
|
-
|
205
191
|
config.verbose_sql_logs = true
|
206
192
|
```
|
207
193
|
|
208
194
|
This feature is enabled by default in a production Rails environment. You can override this setting via `ONLINE_MIGRATIONS_VERBOSE_SQL_LOGS` environment variable.
|
195
|
+
|
196
|
+
## Analyze Tables
|
197
|
+
|
198
|
+
Analyze tables automatically (to update planner statistics) after an index is added.
|
199
|
+
Add to an initializer file:
|
200
|
+
|
201
|
+
```ruby
|
202
|
+
config.auto_analyze = true
|
203
|
+
```
|
204
|
+
|
205
|
+
## Schema Sanity
|
206
|
+
|
207
|
+
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/).
|
208
|
+
To alphabetize columns:
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
config.alphabetize_schema = true
|
212
|
+
```
|
@@ -23,6 +23,13 @@ OnlineMigrations.configure do |config|
|
|
23
23
|
# It is considered safe to perform most of the dangerous operations on them.
|
24
24
|
# config.small_tables = []
|
25
25
|
|
26
|
+
# Analyze tables after indexes are added.
|
27
|
+
# Outdated statistics can sometimes hurt performance.
|
28
|
+
# config.auto_analyze = true
|
29
|
+
|
30
|
+
# Alphabetize table columns when dumping the schema.
|
31
|
+
# config.alphabetize_schema = true
|
32
|
+
|
26
33
|
# Disable specific checks.
|
27
34
|
# For the list of available checks look at `lib/error_messages` folder.
|
28
35
|
# config.disable_check(:remove_index)
|
@@ -42,12 +42,18 @@ module OnlineMigrations
|
|
42
42
|
old_values = copy_from.map do |from_column|
|
43
43
|
old_value = arel_table[from_column]
|
44
44
|
if (type_cast_function = type_cast_functions[from_column])
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
45
|
+
old_value =
|
46
|
+
if type_cast_function =~ /\A\w+\z/
|
47
|
+
if Utils.ar_version <= 5.2
|
48
|
+
# Active Record <= 5.2 does not support quoting of Arel::Nodes::NamedFunction
|
49
|
+
Arel.sql("#{type_cast_function}(#{connection.quote_column_name(from_column)})")
|
50
|
+
else
|
51
|
+
Arel::Nodes::NamedFunction.new(type_cast_function, [old_value])
|
52
|
+
end
|
53
|
+
else
|
54
|
+
# We got a cast expression.
|
55
|
+
Arel.sql(type_cast_function)
|
56
|
+
end
|
51
57
|
end
|
52
58
|
old_value
|
53
59
|
end
|
@@ -415,13 +415,13 @@ module OnlineMigrations
|
|
415
415
|
end
|
416
416
|
end
|
417
417
|
|
418
|
-
|
419
|
-
|
420
|
-
raise "The index #{index.name} can not be copied as it does not " \
|
421
|
-
"mention the old column. You have to rename this index manually first."
|
418
|
+
if index.name.include?(from_column)
|
419
|
+
name = index.name.gsub(from_column, to_column)
|
422
420
|
end
|
423
421
|
|
424
|
-
name
|
422
|
+
# Generate a shorter name if needed.
|
423
|
+
max_identifier_length = 63 # could use just `max_identifier_length` method for ActiveRecord >= 5.0.
|
424
|
+
name = index_name(table_name, new_columns) if !name || name.length > max_identifier_length
|
425
425
|
|
426
426
|
options = {
|
427
427
|
unique: index.unique,
|
@@ -1,7 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "erb"
|
4
|
-
require "openssl"
|
5
4
|
require "set"
|
6
5
|
|
7
6
|
module OnlineMigrations
|
@@ -53,6 +52,36 @@ module OnlineMigrations
|
|
53
52
|
end
|
54
53
|
|
55
54
|
private
|
55
|
+
ERROR_MESSAGE_TO_LINK = {
|
56
|
+
multiple_foreign_keys: "adding-multiple-foreign-keys",
|
57
|
+
create_table: "creating-a-table-with-the-force-option",
|
58
|
+
short_primary_key_type: "using-primary-key-with-short-integer-type",
|
59
|
+
drop_table_multiple_foreign_keys: "removing-a-table-with-multiple-foreign-keys",
|
60
|
+
rename_table: "renaming-a-table",
|
61
|
+
add_column_with_default_null: "adding-a-column-with-a-default-value",
|
62
|
+
add_column_with_default: "adding-a-column-with-a-default-value",
|
63
|
+
add_column_generated_stored: "adding-a-stored-generated-column",
|
64
|
+
add_column_json: "adding-a-json-column",
|
65
|
+
rename_column: "renaming-a-column",
|
66
|
+
change_column: "changing-the-type-of-a-column",
|
67
|
+
change_column_default: "changing-the-default-value-of-a-column",
|
68
|
+
change_column_null: "setting-not-null-on-an-existing-column",
|
69
|
+
remove_column: "removing-a-column",
|
70
|
+
add_timestamps_with_default: "adding-a-column-with-a-default-value",
|
71
|
+
add_hash_index: "hash-indexes",
|
72
|
+
add_reference: "adding-a-reference",
|
73
|
+
add_index: "adding-an-index-non-concurrently",
|
74
|
+
replace_index: "replacing-an-index",
|
75
|
+
remove_index: "removing-an-index-non-concurrently",
|
76
|
+
add_foreign_key: "adding-a-foreign-key",
|
77
|
+
add_exclusion_constraint: "adding-an-exclusion-constraint",
|
78
|
+
add_check_constraint: "adding-a-check-constraint",
|
79
|
+
add_unique_constraint: "adding-a-unique-constraint",
|
80
|
+
execute: "executing-SQL-directly",
|
81
|
+
add_inheritance_column: "adding-a-single-table-inheritance-column",
|
82
|
+
mismatched_foreign_key_type: "mismatched-reference-column-types",
|
83
|
+
}
|
84
|
+
|
56
85
|
def check_database_version
|
57
86
|
return if defined?(@database_version_checked)
|
58
87
|
|
@@ -522,6 +551,11 @@ module OnlineMigrations
|
|
522
551
|
end
|
523
552
|
end
|
524
553
|
end
|
554
|
+
|
555
|
+
# Outdated statistics + a new index can hurt performance of existing queries.
|
556
|
+
if OnlineMigrations.config.auto_analyze && direction == :up
|
557
|
+
connection.execute("ANALYZE #{table_name}")
|
558
|
+
end
|
525
559
|
end
|
526
560
|
end
|
527
561
|
|
@@ -586,7 +620,7 @@ module OnlineMigrations
|
|
586
620
|
def add_unique_constraint(table_name, column_name = nil, **options)
|
587
621
|
return if new_or_small_table?(table_name) || options[:using_index] || !column_name
|
588
622
|
|
589
|
-
index_name = index_name(table_name, column_name)
|
623
|
+
index_name = Utils.index_name(table_name, column_name)
|
590
624
|
|
591
625
|
raise_error :add_unique_constraint,
|
592
626
|
add_index_code: command_str(:add_index, table_name, column_name, unique: true, name: index_name, algorithm: :concurrently),
|
@@ -594,22 +628,6 @@ module OnlineMigrations
|
|
594
628
|
remove_code: command_str(:remove_unique_constraint, table_name, column_name)
|
595
629
|
end
|
596
630
|
|
597
|
-
# Implementation is from Active Record
|
598
|
-
def index_name(table_name, column_name)
|
599
|
-
max_index_name_size = 62
|
600
|
-
name = "index_#{table_name}_on_#{Array(column_name) * '_and_'}"
|
601
|
-
return name if name.bytesize <= max_index_name_size
|
602
|
-
|
603
|
-
# Fallback to short version, add hash to ensure uniqueness
|
604
|
-
hashed_identifier = "_#{OpenSSL::Digest::SHA256.hexdigest(name).first(10)}"
|
605
|
-
name = "idx_on_#{Array(column_name) * '_'}"
|
606
|
-
|
607
|
-
short_limit = max_index_name_size - hashed_identifier.bytesize
|
608
|
-
short_name = name[0, short_limit]
|
609
|
-
|
610
|
-
"#{short_name}#{hashed_identifier}"
|
611
|
-
end
|
612
|
-
|
613
631
|
def validate_constraint(*)
|
614
632
|
if crud_blocked?
|
615
633
|
raise_error :validate_constraint
|
@@ -735,6 +753,10 @@ module OnlineMigrations
|
|
735
753
|
message = ERB.new(template, nil, "<>").result(b)
|
736
754
|
end
|
737
755
|
|
756
|
+
if (link = ERROR_MESSAGE_TO_LINK[message_key])
|
757
|
+
message += "\nFor more details, see https://github.com/fatkodima/online_migrations##{link}"
|
758
|
+
end
|
759
|
+
|
738
760
|
@migration.stop!(message, header: header || "Dangerous operation detected")
|
739
761
|
end
|
740
762
|
|
@@ -84,6 +84,16 @@ module OnlineMigrations
|
|
84
84
|
#
|
85
85
|
attr_accessor :error_messages
|
86
86
|
|
87
|
+
# Whether to automatically run ANALYZE on the table after the index was added
|
88
|
+
# @return [Boolean]
|
89
|
+
#
|
90
|
+
attr_accessor :auto_analyze
|
91
|
+
|
92
|
+
# Whether to alphabetize schema
|
93
|
+
# @return [Boolean]
|
94
|
+
#
|
95
|
+
attr_accessor :alphabetize_schema
|
96
|
+
|
87
97
|
# Maximum allowed lock timeout value (in seconds)
|
88
98
|
#
|
89
99
|
# If set lock timeout is greater than this value, the migration will fail.
|
@@ -184,6 +194,8 @@ module OnlineMigrations
|
|
184
194
|
@target_version = nil
|
185
195
|
@small_tables = []
|
186
196
|
@check_down = false
|
197
|
+
@auto_analyze = false
|
198
|
+
@alphabetize_schema = false
|
187
199
|
@enabled_checks = @error_messages.keys.map { |k| [k, {}] }.to_h
|
188
200
|
@verbose_sql_logs = defined?(Rails.env) && Rails.env.production?
|
189
201
|
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "delegate"
|
4
|
+
|
5
|
+
module OnlineMigrations
|
6
|
+
module SchemaDumper
|
7
|
+
def initialize(connection, *args, **options)
|
8
|
+
if OnlineMigrations.config.alphabetize_schema
|
9
|
+
connection = WrappedConnection.new(connection)
|
10
|
+
end
|
11
|
+
|
12
|
+
super
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class WrappedConnection < SimpleDelegator
|
17
|
+
def columns(table_name)
|
18
|
+
super.sort_by(&:name)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -777,6 +777,22 @@ module OnlineMigrations
|
|
777
777
|
end
|
778
778
|
end
|
779
779
|
|
780
|
+
# @private
|
781
|
+
# From ActiveRecord. Will not be needed for ActiveRecord >= 7.1.
|
782
|
+
def index_name(table_name, options)
|
783
|
+
if options.is_a?(Hash)
|
784
|
+
if options[:column]
|
785
|
+
Utils.index_name(table_name, options[:column])
|
786
|
+
elsif options[:name]
|
787
|
+
options[:name]
|
788
|
+
else
|
789
|
+
raise ArgumentError, "You must specify the index name"
|
790
|
+
end
|
791
|
+
else
|
792
|
+
index_name(table_name, column: options)
|
793
|
+
end
|
794
|
+
end
|
795
|
+
|
780
796
|
# Extends default method to be idempotent and accept `:validate` option for Active Record < 5.2.
|
781
797
|
#
|
782
798
|
# @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_foreign_key
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "openssl"
|
4
|
+
|
3
5
|
module OnlineMigrations
|
4
6
|
# @private
|
5
7
|
module Utils
|
@@ -78,6 +80,23 @@ module OnlineMigrations
|
|
78
80
|
end
|
79
81
|
end
|
80
82
|
|
83
|
+
# Implementation is from ActiveRecord.
|
84
|
+
# This is not needed for ActiveRecord < 7.1 (https://github.com/rails/rails/pull/47753).
|
85
|
+
def index_name(table_name, column_name)
|
86
|
+
max_index_name_size = 62
|
87
|
+
name = "index_#{table_name}_on_#{Array(column_name) * '_and_'}"
|
88
|
+
return name if name.bytesize <= max_index_name_size
|
89
|
+
|
90
|
+
# Fallback to short version, add hash to ensure uniqueness
|
91
|
+
hashed_identifier = "_#{OpenSSL::Digest::SHA256.hexdigest(name).first(10)}"
|
92
|
+
name = "idx_on_#{Array(column_name) * '_'}"
|
93
|
+
|
94
|
+
short_limit = max_index_name_size - hashed_identifier.bytesize
|
95
|
+
short_name = name[0, short_limit]
|
96
|
+
|
97
|
+
"#{short_name}#{hashed_identifier}"
|
98
|
+
end
|
99
|
+
|
81
100
|
def ar_partial_writes?
|
82
101
|
ActiveRecord::Base.public_send(ar_partial_writes_setting)
|
83
102
|
end
|
data/lib/online_migrations.rb
CHANGED
@@ -16,6 +16,7 @@ module OnlineMigrations
|
|
16
16
|
autoload :VerboseSqlLogs
|
17
17
|
autoload :Migration
|
18
18
|
autoload :Migrator
|
19
|
+
autoload :SchemaDumper
|
19
20
|
autoload :DatabaseTasks
|
20
21
|
autoload :ForeignKeyDefinition
|
21
22
|
autoload :ForeignKeysCollector
|
@@ -81,6 +82,7 @@ module OnlineMigrations
|
|
81
82
|
|
82
83
|
ActiveRecord::Migration.prepend(OnlineMigrations::Migration)
|
83
84
|
ActiveRecord::Migrator.prepend(OnlineMigrations::Migrator)
|
85
|
+
ActiveRecord::SchemaDumper.prepend(OnlineMigrations::SchemaDumper)
|
84
86
|
|
85
87
|
ActiveRecord::Tasks::DatabaseTasks.singleton_class.prepend(OnlineMigrations::DatabaseTasks)
|
86
88
|
ActiveRecord::Migration::CommandRecorder.include(OnlineMigrations::CommandRecorder)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: online_migrations
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- fatkodima
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-
|
11
|
+
date: 2023-12-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -76,6 +76,7 @@ files:
|
|
76
76
|
- lib/online_migrations/migration.rb
|
77
77
|
- lib/online_migrations/migrator.rb
|
78
78
|
- lib/online_migrations/schema_cache.rb
|
79
|
+
- lib/online_migrations/schema_dumper.rb
|
79
80
|
- lib/online_migrations/schema_statements.rb
|
80
81
|
- lib/online_migrations/utils.rb
|
81
82
|
- lib/online_migrations/verbose_sql_logs.rb
|