online_migrations 0.10.0 → 0.11.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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -0
  3. data/README.md +34 -31
  4. data/docs/background_migrations.md +36 -4
  5. data/docs/configuring.md +3 -2
  6. data/lib/generators/online_migrations/install_generator.rb +3 -7
  7. data/lib/generators/online_migrations/templates/add_sharding_to_online_migrations.rb.tt +18 -0
  8. data/lib/generators/online_migrations/templates/initializer.rb.tt +5 -3
  9. data/lib/generators/online_migrations/templates/migration.rb.tt +8 -3
  10. data/lib/generators/online_migrations/upgrade_generator.rb +33 -0
  11. data/lib/online_migrations/background_migrations/application_record.rb +13 -0
  12. data/lib/online_migrations/background_migrations/backfill_column.rb +1 -1
  13. data/lib/online_migrations/background_migrations/copy_column.rb +6 -20
  14. data/lib/online_migrations/background_migrations/delete_orphaned_records.rb +2 -20
  15. data/lib/online_migrations/background_migrations/migration.rb +123 -34
  16. data/lib/online_migrations/background_migrations/migration_helpers.rb +0 -4
  17. data/lib/online_migrations/background_migrations/migration_job.rb +15 -12
  18. data/lib/online_migrations/background_migrations/migration_job_runner.rb +2 -2
  19. data/lib/online_migrations/background_migrations/migration_runner.rb +56 -11
  20. data/lib/online_migrations/background_migrations/reset_counters.rb +3 -9
  21. data/lib/online_migrations/background_migrations/scheduler.rb +5 -15
  22. data/lib/online_migrations/change_column_type_helpers.rb +68 -83
  23. data/lib/online_migrations/command_checker.rb +11 -29
  24. data/lib/online_migrations/config.rb +7 -15
  25. data/lib/online_migrations/copy_trigger.rb +15 -10
  26. data/lib/online_migrations/error_messages.rb +13 -25
  27. data/lib/online_migrations/foreign_keys_collector.rb +2 -2
  28. data/lib/online_migrations/indexes_collector.rb +3 -3
  29. data/lib/online_migrations/lock_retrier.rb +4 -9
  30. data/lib/online_migrations/schema_cache.rb +0 -6
  31. data/lib/online_migrations/schema_dumper.rb +1 -1
  32. data/lib/online_migrations/schema_statements.rb +64 -256
  33. data/lib/online_migrations/utils.rb +18 -56
  34. data/lib/online_migrations/verbose_sql_logs.rb +3 -2
  35. data/lib/online_migrations/version.rb +1 -1
  36. data/lib/online_migrations.rb +7 -6
  37. metadata +8 -7
  38. data/lib/online_migrations/background_migrations/advisory_lock.rb +0 -62
  39. data/lib/online_migrations/foreign_key_definition.rb +0 -17
@@ -96,9 +96,8 @@ module OnlineMigrations
96
96
  else
97
97
  yield
98
98
  end
99
- # ActiveRecord::LockWaitTimeout can be used for Active Record 5.2+
100
- rescue ActiveRecord::StatementInvalid => e
101
- if lock_timeout_error?(e) && current_attempt <= attempts
99
+ rescue ActiveRecord::LockWaitTimeout
100
+ if current_attempt <= attempts
102
101
  current_delay = delay(current_attempt)
103
102
  Utils.say("Lock timeout. Retrying in #{current_delay} seconds...")
104
103
  sleep(current_delay)
@@ -122,10 +121,6 @@ module OnlineMigrations
122
121
  ensure
123
122
  connection.execute("SET lock_timeout TO #{connection.quote(prev_value)}")
124
123
  end
125
-
126
- def lock_timeout_error?(error)
127
- error.message.include?("canceling statement due to lock timeout")
128
- end
129
124
  end
130
125
 
131
126
  # `LockRetrier` implementation that has a constant delay between tries
@@ -181,10 +176,10 @@ module OnlineMigrations
181
176
  #
182
177
  # @example
183
178
  # # This will attempt 30 retries starting with delay of 10ms between each unsuccessful try, increasing exponentially
184
- # # up to the maximum delay of 1 minute and 50ms set as lock timeout for each try:
179
+ # # up to the maximum delay of 1 minute and 200ms set as lock timeout for each try:
185
180
  #
186
181
  # config.retrier = OnlineMigrations::ConstantLockRetrier.new(attempts: 30,
187
- # base_delay: 0.01.seconds, max_delay: 1.minute, lock_timeout: 0.05.seconds)
182
+ # base_delay: 0.01.seconds, max_delay: 1.minute, lock_timeout: 0.2.seconds)
188
183
  #
189
184
  class ExponentialLockRetrier < LockRetrier
190
185
  # LockRetrier API implementation
@@ -28,9 +28,6 @@ module OnlineMigrations
28
28
  end
29
29
 
30
30
  def indexes(table_name)
31
- # Available only in Active Record 6.0+
32
- return if !defined?(super)
33
-
34
31
  if (renamed_table = renamed_table?(table_name))
35
32
  super(renamed_table)
36
33
  elsif renamed_column?(table_name)
@@ -109,9 +106,6 @@ module OnlineMigrations
109
106
  end
110
107
 
111
108
  def indexes(connection, table_name)
112
- # Available only in Active Record 6.0+
113
- return if !defined?(super)
114
-
115
109
  if (renamed_table = renamed_table?(connection, table_name))
116
110
  super(connection, renamed_table)
117
111
  elsif renamed_column?(connection, table_name)
@@ -4,7 +4,7 @@ require "delegate"
4
4
 
5
5
  module OnlineMigrations
6
6
  module SchemaDumper
7
- def initialize(connection, *args, **options)
7
+ def initialize(connection, options = {})
8
8
  if OnlineMigrations.config.alphabetize_schema
9
9
  connection = WrappedConnection.new(connection)
10
10
  end
@@ -69,7 +69,7 @@ module OnlineMigrations
69
69
  batch_size: 1000, batch_column_name: primary_key(table_name), progress: false, pause_ms: 50)
70
70
  __ensure_not_in_transaction!
71
71
 
72
- if !columns_and_values.is_a?(Array) || !columns_and_values.all? { |e| e.is_a?(Array) }
72
+ if !columns_and_values.is_a?(Array) || !columns_and_values.all?(Array)
73
73
  raise ArgumentError, "columns_and_values must be an array of arrays"
74
74
  end
75
75
 
@@ -81,13 +81,13 @@ module OnlineMigrations
81
81
  end
82
82
  end
83
83
 
84
- model = Utils.define_model(table_name, self)
84
+ model = Utils.define_model(table_name)
85
85
 
86
- conditions = columns_and_values.map do |(column_name, value)|
86
+ conditions = columns_and_values.filter_map do |(column_name, value)|
87
87
  value = Arel.sql(value.call.to_s) if value.is_a?(Proc)
88
88
 
89
89
  # Ignore subqueries in conditions
90
- if !value.is_a?(Arel::Nodes::SqlLiteral) || value.to_s !~ /select\s+/i
90
+ if !value.is_a?(Arel::Nodes::SqlLiteral) || !value.to_s.match?(/select\s+/i)
91
91
  arel_column = model.arel_table[column_name]
92
92
  if value.nil?
93
93
  arel_column.not_eq(nil)
@@ -95,7 +95,7 @@ module OnlineMigrations
95
95
  arel_column.not_eq(value).or(arel_column.eq(nil))
96
96
  end
97
97
  end
98
- end.compact
98
+ end
99
99
 
100
100
  batch_relation = model.where(conditions.inject(:or))
101
101
  batch_relation = yield batch_relation if block_given?
@@ -103,30 +103,9 @@ module OnlineMigrations
103
103
  iterator = BatchIterator.new(batch_relation)
104
104
  iterator.each_batch(of: batch_size, column: batch_column_name) do |relation|
105
105
  updates =
106
- if Utils.ar_version <= 5.2
107
- columns_and_values.map do |(column_name, value)|
108
- rhs =
109
- # Active Record <= 5.2 can't quote these - we need to handle these cases manually
110
- case value
111
- when Arel::Attributes::Attribute
112
- quote_column_name(value.name)
113
- when Arel::Nodes::SqlLiteral
114
- value
115
- when Arel::Nodes::NamedFunction
116
- "#{value.name}(#{quote_column_name(value.expressions.first.name)})"
117
- when Proc
118
- value.call
119
- else
120
- quote(value)
121
- end
122
-
123
- "#{quote_column_name(column_name)} = #{rhs}"
124
- end.join(", ")
125
- else
126
- columns_and_values.map do |(column, value)|
127
- value = Arel.sql(value.call.to_s) if value.is_a?(Proc)
128
- [column, value]
129
- end.to_h
106
+ columns_and_values.to_h do |(column, value)|
107
+ value = Arel.sql(value.call.to_s) if value.is_a?(Proc)
108
+ [column, value]
130
109
  end
131
110
 
132
111
  relation.update_all(updates)
@@ -454,9 +433,6 @@ module OnlineMigrations
454
433
  #
455
434
  def add_column_with_default(table_name, column_name, type, **options)
456
435
  default = options.fetch(:default)
457
- if default.is_a?(Proc) && Utils.ar_version < 5.0 # https://github.com/rails/rails/pull/20005
458
- raise ArgumentError, "Expressions as default are not supported"
459
- end
460
436
 
461
437
  if raw_connection.server_version >= 11_00_00 && !Utils.volatile_default?(self, type, default)
462
438
  add_column(table_name, column_name, type, **options)
@@ -507,7 +483,8 @@ module OnlineMigrations
507
483
  # add_not_null_constraint(:users, :email, validate: false)
508
484
  #
509
485
  def add_not_null_constraint(table_name, column_name, name: nil, validate: true)
510
- if __column_not_nullable?(table_name, column_name) ||
486
+ column = column_for(table_name, column_name)
487
+ if column.null == false ||
511
488
  __not_null_constraint_exists?(table_name, column_name, name: name)
512
489
  Utils.say("NOT NULL constraint was not created: column #{table_name}.#{column_name} is already defined as `NOT NULL`")
513
490
  else
@@ -577,7 +554,7 @@ module OnlineMigrations
577
554
  # @note This helper must be used only with text columns
578
555
  #
579
556
  def add_text_limit_constraint(table_name, column_name, limit, name: nil, validate: true)
580
- column = __column_for(table_name, column_name)
557
+ column = column_for(table_name, column_name)
581
558
  if column.type != :text
582
559
  raise "add_text_limit_constraint must be used only with :text columns"
583
560
  end
@@ -666,13 +643,13 @@ module OnlineMigrations
666
643
 
667
644
  column_name = "#{ref_name}_id"
668
645
  if !column_exists?(table_name, column_name)
669
- type = options[:type] || (Utils.ar_version >= 5.1 ? :bigint : :integer)
646
+ type = options[:type] || :bigint
670
647
  allow_null = options.fetch(:null, true)
671
648
  add_column(table_name, column_name, type, null: allow_null)
672
649
  end
673
650
 
674
651
  # Always added by default in 5.0+
675
- index = options.fetch(:index) { Utils.ar_version >= 5.0 }
652
+ index = options.fetch(:index, true)
676
653
 
677
654
  if index
678
655
  index = {} if index == true
@@ -708,7 +685,7 @@ module OnlineMigrations
708
685
 
709
686
  __ensure_not_in_transaction! if algorithm == :concurrently
710
687
 
711
- column_names = __index_column_names(column_name || options[:column])
688
+ column_names = index_column_names(column_name || options[:column])
712
689
 
713
690
  index_name = options[:name]
714
691
  index_name ||= index_name(table_name, column_names)
@@ -734,7 +711,7 @@ module OnlineMigrations
734
711
  end
735
712
  end
736
713
 
737
- # Extends default method to be idempotent and accept `:algorithm` option for Active Record <= 4.2.
714
+ # Extends default method to be idempotent.
738
715
  #
739
716
  # @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-remove_index
740
717
  #
@@ -743,33 +720,15 @@ module OnlineMigrations
743
720
 
744
721
  __ensure_not_in_transaction! if algorithm == :concurrently
745
722
 
746
- column_names = __index_column_names(column_name || options[:column])
747
- index_name = options[:name]
748
- index_name ||= index_name(table_name, column_names)
723
+ column_names = index_column_names(column_name || options[:column])
749
724
 
750
- index_exists =
751
- if Utils.ar_version <= 5.0
752
- # Older Active Record is unable to handle blank columns correctly in `index_exists?`,
753
- # so we need to use `index_name_exists?`.
754
- index_name_exists?(table_name, index_name, nil)
755
- elsif Utils.ar_version <= 6.0
756
- index_name_exists?(table_name, index_name)
757
- else
758
- index_exists?(table_name, column_names, **options)
759
- end
760
-
761
- if index_exists
725
+ if index_exists?(table_name, column_names, **options)
762
726
  disable_statement_timeout do
763
727
  # "DROP INDEX CONCURRENTLY" requires a "SHARE UPDATE EXCLUSIVE" lock.
764
728
  # It only conflicts with constraint validations, other creating/removing indexes,
765
729
  # and some "ALTER TABLE"s.
766
730
 
767
- # Active Record <= 4.2 does not support removing indexes concurrently
768
- if Utils.ar_version <= 4.2 && algorithm == :concurrently
769
- execute("DROP INDEX CONCURRENTLY #{quote_table_name(index_name)}")
770
- else
771
- super(table_name, **options.merge(column: column_names))
772
- end
731
+ super(table_name, **options.merge(column: column_names))
773
732
  end
774
733
  else
775
734
  Utils.say("Index was not removed because it does not exist (this may be due to an aborted migration " \
@@ -793,132 +752,83 @@ module OnlineMigrations
793
752
  end
794
753
  end
795
754
 
796
- # Extends default method to be idempotent and accept `:validate` option for Active Record < 5.2.
755
+ # Extends default method to be idempotent.
797
756
  #
798
757
  # @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_foreign_key
799
758
  #
800
- def add_foreign_key(from_table, to_table, validate: true, **options)
759
+ def add_foreign_key(from_table, to_table, **options)
801
760
  if foreign_key_exists?(from_table, to_table, **options)
802
- message = "Foreign key was not created because it already exists " \
803
- "(this can be due to an aborted migration or similar): from_table: #{from_table}, to_table: #{to_table}".dup
761
+ message = +"Foreign key was not created because it already exists " \
762
+ "(this can be due to an aborted migration or similar): from_table: #{from_table}, to_table: #{to_table}"
804
763
  message << ", #{options.inspect}" if options.any?
805
764
 
806
765
  Utils.say(message)
807
766
  else
808
- # Active Record >= 5.2 supports adding non-validated foreign keys natively
809
- options = options.dup
810
- options[:column] ||= "#{to_table.to_s.singularize}_id"
811
- options[:primary_key] ||= "id"
812
- options[:name] ||= __foreign_key_name(to_table, options[:column])
813
-
814
- query = <<-SQL.strip_heredoc.dup
815
- ALTER TABLE #{quote_table_name(from_table)}
816
- ADD CONSTRAINT #{quote_column_name(options[:name])}
817
- FOREIGN KEY (#{quote_column_name(options[:column])})
818
- REFERENCES #{quote_table_name(to_table)} (#{quote_column_name(options[:primary_key])})
819
- SQL
820
- query << "#{__action_sql('DELETE', options[:on_delete])}\n" if options[:on_delete].present?
821
- query << "#{__action_sql('UPDATE', options[:on_update])}\n" if options[:on_update].present?
822
- query << "NOT VALID\n" if !validate
823
- if Utils.ar_version >= 7.0 && options[:deferrable]
824
- query << " DEFERRABLE"
825
- query << " INITIALLY #{options[:deferrable].to_s.upcase}\n" if options[:deferrable] != true
826
- end
827
-
828
- execute(query.squish)
767
+ super
829
768
  end
830
769
  end
831
770
 
832
771
  # Extends default method with disabled statement timeout while validation is run
833
772
  #
834
773
  # @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/PostgreSQL/SchemaStatements.html#method-i-validate_foreign_key
835
- # @note This method was added in Active Record 5.2
836
774
  #
837
775
  def validate_foreign_key(from_table, to_table = nil, **options)
838
- fk_name_to_validate = __foreign_key_for!(from_table, to_table: to_table, **options).name
776
+ foreign_key = foreign_key_for!(from_table, to_table: to_table, **options)
839
777
 
840
778
  # Skip costly operation if already validated.
841
- return if __constraint_validated?(from_table, fk_name_to_validate, type: :foreign_key)
779
+ return if foreign_key.validated?
842
780
 
843
781
  disable_statement_timeout do
844
782
  # "VALIDATE CONSTRAINT" requires a "SHARE UPDATE EXCLUSIVE" lock.
845
783
  # It only conflicts with other validations, creating/removing indexes,
846
784
  # and some other "ALTER TABLE"s.
847
- execute("ALTER TABLE #{quote_table_name(from_table)} VALIDATE CONSTRAINT #{quote_column_name(fk_name_to_validate)}")
785
+ super
848
786
  end
849
787
  end
850
788
 
851
- def foreign_key_exists?(from_table, to_table = nil, **options)
852
- foreign_keys(from_table).any? { |fk| fk.defined_for?(to_table: to_table, **options) }
853
- end
854
-
855
789
  # Extends default method to be idempotent
856
790
  #
857
791
  # @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_check_constraint
858
- # @note This method was added in Active Record 6.1
859
792
  #
860
- def add_check_constraint(table_name, expression, validate: true, **options)
861
- constraint_name = __check_constraint_name(table_name, expression: expression, **options)
862
-
863
- if __check_constraint_exists?(table_name, constraint_name)
864
- Utils.say("Check constraint was not created because it already exists (this may be due to an aborted migration " \
865
- "or similar) table_name: #{table_name}, expression: #{expression}, constraint name: #{constraint_name}")
793
+ def add_check_constraint(table_name, expression, **options)
794
+ if __check_constraint_exists?(table_name, expression: expression, **options)
795
+ Utils.say(<<~MSG.squish)
796
+ Check constraint was not created because it already exists (this may be due to an aborted migration or similar).
797
+ table_name: #{table_name}, expression: #{expression}
798
+ MSG
866
799
  else
867
- query = <<-SQL.squish
868
- ALTER TABLE #{quote_table_name(table_name)}
869
- ADD CONSTRAINT #{quote_column_name(constraint_name)} CHECK (#{expression})
870
- SQL
871
- query += " NOT VALID" if !validate
872
-
873
- execute(query)
800
+ super
874
801
  end
875
802
  end
876
803
 
877
804
  # Extends default method with disabled statement timeout while validation is run
878
805
  #
879
806
  # @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/PostgreSQL/SchemaStatements.html#method-i-validate_check_constraint
880
- # @note This method was added in Active Record 6.1
881
807
  #
882
808
  def validate_check_constraint(table_name, **options)
883
- constraint_name = __check_constraint_name!(table_name, **options)
809
+ check_constraint = check_constraint_for!(table_name, **options)
884
810
 
885
811
  # Skip costly operation if already validated.
886
- return if __constraint_validated?(table_name, constraint_name, type: :check)
812
+ return if check_constraint.validated?
887
813
 
888
814
  disable_statement_timeout do
889
815
  # "VALIDATE CONSTRAINT" requires a "SHARE UPDATE EXCLUSIVE" lock.
890
816
  # It only conflicts with other validations, creating/removing indexes,
891
817
  # and some other "ALTER TABLE"s.
892
- execute(<<-SQL.squish)
893
- ALTER TABLE #{quote_table_name(table_name)}
894
- VALIDATE CONSTRAINT #{quote_column_name(constraint_name)}
895
- SQL
896
- end
897
- end
898
-
899
- if Utils.ar_version < 6.1
900
- # @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-remove_check_constraint
901
- # @note This method was added in Active Record 6.1
902
- #
903
- def remove_check_constraint(table_name, expression = nil, **options)
904
- constraint_name = __check_constraint_name!(table_name, expression: expression, **options)
905
- execute(<<-SQL.squish)
906
- ALTER TABLE #{quote_table_name(table_name)}
907
- DROP CONSTRAINT #{quote_column_name(constraint_name)}
908
- SQL
818
+ super
909
819
  end
910
820
  end
911
821
 
912
- if Utils.ar_version <= 4.2
913
- # @private
914
- def views
915
- select_values(<<-SQL, "SCHEMA")
916
- SELECT c.relname
917
- FROM pg_class c
918
- LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
919
- WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view
920
- AND n.nspname = ANY (current_schemas(false))
921
- SQL
822
+ if Utils.ar_version >= 7.1
823
+ def add_exclusion_constraint(table_name, expression, **options)
824
+ if __exclusion_constraint_exists?(table_name, expression: expression, **options)
825
+ Utils.say(<<~MSG.squish)
826
+ Exclusion constraint was not created because it already exists (this may be due to an aborted migration or similar).
827
+ table_name: #{table_name}, expression: #{expression}
828
+ MSG
829
+ else
830
+ super
831
+ end
922
832
  end
923
833
  end
924
834
 
@@ -986,7 +896,7 @@ module OnlineMigrations
986
896
  # Active Record methods
987
897
  def __ensure_not_in_transaction!(method_name = caller[0])
988
898
  if transaction_open?
989
- raise <<-MSG.strip_heredoc
899
+ raise <<~MSG
990
900
  `#{method_name}` cannot run inside a transaction block.
991
901
 
992
902
  You can remove transaction block by calling `disable_ddl_transaction!` in the body of
@@ -995,49 +905,27 @@ module OnlineMigrations
995
905
  end
996
906
  end
997
907
 
998
- def __column_not_nullable?(table_name, column_name)
999
- schema = __schema_for_table(table_name)
1000
-
1001
- query = <<-SQL.strip_heredoc
1002
- SELECT is_nullable
1003
- FROM information_schema.columns
1004
- WHERE table_schema = #{schema}
1005
- AND table_name = #{quote(table_name)}
1006
- AND column_name = #{quote(column_name)}
1007
- SQL
1008
-
1009
- select_value(query) == "NO"
1010
- end
1011
-
1012
908
  def __not_null_constraint_exists?(table_name, column_name, name: nil)
1013
909
  name ||= __not_null_constraint_name(table_name, column_name)
1014
- __check_constraint_exists?(table_name, name)
910
+ __check_constraint_exists?(table_name, name: name)
1015
911
  end
1016
912
 
1017
913
  def __not_null_constraint_name(table_name, column_name)
1018
- __check_constraint_name(table_name, expression: "#{column_name}_not_null")
914
+ check_constraint_name(table_name, expression: "#{column_name}_not_null")
1019
915
  end
1020
916
 
1021
917
  def __text_limit_constraint_name(table_name, column_name)
1022
- __check_constraint_name(table_name, expression: "#{column_name}_max_length")
918
+ check_constraint_name(table_name, expression: "#{column_name}_max_length")
1023
919
  end
1024
920
 
1025
921
  def __text_limit_constraint_exists?(table_name, column_name, name: nil)
1026
922
  name ||= __text_limit_constraint_name(table_name, column_name)
1027
- __check_constraint_exists?(table_name, name)
1028
- end
1029
-
1030
- def __index_column_names(column_names)
1031
- if column_names.is_a?(String) && /\W/.match(column_names)
1032
- column_names
1033
- elsif column_names.present?
1034
- Array(column_names)
1035
- end
923
+ __check_constraint_exists?(table_name, name: name)
1036
924
  end
1037
925
 
926
+ # Can use index validity attribute for Active Record >= 7.1.
1038
927
  def __index_valid?(index_name, schema:)
1039
- # Active Record <= 4.2 returns a string, instead of automatically casting to boolean
1040
- valid = select_value <<-SQL.strip_heredoc
928
+ select_value(<<~SQL)
1041
929
  SELECT indisvalid
1042
930
  FROM pg_index i
1043
931
  JOIN pg_class c
@@ -1047,28 +935,6 @@ module OnlineMigrations
1047
935
  WHERE n.nspname = #{schema}
1048
936
  AND c.relname = #{quote(index_name)}
1049
937
  SQL
1050
-
1051
- Utils.to_bool(valid)
1052
- end
1053
-
1054
- def __column_for(table_name, column_name)
1055
- column_name = column_name.to_s
1056
-
1057
- columns(table_name).find { |c| c.name == column_name } ||
1058
- raise("No such column: #{table_name}.#{column_name}")
1059
- end
1060
-
1061
- def __action_sql(action, dependency)
1062
- case dependency
1063
- when :nullify then "ON #{action} SET NULL"
1064
- when :cascade then "ON #{action} CASCADE"
1065
- when :restrict then "ON #{action} RESTRICT"
1066
- else
1067
- raise ArgumentError, <<-MSG.strip_heredoc
1068
- '#{dependency}' is not supported for :on_update or :on_delete.
1069
- Supported values are: :nullify, :cascade, :restrict
1070
- MSG
1071
- end
1072
938
  end
1073
939
 
1074
940
  def __copy_foreign_key(fk, to_column, **options)
@@ -1087,84 +953,26 @@ module OnlineMigrations
1087
953
  **fkey_options
1088
954
  )
1089
955
 
1090
- if !fk.respond_to?(:validated?) || fk.validated?
956
+ if fk.validated?
1091
957
  validate_foreign_key(fk.from_table, fk.to_table, column: to_column, **options)
1092
958
  end
1093
959
  end
1094
960
 
1095
- def __foreign_key_name(table_name, column_name)
1096
- identifier = "#{table_name}_#{column_name}_fk"
1097
- hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
1098
-
1099
- "fk_rails_#{hashed_identifier}"
1100
- end
1101
-
1102
- if Utils.ar_version <= 4.2
1103
- def foreign_key_for(from_table, **options)
1104
- foreign_keys(from_table).detect { |fk| fk.defined_for?(**options) }
961
+ # Can be replaced by native method in Active Record >= 7.1.
962
+ def __check_constraint_exists?(table_name, **options)
963
+ if !options.key?(:name) && !options.key?(:expression)
964
+ raise ArgumentError, "At least one of :name or :expression must be supplied"
1105
965
  end
1106
- end
1107
966
 
1108
- def __foreign_key_for!(from_table, **options)
1109
- foreign_key_for(from_table, **options) ||
1110
- raise(ArgumentError, "Table '#{from_table}' has no foreign key for #{options[:to_table] || options}")
967
+ check_constraint_for(table_name, **options).present?
1111
968
  end
1112
969
 
1113
- def __constraint_validated?(table_name, name, type:)
1114
- schema = __schema_for_table(table_name)
1115
- contype = type == :check ? "c" : "f"
1116
-
1117
- validated = select_value(<<-SQL.strip_heredoc)
1118
- SELECT convalidated
1119
- FROM pg_catalog.pg_constraint con
1120
- INNER JOIN pg_catalog.pg_namespace nsp
1121
- ON nsp.oid = con.connamespace
1122
- WHERE con.conrelid = #{quote(table_name)}::regclass
1123
- AND con.conname = #{quote(name)}
1124
- AND con.contype = '#{contype}'
1125
- AND nsp.nspname = #{schema}
1126
- SQL
1127
-
1128
- Utils.to_bool(validated)
1129
- end
1130
-
1131
- def __check_constraint_name!(table_name, expression: nil, **options)
1132
- constraint_name = __check_constraint_name(table_name, expression: expression, **options)
1133
-
1134
- if __check_constraint_exists?(table_name, constraint_name)
1135
- constraint_name
1136
- else
1137
- raise(ArgumentError, "Table '#{table_name}' has no check constraint for #{expression || options}")
970
+ def __exclusion_constraint_exists?(table_name, **options)
971
+ if !options.key?(:name) && !options.key?(:expression)
972
+ raise ArgumentError, "At least one of :name or :expression must be supplied"
1138
973
  end
1139
- end
1140
-
1141
- def __check_constraint_name(table_name, **options)
1142
- options.fetch(:name) do
1143
- expression = options.fetch(:expression)
1144
- identifier = "#{table_name}_#{expression}_chk"
1145
- hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
1146
-
1147
- "chk_rails_#{hashed_identifier}"
1148
- end
1149
- end
1150
-
1151
- def __check_constraint_exists?(table_name, constraint_name)
1152
- schema = __schema_for_table(table_name)
1153
-
1154
- check_sql = <<-SQL.strip_heredoc
1155
- SELECT COUNT(*)
1156
- FROM pg_catalog.pg_constraint con
1157
- INNER JOIN pg_catalog.pg_class cl
1158
- ON cl.oid = con.conrelid
1159
- INNER JOIN pg_catalog.pg_namespace nsp
1160
- ON nsp.oid = con.connamespace
1161
- WHERE con.contype = 'c'
1162
- AND con.conname = #{quote(constraint_name)}
1163
- AND cl.relname = #{quote(table_name)}
1164
- AND nsp.nspname = #{schema}
1165
- SQL
1166
974
 
1167
- select_value(check_sql).to_i > 0
975
+ exclusion_constraint_for(table_name, **options).present?
1168
976
  end
1169
977
 
1170
978
  def __schema_for_table(table_name)
@@ -1179,7 +987,7 @@ module OnlineMigrations
1179
987
  "#{quote_column_name(column_name)} AS #{quote_column_name(new_column_name)}"
1180
988
  end.join(", ")
1181
989
 
1182
- execute(<<-SQL.squish)
990
+ execute(<<~SQL.squish)
1183
991
  CREATE VIEW #{quote_table_name(table_name)} AS
1184
992
  SELECT *, #{column_mapping}
1185
993
  FROM #{quote_table_name(tmp_table)}