online_migrations 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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)}
|