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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -0
- data/README.md +155 -150
- data/docs/background_migrations.md +43 -10
- data/docs/configuring.md +23 -18
- data/lib/generators/online_migrations/install_generator.rb +3 -7
- data/lib/generators/online_migrations/templates/add_sharding_to_online_migrations.rb.tt +18 -0
- data/lib/generators/online_migrations/templates/initializer.rb.tt +12 -3
- data/lib/generators/online_migrations/templates/migration.rb.tt +8 -3
- data/lib/generators/online_migrations/upgrade_generator.rb +33 -0
- data/lib/online_migrations/background_migrations/application_record.rb +13 -0
- data/lib/online_migrations/background_migrations/backfill_column.rb +1 -1
- data/lib/online_migrations/background_migrations/copy_column.rb +11 -19
- data/lib/online_migrations/background_migrations/delete_orphaned_records.rb +2 -20
- data/lib/online_migrations/background_migrations/migration.rb +123 -34
- data/lib/online_migrations/background_migrations/migration_helpers.rb +0 -4
- data/lib/online_migrations/background_migrations/migration_job.rb +15 -12
- data/lib/online_migrations/background_migrations/migration_job_runner.rb +2 -2
- data/lib/online_migrations/background_migrations/migration_runner.rb +56 -11
- data/lib/online_migrations/background_migrations/reset_counters.rb +3 -9
- data/lib/online_migrations/background_migrations/scheduler.rb +5 -15
- data/lib/online_migrations/change_column_type_helpers.rb +71 -86
- data/lib/online_migrations/command_checker.rb +50 -46
- data/lib/online_migrations/config.rb +19 -15
- data/lib/online_migrations/copy_trigger.rb +15 -10
- data/lib/online_migrations/error_messages.rb +13 -25
- data/lib/online_migrations/foreign_keys_collector.rb +2 -2
- data/lib/online_migrations/indexes_collector.rb +3 -3
- data/lib/online_migrations/lock_retrier.rb +4 -9
- data/lib/online_migrations/schema_cache.rb +0 -6
- data/lib/online_migrations/schema_dumper.rb +21 -0
- data/lib/online_migrations/schema_statements.rb +80 -256
- data/lib/online_migrations/utils.rb +36 -55
- data/lib/online_migrations/verbose_sql_logs.rb +3 -2
- data/lib/online_migrations/version.rb +1 -1
- data/lib/online_migrations.rb +9 -6
- metadata +9 -7
- data/lib/online_migrations/background_migrations/advisory_lock.rb +0 -62
- 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?
|
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
|
84
|
+
model = Utils.define_model(table_name)
|
85
85
|
|
86
|
-
conditions = columns_and_values.
|
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
|
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
|
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
|
-
|
107
|
-
|
108
|
-
|
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
|
-
|
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 =
|
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] ||
|
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)
|
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 =
|
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
|
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 =
|
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
|
-
|
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
|
-
#
|
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,
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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,
|
845
|
-
|
846
|
-
|
847
|
-
|
848
|
-
|
849
|
-
|
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
|
-
|
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
|
-
|
809
|
+
check_constraint = check_constraint_for!(table_name, **options)
|
868
810
|
|
869
811
|
# Skip costly operation if already validated.
|
870
|
-
return if
|
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
|
-
|
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
|
897
|
-
|
898
|
-
|
899
|
-
|
900
|
-
|
901
|
-
|
902
|
-
|
903
|
-
|
904
|
-
|
905
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
1080
|
-
|
1081
|
-
|
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
|
-
|
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
|
1116
|
-
|
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
|
-
|
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(
|
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
|
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
|
-
|
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(
|
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
|
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 =
|
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
|