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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f6b7685527d71319ba50e3a5c29744d9ef28103c1cacfdc650cba12c276782d8
4
- data.tar.gz: e10e0d1dfb1c807b05927189f682633a7ab17c53f37974c5df5a2b452d78ab92
3
+ metadata.gz: cbca384d5eaf4ab575e80c60160d42326ef86f1baf21419f290663e0d9698eb0
4
+ data.tar.gz: b90abf4e278ee1b2ab27e60472f6bb127b124797084f53205a0929a5cc98268b
5
5
  SHA512:
6
- metadata.gz: d7c62d15765a2837cf0dcba5e9a0c63c89ecc73f33fbd5500284184a78027f80a4ad12d980b5ac5de8235550384bd123708a075adab0243a06202154348268a3
7
- data.tar.gz: 24449c58e1bec9b3a31e925a27df17c56c7cd923f2cdd33a5d6781f1f253204776901e92fd4024d3340e4eeb533b043a71a1afeaacb046bcc6ea62d5e5444668
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.0+
20
- - Rails 7.0+
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
- Consider the following migration:
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
- Adding a column with a non-null default blocks reads and writes while the entire table is rewritten.
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
- add_column_with_default takes care of all this steps:
75
+ 1. Ignore the column:
126
76
 
127
- class AddAdminToUsers < ActiveRecord::Migration[8.0]
128
- disable_ddl_transaction!
77
+ class User < ApplicationRecord
78
+ self.ignored_columns += ["name"]
79
+ end
129
80
 
130
- def change
131
- add_column_with_default :users, :admin, :boolean, default: false, null: false
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
- It suggests how to safely implement a migration, which essentially runs the steps similar to described in the previous example.
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 AddShardingToOnlineMigrations < <%= migration_parent %>
2
2
  def change
3
- safety_assured do
3
+ safety_assured("Table is small") do
4
4
  remove_index :background_migrations, [:migration_name, :arguments], unique: true
5
5
 
6
6
  change_table :background_migrations do |t|
@@ -1,6 +1,6 @@
1
1
  class AddTimestampsToBackgroundMigrations < <%= migration_parent %>
2
2
  def change
3
- safety_assured do
3
+ safety_assured("Table is small") do
4
4
  add_column :background_migrations, :started_at, :datetime
5
5
  add_column :background_migrations, :finished_at, :datetime
6
6
 
@@ -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
- # Use index validity from https://github.com/rails/rails/pull/45160
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
- unscopes = Utils.ar_version < 7.1 ? [:includes] : [:includes, :preload, :eager_load]
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
- relation = relation.where(relation.arel_table[column].public_send((order == :asc ? :gteq : :lteq), start))
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
- relation = relation.where(relation.arel_table[column].public_send((order == :asc ? :lteq : :gteq), finish))
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
- conversions = column_names.index_with do |column_name|
362
- __change_type_column(column_name)
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, conversions.keys, conversions.values)
367
- remove_columns(table_name, *conversions.values)
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 index.columns.is_a?(String)
407
- index.columns.gsub(/\b#{from_column}\b/, to_column)
412
+ if columns.is_a?(String)
413
+ columns.gsub(/\b#{from_column}\b/, to_column)
408
414
  else
409
- index.columns.map do |column|
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["def"] == "CHECK ((#{column_name} IS NOT NULL))" ||
393
- c["def"] == "CHECK ((#{connection.quote_column_name(column_name)} IS NOT NULL))"
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 = Utils.index_name(table_name, column_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
- # Expression index
838
- (index.columns.is_a?(String) && index.columns.include?(column)) ||
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
- (columns & Array(other.columns)).any?
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
- # Rewrite this with `IndexDefinition#defined_for?` when Active Record >= 7.1 is supported.
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
- schema = __schema_for_table(table_name)
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 __check_constraint_exists?(table_name, expression: expression, **options)
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 __check_constraint_exists?(table_name, expression: expression, **options)
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
- if Utils.ar_version >= 7.1
878
- def add_exclusion_constraint(table_name, expression, **options)
879
- if __exclusion_constraint_exists?(table_name, expression: expression, **options)
880
- Utils.say(<<~MSG.squish)
881
- Exclusion constraint was not created because it already exists (this may be due to an aborted migration or similar).
882
- table_name: #{table_name}, expression: #{expression}
883
- MSG
884
- else
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
- __check_constraint_exists?(table_name, name: name)
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
- __check_constraint_exists?(table_name, name: name)
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
- # .uniq call is not needed for Active Record 7.1+
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OnlineMigrations
4
- VERSION = "0.25.0"
4
+ VERSION = "0.26.0"
5
5
  end
@@ -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.25.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-02-03 00:00:00.000000000 Z
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.0'
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.0'
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.0'
110
+ version: '3.1'
111
111
  required_rubygems_version: !ruby/object:Gem::Requirement
112
112
  requirements:
113
113
  - - ">="