# frozen_string_literal: true require 'spec_helper' RSpec.describe Gitlab::Database::EachDatabase do describe '.each_database_connection', :add_ci_connection do let(:database_base_models) { { main: ActiveRecord::Base, ci: Ci::ApplicationRecord }.with_indifferent_access } before do allow(Gitlab::Database).to receive(:database_base_models_with_gitlab_shared).and_return(database_base_models) end it 'yields each connection after connecting SharedModel' do expect(Gitlab::Database::SharedModel).to receive(:using_connection) .with(ActiveRecord::Base.connection).ordered.and_yield expect(Gitlab::Database::SharedModel).to receive(:using_connection) .with(Ci::ApplicationRecord.connection).ordered.and_yield expect { |b| described_class.each_database_connection(&b) } .to yield_successive_args( [ActiveRecord::Base.connection, 'main'], [Ci::ApplicationRecord.connection, 'ci'] ) end context 'when only certain databases are selected' do it 'yields the selected connections after connecting SharedModel' do expect(Gitlab::Database::SharedModel).to receive(:using_connection) .with(Ci::ApplicationRecord.connection).ordered.and_yield expect { |b| described_class.each_database_connection(only: 'ci', &b) } .to yield_successive_args([Ci::ApplicationRecord.connection, 'ci']) end context 'when the selected names are passed as symbols' do it 'yields the selected connections after connecting SharedModel' do expect(Gitlab::Database::SharedModel).to receive(:using_connection) .with(Ci::ApplicationRecord.connection).ordered.and_yield expect { |b| described_class.each_database_connection(only: :ci, &b) } .to yield_successive_args([Ci::ApplicationRecord.connection, 'ci']) end end context 'when the selected names are invalid' do it 'does not yield any connections' do expect do |b| described_class.each_database_connection(only: :notvalid, &b) rescue ArgumentError => e expect(e.message).to match(/notvalid is not a valid database name/) end.not_to yield_control end it 'raises an error' do expect do described_class.each_database_connection(only: :notvalid) {} end.to raise_error(ArgumentError, /notvalid is not a valid database name/) end end end context 'when shared connections are not included' do def clear_memoization(key) Gitlab::Database.remove_instance_variable(key) if Gitlab::Database.instance_variable_defined?(key) end before do allow(Gitlab::Database).to receive(:database_base_models).and_return(database_base_models) # Clear the memoization because the return of Gitlab::Database#schemas_to_base_models depends stubbed value clear_memoization(:@schemas_to_base_models) clear_memoization(:@schemas_to_base_models_ee) end it 'only yields the unshared connections' do expect(Gitlab::Database).to receive(:db_config_share_with).exactly(3).times.and_return(nil, 'main', 'main') expect { |b| described_class.each_database_connection(include_shared: false, &b) } .to yield_successive_args([ActiveRecord::Base.connection, 'main']) end end end describe '.each_model_connection' do context 'when the model inherits from SharedModel', :add_ci_connection do let(:model1) { Class.new(Gitlab::Database::SharedModel) } let(:model2) { Class.new(Gitlab::Database::SharedModel) } before do allow(Gitlab::Database).to receive(:database_base_models_with_gitlab_shared) .and_return({ main: ActiveRecord::Base, ci: Ci::ApplicationRecord }.with_indifferent_access) end it 'yields each model with SharedModel connected to each database connection' do expect_yielded_models([model1, model2], [ { model: model1, connection: ActiveRecord::Base.connection, name: 'main' }, { model: model1, connection: Ci::ApplicationRecord.connection, name: 'ci' }, { model: model2, connection: ActiveRecord::Base.connection, name: 'main' }, { model: model2, connection: Ci::ApplicationRecord.connection, name: 'ci' } ]) end context 'when the model limits connection names' do before do model1.limit_connection_names = %i[main] model2.limit_connection_names = %i[ci] end it 'only yields the model with SharedModel connected to the limited connections' do expect_yielded_models([model1, model2], [ { model: model1, connection: ActiveRecord::Base.connection, name: 'main' }, { model: model2, connection: Ci::ApplicationRecord.connection, name: 'ci' } ]) end end end context 'when the model does not inherit from SharedModel' do let(:main_model) { Class.new(ActiveRecord::Base) } let(:ci_model) { Class.new(Ci::ApplicationRecord) } let(:main_connection) { double(:connection) } let(:ci_connection) { double(:connection) } before do allow(main_model).to receive(:connection).and_return(main_connection) allow(ci_model).to receive(:connection).and_return(ci_connection) allow(main_model).to receive_message_chain('connection_db_config.name').and_return('main') allow(ci_model).to receive_message_chain('connection_db_config.name').and_return('ci') end it 'yields each model after connecting SharedModel' do expect_yielded_models([main_model, ci_model], [ { model: main_model, connection: main_connection, name: 'main' }, { model: ci_model, connection: ci_connection, name: 'ci' } ]) end end context 'when the database connections are limited by the only_on option' do let(:shared_model) { Class.new(Gitlab::Database::SharedModel) } let(:main_model) { Class.new(ActiveRecord::Base) } let(:ci_model) { Class.new(Ci::ApplicationRecord) } before do allow(Gitlab::Database).to receive(:database_base_models_with_gitlab_shared) .and_return({ main: ActiveRecord::Base, ci: Ci::ApplicationRecord }.with_indifferent_access) allow(main_model).to receive_message_chain('connection_db_config.name').and_return('main') allow(ci_model).to receive_message_chain('connection_db_config.name').and_return('ci') end context 'when a single name is passed in' do it 'yields models only connected to the given database' do expect_yielded_models([main_model, ci_model, shared_model], [ { model: ci_model, connection: Ci::ApplicationRecord.connection, name: 'ci' }, { model: shared_model, connection: Ci::ApplicationRecord.connection, name: 'ci' } ], only_on: 'ci') end end context 'when a list of names are passed in' do it 'yields models only connected to the given databases' do expect_yielded_models([main_model, ci_model, shared_model], [ { model: main_model, connection: ActiveRecord::Base.connection, name: 'main' }, { model: ci_model, connection: Ci::ApplicationRecord.connection, name: 'ci' }, { model: shared_model, connection: ActiveRecord::Base.connection, name: 'main' }, { model: shared_model, connection: Ci::ApplicationRecord.connection, name: 'ci' } ], only_on: %i[main ci]) end end end def expect_yielded_models(models_to_iterate, expected_values, only_on: nil) times_yielded = 0 described_class.each_model_connection(models_to_iterate, only_on: only_on) do |model, name| expected = expected_values[times_yielded] expect(model).to be(expected[:model]) expect(model.connection).to be(expected[:connection]) expect(name).to eq(expected[:name]) times_yielded += 1 end expect(times_yielded).to eq(expected_values.size) end end end