online_migrations 0.9.2 → 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 +41 -0
  3. data/README.md +155 -150
  4. data/docs/background_migrations.md +43 -10
  5. data/docs/configuring.md +23 -18
  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 +12 -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 +11 -19
  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 +71 -86
  23. data/lib/online_migrations/command_checker.rb +50 -46
  24. data/lib/online_migrations/config.rb +19 -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 +21 -0
  32. data/lib/online_migrations/schema_statements.rb +80 -256
  33. data/lib/online_migrations/utils.rb +36 -55
  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 +9 -6
  37. metadata +9 -7
  38. data/lib/online_migrations/background_migrations/advisory_lock.rb +0 -62
  39. data/lib/online_migrations/foreign_key_definition.rb +0 -17
@@ -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)
749
-
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
723
+ column_names = index_column_names(column_name || options[:column])
760
724
 
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 " \
@@ -777,132 +736,99 @@ module OnlineMigrations
777
736
  end
778
737
  end
779
738
 
780
- # Extends default method to be idempotent and accept `:validate` option for Active Record < 5.2.
739
+ # @private
740
+ # From ActiveRecord. Will not be needed for ActiveRecord >= 7.1.
741
+ def index_name(table_name, options)
742
+ if options.is_a?(Hash)
743
+ if options[:column]
744
+ Utils.index_name(table_name, options[:column])
745
+ elsif options[:name]
746
+ options[:name]
747
+ else
748
+ raise ArgumentError, "You must specify the index name"
749
+ end
750
+ else
751
+ index_name(table_name, column: options)
752
+ end
753
+ end
754
+
755
+ # Extends default method to be idempotent.
781
756
  #
782
757
  # @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_foreign_key
783
758
  #
784
- def add_foreign_key(from_table, to_table, validate: true, **options)
759
+ def add_foreign_key(from_table, to_table, **options)
785
760
  if foreign_key_exists?(from_table, to_table, **options)
786
- message = "Foreign key was not created because it already exists " \
787
- "(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}"
788
763
  message << ", #{options.inspect}" if options.any?
789
764
 
790
765
  Utils.say(message)
791
766
  else
792
- # Active Record >= 5.2 supports adding non-validated foreign keys natively
793
- options = options.dup
794
- options[:column] ||= "#{to_table.to_s.singularize}_id"
795
- options[:primary_key] ||= "id"
796
- options[:name] ||= __foreign_key_name(to_table, options[:column])
797
-
798
- query = <<-SQL.strip_heredoc.dup
799
- ALTER TABLE #{quote_table_name(from_table)}
800
- ADD CONSTRAINT #{quote_column_name(options[:name])}
801
- FOREIGN KEY (#{quote_column_name(options[:column])})
802
- REFERENCES #{quote_table_name(to_table)} (#{quote_column_name(options[:primary_key])})
803
- SQL
804
- query << "#{__action_sql('DELETE', options[:on_delete])}\n" if options[:on_delete].present?
805
- query << "#{__action_sql('UPDATE', options[:on_update])}\n" if options[:on_update].present?
806
- query << "NOT VALID\n" if !validate
807
- if Utils.ar_version >= 7.0 && options[:deferrable]
808
- query << " DEFERRABLE"
809
- query << " INITIALLY #{options[:deferrable].to_s.upcase}\n" if options[:deferrable] != true
810
- end
811
-
812
- execute(query.squish)
767
+ super
813
768
  end
814
769
  end
815
770
 
816
771
  # Extends default method with disabled statement timeout while validation is run
817
772
  #
818
773
  # @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/PostgreSQL/SchemaStatements.html#method-i-validate_foreign_key
819
- # @note This method was added in Active Record 5.2
820
774
  #
821
775
  def validate_foreign_key(from_table, to_table = nil, **options)
822
- 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)
823
777
 
824
778
  # Skip costly operation if already validated.
825
- return if __constraint_validated?(from_table, fk_name_to_validate, type: :foreign_key)
779
+ return if foreign_key.validated?
826
780
 
827
781
  disable_statement_timeout do
828
782
  # "VALIDATE CONSTRAINT" requires a "SHARE UPDATE EXCLUSIVE" lock.
829
783
  # It only conflicts with other validations, creating/removing indexes,
830
784
  # and some other "ALTER TABLE"s.
831
- execute("ALTER TABLE #{quote_table_name(from_table)} VALIDATE CONSTRAINT #{quote_column_name(fk_name_to_validate)}")
785
+ super
832
786
  end
833
787
  end
834
788
 
835
- def foreign_key_exists?(from_table, to_table = nil, **options)
836
- foreign_keys(from_table).any? { |fk| fk.defined_for?(to_table: to_table, **options) }
837
- end
838
-
839
789
  # Extends default method to be idempotent
840
790
  #
841
791
  # @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-add_check_constraint
842
- # @note This method was added in Active Record 6.1
843
792
  #
844
- def add_check_constraint(table_name, expression, validate: true, **options)
845
- constraint_name = __check_constraint_name(table_name, expression: expression, **options)
846
-
847
- if __check_constraint_exists?(table_name, constraint_name)
848
- Utils.say("Check constraint was not created because it already exists (this may be due to an aborted migration " \
849
- "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
850
799
  else
851
- query = <<-SQL.squish
852
- ALTER TABLE #{quote_table_name(table_name)}
853
- ADD CONSTRAINT #{quote_column_name(constraint_name)} CHECK (#{expression})
854
- SQL
855
- query += " NOT VALID" if !validate
856
-
857
- execute(query)
800
+ super
858
801
  end
859
802
  end
860
803
 
861
804
  # Extends default method with disabled statement timeout while validation is run
862
805
  #
863
806
  # @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/PostgreSQL/SchemaStatements.html#method-i-validate_check_constraint
864
- # @note This method was added in Active Record 6.1
865
807
  #
866
808
  def validate_check_constraint(table_name, **options)
867
- constraint_name = __check_constraint_name!(table_name, **options)
809
+ check_constraint = check_constraint_for!(table_name, **options)
868
810
 
869
811
  # Skip costly operation if already validated.
870
- return if __constraint_validated?(table_name, constraint_name, type: :check)
812
+ return if check_constraint.validated?
871
813
 
872
814
  disable_statement_timeout do
873
815
  # "VALIDATE CONSTRAINT" requires a "SHARE UPDATE EXCLUSIVE" lock.
874
816
  # It only conflicts with other validations, creating/removing indexes,
875
817
  # and some other "ALTER TABLE"s.
876
- execute(<<-SQL.squish)
877
- ALTER TABLE #{quote_table_name(table_name)}
878
- VALIDATE CONSTRAINT #{quote_column_name(constraint_name)}
879
- SQL
880
- end
881
- end
882
-
883
- if Utils.ar_version < 6.1
884
- # @see https://edgeapi.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-remove_check_constraint
885
- # @note This method was added in Active Record 6.1
886
- #
887
- def remove_check_constraint(table_name, expression = nil, **options)
888
- constraint_name = __check_constraint_name!(table_name, expression: expression, **options)
889
- execute(<<-SQL.squish)
890
- ALTER TABLE #{quote_table_name(table_name)}
891
- DROP CONSTRAINT #{quote_column_name(constraint_name)}
892
- SQL
818
+ super
893
819
  end
894
820
  end
895
821
 
896
- if Utils.ar_version <= 4.2
897
- # @private
898
- def views
899
- select_values(<<-SQL, "SCHEMA")
900
- SELECT c.relname
901
- FROM pg_class c
902
- LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
903
- WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view
904
- AND n.nspname = ANY (current_schemas(false))
905
- 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
906
832
  end
907
833
  end
908
834
 
@@ -970,7 +896,7 @@ module OnlineMigrations
970
896
  # Active Record methods
971
897
  def __ensure_not_in_transaction!(method_name = caller[0])
972
898
  if transaction_open?
973
- raise <<-MSG.strip_heredoc
899
+ raise <<~MSG
974
900
  `#{method_name}` cannot run inside a transaction block.
975
901
 
976
902
  You can remove transaction block by calling `disable_ddl_transaction!` in the body of
@@ -979,49 +905,27 @@ module OnlineMigrations
979
905
  end
980
906
  end
981
907
 
982
- def __column_not_nullable?(table_name, column_name)
983
- schema = __schema_for_table(table_name)
984
-
985
- query = <<-SQL.strip_heredoc
986
- SELECT is_nullable
987
- FROM information_schema.columns
988
- WHERE table_schema = #{schema}
989
- AND table_name = #{quote(table_name)}
990
- AND column_name = #{quote(column_name)}
991
- SQL
992
-
993
- select_value(query) == "NO"
994
- end
995
-
996
908
  def __not_null_constraint_exists?(table_name, column_name, name: nil)
997
909
  name ||= __not_null_constraint_name(table_name, column_name)
998
- __check_constraint_exists?(table_name, name)
910
+ __check_constraint_exists?(table_name, name: name)
999
911
  end
1000
912
 
1001
913
  def __not_null_constraint_name(table_name, column_name)
1002
- __check_constraint_name(table_name, expression: "#{column_name}_not_null")
914
+ check_constraint_name(table_name, expression: "#{column_name}_not_null")
1003
915
  end
1004
916
 
1005
917
  def __text_limit_constraint_name(table_name, column_name)
1006
- __check_constraint_name(table_name, expression: "#{column_name}_max_length")
918
+ check_constraint_name(table_name, expression: "#{column_name}_max_length")
1007
919
  end
1008
920
 
1009
921
  def __text_limit_constraint_exists?(table_name, column_name, name: nil)
1010
922
  name ||= __text_limit_constraint_name(table_name, column_name)
1011
- __check_constraint_exists?(table_name, name)
1012
- end
1013
-
1014
- def __index_column_names(column_names)
1015
- if column_names.is_a?(String) && /\W/.match(column_names)
1016
- column_names
1017
- elsif column_names.present?
1018
- Array(column_names)
1019
- end
923
+ __check_constraint_exists?(table_name, name: name)
1020
924
  end
1021
925
 
926
+ # Can use index validity attribute for Active Record >= 7.1.
1022
927
  def __index_valid?(index_name, schema:)
1023
- # Active Record <= 4.2 returns a string, instead of automatically casting to boolean
1024
- valid = select_value <<-SQL.strip_heredoc
928
+ select_value(<<~SQL)
1025
929
  SELECT indisvalid
1026
930
  FROM pg_index i
1027
931
  JOIN pg_class c
@@ -1031,28 +935,6 @@ module OnlineMigrations
1031
935
  WHERE n.nspname = #{schema}
1032
936
  AND c.relname = #{quote(index_name)}
1033
937
  SQL
1034
-
1035
- Utils.to_bool(valid)
1036
- end
1037
-
1038
- def __column_for(table_name, column_name)
1039
- column_name = column_name.to_s
1040
-
1041
- columns(table_name).find { |c| c.name == column_name } ||
1042
- raise("No such column: #{table_name}.#{column_name}")
1043
- end
1044
-
1045
- def __action_sql(action, dependency)
1046
- case dependency
1047
- when :nullify then "ON #{action} SET NULL"
1048
- when :cascade then "ON #{action} CASCADE"
1049
- when :restrict then "ON #{action} RESTRICT"
1050
- else
1051
- raise ArgumentError, <<-MSG.strip_heredoc
1052
- '#{dependency}' is not supported for :on_update or :on_delete.
1053
- Supported values are: :nullify, :cascade, :restrict
1054
- MSG
1055
- end
1056
938
  end
1057
939
 
1058
940
  def __copy_foreign_key(fk, to_column, **options)
@@ -1071,84 +953,26 @@ module OnlineMigrations
1071
953
  **fkey_options
1072
954
  )
1073
955
 
1074
- if !fk.respond_to?(:validated?) || fk.validated?
956
+ if fk.validated?
1075
957
  validate_foreign_key(fk.from_table, fk.to_table, column: to_column, **options)
1076
958
  end
1077
959
  end
1078
960
 
1079
- def __foreign_key_name(table_name, column_name)
1080
- identifier = "#{table_name}_#{column_name}_fk"
1081
- hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
1082
-
1083
- "fk_rails_#{hashed_identifier}"
1084
- end
1085
-
1086
- if Utils.ar_version <= 4.2
1087
- def foreign_key_for(from_table, **options)
1088
- 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"
1089
965
  end
1090
- end
1091
-
1092
- def __foreign_key_for!(from_table, **options)
1093
- foreign_key_for(from_table, **options) ||
1094
- raise(ArgumentError, "Table '#{from_table}' has no foreign key for #{options[:to_table] || options}")
1095
- end
1096
966
 
1097
- def __constraint_validated?(table_name, name, type:)
1098
- schema = __schema_for_table(table_name)
1099
- contype = type == :check ? "c" : "f"
1100
-
1101
- validated = select_value(<<-SQL.strip_heredoc)
1102
- SELECT convalidated
1103
- FROM pg_catalog.pg_constraint con
1104
- INNER JOIN pg_catalog.pg_namespace nsp
1105
- ON nsp.oid = con.connamespace
1106
- WHERE con.conrelid = #{quote(table_name)}::regclass
1107
- AND con.conname = #{quote(name)}
1108
- AND con.contype = '#{contype}'
1109
- AND nsp.nspname = #{schema}
1110
- SQL
1111
-
1112
- Utils.to_bool(validated)
967
+ check_constraint_for(table_name, **options).present?
1113
968
  end
1114
969
 
1115
- def __check_constraint_name!(table_name, expression: nil, **options)
1116
- constraint_name = __check_constraint_name(table_name, expression: expression, **options)
1117
-
1118
- if __check_constraint_exists?(table_name, constraint_name)
1119
- constraint_name
1120
- else
1121
- 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"
1122
973
  end
1123
- end
1124
-
1125
- def __check_constraint_name(table_name, **options)
1126
- options.fetch(:name) do
1127
- expression = options.fetch(:expression)
1128
- identifier = "#{table_name}_#{expression}_chk"
1129
- hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10)
1130
-
1131
- "chk_rails_#{hashed_identifier}"
1132
- end
1133
- end
1134
-
1135
- def __check_constraint_exists?(table_name, constraint_name)
1136
- schema = __schema_for_table(table_name)
1137
-
1138
- check_sql = <<-SQL.strip_heredoc
1139
- SELECT COUNT(*)
1140
- FROM pg_catalog.pg_constraint con
1141
- INNER JOIN pg_catalog.pg_class cl
1142
- ON cl.oid = con.conrelid
1143
- INNER JOIN pg_catalog.pg_namespace nsp
1144
- ON nsp.oid = con.connamespace
1145
- WHERE con.contype = 'c'
1146
- AND con.conname = #{quote(constraint_name)}
1147
- AND cl.relname = #{quote(table_name)}
1148
- AND nsp.nspname = #{schema}
1149
- SQL
1150
974
 
1151
- select_value(check_sql).to_i > 0
975
+ exclusion_constraint_for(table_name, **options).present?
1152
976
  end
1153
977
 
1154
978
  def __schema_for_table(table_name)
@@ -1163,7 +987,7 @@ module OnlineMigrations
1163
987
  "#{quote_column_name(column_name)} AS #{quote_column_name(new_column_name)}"
1164
988
  end.join(", ")
1165
989
 
1166
- execute(<<-SQL.squish)
990
+ execute(<<~SQL.squish)
1167
991
  CREATE VIEW #{quote_table_name(table_name)} AS
1168
992
  SELECT *, #{column_mapping}
1169
993
  FROM #{quote_table_name(tmp_table)}
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "openssl"
4
+
3
5
  module OnlineMigrations
4
6
  # @private
5
7
  module Utils
@@ -25,51 +27,15 @@ module OnlineMigrations
25
27
  Kernel.warn("[online_migrations] #{message}")
26
28
  end
27
29
 
28
- def supports_multiple_dbs?
29
- # Technically, Active Record 6.0+ supports multiple databases,
30
- # but we can not get the database spec name for this version.
31
- ar_version >= 6.1
32
- end
33
-
34
- def migration_parent
35
- if ar_version <= 4.2
36
- ActiveRecord::Migration
37
- else
38
- ActiveRecord::Migration[ar_version]
39
- end
40
- end
41
-
42
- def migration_parent_string
43
- if ar_version <= 4.2
44
- "ActiveRecord::Migration"
45
- else
46
- "ActiveRecord::Migration[#{ar_version}]"
47
- end
48
- end
49
-
50
- def model_parent_string
51
- if ar_version >= 5.0
52
- "ApplicationRecord"
53
- else
54
- "ActiveRecord::Base"
55
- end
56
- end
57
-
58
- def define_model(table_name, connection = ActiveRecord::Base.connection)
30
+ def define_model(table_name)
59
31
  Class.new(ActiveRecord::Base) do
60
32
  self.table_name = table_name
61
33
  self.inheritance_column = :_type_disabled
62
-
63
- @online_migrations_connection = connection
64
-
65
- def self.connection
66
- @online_migrations_connection
67
- end
68
34
  end
69
35
  end
70
36
 
71
37
  def to_bool(value)
72
- !value.to_s.match(/^(true|t|yes|y|1|on)$/i).nil?
38
+ value.to_s.match?(/^true|t|yes|y|1|on$/i)
73
39
  end
74
40
 
75
41
  def foreign_table_name(ref_name, options)
@@ -78,6 +44,23 @@ module OnlineMigrations
78
44
  end
79
45
  end
80
46
 
47
+ # Implementation is from ActiveRecord.
48
+ # This is not needed for ActiveRecord >= 7.1 (https://github.com/rails/rails/pull/47753).
49
+ def index_name(table_name, column_name)
50
+ max_index_name_size = 62
51
+ name = "index_#{table_name}_on_#{Array(column_name) * '_and_'}"
52
+ return name if name.bytesize <= max_index_name_size
53
+
54
+ # Fallback to short version, add hash to ensure uniqueness
55
+ hashed_identifier = "_#{OpenSSL::Digest::SHA256.hexdigest(name).first(10)}"
56
+ name = "idx_on_#{Array(column_name) * '_'}"
57
+
58
+ short_limit = max_index_name_size - hashed_identifier.bytesize
59
+ short_name = name[0, short_limit]
60
+
61
+ "#{short_name}#{hashed_identifier}"
62
+ end
63
+
81
64
  def ar_partial_writes?
82
65
  ActiveRecord::Base.public_send(ar_partial_writes_setting)
83
66
  end
@@ -103,7 +86,7 @@ module OnlineMigrations
103
86
  def estimated_count(connection, table_name)
104
87
  quoted_table = connection.quote(table_name)
105
88
 
106
- count = connection.select_value(<<-SQL.strip_heredoc)
89
+ count = connection.select_value(<<~SQL)
107
90
  SELECT
108
91
  (reltuples / COALESCE(NULLIF(relpages, 0), 1)) *
109
92
  (pg_relation_size(#{quoted_table}) / (current_setting('block_size')::integer))
@@ -116,25 +99,11 @@ module OnlineMigrations
116
99
  count = count.to_i
117
100
  # If the table has never yet been vacuumed or analyzed, reltuples contains -1
118
101
  # indicating that the row count is unknown.
119
- count = 0 if count == -1
102
+ count = 0 if count < 0
120
103
  count
121
104
  end
122
105
  end
123
106
 
124
- def ar_where_not_multiple_conditions(relation, conditions)
125
- if Utils.ar_version >= 6.1
126
- relation.where.not(conditions)
127
- else
128
- # In Active Record < 6.1, NOT with multiple conditions behaves as NOR,
129
- # which should really behave as NAND.
130
- # https://www.bigbinary.com/blog/rails-6-deprecates-where-not-working-as-nor-and-will-change-to-nand-in-rails-6-1
131
- arel_table = relation.arel_table
132
- conditions = conditions.map { |column, value| arel_table[column].not_eq(value) }
133
- conditions = conditions.inject(:or)
134
- relation.where(conditions)
135
- end
136
- end
137
-
138
107
  FUNCTION_CALL_RE = /(\w+)\s*\(/
139
108
  private_constant :FUNCTION_CALL_RE
140
109
 
@@ -148,7 +117,7 @@ module OnlineMigrations
148
117
  end
149
118
 
150
119
  def volatile_function?(connection, function_name)
151
- query = <<-SQL.strip_heredoc
120
+ query = <<~SQL
152
121
  SELECT provolatile
153
122
  FROM pg_catalog.pg_proc
154
123
  WHERE proname = #{connection.quote(function_name)}
@@ -156,6 +125,18 @@ module OnlineMigrations
156
125
 
157
126
  connection.select_value(query) == "v"
158
127
  end
128
+
129
+ def shard_names(model)
130
+ model.ancestors.each do |ancestor|
131
+ # There is no official method to get shard names from the model.
132
+ # This is the way that currently is used in ActiveRecord tests themselves.
133
+ pool_manager = ActiveRecord::Base.connection_handler.send(:get_pool_manager, ancestor.name)
134
+
135
+ # .uniq call is not needed for Active Record 7.1+
136
+ # See https://github.com/rails/rails/pull/49284.
137
+ return pool_manager.shard_names.uniq if pool_manager
138
+ end
139
+ end
159
140
  end
160
141
  end
161
142
  end