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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +33 -0
- data/README.md +34 -31
- data/docs/background_migrations.md +36 -4
- data/docs/configuring.md +3 -2
- 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 +5 -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 +6 -20
- 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 +68 -83
- data/lib/online_migrations/command_checker.rb +11 -29
- data/lib/online_migrations/config.rb +7 -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 +1 -1
- data/lib/online_migrations/schema_statements.rb +64 -256
- data/lib/online_migrations/utils.rb +18 -56
- data/lib/online_migrations/verbose_sql_logs.rb +3 -2
- data/lib/online_migrations/version.rb +1 -1
- data/lib/online_migrations.rb +7 -6
- metadata +8 -7
- data/lib/online_migrations/background_migrations/advisory_lock.rb +0 -62
- 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
|
-
|
100
|
-
|
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
|
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.
|
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)
|
@@ -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)
|
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
|
-
|
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
|
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,
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
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,
|
861
|
-
|
862
|
-
|
863
|
-
|
864
|
-
|
865
|
-
|
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
|
-
|
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
|
-
|
809
|
+
check_constraint = check_constraint_for!(table_name, **options)
|
884
810
|
|
885
811
|
# Skip costly operation if already validated.
|
886
|
-
return if
|
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
|
-
|
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
|
913
|
-
|
914
|
-
|
915
|
-
|
916
|
-
|
917
|
-
|
918
|
-
|
919
|
-
|
920
|
-
|
921
|
-
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
-
|
1096
|
-
|
1097
|
-
|
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
|
-
|
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
|
1114
|
-
|
1115
|
-
|
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
|
-
|
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(
|
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)}
|