Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-07-20 12:55:51 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-07-20 12:55:51 +0300
commite8d2c2579383897a1dd7f9debd359abe8ae8373d (patch)
treec42be41678c2586d49a75cabce89322082698334 /spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb
parentfc845b37ec3a90aaa719975f607740c22ba6a113 (diff)
Add latest changes from gitlab-org/gitlab@14-1-stable-eev14.1.0-rc42
Diffstat (limited to 'spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb')
-rw-r--r--spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb250
1 files changed, 100 insertions, 150 deletions
diff --git a/spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb b/spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb
index 83f2436043c..a524fe681e9 100644
--- a/spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb
+++ b/spec/lib/gitlab/database/partitioning_migration_helpers/foreign_key_helpers_spec.rb
@@ -3,192 +3,142 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::ForeignKeyHelpers do
- include Database::TriggerHelpers
+ include Database::TableSchemaHelpers
- let(:model) do
- ActiveRecord::Migration.new.extend(described_class)
+ let(:migration) do
+ ActiveRecord::Migration.new.extend(Gitlab::Database::PartitioningMigrationHelpers)
end
- let_it_be(:connection) { ActiveRecord::Base.connection }
-
- let(:referenced_table) { :issues }
- let(:function_name) { '_test_partitioned_foreign_keys_function' }
- let(:trigger_name) { '_test_partitioned_foreign_keys_trigger' }
+ let(:source_table_name) { '_test_partitioned_table' }
+ let(:target_table_name) { '_test_referenced_table' }
+ let(:column_name) { "#{target_table_name}_id" }
+ let(:foreign_key_name) { '_test_partitioned_fk' }
+ let(:partition_schema) { 'gitlab_partitions_dynamic' }
+ let(:partition1_name) { "#{partition_schema}.#{source_table_name}_202001" }
+ let(:partition2_name) { "#{partition_schema}.#{source_table_name}_202002" }
+ let(:options) do
+ {
+ column: column_name,
+ name: foreign_key_name,
+ on_delete: :cascade,
+ validate: true
+ }
+ end
before do
- allow(model).to receive(:puts)
- allow(model).to receive(:fk_function_name).and_return(function_name)
- allow(model).to receive(:fk_trigger_name).and_return(trigger_name)
+ allow(migration).to receive(:puts)
+
+ connection.execute(<<~SQL)
+ CREATE TABLE #{target_table_name} (
+ id serial NOT NULL,
+ PRIMARY KEY (id)
+ );
+
+ CREATE TABLE #{source_table_name} (
+ id serial NOT NULL,
+ #{column_name} int NOT NULL,
+ created_at timestamptz NOT NULL,
+ PRIMARY KEY (id, created_at)
+ ) PARTITION BY RANGE (created_at);
+
+ CREATE TABLE #{partition1_name} PARTITION OF #{source_table_name}
+ FOR VALUES FROM ('2020-01-01') TO ('2020-02-01');
+
+ CREATE TABLE #{partition2_name} PARTITION OF #{source_table_name}
+ FOR VALUES FROM ('2020-02-01') TO ('2020-03-01');
+ SQL
end
- describe 'adding a foreign key' do
+ describe '#add_concurrent_partitioned_foreign_key' do
before do
- allow(model).to receive(:transaction_open?).and_return(false)
- end
-
- context 'when the table has no foreign keys' do
- it 'creates a trigger function to handle the single cascade' do
- model.add_partitioned_foreign_key :issue_assignees, referenced_table
-
- expect_function_to_contain(function_name, 'delete from issue_assignees where issue_id = old.id')
- expect_valid_function_trigger(referenced_table, trigger_name, function_name, after: 'delete')
- end
- end
-
- context 'when the table already has foreign keys' do
- context 'when the foreign key is from a different table' do
- before do
- model.add_partitioned_foreign_key :issue_assignees, referenced_table
- end
-
- it 'creates a trigger function to handle the multiple cascades' do
- model.add_partitioned_foreign_key :epic_issues, referenced_table
-
- expect_function_to_contain(function_name,
- 'delete from issue_assignees where issue_id = old.id',
- 'delete from epic_issues where issue_id = old.id')
- expect_valid_function_trigger(referenced_table, trigger_name, function_name, after: 'delete')
- end
- end
-
- context 'when the foreign key is from the same table' do
- before do
- model.add_partitioned_foreign_key :issues, referenced_table, column: :moved_to_id
- end
-
- context 'when the foreign key is from a different column' do
- it 'creates a trigger function to handle the multiple cascades' do
- model.add_partitioned_foreign_key :issues, referenced_table, column: :duplicated_to_id
-
- expect_function_to_contain(function_name,
- 'delete from issues where moved_to_id = old.id',
- 'delete from issues where duplicated_to_id = old.id')
- expect_valid_function_trigger(referenced_table, trigger_name, function_name, after: 'delete')
- end
- end
-
- context 'when the foreign key is from the same column' do
- it 'ignores the duplicate and properly recreates the trigger function' do
- model.add_partitioned_foreign_key :issues, referenced_table, column: :moved_to_id
-
- expect_function_to_contain(function_name, 'delete from issues where moved_to_id = old.id')
- expect_valid_function_trigger(referenced_table, trigger_name, function_name, after: 'delete')
- end
- end
- end
- end
+ allow(migration).to receive(:foreign_key_exists?)
+ .with(source_table_name, target_table_name, anything)
+ .and_return(false)
- context 'when the foreign key is set to nullify' do
- it 'creates a trigger function that nullifies the foreign key' do
- model.add_partitioned_foreign_key :issue_assignees, referenced_table, on_delete: :nullify
-
- expect_function_to_contain(function_name, 'update issue_assignees set issue_id = null where issue_id = old.id')
- expect_valid_function_trigger(referenced_table, trigger_name, function_name, after: 'delete')
- end
+ allow(migration).to receive(:with_lock_retries).and_yield
end
- context 'when the referencing column is a custom value' do
- it 'creates a trigger function with the correct column name' do
- model.add_partitioned_foreign_key :issues, referenced_table, column: :duplicated_to_id
+ context 'when the foreign key does not exist on the parent table' do
+ it 'creates the foreign key on each partition, and the parent table' do
+ expect(migration).to receive(:foreign_key_exists?)
+ .with(source_table_name, target_table_name, **options)
+ .and_return(false)
- expect_function_to_contain(function_name, 'delete from issues where duplicated_to_id = old.id')
- expect_valid_function_trigger(referenced_table, trigger_name, function_name, after: 'delete')
- end
- end
+ expect(migration).to receive(:concurrent_partitioned_foreign_key_name).and_return(foreign_key_name)
- context 'when the referenced column is a custom value' do
- let(:referenced_table) { :user_details }
+ expect_add_concurrent_fk_and_call_original(partition1_name, target_table_name, **options)
+ expect_add_concurrent_fk_and_call_original(partition2_name, target_table_name, **options)
- it 'creates a trigger function with the correct column name' do
- model.add_partitioned_foreign_key :user_preferences, referenced_table, column: :user_id, primary_key: :user_id
+ expect(migration).to receive(:with_lock_retries).ordered.and_yield
+ expect(migration).to receive(:add_foreign_key)
+ .with(source_table_name, target_table_name, **options)
+ .ordered
+ .and_call_original
- expect_function_to_contain(function_name, 'delete from user_preferences where user_id = old.user_id')
- expect_valid_function_trigger(referenced_table, trigger_name, function_name, after: 'delete')
- end
- end
+ migration.add_concurrent_partitioned_foreign_key(source_table_name, target_table_name, column: column_name)
- context 'when the given key definition is invalid' do
- it 'raises an error with the appropriate message' do
- expect do
- model.add_partitioned_foreign_key :issue_assignees, referenced_table, column: :not_a_real_issue_id
- end.to raise_error(/From column must be a valid column/)
+ expect_foreign_key_to_exist(source_table_name, foreign_key_name)
end
- end
-
- context 'when run inside a transaction' do
- it 'raises an error' do
- expect(model).to receive(:transaction_open?).and_return(true)
- expect do
- model.add_partitioned_foreign_key :issue_assignees, referenced_table
- end.to raise_error(/can not be run inside a transaction/)
+ def expect_add_concurrent_fk_and_call_original(source_table_name, target_table_name, options)
+ expect(migration).to receive(:add_concurrent_foreign_key)
+ .ordered
+ .with(source_table_name, target_table_name, options)
+ .and_wrap_original do |_, source_table_name, target_table_name, options|
+ connection.add_foreign_key(source_table_name, target_table_name, **options)
+ end
end
end
- end
- context 'removing a foreign key' do
- before do
- allow(model).to receive(:transaction_open?).and_return(false)
- end
+ context 'when the foreign key exists on the parent table' do
+ it 'does not attempt to create any foreign keys' do
+ expect(migration).to receive(:concurrent_partitioned_foreign_key_name).and_return(foreign_key_name)
- context 'when the table has multiple foreign keys' do
- before do
- model.add_partitioned_foreign_key :issue_assignees, referenced_table
- model.add_partitioned_foreign_key :epic_issues, referenced_table
- end
+ expect(migration).to receive(:foreign_key_exists?)
+ .with(source_table_name, target_table_name, **options)
+ .and_return(true)
- it 'creates a trigger function without the removed cascade' do
- expect_function_to_contain(function_name,
- 'delete from issue_assignees where issue_id = old.id',
- 'delete from epic_issues where issue_id = old.id')
- expect_valid_function_trigger(referenced_table, trigger_name, function_name, after: 'delete')
+ expect(migration).not_to receive(:add_concurrent_foreign_key)
+ expect(migration).not_to receive(:with_lock_retries)
+ expect(migration).not_to receive(:add_foreign_key)
- model.remove_partitioned_foreign_key :issue_assignees, referenced_table
+ migration.add_concurrent_partitioned_foreign_key(source_table_name, target_table_name, column: column_name)
- expect_function_to_contain(function_name, 'delete from epic_issues where issue_id = old.id')
- expect_valid_function_trigger(referenced_table, trigger_name, function_name, after: 'delete')
+ expect_foreign_key_not_to_exist(source_table_name, foreign_key_name)
end
end
- context 'when the table has only one remaining foreign key' do
- before do
- model.add_partitioned_foreign_key :issue_assignees, referenced_table
+ context 'when additional foreign key options are given' do
+ let(:options) do
+ {
+ column: column_name,
+ name: '_my_fk_name',
+ on_delete: :restrict,
+ validate: true
+ }
end
- it 'removes the trigger function altogether' do
- expect_function_to_contain(function_name, 'delete from issue_assignees where issue_id = old.id')
- expect_valid_function_trigger(referenced_table, trigger_name, function_name, after: 'delete')
-
- model.remove_partitioned_foreign_key :issue_assignees, referenced_table
-
- expect_function_not_to_exist(function_name)
- expect_trigger_not_to_exist(referenced_table, trigger_name)
- end
- end
+ it 'forwards them to the foreign key helper methods' do
+ expect(migration).to receive(:foreign_key_exists?)
+ .with(source_table_name, target_table_name, **options)
+ .and_return(false)
- context 'when the foreign key does not exist' do
- before do
- model.add_partitioned_foreign_key :issue_assignees, referenced_table
- end
+ expect(migration).not_to receive(:concurrent_partitioned_foreign_key_name)
- it 'ignores the invalid key and properly recreates the trigger function' do
- expect_function_to_contain(function_name, 'delete from issue_assignees where issue_id = old.id')
- expect_valid_function_trigger(referenced_table, trigger_name, function_name, after: 'delete')
+ expect_add_concurrent_fk(partition1_name, target_table_name, **options)
+ expect_add_concurrent_fk(partition2_name, target_table_name, **options)
- model.remove_partitioned_foreign_key :issues, referenced_table, column: :moved_to_id
+ expect(migration).to receive(:with_lock_retries).ordered.and_yield
+ expect(migration).to receive(:add_foreign_key).with(source_table_name, target_table_name, **options).ordered
- expect_function_to_contain(function_name, 'delete from issue_assignees where issue_id = old.id')
- expect_valid_function_trigger(referenced_table, trigger_name, function_name, after: 'delete')
+ migration.add_concurrent_partitioned_foreign_key(source_table_name, target_table_name,
+ column: column_name, name: '_my_fk_name', on_delete: :restrict)
end
- end
-
- context 'when run outside a transaction' do
- it 'raises an error' do
- expect(model).to receive(:transaction_open?).and_return(true)
- expect do
- model.remove_partitioned_foreign_key :issue_assignees, referenced_table
- end.to raise_error(/can not be run inside a transaction/)
+ def expect_add_concurrent_fk(source_table_name, target_table_name, options)
+ expect(migration).to receive(:add_concurrent_foreign_key)
+ .ordered
+ .with(source_table_name, target_table_name, options)
end
end
end