online_migrations 0.10.0 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
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)}