Commit 398ea5bb authored by manojmj's avatar manojmj

Add explicit options

parent 3e1e8b31
...@@ -155,6 +155,7 @@ module Gitlab ...@@ -155,6 +155,7 @@ module Gitlab
# column - The name of the column to create the foreign key on. # column - The name of the column to create the foreign key on.
# on_delete - The action to perform when associated data is removed, # on_delete - The action to perform when associated data is removed,
# defaults to "CASCADE". # defaults to "CASCADE".
# name - The name of the foreign key.
# #
# rubocop:disable Gitlab/RailsLogger # rubocop:disable Gitlab/RailsLogger
def add_concurrent_foreign_key(source, target, column:, on_delete: :cascade, name: nil) def add_concurrent_foreign_key(source, target, column:, on_delete: :cascade, name: nil)
...@@ -164,21 +165,17 @@ module Gitlab ...@@ -164,21 +165,17 @@ module Gitlab
raise 'add_concurrent_foreign_key can not be run inside a transaction' raise 'add_concurrent_foreign_key can not be run inside a transaction'
end end
options = {} options = {
options[:on_delete] = on_delete column: column,
on_delete: on_delete,
if name name: name.presence || concurrent_foreign_key_name(source, column)
key_name = name }
options[:name] = name
else
key_name = concurrent_foreign_key_name(source, column)
options[:column] = column
end
if foreign_key_exists?(source, target, options) if foreign_key_exists?(source, target, options)
warning_message = "Foreign key not created because it exists already " \ warning_message = "Foreign key not created because it exists already " \
"(this may be due to an aborted migration or similar): " \ "(this may be due to an aborted migration or similar): " \
"source: #{source}, target: #{target}, column: #{column}, name: #{name}, on_delete: #{on_delete}" "source: #{source}, target: #{target}, column: #{options[:column]}, "\
"name: #{options[:name]}, on_delete: #{options[:on_delete]}"
Rails.logger.warn warning_message Rails.logger.warn warning_message
else else
...@@ -187,14 +184,12 @@ module Gitlab ...@@ -187,14 +184,12 @@ module Gitlab
# short period of time. The key _is_ enforced for any newly created # short period of time. The key _is_ enforced for any newly created
# data. # data.
on_delete = 'SET NULL' if on_delete == :nullify
execute <<-EOF.strip_heredoc execute <<-EOF.strip_heredoc
ALTER TABLE #{source} ALTER TABLE #{source}
ADD CONSTRAINT #{key_name} ADD CONSTRAINT #{options[:name]}
FOREIGN KEY (#{column}) FOREIGN KEY (#{options[:column]})
REFERENCES #{target} (id) REFERENCES #{target} (id)
#{on_delete ? "ON DELETE #{on_delete.upcase}" : ''} #{on_delete_statement(options[:on_delete])}
NOT VALID; NOT VALID;
EOF EOF
end end
...@@ -205,15 +200,15 @@ module Gitlab ...@@ -205,15 +200,15 @@ module Gitlab
# #
# Note this is a no-op in case the constraint is VALID already # Note this is a no-op in case the constraint is VALID already
disable_statement_timeout do disable_statement_timeout do
execute("ALTER TABLE #{source} VALIDATE CONSTRAINT #{key_name};") execute("ALTER TABLE #{source} VALIDATE CONSTRAINT #{options[:name]};")
end end
end end
# rubocop:enable Gitlab/RailsLogger # rubocop:enable Gitlab/RailsLogger
def foreign_key_exists?(source, target = nil, **options) def foreign_key_exists?(source, target = nil, **options)
foreign_keys(source).any? do |foreign_key| foreign_keys(source).any? do |foreign_key|
(target.nil? || foreign_key.to_table.to_s == target.to_s) && tables_match?(target.to_s, foreign_key.to_table.to_s) &&
options.all? { |k, v| foreign_key.options[k].to_s == v.to_s } options_match?(foreign_key.options, options)
end end
end end
...@@ -1059,6 +1054,21 @@ into similar problems in the future (e.g. when new tables are created). ...@@ -1059,6 +1054,21 @@ into similar problems in the future (e.g. when new tables are created).
private private
def tables_match?(target_table, foreign_key_table)
target_table.blank? || foreign_key_table == target_table
end
def options_match?(foreign_key_options, options)
options.all? { |k, v| foreign_key_options[k].to_s == v.to_s }
end
def on_delete_statement(on_delete)
return '' if on_delete.blank?
return 'ON DELETE SET NULL' if on_delete == :nullify
"ON DELETE #{on_delete.upcase}"
end
def create_column_from(table, old, new, type: nil) def create_column_from(table, old, new, type: nil)
old_col = column_for(table, old) old_col = column_for(table, old)
new_type = type || old_col.type new_type = type || old_col.type
......
...@@ -212,31 +212,71 @@ describe Gitlab::Database::MigrationHelpers do ...@@ -212,31 +212,71 @@ describe Gitlab::Database::MigrationHelpers do
allow(model).to receive(:transaction_open?).and_return(false) allow(model).to receive(:transaction_open?).and_return(false)
end end
context 'when no custom key name is supplied' do context 'ON DELETE statements' do
it 'creates a concurrent foreign key and validates it' do context 'on_delete: :nullify' do
it 'appends ON DELETE SET NULL statement' do
expect(model).to receive(:disable_statement_timeout).and_call_original expect(model).to receive(:disable_statement_timeout).and_call_original
expect(model).to receive(:execute).with(/statement_timeout/) expect(model).to receive(:execute).with(/statement_timeout/)
expect(model).to receive(:execute).ordered.with(/NOT VALID/)
expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/) expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
expect(model).to receive(:execute).with(/RESET ALL/) expect(model).to receive(:execute).with(/RESET ALL/)
model.add_concurrent_foreign_key(:projects, :users, column: :user_id) expect(model).to receive(:execute).with(/ON DELETE SET NULL/)
model.add_concurrent_foreign_key(:projects, :users,
column: :user_id,
on_delete: :nullify)
end
end end
it 'appends a valid ON DELETE statement' do context 'on_delete: :cascade' do
it 'appends ON DELETE CASCADE statement' do
expect(model).to receive(:disable_statement_timeout).and_call_original expect(model).to receive(:disable_statement_timeout).and_call_original
expect(model).to receive(:execute).with(/statement_timeout/) expect(model).to receive(:execute).with(/statement_timeout/)
expect(model).to receive(:execute).with(/ON DELETE SET NULL/)
expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/) expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
expect(model).to receive(:execute).with(/RESET ALL/) expect(model).to receive(:execute).with(/RESET ALL/)
expect(model).to receive(:execute).with(/ON DELETE CASCADE/)
model.add_concurrent_foreign_key(:projects, :users, model.add_concurrent_foreign_key(:projects, :users,
column: :user_id, column: :user_id,
on_delete: :nullify) on_delete: :cascade)
end
end
context 'on_delete: nil' do
it 'appends no ON DELETE statement' do
expect(model).to receive(:disable_statement_timeout).and_call_original
expect(model).to receive(:execute).with(/statement_timeout/)
expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
expect(model).to receive(:execute).with(/RESET ALL/)
expect(model).not_to receive(:execute).with(/ON DELETE/)
model.add_concurrent_foreign_key(:projects, :users,
column: :user_id,
on_delete: nil)
end
end
end
context 'when no custom key name is supplied' do
it 'creates a concurrent foreign key and validates it' do
expect(model).to receive(:disable_statement_timeout).and_call_original
expect(model).to receive(:execute).with(/statement_timeout/)
expect(model).to receive(:execute).ordered.with(/NOT VALID/)
expect(model).to receive(:execute).ordered.with(/VALIDATE CONSTRAINT/)
expect(model).to receive(:execute).with(/RESET ALL/)
model.add_concurrent_foreign_key(:projects, :users, column: :user_id)
end end
it 'does not create a foreign key if it exists already' do it 'does not create a foreign key if it exists already' do
expect(model).to receive(:foreign_key_exists?).with(:projects, :users, column: :user_id, on_delete: :cascade).and_return(true) name = model.concurrent_foreign_key_name(:projects, :user_id)
expect(model).to receive(:foreign_key_exists?).with(:projects, :users,
column: :user_id,
on_delete: :cascade,
name: name).and_return(true)
expect(model).not_to receive(:execute).with(/ADD CONSTRAINT/) expect(model).not_to receive(:execute).with(/ADD CONSTRAINT/)
expect(model).to receive(:execute).with(/VALIDATE CONSTRAINT/) expect(model).to receive(:execute).with(/VALIDATE CONSTRAINT/)
...@@ -260,7 +300,10 @@ describe Gitlab::Database::MigrationHelpers do ...@@ -260,7 +300,10 @@ describe Gitlab::Database::MigrationHelpers do
context 'for creating a duplicate foreign key for a column that presently exists' do context 'for creating a duplicate foreign key for a column that presently exists' do
context 'when the supplied key name is the same as the existing foreign key name' do context 'when the supplied key name is the same as the existing foreign key name' do
it 'does not create a new foreign key' do it 'does not create a new foreign key' do
expect(model).to receive(:foreign_key_exists?).with(:projects, :users, name: :foo, on_delete: :cascade).and_return(true) expect(model).to receive(:foreign_key_exists?).with(:projects, :users,
name: :foo,
on_delete: :cascade,
column: :user_id).and_return(true)
expect(model).not_to receive(:execute).with(/ADD CONSTRAINT/) expect(model).not_to receive(:execute).with(/ADD CONSTRAINT/)
expect(model).to receive(:execute).with(/VALIDATE CONSTRAINT/) expect(model).to receive(:execute).with(/VALIDATE CONSTRAINT/)
...@@ -301,43 +344,57 @@ describe Gitlab::Database::MigrationHelpers do ...@@ -301,43 +344,57 @@ describe Gitlab::Database::MigrationHelpers do
allow(model).to receive(:foreign_keys).with(:projects).and_return([key]) allow(model).to receive(:foreign_keys).with(:projects).and_return([key])
end end
shared_examples_for 'foreign key checks' do
it 'finds existing foreign keys by column' do it 'finds existing foreign keys by column' do
expect(model.foreign_key_exists?(:projects, :users, column: :non_standard_id)).to be_truthy expect(model.foreign_key_exists?(:projects, target_table, column: :non_standard_id)).to be_truthy
end end
it 'finds existing foreign keys by name' do it 'finds existing foreign keys by name' do
expect(model.foreign_key_exists?(:projects, :users, name: :fk_projects_users_non_standard_id)).to be_truthy expect(model.foreign_key_exists?(:projects, target_table, name: :fk_projects_users_non_standard_id)).to be_truthy
end end
it 'finds existing foreign_keys by name and column' do it 'finds existing foreign_keys by name and column' do
expect(model.foreign_key_exists?(:projects, :users, name: :fk_projects_users_non_standard_id, column: :non_standard_id)).to be_truthy expect(model.foreign_key_exists?(:projects, target_table, name: :fk_projects_users_non_standard_id, column: :non_standard_id)).to be_truthy
end end
it 'finds existing foreign_keys by name, column and on_delete' do it 'finds existing foreign_keys by name, column and on_delete' do
expect(model.foreign_key_exists?(:projects, :users, name: :fk_projects_users_non_standard_id, column: :non_standard_id, on_delete: :cascade)).to be_truthy expect(model.foreign_key_exists?(:projects, target_table, name: :fk_projects_users_non_standard_id, column: :non_standard_id, on_delete: :cascade)).to be_truthy
end end
it 'finds existing foreign keys by target table only' do it 'finds existing foreign keys by target table only' do
expect(model.foreign_key_exists?(:projects, :users)).to be_truthy expect(model.foreign_key_exists?(:projects, target_table)).to be_truthy
end end
it 'compares by column name if given' do it 'compares by column name if given' do
expect(model.foreign_key_exists?(:projects, :users, column: :user_id)).to be_falsey expect(model.foreign_key_exists?(:projects, target_table, column: :user_id)).to be_falsey
end end
it 'compares by foreign key name if given' do it 'compares by foreign key name if given' do
expect(model.foreign_key_exists?(:projects, :users, name: :non_existent_foreign_key_name)).to be_falsey expect(model.foreign_key_exists?(:projects, target_table, name: :non_existent_foreign_key_name)).to be_falsey
end end
it 'compares by foreign key name and column if given' do it 'compares by foreign key name and column if given' do
expect(model.foreign_key_exists?(:projects, :users, name: :non_existent_foreign_key_name, column: :non_standard_id)).to be_falsey expect(model.foreign_key_exists?(:projects, target_table, name: :non_existent_foreign_key_name, column: :non_standard_id)).to be_falsey
end end
it 'compares by foreign key name, column and on_delete if given' do it 'compares by foreign key name, column and on_delete if given' do
expect(model.foreign_key_exists?(:projects, :users, name: :fk_projects_users_non_standard_id, column: :non_standard_id, on_delete: :nullify)).to be_falsey expect(model.foreign_key_exists?(:projects, target_table, name: :fk_projects_users_non_standard_id, column: :non_standard_id, on_delete: :nullify)).to be_falsey
end
end
context 'without specifying a target table' do
let(:target_table) { nil }
it_behaves_like 'foreign key checks'
end
context 'specifying a target table' do
let(:target_table) { :users }
it_behaves_like 'foreign key checks'
end end
it 'compares by target if no column given' do it 'compares by target table if no column given' do
expect(model.foreign_key_exists?(:projects, :other_table)).to be_falsey expect(model.foreign_key_exists?(:projects, :other_table)).to be_falsey
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment