# frozen_string_literal: true require 'spec_helper' RSpec.describe Gitlab::Database::PartitioningMigrationHelpers::BackfillPartitionedTable, '#perform' do subject { described_class.new } let(:source_table) { '_test_partitioning_backfills' } let(:destination_table) { "#{source_table}_part" } let(:unique_key) { 'id' } before do allow(subject).to receive(:transaction_open?).and_return(false) end context 'when the destination table exists' do before do connection.execute(<<~SQL) CREATE TABLE #{source_table} ( id serial NOT NULL PRIMARY KEY, col1 int NOT NULL, col2 text NOT NULL, created_at timestamptz NOT NULL ) SQL connection.execute(<<~SQL) CREATE TABLE #{destination_table} ( id serial NOT NULL, col1 int NOT NULL, col2 text NOT NULL, created_at timestamptz NOT NULL, PRIMARY KEY (id, created_at) ) PARTITION BY RANGE (created_at) SQL connection.execute(<<~SQL) CREATE TABLE #{destination_table}_202001 PARTITION OF #{destination_table} FOR VALUES FROM ('2020-01-01') TO ('2020-02-01') SQL connection.execute(<<~SQL) CREATE TABLE #{destination_table}_202002 PARTITION OF #{destination_table} FOR VALUES FROM ('2020-02-01') TO ('2020-03-01') SQL source_model.table_name = source_table destination_model.table_name = destination_table stub_const("#{described_class}::SUB_BATCH_SIZE", 2) stub_const("#{described_class}::PAUSE_SECONDS", pause_seconds) allow(subject).to receive(:sleep) end let(:connection) { ActiveRecord::Base.connection } let(:source_model) { Class.new(ActiveRecord::Base) } let(:destination_model) { Class.new(ActiveRecord::Base) } let(:timestamp) { Time.utc(2020, 1, 2).round } let(:pause_seconds) { 1 } let!(:source1) { create_source_record(timestamp) } let!(:source2) { create_source_record(timestamp + 1.day) } let!(:source3) { create_source_record(timestamp + 1.month) } it 'copies data into the destination table idempotently' do expect(destination_model.count).to eq(0) subject.perform(source1.id, source3.id, source_table, destination_table, unique_key) expect(destination_model.count).to eq(3) source_model.find_each do |source_record| destination_record = destination_model.find_by_id(source_record.id) expect(destination_record.attributes).to eq(source_record.attributes) end subject.perform(source1.id, source3.id, source_table, destination_table, unique_key) expect(destination_model.count).to eq(3) end it 'breaks the assigned batch into smaller batches' do expect_next_instance_of(described_class::BulkCopy) do |bulk_copy| expect(bulk_copy).to receive(:copy_between).with(source1.id, source2.id) expect(bulk_copy).to receive(:copy_between).with(source3.id, source3.id) end subject.perform(source1.id, source3.id, source_table, destination_table, unique_key) end it 'pauses after copying each sub-batch' do expect(subject).to receive(:sleep).with(pause_seconds).twice subject.perform(source1.id, source3.id, source_table, destination_table, unique_key) end it 'marks each job record as succeeded after processing' do create(:background_migration_job, class_name: "::#{described_class.name}", arguments: [source1.id, source3.id, source_table, destination_table, unique_key]) expect(::Gitlab::Database::BackgroundMigrationJob).to receive(:mark_all_as_succeeded).and_call_original expect do subject.perform(source1.id, source3.id, source_table, destination_table, unique_key) end.to change { ::Gitlab::Database::BackgroundMigrationJob.succeeded.count }.from(0).to(1) end it 'returns the number of job records marked as succeeded' do create(:background_migration_job, class_name: "::#{described_class.name}", arguments: [source1.id, source3.id, source_table, destination_table, unique_key]) jobs_updated = subject.perform(source1.id, source3.id, source_table, destination_table, unique_key) expect(jobs_updated).to eq(1) end context 'when the job is run within an explicit transaction block' do let(:mock_connection) { double('connection') } before do allow(subject).to receive(:connection).and_return(mock_connection) allow(subject).to receive(:transaction_open?).and_return(true) end it 'raises an error before copying data' do expect(mock_connection).not_to receive(:execute) expect do subject.perform(1, 100, source_table, destination_table, unique_key) end.to raise_error(/Aborting job to backfill partitioned #{source_table}/) expect(destination_model.count).to eq(0) end end end context 'when the destination table does not exist' do let(:mock_connection) { double('connection') } let(:mock_logger) { double('logger') } before do allow(subject).to receive(:connection).and_return(mock_connection) allow(subject).to receive(:logger).and_return(mock_logger) expect(mock_connection).to receive(:table_exists?).with(destination_table).and_return(false) allow(mock_logger).to receive(:warn) end it 'exits without attempting to copy data' do expect(mock_connection).not_to receive(:execute) subject.perform(1, 100, source_table, destination_table, unique_key) end it 'logs a warning message that the job was skipped' do expect(mock_logger).to receive(:warn).with(/#{destination_table} does not exist/) subject.perform(1, 100, source_table, destination_table, unique_key) end end def create_source_record(timestamp) source_model.create!(col1: 123, col2: 'original value', created_at: timestamp) end end