352 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Ruby
		
	
	
	
			
		
		
	
	
			352 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Ruby
		
	
	
	
# frozen_string_literal: true
 | 
						|
 | 
						|
require 'spec_helper'
 | 
						|
 | 
						|
RSpec.describe Gitlab::Database::AsyncConstraints::MigrationHelpers, feature_category: :database do
 | 
						|
  let(:migration) { Gitlab::Database::Migration[2.1].new }
 | 
						|
  let(:connection) { ApplicationRecord.connection }
 | 
						|
  let(:constraint_model) { Gitlab::Database::AsyncConstraints::PostgresAsyncConstraintValidation }
 | 
						|
  let(:table_name) { '_test_async_fks' }
 | 
						|
  let(:column_name) { 'parent_id' }
 | 
						|
  let(:fk_name) { nil }
 | 
						|
 | 
						|
  context 'with async FK validation on regular tables' do
 | 
						|
    before do
 | 
						|
      allow(migration).to receive(:puts)
 | 
						|
      allow(migration.connection).to receive(:transaction_open?).and_return(false)
 | 
						|
 | 
						|
      connection.create_table(table_name) do |t|
 | 
						|
        t.integer column_name
 | 
						|
      end
 | 
						|
 | 
						|
      migration.add_concurrent_foreign_key(
 | 
						|
        table_name, table_name,
 | 
						|
        column: column_name, validate: false, name: fk_name)
 | 
						|
    end
 | 
						|
 | 
						|
    describe '#prepare_async_foreign_key_validation' do
 | 
						|
      it 'creates the record for the async FK validation' do
 | 
						|
        expect do
 | 
						|
          migration.prepare_async_foreign_key_validation(table_name, column_name)
 | 
						|
        end.to change { constraint_model.where(table_name: table_name).count }.by(1)
 | 
						|
 | 
						|
        record = constraint_model.find_by(table_name: table_name)
 | 
						|
 | 
						|
        expect(record.name).to start_with('fk_')
 | 
						|
        expect(record).to be_foreign_key
 | 
						|
      end
 | 
						|
 | 
						|
      context 'when an explicit name is given' do
 | 
						|
        let(:fk_name) { 'my_fk_name' }
 | 
						|
 | 
						|
        it 'creates the record with the given name' do
 | 
						|
          expect do
 | 
						|
            migration.prepare_async_foreign_key_validation(table_name, name: fk_name)
 | 
						|
          end.to change { constraint_model.where(name: fk_name).count }.by(1)
 | 
						|
 | 
						|
          record = constraint_model.find_by(name: fk_name)
 | 
						|
 | 
						|
          expect(record.table_name).to eq(table_name)
 | 
						|
          expect(record).to be_foreign_key
 | 
						|
        end
 | 
						|
      end
 | 
						|
 | 
						|
      context 'when the FK does not exist' do
 | 
						|
        it 'returns an error' do
 | 
						|
          expect do
 | 
						|
            migration.prepare_async_foreign_key_validation(table_name, name: 'no_fk')
 | 
						|
          end.to raise_error RuntimeError, /Could not find foreign key "no_fk" on table "_test_async_fks"/
 | 
						|
        end
 | 
						|
      end
 | 
						|
 | 
						|
      context 'when the record already exists' do
 | 
						|
        let(:fk_name) { 'my_fk_name' }
 | 
						|
 | 
						|
        it 'does attempt to create the record' do
 | 
						|
          create(:postgres_async_constraint_validation, table_name: table_name, name: fk_name)
 | 
						|
 | 
						|
          expect do
 | 
						|
            migration.prepare_async_foreign_key_validation(table_name, name: fk_name)
 | 
						|
          end.not_to change { constraint_model.where(name: fk_name).count }
 | 
						|
        end
 | 
						|
      end
 | 
						|
 | 
						|
      context 'when the async FK validation table does not exist' do
 | 
						|
        it 'does not raise an error' do
 | 
						|
          connection.drop_table(constraint_model.table_name)
 | 
						|
 | 
						|
          expect(constraint_model).not_to receive(:safe_find_or_create_by!)
 | 
						|
 | 
						|
          expect { migration.prepare_async_foreign_key_validation(table_name, column_name) }.not_to raise_error
 | 
						|
        end
 | 
						|
      end
 | 
						|
    end
 | 
						|
 | 
						|
    describe '#unprepare_async_foreign_key_validation' do
 | 
						|
      context 'with foreign keys' do
 | 
						|
        before do
 | 
						|
          migration.prepare_async_foreign_key_validation(table_name, column_name, name: fk_name)
 | 
						|
        end
 | 
						|
 | 
						|
        it 'destroys the record' do
 | 
						|
          expect do
 | 
						|
            migration.unprepare_async_foreign_key_validation(table_name, column_name)
 | 
						|
          end.to change { constraint_model.where(table_name: table_name).count }.by(-1)
 | 
						|
        end
 | 
						|
 | 
						|
        context 'when an explicit name is given' do
 | 
						|
          let(:fk_name) { 'my_test_async_fk' }
 | 
						|
 | 
						|
          it 'destroys the record' do
 | 
						|
            expect do
 | 
						|
              migration.unprepare_async_foreign_key_validation(table_name, name: fk_name)
 | 
						|
            end.to change { constraint_model.where(name: fk_name).count }.by(-1)
 | 
						|
          end
 | 
						|
        end
 | 
						|
 | 
						|
        context 'when the async fk validation table does not exist' do
 | 
						|
          it 'does not raise an error' do
 | 
						|
            connection.drop_table(constraint_model.table_name)
 | 
						|
 | 
						|
            expect(constraint_model).not_to receive(:find_by)
 | 
						|
 | 
						|
            expect { migration.unprepare_async_foreign_key_validation(table_name, column_name) }.not_to raise_error
 | 
						|
          end
 | 
						|
        end
 | 
						|
      end
 | 
						|
 | 
						|
      context 'with other types of constraints' do
 | 
						|
        let(:name) { 'my_test_async_constraint' }
 | 
						|
        let(:constraint) { create(:postgres_async_constraint_validation, table_name: table_name, name: name) }
 | 
						|
 | 
						|
        it 'does not destroy the record' do
 | 
						|
          constraint.update_column(:constraint_type, 99)
 | 
						|
 | 
						|
          expect do
 | 
						|
            migration.unprepare_async_foreign_key_validation(table_name, name: name)
 | 
						|
          end.not_to change { constraint_model.where(name: name).count }
 | 
						|
 | 
						|
          expect(constraint).to be_present
 | 
						|
        end
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  context 'with async FK validation on partitioned tables' do
 | 
						|
    let(:partition_schema) { 'gitlab_partitions_dynamic' }
 | 
						|
    let(:partition1_name) { "#{partition_schema}.#{table_name}_202001" }
 | 
						|
    let(:partition2_name) { "#{partition_schema}.#{table_name}_202002" }
 | 
						|
    let(:fk_name) { 'my_partitioned_fk_name' }
 | 
						|
 | 
						|
    before do
 | 
						|
      connection.execute(<<~SQL)
 | 
						|
        CREATE TABLE #{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 #{table_name}
 | 
						|
        FOR VALUES FROM ('2020-01-01') TO ('2020-02-01');
 | 
						|
 | 
						|
        CREATE TABLE #{partition2_name} PARTITION OF #{table_name}
 | 
						|
        FOR VALUES FROM ('2020-02-01') TO ('2020-03-01');
 | 
						|
      SQL
 | 
						|
    end
 | 
						|
 | 
						|
    describe '#prepare_partitioned_async_foreign_key_validation' do
 | 
						|
      it 'delegates to prepare_async_foreign_key_validation for each partition' do
 | 
						|
        expect(migration)
 | 
						|
          .to receive(:prepare_async_foreign_key_validation)
 | 
						|
          .with(partition1_name, column_name, name: fk_name)
 | 
						|
 | 
						|
        expect(migration)
 | 
						|
          .to receive(:prepare_async_foreign_key_validation)
 | 
						|
          .with(partition2_name, column_name, name: fk_name)
 | 
						|
 | 
						|
        migration.prepare_partitioned_async_foreign_key_validation(table_name, column_name, name: fk_name)
 | 
						|
      end
 | 
						|
    end
 | 
						|
 | 
						|
    describe '#unprepare_partitioned_async_foreign_key_validation' do
 | 
						|
      it 'delegates to unprepare_async_foreign_key_validation for each partition' do
 | 
						|
        expect(migration)
 | 
						|
          .to receive(:unprepare_async_foreign_key_validation)
 | 
						|
          .with(partition1_name, column_name, name: fk_name)
 | 
						|
 | 
						|
        expect(migration)
 | 
						|
          .to receive(:unprepare_async_foreign_key_validation)
 | 
						|
          .with(partition2_name, column_name, name: fk_name)
 | 
						|
 | 
						|
        migration.unprepare_partitioned_async_foreign_key_validation(table_name, column_name, name: fk_name)
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  context 'with async check constraint validations on regular tables' do
 | 
						|
    let(:table_name) { '_test_async_check_constraints' }
 | 
						|
    let(:check_name) { 'partitioning_constraint' }
 | 
						|
 | 
						|
    before do
 | 
						|
      allow(migration).to receive(:puts)
 | 
						|
      allow(migration.connection).to receive(:transaction_open?).and_return(false)
 | 
						|
 | 
						|
      connection.create_table(table_name) do |t|
 | 
						|
        t.integer column_name
 | 
						|
      end
 | 
						|
 | 
						|
      migration.add_check_constraint(
 | 
						|
        table_name, "#{column_name} = 1",
 | 
						|
        check_name, validate: false)
 | 
						|
    end
 | 
						|
 | 
						|
    describe '#prepare_async_check_constraint_validation' do
 | 
						|
      it 'creates the record for async validation' do
 | 
						|
        expect do
 | 
						|
          migration.prepare_async_check_constraint_validation(table_name, name: check_name)
 | 
						|
        end.to change { constraint_model.where(name: check_name).count }.by(1)
 | 
						|
 | 
						|
        record = constraint_model.find_by(name: check_name)
 | 
						|
 | 
						|
        expect(record.table_name).to eq(table_name)
 | 
						|
        expect(record).to be_check_constraint
 | 
						|
      end
 | 
						|
 | 
						|
      context 'when the check constraint does not exist' do
 | 
						|
        it 'returns an error' do
 | 
						|
          expect do
 | 
						|
            migration.prepare_async_check_constraint_validation(table_name, name: 'missing')
 | 
						|
          end.to raise_error RuntimeError, /Could not find check constraint "missing" on table "#{table_name}"/
 | 
						|
        end
 | 
						|
      end
 | 
						|
 | 
						|
      context 'when the record already exists' do
 | 
						|
        it 'does attempt to create the record' do
 | 
						|
          create(:postgres_async_constraint_validation,
 | 
						|
            table_name: table_name,
 | 
						|
            name: check_name,
 | 
						|
            constraint_type: :check_constraint)
 | 
						|
 | 
						|
          expect do
 | 
						|
            migration.prepare_async_check_constraint_validation(table_name, name: check_name)
 | 
						|
          end.not_to change { constraint_model.where(name: check_name).count }
 | 
						|
        end
 | 
						|
      end
 | 
						|
 | 
						|
      context 'when the async validation table does not exist' do
 | 
						|
        it 'does not raise an error' do
 | 
						|
          connection.drop_table(constraint_model.table_name)
 | 
						|
 | 
						|
          expect(constraint_model).not_to receive(:safe_find_or_create_by!)
 | 
						|
 | 
						|
          expect { migration.prepare_async_check_constraint_validation(table_name, name: check_name) }
 | 
						|
            .not_to raise_error
 | 
						|
        end
 | 
						|
      end
 | 
						|
    end
 | 
						|
 | 
						|
    describe '#unprepare_async_check_constraint_validation' do
 | 
						|
      context 'with check constraints' do
 | 
						|
        before do
 | 
						|
          migration.prepare_async_check_constraint_validation(table_name, name: check_name)
 | 
						|
        end
 | 
						|
 | 
						|
        it 'destroys the record' do
 | 
						|
          expect do
 | 
						|
            migration.unprepare_async_check_constraint_validation(table_name, name: check_name)
 | 
						|
          end.to change { constraint_model.where(name: check_name).count }.by(-1)
 | 
						|
        end
 | 
						|
 | 
						|
        context 'when the async validation table does not exist' do
 | 
						|
          it 'does not raise an error' do
 | 
						|
            connection.drop_table(constraint_model.table_name)
 | 
						|
 | 
						|
            expect(constraint_model).not_to receive(:find_by)
 | 
						|
 | 
						|
            expect { migration.unprepare_async_check_constraint_validation(table_name, name: check_name) }
 | 
						|
              .not_to raise_error
 | 
						|
          end
 | 
						|
        end
 | 
						|
      end
 | 
						|
 | 
						|
      context 'with other types of constraints' do
 | 
						|
        let(:constraint) { create(:postgres_async_constraint_validation, table_name: table_name, name: check_name) }
 | 
						|
 | 
						|
        it 'does not destroy the record' do
 | 
						|
          constraint.update_column(:constraint_type, 99)
 | 
						|
 | 
						|
          expect do
 | 
						|
            migration.unprepare_async_check_constraint_validation(table_name, name: check_name)
 | 
						|
          end.not_to change { constraint_model.where(name: check_name).count }
 | 
						|
 | 
						|
          expect(constraint).to be_present
 | 
						|
        end
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
 | 
						|
  context 'with async check constraint validations on partitioned tables' do
 | 
						|
    let(:partition_schema) { 'gitlab_partitions_dynamic' }
 | 
						|
    let(:partition1_name) { "#{partition_schema}.#{table_name}_202001" }
 | 
						|
    let(:partition2_name) { "#{partition_schema}.#{table_name}_202002" }
 | 
						|
    let(:check_name) { 'partitioning_constraint' }
 | 
						|
 | 
						|
    before do
 | 
						|
      allow(migration).to receive(:puts)
 | 
						|
      allow(migration.connection).to receive(:transaction_open?).and_return(false)
 | 
						|
 | 
						|
      connection.execute(<<~SQL)
 | 
						|
        CREATE TABLE #{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 #{table_name}
 | 
						|
        FOR VALUES FROM ('2020-01-01') TO ('2020-02-01');
 | 
						|
 | 
						|
        CREATE TABLE #{partition2_name} PARTITION OF #{table_name}
 | 
						|
        FOR VALUES FROM ('2020-02-01') TO ('2020-03-01');
 | 
						|
      SQL
 | 
						|
 | 
						|
      migration.add_check_constraint(
 | 
						|
        partition1_name, "#{column_name} = 1",
 | 
						|
        check_name, validate: false)
 | 
						|
 | 
						|
      migration.add_check_constraint(
 | 
						|
        partition2_name, "#{column_name} = 1",
 | 
						|
        check_name, validate: false)
 | 
						|
    end
 | 
						|
 | 
						|
    describe '#prepare_partitioned_async_check_constraint_validation' do
 | 
						|
      it 'delegates to prepare_async_check_constraint_validation for each partition' do
 | 
						|
        expect(migration)
 | 
						|
          .to receive(:prepare_async_check_constraint_validation)
 | 
						|
          .with(partition1_name, name: check_name)
 | 
						|
 | 
						|
        expect(migration)
 | 
						|
          .to receive(:prepare_async_check_constraint_validation)
 | 
						|
          .with(partition2_name, name: check_name)
 | 
						|
 | 
						|
        migration.prepare_partitioned_async_check_constraint_validation(table_name, name: check_name)
 | 
						|
      end
 | 
						|
    end
 | 
						|
 | 
						|
    describe '#unprepare_partitioned_async_check_constraint_validation' do
 | 
						|
      it 'delegates to unprepare_async_check_constraint_validation for each partition' do
 | 
						|
        expect(migration)
 | 
						|
          .to receive(:unprepare_async_check_constraint_validation)
 | 
						|
          .with(partition1_name, name: check_name)
 | 
						|
 | 
						|
        expect(migration)
 | 
						|
          .to receive(:unprepare_async_check_constraint_validation)
 | 
						|
          .with(partition2_name, name: check_name)
 | 
						|
 | 
						|
        migration.unprepare_partitioned_async_check_constraint_validation(table_name, name: check_name)
 | 
						|
      end
 | 
						|
    end
 | 
						|
  end
 | 
						|
end
 |