Module: Gitlab::Database::Migrations::ForeignKeyHelpers
- Includes:
- LockRetriesHelpers, TimeoutHelpers, PartitionHelpers
- Included in:
- Gitlab::Database::MigrationHelpers
- Defined in:
- lib/gitlab/database/migrations/foreign_key_helpers.rb
Instance Method Summary collapse
-
#add_concurrent_foreign_key(source, target, column:, **options) ⇒ Object
Adds a foreign key with only minimal locking on the tables involved.
-
#concurrent_foreign_key_name(table, column, prefix: 'fk_') ⇒ Object
Returns the name for a concurrent foreign key.
- #foreign_key_exists?(source, target = nil, **options) ⇒ Boolean
- #remove_foreign_key_if_exists(source, target = nil, **kwargs) ⇒ Object
- #remove_foreign_key_without_error(*args, **kwargs) ⇒ Object
- #validate_foreign_key(source, column, name: nil) ⇒ Object
Methods included from PartitionHelpers
#partition?, #table_partitioned?
Methods included from LockRetriesHelpers
Methods included from TimeoutHelpers
Instance Method Details
#add_concurrent_foreign_key(source, target, column:, **options) ⇒ Object
Adds a foreign key with only minimal locking on the tables involved.
This method only requires minimal locking
source - The source table containing the foreign key. target - The target table the key points to. column - The name of the column to create the foreign key on. target_column - The name of the referenced column, defaults to “id”. on_delete - The action to perform when associated data is removed,
defaults to "CASCADE".
on_update - The action to perform when associated data is updated,
defaults to nil. This is useful for multi column FKs if
it's desirable to update one of the columns.
name - The name of the foreign key. validate - Flag that controls whether the new foreign key will be validated after creation.
If the flag is not set, the constraint will only be enforced for new data.
reverse_lock_order - Flag that controls whether we should attempt to acquire locks in the reverse
order of the ALTER TABLE. This can be useful in situations where the foreign
key creation could deadlock with another process.
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/gitlab/database/migrations/foreign_key_helpers.rb', line 31 def add_concurrent_foreign_key(source, target, column:, **) .reverse_merge!({ on_delete: :cascade, on_update: nil, target_column: :id, validate: true, reverse_lock_order: false, allow_partitioned: false, column: column }) # Transactions would result in ALTER TABLE locks being held for the # duration of the transaction, defeating the purpose of this method. raise 'add_concurrent_foreign_key can not be run inside a transaction' if transaction_open? if !.delete(:allow_partitioned) && table_partitioned?(source) raise ArgumentError, 'add_concurrent_foreign_key can not be used on a partitioned ' \ 'table. Please use add_concurrent_partitioned_foreign_key on the partitioned table ' \ 'as we need to create foreign keys on each partition and a FK on the parent table' end [:name] ||= concurrent_foreign_key_name(source, column) [:primary_key] = [:target_column] = .slice(:column, :on_delete, :on_update, :name, :primary_key) if foreign_key_exists?(source, target, **) = "Foreign key not created because it exists already " \ "(this may be due to an aborted migration or similar): " \ "source: #{source}, target: #{target}, column: #{options[:column]}, " \ "name: #{options[:name]}, on_update: #{options[:on_update]}, " \ "on_delete: #{options[:on_delete]}" Gitlab::AppLogger.warn else execute_add_concurrent_foreign_key(source, target, ) end # Validate the existing constraint. This can potentially take a very # long time to complete, but fortunately does not lock the source table # while running. # Disable this check by passing `validate: false` to the method call # The check will be enforced for new data (inserts) coming in, # but validating existing data is delayed. # # Note this is a no-op in case the constraint is VALID already return unless [:validate] begin disable_statement_timeout do execute("ALTER TABLE #{source} VALIDATE CONSTRAINT #{options[:name]};") end rescue PG::ForeignKeyViolation => e with_lock_retries do execute("ALTER TABLE #{source} DROP CONSTRAINT #{options[:name]};") end raise "Migration failed intentionally due to ForeignKeyViolation: #{e.message}" end end |
#concurrent_foreign_key_name(table, column, prefix: 'fk_') ⇒ Object
Returns the name for a concurrent foreign key.
PostgreSQL constraint names have a limit of 63 bytes. The logic used here is based on Rails’ foreign_key_name() method, which unfortunately is private so we can’t rely on it directly.
prefix:
-
The default prefix is
fk_for backward compatibility with the existing
concurrent foreign key helpers.
-
For standard rails foreign keys the prefix is
fk_rails_
145 146 147 148 149 150 |
# File 'lib/gitlab/database/migrations/foreign_key_helpers.rb', line 145 def concurrent_foreign_key_name(table, column, prefix: 'fk_') identifier = "#{table}_#{multiple_columns(column, separator: '_')}_fk" hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10) "#{prefix}#{hashed_identifier}" end |
#foreign_key_exists?(source, target = nil, **options) ⇒ Boolean
103 104 105 106 107 108 109 110 111 112 |
# File 'lib/gitlab/database/migrations/foreign_key_helpers.rb', line 103 def foreign_key_exists?(source, target = nil, **) fks = Gitlab::Database::PostgresForeignKey.by_constrained_table_name_or_identifier(source) fks = fks.by_referenced_table_name(target) if target fks = fks.by_name([:name]) if [:name] fks = fks.by_constrained_columns([:column]) if [:column] fks = fks.by_referenced_columns([:primary_key]) if [:primary_key] fks = fks.by_on_delete_action([:on_delete]) if [:on_delete] fks.exists? end |
#remove_foreign_key_if_exists(source, target = nil, **kwargs) ⇒ Object
114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/gitlab/database/migrations/foreign_key_helpers.rb', line 114 def remove_foreign_key_if_exists(source, target = nil, **kwargs) reverse_lock_order = kwargs.delete(:reverse_lock_order) return unless foreign_key_exists?(source, target, **kwargs) if target && reverse_lock_order && transaction_open? execute("LOCK TABLE #{target}, #{source} IN ACCESS EXCLUSIVE MODE") end if target remove_foreign_key(source, target, **kwargs) else remove_foreign_key(source, **kwargs) end end |
#remove_foreign_key_without_error(*args, **kwargs) ⇒ Object
129 130 131 132 |
# File 'lib/gitlab/database/migrations/foreign_key_helpers.rb', line 129 def remove_foreign_key_without_error(*args, **kwargs) remove_foreign_key(*args, **kwargs) rescue ArgumentError end |
#validate_foreign_key(source, column, name: nil) ⇒ Object
91 92 93 94 95 96 97 98 99 100 101 |
# File 'lib/gitlab/database/migrations/foreign_key_helpers.rb', line 91 def validate_foreign_key(source, column, name: nil) fk_name = name || concurrent_foreign_key_name(source, column) unless foreign_key_exists?(source, name: fk_name) raise (source, "foreign key", fk_name) end disable_statement_timeout do execute("ALTER TABLE #{source} VALIDATE CONSTRAINT #{fk_name};") end end |