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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 579a0db844c82c2c9153a1a3f44bfcffe929011112b132afe73b1dfdf68f4583
4
- data.tar.gz: b88f26034f0c1d044607ef191ca3feae71e3c6093cb05bd9a277790447da01af
3
+ metadata.gz: 966629fafbc4d8c9de88240bc705799f66ab205754cd1acc99a1ae129d737de5
4
+ data.tar.gz: 61379d6eeec17e532314551d5a643ed465ba23bac69739be063f2fd704657451
5
5
  SHA512:
6
- metadata.gz: 229a2a31c44a358425a684ea8d1771d98ecee6412c9b4480e30e2cd68e53c0c9e77039959fa8f379ad0a83fb9f592d133d29fe43e2c79ae4dbf674cd9c210598
7
- data.tar.gz: 9255d5ad7b7d350021cc553b5d91bec852cfbecc0b9c7d6296606beffae266de72096cdf5a4d12dd88d921fb0af60a9e870d08a676a90fad552c5bd2d36096f6
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
- ```ruby
181
- # For Active Record 5+
182
- class User < ApplicationRecord
183
- self.ignored_columns = ["name"]
184
- end
180
+ ```ruby
181
+ # For Active Record 5+
182
+ class User < ApplicationRecord
183
+ self.ignored_columns = ["name"]
184
+ end
185
185
 
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
- ```
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
- ```ruby
198
- class RemoveNameFromUsers < ActiveRecord::Migration[7.1]
199
- def change
200
- safety_assured { remove_column :users, :name }
201
- end
202
- end
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
- ```ruby
326
- class InitializeChangeFilesSizeType < ActiveRecord::Migration[7.1]
327
- def change
328
- initialize_column_type_change :files, :size, :bigint
329
- end
330
- end
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
- ```ruby
339
- class BackfillChangeFilesSizeType < ActiveRecord::Migration[7.1]
340
- disable_ddl_transaction!
338
+ ```ruby
339
+ class BackfillChangeFilesSizeType < ActiveRecord::Migration[7.1]
340
+ disable_ddl_transaction!
341
341
 
342
- def up
343
- backfill_column_for_type_change :files, :size
344
- end
342
+ def up
343
+ backfill_column_for_type_change :files, :size
344
+ end
345
345
 
346
- def down
347
- # no op
348
- end
349
- end
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
- ```ruby
355
- class FinalizeChangeFilesSizeType < ActiveRecord::Migration[7.1]
356
- disable_ddl_transaction!
354
+ ```ruby
355
+ class FinalizeChangeFilesSizeType < ActiveRecord::Migration[7.1]
356
+ disable_ddl_transaction!
357
357
 
358
- def change
359
- finalize_column_type_change :files, :size
360
- end
361
- end
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
- ```ruby
368
- class CleanupChangeFilesSizeType < ActiveRecord::Migration[7.1]
369
- def up
370
- cleanup_column_type_change :files, :size
371
- end
367
+ ```ruby
368
+ class CleanupChangeFilesSizeType < ActiveRecord::Migration[7.1]
369
+ def up
370
+ cleanup_column_type_change :files, :size
371
+ end
372
372
 
373
- def down
374
- initialize_column_type_change :files, :size, :integer
375
- end
376
- end
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
- "users" => {
436
- "name" => "first_name"
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
- # Or for Active Record < 7
448
- config.active_record.partial_writes = true
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
- def change
459
- initialize_column_rename :users, :name, :first_name
460
- end
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
- (is disabled by default in Active Record >= 7), then you need to ignore old column:
467
-
468
- ```ruby
469
- # For Active Record 5+
470
- class User < ApplicationRecord
471
- self.ignored_columns = ["name"]
472
- end
473
-
474
- # For Active Record < 5
475
- class User < ActiveRecord::Base
476
- def self.columns
477
- super.reject { |c| c.name == "name" }
478
- end
479
- end
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
- def change
490
- finalize_column_rename :users, :name, :first_name
491
- end
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
- "clients" => "users"
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
- def change
561
- initialize_table_rename :clients, :users
562
- end
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
- def change
574
- finalize_table_rename :clients, :users
575
- end
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
- ```ruby
1161
- # For Active Record 5+
1162
- class User < ApplicationRecord
1163
- self.ignored_columns = ["type"]
1164
- end
1162
+ ```ruby
1163
+ # For Active Record 5+
1164
+ class User < ApplicationRecord
1165
+ self.ignored_columns = ["type"]
1166
+ end
1165
1167
 
1166
- # For Active Record < 5
1167
- class User < ActiveRecord::Base
1168
- def self.columns
1169
- super.reject { |c| c.name == "type" }
1170
- end
1171
- end
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 a background migration iteration" do
141
- rails = Project.create!(name: "rails")
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
- _postgres_issue = postgres.issues.create!
145
+ postgres.issues.create!
146
146
 
147
- BackfillProjectIssuesCount.new.process_batch(Project.all)
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
- if Utils.ar_version <= 5.2
46
- # Active Record <= 5.2 does not support quoting of Arel::Nodes::NamedFunction
47
- old_value = Arel.sql("#{type_cast_function}(#{connection.quote_column_name(from_column)})")
48
- else
49
- old_value = Arel::Nodes::NamedFunction.new(type_cast_function, [old_value])
50
- end
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
- # This is necessary as we can't properly rename indexes such as "taggings_idx".
419
- if !index.name.include?(from_column)
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 = index.name.gsub(from_column, to_column)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OnlineMigrations
4
- VERSION = "0.9.2"
4
+ VERSION = "0.10.0"
5
5
  end
@@ -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.9.2
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-02 00:00:00.000000000 Z
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