online_migrations 0.25.0 → 0.26.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 +17 -0
- data/README.md +18 -73
- data/docs/configuring.md +8 -0
- data/lib/generators/online_migrations/templates/add_sharding_to_online_migrations.rb.tt +1 -1
- data/lib/generators/online_migrations/templates/add_timestamps_to_background_migrations.rb.tt +1 -1
- data/lib/generators/online_migrations/templates/background_schema_migrations_change_unique_index.rb.tt +1 -1
- data/lib/generators/online_migrations/templates/initializer.rb.tt +3 -0
- data/lib/online_migrations/background_schema_migrations/migration.rb +6 -4
- data/lib/online_migrations/background_schema_migrations/migration_helpers.rb +1 -1
- data/lib/online_migrations/batch_iterator.rb +7 -4
- data/lib/online_migrations/change_column_type_helpers.rb +72 -12
- data/lib/online_migrations/command_checker.rb +32 -20
- data/lib/online_migrations/config.rb +8 -0
- data/lib/online_migrations/error_messages.rb +16 -0
- data/lib/online_migrations/index_definition.rb +1 -1
- data/lib/online_migrations/migration.rb +8 -1
- data/lib/online_migrations/schema_cache.rb +0 -78
- data/lib/online_migrations/schema_statements.rb +17 -73
- data/lib/online_migrations/utils.rb +1 -20
- data/lib/online_migrations/verbose_sql_logs.rb +1 -7
- data/lib/online_migrations/version.rb +1 -1
- data/lib/online_migrations.rb +1 -8
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cbca384d5eaf4ab575e80c60160d42326ef86f1baf21419f290663e0d9698eb0
|
4
|
+
data.tar.gz: b90abf4e278ee1b2ab27e60472f6bb127b124797084f53205a0929a5cc98268b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: eea056dd2b0c5a24213543d96d6b2ca5a1c230c38aa37f475d674157bc354a8d0c1cb39e6e2c0a6e86867b184e82b51ec9ad0452eed00e1a7b7a6098b203c3ff
|
7
|
+
data.tar.gz: 9636d4b788d655ad04d0141cd28ae61c053b15d83f00c63b236ee181feeb438c18f2a8f34da49b26d955ff3c004c053f0d570d00ff6801e47e15c438b09f2c51
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,22 @@
|
|
1
1
|
## master (unreleased)
|
2
2
|
|
3
|
+
## 0.26.0 (2025-04-28)
|
4
|
+
|
5
|
+
- Drop support for Ruby < 3.1 and Rails < 7.1
|
6
|
+
- Add check for `change_column` for columns with check constraints
|
7
|
+
|
8
|
+
- Allow to require safety reason explanation when calling `safery_assured`
|
9
|
+
|
10
|
+
```ruby
|
11
|
+
# config/initializers/online_migrations.rb
|
12
|
+
config.require_safety_assured_reason = true
|
13
|
+
|
14
|
+
# in migration
|
15
|
+
safety_assured("Table is small") do
|
16
|
+
add_index :users, :email, unique: true
|
17
|
+
end
|
18
|
+
```
|
19
|
+
|
3
20
|
## 0.25.0 (2025-02-03)
|
4
21
|
|
5
22
|
- Track start/finish time of background data migrations
|
data/README.md
CHANGED
@@ -16,8 +16,8 @@ See [comparison to `strong_migrations`](#comparison-to-strong_migrations)
|
|
16
16
|
|
17
17
|
## Requirements
|
18
18
|
|
19
|
-
- Ruby 3.
|
20
|
-
- Rails 7.
|
19
|
+
- Ruby 3.1+
|
20
|
+
- Rails 7.1+
|
21
21
|
- PostgreSQL 12+
|
22
22
|
|
23
23
|
For older Ruby and Rails versions you can use older versions of this gem.
|
@@ -64,76 +64,32 @@ An operation is classified as dangerous if it either:
|
|
64
64
|
|
65
65
|
## Example
|
66
66
|
|
67
|
-
|
68
|
-
|
69
|
-
```ruby
|
70
|
-
class AddAdminToUsers < ActiveRecord::Migration[8.0]
|
71
|
-
def change
|
72
|
-
add_column :users, :admin, :boolean, default: false, null: false
|
73
|
-
end
|
74
|
-
end
|
75
|
-
```
|
76
|
-
|
77
|
-
If the `users` table is large, running this migration on a live PostgreSQL < 11 database will likely cause downtime.
|
78
|
-
|
79
|
-
A safer approach would be to run something like the following:
|
80
|
-
|
81
|
-
```ruby
|
82
|
-
class AddAdminToUsers < ActiveRecord::Migration[8.0]
|
83
|
-
# Do not wrap the migration in a transaction so that locks are held for a shorter time.
|
84
|
-
disable_ddl_transaction!
|
85
|
-
|
86
|
-
def up
|
87
|
-
# Lower PostgreSQL's lock timeout to avoid statement queueing.
|
88
|
-
execute "SET lock_timeout TO '5s'" # The lock_timeout duration is customizable.
|
89
|
-
|
90
|
-
# Add the column without the default value and the not-null constraint.
|
91
|
-
add_column :users, :admin, :boolean
|
92
|
-
|
93
|
-
# Set the column's default value.
|
94
|
-
change_column_default :users, :admin, false
|
95
|
-
|
96
|
-
# Backfill the column in batches.
|
97
|
-
User.in_batches.update_all(admin: false)
|
98
|
-
|
99
|
-
# Add the not-null constraint. Beforehand, set a short statement timeout so that
|
100
|
-
# Postgres does not spend too much time performing the full table scan to verify
|
101
|
-
# the column contains no nulls.
|
102
|
-
execute "SET statement_timeout TO '5s'"
|
103
|
-
change_column_null :users, :admin, false
|
104
|
-
end
|
105
|
-
|
106
|
-
def down
|
107
|
-
remove_column :users, :admin
|
108
|
-
end
|
109
|
-
end
|
110
|
-
```
|
111
|
-
|
112
|
-
When you actually run the original migration, you will get an error message:
|
67
|
+
When you run a migration that's potentially dangerous, you'll see an error message like:
|
113
68
|
|
114
69
|
```txt
|
115
70
|
⚠️ [online_migrations] Dangerous operation detected ⚠️
|
116
71
|
|
117
|
-
|
118
|
-
|
72
|
+
Active Record caches database columns at runtime, so if you drop a column, it can cause exceptions until your app reboots.
|
119
73
|
A safer approach is to:
|
120
|
-
1. add the column without a default value
|
121
|
-
2. change the column default
|
122
|
-
3. backfill existing rows with the new value
|
123
|
-
4. add the NOT NULL constraint
|
124
74
|
|
125
|
-
|
75
|
+
1. Ignore the column:
|
126
76
|
|
127
|
-
class
|
128
|
-
|
77
|
+
class User < ApplicationRecord
|
78
|
+
self.ignored_columns += ["name"]
|
79
|
+
end
|
129
80
|
|
130
|
-
|
131
|
-
|
81
|
+
2. Deploy
|
82
|
+
3. Wrap column removing in a safety_assured { ... } block
|
83
|
+
|
84
|
+
class RemoveColumn < ActiveRecord::Migration[8.0]
|
85
|
+
def change
|
86
|
+
safety_assured { remove_column :users, :name }
|
87
|
+
end
|
132
88
|
end
|
133
|
-
end
|
134
|
-
```
|
135
89
|
|
136
|
-
|
90
|
+
4. Remove column ignoring from step 1
|
91
|
+
5. Deploy
|
92
|
+
```
|
137
93
|
|
138
94
|
## Checks
|
139
95
|
|
@@ -1278,17 +1234,6 @@ Interesting reads:
|
|
1278
1234
|
- [Stop worrying about PostgreSQL locks in your Rails migrations](https://medium.com/doctolib/stop-worrying-about-postgresql-locks-in-your-rails-migrations-3426027e9cc9)
|
1279
1235
|
- [Avoiding integer overflows with zero downtime](https://buildkite.com/blog/avoiding-integer-overflows-with-zero-downtime)
|
1280
1236
|
|
1281
|
-
## Maybe TODO
|
1282
|
-
|
1283
|
-
- support MySQL
|
1284
|
-
- support other ORMs
|
1285
|
-
|
1286
|
-
Background migrations:
|
1287
|
-
|
1288
|
-
- extract as a separate gem
|
1289
|
-
- add UI
|
1290
|
-
- support batching over non-integer and multiple columns
|
1291
|
-
|
1292
1237
|
## Comparison to `strong_migrations`
|
1293
1238
|
|
1294
1239
|
This gem was heavily inspired by the `strong_migrations` and GitLab's approaches to database migrations. This gem is a superset of `strong_migrations`, feature-wise, and has the same APIs.
|
data/docs/configuring.md
CHANGED
@@ -38,6 +38,14 @@ config.disable_check(:remove_index)
|
|
38
38
|
|
39
39
|
Check the [source code](https://github.com/fatkodima/online_migrations/blob/master/lib/online_migrations/error_messages.rb) for the list of keys.
|
40
40
|
|
41
|
+
## Requiring safety_assured reason
|
42
|
+
|
43
|
+
To require safety reason explanation when calling `safery_assured` (disabled by default):
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
config.require_safety_assured_reason = true
|
47
|
+
```
|
48
|
+
|
41
49
|
## Down Migrations / Rollbacks
|
42
50
|
|
43
51
|
By default, checks are disabled when migrating down. Enable them with:
|
@@ -1,6 +1,6 @@
|
|
1
1
|
class BackgroundSchemaMigrationsChangeUniqueIndex < <%= migration_parent %>
|
2
2
|
def change
|
3
|
-
safety_assured do
|
3
|
+
safety_assured("Table is small") do
|
4
4
|
remove_index :background_schema_migrations, name: :index_background_schema_migrations_on_unique_configuration
|
5
5
|
add_index :background_schema_migrations, [:migration_name, :shard, :connection_class_name], unique: true,
|
6
6
|
name: :index_background_schema_migrations_on_unique_configuration
|
@@ -12,6 +12,9 @@ OnlineMigrations.configure do |config|
|
|
12
12
|
# Set the version of the production database so the right checks are run in development.
|
13
13
|
# config.target_version = 17
|
14
14
|
|
15
|
+
# Configure whether to require safety reason explanation when calling #safery_assured.
|
16
|
+
config.require_safety_assured_reason = false
|
17
|
+
|
15
18
|
# Configure whether to perform checks when migrating down.
|
16
19
|
config.check_down = false
|
17
20
|
|
@@ -194,10 +194,7 @@ module OnlineMigrations
|
|
194
194
|
if index_addition?
|
195
195
|
index = connection.indexes(table_name).find { |i| i.name == name }
|
196
196
|
if index
|
197
|
-
|
198
|
-
# when switching to ActiveRecord >= 7.1.
|
199
|
-
schema = connection.send(:__schema_for_table, table_name)
|
200
|
-
if connection.send(:__index_valid?, name, schema: schema)
|
197
|
+
if index.valid?
|
201
198
|
return
|
202
199
|
else
|
203
200
|
connection.remove_index(table_name, name: name, algorithm: :concurrently)
|
@@ -206,6 +203,11 @@ module OnlineMigrations
|
|
206
203
|
end
|
207
204
|
|
208
205
|
connection.execute(definition)
|
206
|
+
|
207
|
+
# Outdated statistics + a new index can hurt performance of existing queries.
|
208
|
+
if OnlineMigrations.config.auto_analyze
|
209
|
+
connection.execute("ANALYZE #{table_name}")
|
210
|
+
end
|
209
211
|
end
|
210
212
|
end
|
211
213
|
end
|
@@ -19,7 +19,7 @@ module OnlineMigrations
|
|
19
19
|
return
|
20
20
|
end
|
21
21
|
|
22
|
-
if index_exists?(table_name, column_name, **options)
|
22
|
+
if index_exists?(table_name, column_name, name: index.name, **options)
|
23
23
|
Utils.raise_or_say("Index creation was not enqueued because the index already exists.")
|
24
24
|
return
|
25
25
|
end
|
@@ -19,8 +19,7 @@ module OnlineMigrations
|
|
19
19
|
end
|
20
20
|
|
21
21
|
relation = apply_limits(self.relation, column, start, finish, order)
|
22
|
-
|
23
|
-
base_relation = relation.unscope(*unscopes).reselect(column).reorder(column => order)
|
22
|
+
base_relation = relation.unscope(:includes, :preload, :eager_load).reselect(column).reorder(column => order)
|
24
23
|
|
25
24
|
start_id = start || begin
|
26
25
|
start_row = base_relation.uncached { base_relation.first }
|
@@ -84,12 +83,16 @@ module OnlineMigrations
|
|
84
83
|
|
85
84
|
private
|
86
85
|
def apply_limits(relation, column, start, finish, order)
|
86
|
+
arel_column = relation.arel_table[column]
|
87
|
+
|
87
88
|
if start
|
88
|
-
|
89
|
+
predicate = order == :asc ? :gteq : :lteq
|
90
|
+
relation = relation.where(arel_column.public_send(predicate, start))
|
89
91
|
end
|
90
92
|
|
91
93
|
if finish
|
92
|
-
|
94
|
+
predicate = order == :asc ? :lteq : :gteq
|
95
|
+
relation = relation.where(arel_column.public_send(predicate, finish))
|
93
96
|
end
|
94
97
|
|
95
98
|
relation
|
@@ -267,11 +267,7 @@ module OnlineMigrations
|
|
267
267
|
__copy_indexes(table_name, column_name, tmp_column_name)
|
268
268
|
__copy_foreign_keys(table_name, column_name, tmp_column_name)
|
269
269
|
__copy_check_constraints(table_name, column_name, tmp_column_name)
|
270
|
-
|
271
|
-
# Exclusion constraints were added in https://github.com/rails/rails/pull/40224.
|
272
|
-
if Utils.ar_version >= 7.1
|
273
|
-
__copy_exclusion_constraints(table_name, column_name, tmp_column_name)
|
274
|
-
end
|
270
|
+
__copy_exclusion_constraints(table_name, column_name, tmp_column_name)
|
275
271
|
|
276
272
|
if column_name == primary_key
|
277
273
|
__finalize_primary_key_type_change(table_name, column_name, column_names)
|
@@ -358,13 +354,22 @@ module OnlineMigrations
|
|
358
354
|
# @see #cleanup_column_type_change
|
359
355
|
#
|
360
356
|
def cleanup_columns_type_change(table_name, *column_names)
|
361
|
-
|
362
|
-
|
357
|
+
tmp_column_names = column_names.map { |column_name| __change_type_column(column_name) }
|
358
|
+
|
359
|
+
# Safely remove existing indexes and foreign keys first, if any.
|
360
|
+
tmp_column_names.each do |column_name|
|
361
|
+
__indexes_for(table_name, column_name).each do |index|
|
362
|
+
remove_index(table_name, name: index.name, algorithm: :concurrently)
|
363
|
+
end
|
364
|
+
|
365
|
+
__foreign_keys_for(table_name, column_name).each do |fk|
|
366
|
+
remove_foreign_key(table_name, name: fk.name)
|
367
|
+
end
|
363
368
|
end
|
364
369
|
|
365
370
|
transaction do
|
366
|
-
__remove_copy_triggers(table_name,
|
367
|
-
remove_columns(table_name, *
|
371
|
+
__remove_copy_triggers(table_name, column_names, tmp_column_names)
|
372
|
+
remove_columns(table_name, *tmp_column_names)
|
368
373
|
end
|
369
374
|
end
|
370
375
|
|
@@ -401,12 +406,13 @@ module OnlineMigrations
|
|
401
406
|
to_column = to_column.to_s
|
402
407
|
|
403
408
|
__indexes_for(table_name, from_column).each do |index|
|
409
|
+
columns = index.columns
|
404
410
|
new_columns =
|
405
411
|
# Expression index.
|
406
|
-
if
|
407
|
-
|
412
|
+
if columns.is_a?(String)
|
413
|
+
columns.gsub(/\b#{from_column}\b/, to_column)
|
408
414
|
else
|
409
|
-
|
415
|
+
columns.map do |column|
|
410
416
|
column == from_column ? to_column : column
|
411
417
|
end
|
412
418
|
end
|
@@ -429,6 +435,16 @@ module OnlineMigrations
|
|
429
435
|
options[:opclass] = opclasses
|
430
436
|
end
|
431
437
|
|
438
|
+
# If the index name is custom - do not rely on auto generated index names, because this
|
439
|
+
# doesn't work (idempotency check does not work, rails does not consider ':where' option)
|
440
|
+
# when there are partial and "classic" indexes on the same columns.
|
441
|
+
if index.name == index_name(table_name, columns)
|
442
|
+
options[:name] = index_name(table_name, new_columns)
|
443
|
+
else
|
444
|
+
truncated_index_name = index.name[0, max_identifier_length - "_2".length]
|
445
|
+
options[:name] = "#{truncated_index_name}_2"
|
446
|
+
end
|
447
|
+
|
432
448
|
add_index(table_name, new_columns, **options, algorithm: :concurrently)
|
433
449
|
end
|
434
450
|
end
|
@@ -511,6 +527,13 @@ module OnlineMigrations
|
|
511
527
|
# Lock the table explicitly to prevent new rows being inserted
|
512
528
|
execute("LOCK TABLE #{quoted_table_name} IN ACCESS EXCLUSIVE MODE")
|
513
529
|
|
530
|
+
# https://stackoverflow.com/questions/47301722/how-can-view-depends-on-primary-key-constraint-in-postgres
|
531
|
+
#
|
532
|
+
# PG::DependentObjectsStillExist: ERROR: cannot drop constraint appointments_pkey on table appointments because other objects depend on it (PG::DependentObjectsStillExist)
|
533
|
+
# DETAIL: view appointment_statuses depends on constraint appointments_pkey on table appointments
|
534
|
+
# HINT: Use DROP ... CASCADE to drop the dependent objects too.
|
535
|
+
views = __drop_dependent_views(table_name)
|
536
|
+
|
514
537
|
swap_column_names(table_name, column_name, tmp_column_name)
|
515
538
|
|
516
539
|
# We need to update the trigger function in order to make PostgreSQL to
|
@@ -530,6 +553,8 @@ module OnlineMigrations
|
|
530
553
|
execute("ALTER TABLE #{quoted_table_name} DROP CONSTRAINT #{quote_table_name(pkey_constraint_name)}")
|
531
554
|
rename_index(table_name, pkey_index_name, pkey_constraint_name)
|
532
555
|
execute("ALTER TABLE #{quoted_table_name} ADD CONSTRAINT #{quote_table_name(pkey_constraint_name)} PRIMARY KEY USING INDEX #{quote_table_name(pkey_constraint_name)}")
|
556
|
+
|
557
|
+
__recreate_views(views)
|
533
558
|
end
|
534
559
|
end
|
535
560
|
|
@@ -579,5 +604,40 @@ module OnlineMigrations
|
|
579
604
|
function_name = __copy_triggers_name(table_name, column_names, tmp_column_names)
|
580
605
|
execute("ALTER FUNCTION #{quote_table_name(function_name)}() RESET ALL")
|
581
606
|
end
|
607
|
+
|
608
|
+
# https://stackoverflow.com/questions/69458819/is-there-any-way-to-list-all-the-views-related-to-a-table-in-the-existing-postgr
|
609
|
+
def __drop_dependent_views(table_name)
|
610
|
+
views = select_all(<<~SQL)
|
611
|
+
SELECT
|
612
|
+
u.view_schema AS schema,
|
613
|
+
u.view_name AS name,
|
614
|
+
v.view_definition AS definition,
|
615
|
+
c.relkind = 'm' AS materialized
|
616
|
+
FROM information_schema.view_table_usage u
|
617
|
+
JOIN information_schema.views v ON u.view_schema = v.table_schema
|
618
|
+
AND u.view_name = v.table_name
|
619
|
+
JOIN pg_class c ON c.relname = u.view_name
|
620
|
+
WHERE u.table_schema NOT IN ('information_schema', 'pg_catalog')
|
621
|
+
AND u.table_name = #{quote(table_name)}
|
622
|
+
ORDER BY u.view_schema, u.view_name
|
623
|
+
SQL
|
624
|
+
|
625
|
+
views.each do |row|
|
626
|
+
execute("DROP VIEW #{quote_table_name(row['schema'])}.#{quote_table_name(row['name'])}")
|
627
|
+
end
|
628
|
+
|
629
|
+
views
|
630
|
+
end
|
631
|
+
|
632
|
+
def __recreate_views(views)
|
633
|
+
views.each do |row|
|
634
|
+
schema, name, definition, materialized = row.values_at("schema", "name", "definition", "materialized")
|
635
|
+
|
636
|
+
execute(<<~SQL)
|
637
|
+
CREATE#{' MATERIALIZED' if materialized} VIEW #{quote_table_name(schema)}.#{quote_table_name(name)} AS
|
638
|
+
#{definition}
|
639
|
+
SQL
|
640
|
+
end
|
641
|
+
end
|
582
642
|
end
|
583
643
|
end
|
@@ -80,6 +80,7 @@ module OnlineMigrations
|
|
80
80
|
add_inheritance_column: "adding-a-single-table-inheritance-column",
|
81
81
|
mismatched_foreign_key_type: "mismatched-reference-column-types",
|
82
82
|
}
|
83
|
+
private_constant :ERROR_MESSAGE_TO_LINK
|
83
84
|
|
84
85
|
def check_database_version
|
85
86
|
return if defined?(@database_version_checked)
|
@@ -375,6 +376,31 @@ module OnlineMigrations
|
|
375
376
|
cleanup_code: command_str(:cleanup_column_type_change, table_name, column_name),
|
376
377
|
cleanup_down_code: command_str(:initialize_column_type_change, table_name, column_name, existing_type)
|
377
378
|
end
|
379
|
+
|
380
|
+
# Constraints must be rechecked.
|
381
|
+
# PostgreSQL recommends dropping constraints before and adding them back.
|
382
|
+
# https://www.postgresql.org/docs/current/ddl-alter.html#DDL-ALTER-COLUMN-TYPE
|
383
|
+
constraints = connection.check_constraints(table_name).select do |c|
|
384
|
+
c.validated? && c.expression.match?(/\b#{column_name}\b/)
|
385
|
+
end
|
386
|
+
|
387
|
+
if constraints.any?
|
388
|
+
change_commands = constraints.map do |c|
|
389
|
+
command_str(:remove_check_constraint, table_name, c.expression, { name: c.name })
|
390
|
+
end
|
391
|
+
change_commands << command_str(:change_column, table_name, column_name, type, **options)
|
392
|
+
constraints.each do |c|
|
393
|
+
change_commands << command_str(:add_check_constraint, table_name, c.expression, { name: c.name, validate: false })
|
394
|
+
end
|
395
|
+
|
396
|
+
validate_commands = constraints.map do |c|
|
397
|
+
command_str(:validate_check_constraint, table_name, { name: c.name })
|
398
|
+
end
|
399
|
+
|
400
|
+
raise_error :change_column_constraint,
|
401
|
+
change_column_code: change_commands.join("\n "),
|
402
|
+
validate_constraint_code: validate_commands.join("\n ")
|
403
|
+
end
|
378
404
|
end
|
379
405
|
end
|
380
406
|
|
@@ -388,9 +414,9 @@ module OnlineMigrations
|
|
388
414
|
if !allow_null && !new_or_small_table?(table_name)
|
389
415
|
# In PostgreSQL 12+ you can add a check constraint to the table
|
390
416
|
# and then "promote" it to NOT NULL for the column.
|
391
|
-
safe = check_constraints(table_name).any? do |c|
|
392
|
-
c
|
393
|
-
c
|
417
|
+
safe = connection.check_constraints(table_name).select(&:validated?).any? do |c|
|
418
|
+
c.expression == "#{column_name} IS NOT NULL" ||
|
419
|
+
c.expression == "#{connection.quote_column_name(column_name)} IS NOT NULL"
|
394
420
|
end
|
395
421
|
|
396
422
|
if !safe
|
@@ -601,7 +627,7 @@ module OnlineMigrations
|
|
601
627
|
def add_unique_constraint(table_name, column_name = nil, **options)
|
602
628
|
return if new_or_small_table?(table_name) || options[:using_index] || !column_name
|
603
629
|
|
604
|
-
index_name =
|
630
|
+
index_name = connection.index_name(table_name, column_name)
|
605
631
|
|
606
632
|
raise_error :add_unique_constraint,
|
607
633
|
add_index_code: command_str(:add_index, table_name, column_name, unique: true, name: index_name, algorithm: :concurrently),
|
@@ -765,18 +791,6 @@ module OnlineMigrations
|
|
765
791
|
"chk_rails_#{hashed_identifier}"
|
766
792
|
end
|
767
793
|
|
768
|
-
def check_constraints(table_name)
|
769
|
-
constraints_query = <<~SQL
|
770
|
-
SELECT pg_get_constraintdef(oid) AS def
|
771
|
-
FROM pg_constraint
|
772
|
-
WHERE contype = 'c'
|
773
|
-
AND convalidated
|
774
|
-
AND conrelid = #{connection.quote(table_name)}::regclass
|
775
|
-
SQL
|
776
|
-
|
777
|
-
connection.select_all(constraints_query).to_a
|
778
|
-
end
|
779
|
-
|
780
794
|
def check_inheritance_column(table_name, column_name, default)
|
781
795
|
if column_name.to_s == ActiveRecord::Base.inheritance_column && !default.nil?
|
782
796
|
raise_error :add_inheritance_column,
|
@@ -834,10 +848,8 @@ module OnlineMigrations
|
|
834
848
|
end
|
835
849
|
|
836
850
|
def index_include_column?(index, column)
|
837
|
-
|
838
|
-
|
839
|
-
index.columns.include?(column) ||
|
840
|
-
(Utils.ar_version >= 7.1 && index.include && index.include.include?(column)) ||
|
851
|
+
index.columns.include?(column) ||
|
852
|
+
(index.include && index.include.include?(column)) ||
|
841
853
|
(index.where && index.where.include?(column))
|
842
854
|
end
|
843
855
|
|
@@ -73,6 +73,13 @@ module OnlineMigrations
|
|
73
73
|
end
|
74
74
|
end
|
75
75
|
|
76
|
+
# Whether to require safety reason explanation when calling #safery_assured
|
77
|
+
#
|
78
|
+
# Disabled by default
|
79
|
+
# @return [Boolean]
|
80
|
+
#
|
81
|
+
attr_accessor :require_safety_assured_reason
|
82
|
+
|
76
83
|
# Whether to perform checks when migrating down
|
77
84
|
#
|
78
85
|
# Disabled by default
|
@@ -230,6 +237,7 @@ module OnlineMigrations
|
|
230
237
|
@start_after = 0
|
231
238
|
@target_version = nil
|
232
239
|
@small_tables = []
|
240
|
+
@require_safety_assured_reason = false
|
233
241
|
@check_down = false
|
234
242
|
@auto_analyze = false
|
235
243
|
@alphabetize_schema = false
|
@@ -242,6 +242,22 @@ during writes works automatically). For most column type changes, this does not
|
|
242
242
|
8. Remove changes from step 3, if any
|
243
243
|
9. Deploy",
|
244
244
|
|
245
|
+
change_column_constraint: "Changing the type of a column that has check constraints blocks reads and writes
|
246
|
+
while every row is checked. Drop the check constraints on the column before
|
247
|
+
changing the type and add them back afterwards.
|
248
|
+
|
249
|
+
class <%= migration_name %> < <%= migration_parent %>
|
250
|
+
def change
|
251
|
+
<%= change_column_code %>
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
class Validate<%= migration_name %> < <%= migration_parent %>
|
256
|
+
def change
|
257
|
+
<%= validate_constraint_code %>
|
258
|
+
end
|
259
|
+
end",
|
260
|
+
|
245
261
|
change_column_default:
|
246
262
|
"Partial writes are enabled, which can cause incorrect values
|
247
263
|
to be inserted when changing the default value of a column.
|
@@ -20,7 +20,7 @@ module OnlineMigrations
|
|
20
20
|
# For ActiveRecord::ConnectionAdapters::IndexDefinition is for expression indexes,
|
21
21
|
# `columns` is a string
|
22
22
|
table == other.table &&
|
23
|
-
(
|
23
|
+
columns.intersect?(Array(other.columns))
|
24
24
|
end
|
25
25
|
|
26
26
|
# @param other [OnlineMigrations::IndexDefinition, ActiveRecord::ConnectionAdapters::IndexDefinition]
|
@@ -45,7 +45,14 @@ module OnlineMigrations
|
|
45
45
|
# @example
|
46
46
|
# safety_assured { remove_column(:users, :some_column) }
|
47
47
|
#
|
48
|
-
def safety_assured(&block)
|
48
|
+
def safety_assured(reason = nil, &block)
|
49
|
+
config = OnlineMigrations.config
|
50
|
+
safe_version = version && version <= config.start_after
|
51
|
+
|
52
|
+
if config.require_safety_assured_reason && reason.blank? && !safe_version
|
53
|
+
raise OnlineMigrations::Error, "Specify a safety reason explanation when calling #safety_assured."
|
54
|
+
end
|
55
|
+
|
49
56
|
command_checker.class.safety_assured(&block)
|
50
57
|
end
|
51
58
|
|
@@ -3,84 +3,6 @@
|
|
3
3
|
module OnlineMigrations
|
4
4
|
# @private
|
5
5
|
module SchemaCache
|
6
|
-
def primary_keys(table_name)
|
7
|
-
if (renamed_table = renamed_table?(table_name))
|
8
|
-
super(renamed_table)
|
9
|
-
elsif renamed_column?(table_name)
|
10
|
-
super(column_rename_table(table_name))
|
11
|
-
else
|
12
|
-
super
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
def columns(table_name)
|
17
|
-
if (renamed_table = renamed_table?(table_name))
|
18
|
-
super(renamed_table)
|
19
|
-
elsif renamed_column?(table_name)
|
20
|
-
columns = super(column_rename_table(table_name))
|
21
|
-
OnlineMigrations.config.column_renames[table_name].each do |old_column_name, new_column_name|
|
22
|
-
duplicate_column(old_column_name, new_column_name, columns)
|
23
|
-
end
|
24
|
-
columns
|
25
|
-
else
|
26
|
-
super.reject { |column| column.name.end_with?("_for_type_change") }
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
def indexes(table_name)
|
31
|
-
if (renamed_table = renamed_table?(table_name))
|
32
|
-
super(renamed_table)
|
33
|
-
elsif renamed_column?(table_name)
|
34
|
-
super(column_rename_table(table_name))
|
35
|
-
else
|
36
|
-
super
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
def clear_data_source_cache!(name)
|
41
|
-
if (renamed_table = renamed_table?(name))
|
42
|
-
super(renamed_table)
|
43
|
-
end
|
44
|
-
|
45
|
-
if renamed_column?(name)
|
46
|
-
super(column_rename_table(name))
|
47
|
-
end
|
48
|
-
|
49
|
-
super
|
50
|
-
end
|
51
|
-
|
52
|
-
private
|
53
|
-
def renamed_table?(table_name)
|
54
|
-
table_renames = OnlineMigrations.config.table_renames
|
55
|
-
if table_renames.key?(table_name)
|
56
|
-
views = connection.views
|
57
|
-
table_renames[table_name] if views.include?(table_name)
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
def renamed_column?(table_name)
|
62
|
-
column_renames = OnlineMigrations.config.column_renames
|
63
|
-
column_renames.key?(table_name) && connection.views.include?(table_name)
|
64
|
-
end
|
65
|
-
|
66
|
-
def column_rename_table(table_name)
|
67
|
-
"#{table_name}_column_rename"
|
68
|
-
end
|
69
|
-
|
70
|
-
def duplicate_column(old_column_name, new_column_name, columns)
|
71
|
-
old_column = columns.find { |column| column.name == old_column_name }
|
72
|
-
new_column = old_column.dup
|
73
|
-
# Active Record defines only reader for :name
|
74
|
-
new_column.instance_variable_set(:@name, new_column_name)
|
75
|
-
# Correspond to the Active Record freezing of each column
|
76
|
-
columns << new_column.freeze
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
# @private
|
81
|
-
module SchemaCache71
|
82
|
-
# Active Record >= 7.1 changed signature of the methods,
|
83
|
-
# see https://github.com/rails/rails/pull/48716.
|
84
6
|
def primary_keys(connection, table_name)
|
85
7
|
if (renamed_table = renamed_table?(connection, table_name))
|
86
8
|
super(connection, renamed_table)
|
@@ -568,8 +568,8 @@ module OnlineMigrations
|
|
568
568
|
#
|
569
569
|
def add_text_limit_constraint(table_name, column_name, limit, name: nil, validate: true)
|
570
570
|
column = column_for(table_name, column_name)
|
571
|
-
if column.type != :text
|
572
|
-
raise "add_text_limit_constraint must be used only with :text columns"
|
571
|
+
if column.type != :text && column.type != :string
|
572
|
+
raise "add_text_limit_constraint must be used only with :text or :string columns"
|
573
573
|
end
|
574
574
|
|
575
575
|
name ||= __text_limit_constraint_name(table_name, column_name)
|
@@ -710,16 +710,12 @@ module OnlineMigrations
|
|
710
710
|
index_name = (options[:name] || index_name(table_name, column_name)).to_s
|
711
711
|
indexes(table_name).find { |i| i.name == index_name }
|
712
712
|
else
|
713
|
-
|
714
|
-
# See https://github.com/rails/rails/pull/45160.
|
715
|
-
indexes(table_name).find { |i| __index_defined_for?(i, column_name, **options) }
|
713
|
+
indexes(table_name).find { |i| i.defined_for?(column_name, **options) }
|
716
714
|
end
|
717
715
|
|
718
716
|
if index
|
719
|
-
|
720
|
-
|
721
|
-
if __index_valid?(index.name, schema: schema)
|
722
|
-
Utils.say("Index was not created because it already exists.")
|
717
|
+
if index.valid?
|
718
|
+
Utils.say("Index #{index.name} was not created because it already exists.")
|
723
719
|
return
|
724
720
|
else
|
725
721
|
Utils.say("Recreating invalid index: table_name: #{table_name}, column_name: #{column_name}")
|
@@ -764,22 +760,6 @@ module OnlineMigrations
|
|
764
760
|
end
|
765
761
|
end
|
766
762
|
|
767
|
-
# @private
|
768
|
-
# From ActiveRecord. Will not be needed for ActiveRecord >= 7.1.
|
769
|
-
def index_name(table_name, options)
|
770
|
-
if options.is_a?(Hash)
|
771
|
-
if options[:column]
|
772
|
-
Utils.index_name(table_name, options[:column])
|
773
|
-
elsif options[:name]
|
774
|
-
options[:name]
|
775
|
-
else
|
776
|
-
raise ArgumentError, "You must specify the index name"
|
777
|
-
end
|
778
|
-
else
|
779
|
-
index_name(table_name, column: options)
|
780
|
-
end
|
781
|
-
end
|
782
|
-
|
783
763
|
# Extends default method to be idempotent.
|
784
764
|
#
|
785
765
|
# @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_foreign_key
|
@@ -833,7 +813,7 @@ module OnlineMigrations
|
|
833
813
|
# @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_check_constraint
|
834
814
|
#
|
835
815
|
def add_check_constraint(table_name, expression, **options)
|
836
|
-
if
|
816
|
+
if check_constraint_exists?(table_name, expression: expression, **options)
|
837
817
|
Utils.say(<<~MSG.squish)
|
838
818
|
Check constraint was not created because it already exists (this may be due to an aborted migration or similar).
|
839
819
|
table_name: #{table_name}, expression: #{expression}
|
@@ -864,7 +844,7 @@ module OnlineMigrations
|
|
864
844
|
# @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-remove_check_constraint
|
865
845
|
#
|
866
846
|
def remove_check_constraint(table_name, expression = nil, **options)
|
867
|
-
if
|
847
|
+
if check_constraint_exists?(table_name, expression: expression, **options)
|
868
848
|
super
|
869
849
|
else
|
870
850
|
Utils.say(<<~MSG.squish)
|
@@ -874,16 +854,14 @@ module OnlineMigrations
|
|
874
854
|
end
|
875
855
|
end
|
876
856
|
|
877
|
-
|
878
|
-
|
879
|
-
|
880
|
-
|
881
|
-
|
882
|
-
|
883
|
-
|
884
|
-
|
885
|
-
super
|
886
|
-
end
|
857
|
+
def add_exclusion_constraint(table_name, expression, **options)
|
858
|
+
if __exclusion_constraint_exists?(table_name, expression: expression, **options)
|
859
|
+
Utils.say(<<~MSG.squish)
|
860
|
+
Exclusion constraint was not created because it already exists (this may be due to an aborted migration or similar).
|
861
|
+
table_name: #{table_name}, expression: #{expression}
|
862
|
+
MSG
|
863
|
+
else
|
864
|
+
super
|
887
865
|
end
|
888
866
|
end
|
889
867
|
|
@@ -932,20 +910,9 @@ module OnlineMigrations
|
|
932
910
|
end
|
933
911
|
end
|
934
912
|
|
935
|
-
# Will not be needed for Active Record >= 7.1
|
936
|
-
def __index_defined_for?(index, columns = nil, name: nil, unique: nil, valid: nil, include: nil, nulls_not_distinct: nil, **options)
|
937
|
-
columns = options[:column] if columns.blank?
|
938
|
-
(columns.nil? || Array(index.columns) == Array(columns).map(&:to_s)) &&
|
939
|
-
(name.nil? || index.name == name.to_s) &&
|
940
|
-
(unique.nil? || index.unique == unique) &&
|
941
|
-
(valid.nil? || index.valid == valid) &&
|
942
|
-
(include.nil? || Array(index.include) == Array(include).map(&:to_s)) &&
|
943
|
-
(nulls_not_distinct.nil? || index.nulls_not_distinct == nulls_not_distinct)
|
944
|
-
end
|
945
|
-
|
946
913
|
def __not_null_constraint_exists?(table_name, column_name, name: nil)
|
947
914
|
name ||= __not_null_constraint_name(table_name, column_name)
|
948
|
-
|
915
|
+
check_constraint_exists?(table_name, name: name)
|
949
916
|
end
|
950
917
|
|
951
918
|
def __not_null_constraint_name(table_name, column_name)
|
@@ -958,21 +925,7 @@ module OnlineMigrations
|
|
958
925
|
|
959
926
|
def __text_limit_constraint_exists?(table_name, column_name, name: nil)
|
960
927
|
name ||= __text_limit_constraint_name(table_name, column_name)
|
961
|
-
|
962
|
-
end
|
963
|
-
|
964
|
-
# Can use index validity attribute for Active Record >= 7.1.
|
965
|
-
def __index_valid?(index_name, schema:)
|
966
|
-
select_value(<<~SQL)
|
967
|
-
SELECT indisvalid
|
968
|
-
FROM pg_index i
|
969
|
-
JOIN pg_class c
|
970
|
-
ON i.indexrelid = c.oid
|
971
|
-
JOIN pg_namespace n
|
972
|
-
ON c.relnamespace = n.oid
|
973
|
-
WHERE n.nspname = #{schema}
|
974
|
-
AND c.relname = #{quote(index_name)}
|
975
|
-
SQL
|
928
|
+
check_constraint_exists?(table_name, name: name)
|
976
929
|
end
|
977
930
|
|
978
931
|
def __copy_foreign_key(fk, to_column, **options)
|
@@ -996,15 +949,6 @@ module OnlineMigrations
|
|
996
949
|
end
|
997
950
|
end
|
998
951
|
|
999
|
-
# Can be replaced by native method in Active Record >= 7.1.
|
1000
|
-
def __check_constraint_exists?(table_name, **options)
|
1001
|
-
if !options.key?(:name) && !options.key?(:expression)
|
1002
|
-
raise ArgumentError, "At least one of :name or :expression must be supplied"
|
1003
|
-
end
|
1004
|
-
|
1005
|
-
check_constraint_for(table_name, **options).present?
|
1006
|
-
end
|
1007
|
-
|
1008
952
|
def __exclusion_constraint_exists?(table_name, **options)
|
1009
953
|
if !options.key?(:name) && !options.key?(:expression)
|
1010
954
|
raise ArgumentError, "At least one of :name or :expression must be supplied"
|
@@ -69,23 +69,6 @@ module OnlineMigrations
|
|
69
69
|
end
|
70
70
|
end
|
71
71
|
|
72
|
-
# Implementation is from ActiveRecord.
|
73
|
-
# This is not needed for ActiveRecord >= 7.1 (https://github.com/rails/rails/pull/47753).
|
74
|
-
def index_name(table_name, column_name)
|
75
|
-
max_index_name_size = 62
|
76
|
-
name = "index_#{table_name}_on_#{Array(column_name) * '_and_'}"
|
77
|
-
return name if name.bytesize <= max_index_name_size
|
78
|
-
|
79
|
-
# Fallback to short version, add hash to ensure uniqueness
|
80
|
-
hashed_identifier = "_#{OpenSSL::Digest::SHA256.hexdigest(name).first(10)}"
|
81
|
-
name = "idx_on_#{Array(column_name) * '_'}"
|
82
|
-
|
83
|
-
short_limit = max_index_name_size - hashed_identifier.bytesize
|
84
|
-
short_name = name[0, short_limit]
|
85
|
-
|
86
|
-
"#{short_name}#{hashed_identifier}"
|
87
|
-
end
|
88
|
-
|
89
72
|
# Returns estimated rows count for a table.
|
90
73
|
# https://www.citusdata.com/blog/2016/10/12/count-performance/
|
91
74
|
def estimated_count(connection, table_name)
|
@@ -144,9 +127,7 @@ module OnlineMigrations
|
|
144
127
|
# This is the way that currently is used in ActiveRecord tests themselves.
|
145
128
|
pool_manager = ActiveRecord::Base.connection_handler.send(:get_pool_manager, ancestor.name)
|
146
129
|
|
147
|
-
|
148
|
-
# See https://github.com/rails/rails/pull/49284.
|
149
|
-
return pool_manager.shard_names.uniq if pool_manager
|
130
|
+
return pool_manager.shard_names if pool_manager
|
150
131
|
end
|
151
132
|
end
|
152
133
|
|
@@ -14,13 +14,7 @@ module OnlineMigrations
|
|
14
14
|
stdout_logger.level = @activerecord_logger_was.level
|
15
15
|
stdout_logger = ActiveSupport::TaggedLogging.new(stdout_logger)
|
16
16
|
|
17
|
-
combined_logger =
|
18
|
-
# Broadcasting logs API was changed in https://github.com/rails/rails/pull/48615.
|
19
|
-
if Utils.ar_version >= 7.1
|
20
|
-
ActiveSupport::BroadcastLogger.new(stdout_logger, @activerecord_logger_was)
|
21
|
-
else
|
22
|
-
stdout_logger.extend(ActiveSupport::Logger.broadcast(@activerecord_logger_was))
|
23
|
-
end
|
17
|
+
combined_logger = ActiveSupport::BroadcastLogger.new(stdout_logger, @activerecord_logger_was)
|
24
18
|
|
25
19
|
ActiveRecord::Base.logger = combined_logger
|
26
20
|
ActiveRecord.verbose_query_logs = false
|
data/lib/online_migrations.rb
CHANGED
@@ -102,12 +102,7 @@ module OnlineMigrations
|
|
102
102
|
end
|
103
103
|
|
104
104
|
def deprecator
|
105
|
-
@deprecator ||=
|
106
|
-
if Utils.ar_version >= 7.1
|
107
|
-
ActiveSupport::Deprecation.new(nil, "online_migrations")
|
108
|
-
else
|
109
|
-
ActiveSupport::Deprecation
|
110
|
-
end
|
105
|
+
@deprecator ||= ActiveSupport::Deprecation.new(nil, "online_migrations")
|
111
106
|
end
|
112
107
|
|
113
108
|
# @private
|
@@ -124,8 +119,6 @@ module OnlineMigrations
|
|
124
119
|
|
125
120
|
if OnlineMigrations::Utils.ar_version >= 7.2
|
126
121
|
ActiveRecord::ConnectionAdapters::SchemaCache.prepend(OnlineMigrations::SchemaCache72)
|
127
|
-
elsif OnlineMigrations::Utils.ar_version >= 7.1
|
128
|
-
ActiveRecord::ConnectionAdapters::SchemaCache.prepend(OnlineMigrations::SchemaCache71)
|
129
122
|
else
|
130
123
|
ActiveRecord::ConnectionAdapters::SchemaCache.prepend(OnlineMigrations::SchemaCache)
|
131
124
|
end
|
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.26.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- fatkodima
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
11
|
+
date: 2025-04-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '7.
|
19
|
+
version: '7.1'
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '7.
|
26
|
+
version: '7.1'
|
27
27
|
description:
|
28
28
|
email:
|
29
29
|
- fatkodima123@gmail.com
|
@@ -107,7 +107,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
107
107
|
requirements:
|
108
108
|
- - ">="
|
109
109
|
- !ruby/object:Gem::Version
|
110
|
-
version: '3.
|
110
|
+
version: '3.1'
|
111
111
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
112
112
|
requirements:
|
113
113
|
- - ">="
|