# frozen_string_literal: true require 'spec_helper' RSpec.describe Gitlab::Database do before do stub_const('MigrationTest', Class.new { include Gitlab::Database }) end describe 'EXTRA_SCHEMAS' do it 'contains only schemas starting with gitlab_ prefix' do described_class::EXTRA_SCHEMAS.each do |schema| expect(schema.to_s).to start_with('gitlab_') end end end describe '.default_pool_size' do before do allow(Gitlab::Runtime).to receive(:max_threads).and_return(7) end it 'returns the max thread size plus a fixed headroom of 10' do expect(described_class.default_pool_size).to eq(17) end it 'returns the max thread size plus a DB_POOL_HEADROOM if this env var is present' do stub_env('DB_POOL_HEADROOM', '7') expect(described_class.default_pool_size).to eq(14) end end describe '.has_config?' do context 'two tier database config' do before do allow(Gitlab::Application).to receive_message_chain(:config, :database_configuration, :[]).with(Rails.env) .and_return({ "adapter" => "postgresql", "database" => "gitlabhq_test" }) end it 'returns false for primary' do expect(described_class.has_config?(:primary)).to eq(false) end it 'returns false for ci' do expect(described_class.has_config?(:ci)).to eq(false) end end context 'three tier database config' do before do allow(Gitlab::Application).to receive_message_chain(:config, :database_configuration, :[]).with(Rails.env) .and_return({ "primary" => { "adapter" => "postgresql", "database" => "gitlabhq_test" }, "ci" => { "adapter" => "postgresql", "database" => "gitlabhq_test_ci" } }) end it 'returns true for primary' do expect(described_class.has_config?(:primary)).to eq(true) end it 'returns true for ci' do expect(described_class.has_config?(:ci)).to eq(true) end it 'returns false for non-existent' do expect(described_class.has_config?(:nonexistent)).to eq(false) end end end describe '.main_database?' do using RSpec::Parameterized::TableSyntax where(:database_name, :result) do :main | true 'main' | true :ci | false 'ci' | false :archive | false 'archive' | false end with_them do it { expect(described_class.main_database?(database_name)).to eq(result) } end end describe '.ci_database?' do using RSpec::Parameterized::TableSyntax where(:database_name, :result) do :main | false 'main' | false :ci | true 'ci' | true :archive | false 'archive' | false end with_them do it { expect(described_class.ci_database?(database_name)).to eq(result) } end end describe '.check_for_non_superuser' do subject { described_class.check_for_non_superuser } let(:non_superuser) { Gitlab::Database::PgUser.new(usename: 'foo', usesuper: false ) } let(:superuser) { Gitlab::Database::PgUser.new(usename: 'bar', usesuper: true) } it 'prints user details if not superuser' do allow(Gitlab::Database::PgUser).to receive(:find_by).with('usename = CURRENT_USER').and_return(non_superuser) expect(Gitlab::AppLogger).to receive(:info).with("Account details: User: \"foo\", UseSuper: (false)") subject end it 'raises an exception if superuser' do allow(Gitlab::Database::PgUser).to receive(:find_by).with('usename = CURRENT_USER').and_return(superuser) expect(Gitlab::AppLogger).to receive(:info).with("Account details: User: \"bar\", UseSuper: (true)") expect { subject }.to raise_error('Error: detected superuser') end it 'catches exception if find_by fails' do allow(Gitlab::Database::PgUser).to receive(:find_by).with('usename = CURRENT_USER').and_raise(ActiveRecord::StatementInvalid) expect { subject }.to raise_error('User CURRENT_USER not found') end end describe '.check_postgres_version_and_print_warning' do let(:reflect) { instance_spy(Gitlab::Database::Reflection) } subject { described_class.check_postgres_version_and_print_warning } before do allow(Gitlab::Database::Reflection) .to receive(:new) .and_return(reflect) end it 'prints a warning if not compliant with minimum postgres version' do allow(reflect).to receive(:postgresql_minimum_supported_version?).and_return(false) expect(Kernel) .to receive(:warn) .with(/You are using PostgreSQL/) .exactly(Gitlab::Database.database_base_models.length) .times subject end it 'doesnt print a warning if compliant with minimum postgres version' do allow(reflect).to receive(:postgresql_minimum_supported_version?).and_return(true) expect(Kernel).not_to receive(:warn).with(/You are using PostgreSQL/) subject end it 'doesnt print a warning in Rails runner environment' do allow(reflect).to receive(:postgresql_minimum_supported_version?).and_return(false) allow(Gitlab::Runtime).to receive(:rails_runner?).and_return(true) expect(Kernel).not_to receive(:warn).with(/You are using PostgreSQL/) subject end it 'ignores ActiveRecord errors' do allow(reflect).to receive(:postgresql_minimum_supported_version?).and_raise(ActiveRecord::ActiveRecordError) expect { subject }.not_to raise_error end it 'ignores Postgres errors' do allow(reflect).to receive(:postgresql_minimum_supported_version?).and_raise(PG::Error) expect { subject }.not_to raise_error end end describe '.nulls_last_order' do it { expect(described_class.nulls_last_order('column', 'ASC')).to eq 'column ASC NULLS LAST'} it { expect(described_class.nulls_last_order('column', 'DESC')).to eq 'column DESC NULLS LAST'} end describe '.nulls_first_order' do it { expect(described_class.nulls_first_order('column', 'ASC')).to eq 'column ASC NULLS FIRST'} it { expect(described_class.nulls_first_order('column', 'DESC')).to eq 'column DESC NULLS FIRST'} end describe '.db_config_for_connection' do context 'when the regular connection is used' do it 'returns db_config' do connection = ActiveRecord::Base.retrieve_connection expect(described_class.db_config_for_connection(connection)).to eq(connection.pool.db_config) end end context 'when the connection is LoadBalancing::ConnectionProxy' do it 'returns primary_db_config' do lb_config = ::Gitlab::Database::LoadBalancing::Configuration.new(ActiveRecord::Base) lb = ::Gitlab::Database::LoadBalancing::LoadBalancer.new(lb_config) proxy = ::Gitlab::Database::LoadBalancing::ConnectionProxy.new(lb) expect(described_class.db_config_for_connection(proxy)).to eq(lb_config.primary_db_config) end end context 'when the pool is a NullPool' do it 'returns nil' do connection = double(:active_record_connection, pool: ActiveRecord::ConnectionAdapters::NullPool.new) expect(described_class.db_config_for_connection(connection)).to be_nil end end end describe '.db_config_name' do it 'returns the db_config name for the connection' do model = ActiveRecord::Base # This is a ConnectionProxy expect(described_class.db_config_name(model.connection)) .to eq('main') # This is an actual connection expect(described_class.db_config_name(model.retrieve_connection)) .to eq('main') end context 'when replicas are configured', :database_replica do it 'returns the name for a replica' do replica = ActiveRecord::Base.load_balancer.host expect(described_class.db_config_name(replica)).to eq('main_replica') end end end describe '.gitlab_schemas_for_connection' do it 'does raise exception for invalid connection' do expect { described_class.gitlab_schemas_for_connection(:invalid) }.to raise_error /key not found: "unknown"/ end it 'does return a valid schema depending on a base model used', :request_store do # This is currently required as otherwise the `Ci::Build.connection` == `Project.connection` # ENV due to lib/gitlab/database/load_balancing/setup.rb:93 stub_env('GITLAB_USE_MODEL_LOAD_BALANCING', '1') # FF due to lib/gitlab/database/load_balancing/configuration.rb:92 stub_feature_flags(force_no_sharing_primary_model: true) expect(described_class.gitlab_schemas_for_connection(Project.connection)).to include(:gitlab_main, :gitlab_shared) expect(described_class.gitlab_schemas_for_connection(Ci::Build.connection)).to include(:gitlab_ci, :gitlab_shared) end it 'does return gitlab_ci when a ActiveRecord::Base is using CI connection' do with_reestablished_active_record_base do reconfigure_db_connection(model: ActiveRecord::Base, config_model: Ci::Build) expect(described_class.gitlab_schemas_for_connection(ActiveRecord::Base.connection)).to include(:gitlab_ci, :gitlab_shared) end end end describe '#true_value' do it 'returns correct value' do expect(described_class.true_value).to eq "'t'" end end describe '#false_value' do it 'returns correct value' do expect(described_class.false_value).to eq "'f'" end end describe '#sanitize_timestamp' do let(:max_timestamp) { Time.at((1 << 31) - 1) } subject { described_class.sanitize_timestamp(timestamp) } context 'with a timestamp smaller than MAX_TIMESTAMP_VALUE' do let(:timestamp) { max_timestamp - 10.years } it 'returns the given timestamp' do expect(subject).to eq(timestamp) end end context 'with a timestamp larger than MAX_TIMESTAMP_VALUE' do let(:timestamp) { max_timestamp + 1.second } it 'returns MAX_TIMESTAMP_VALUE' do expect(subject).to eq(max_timestamp) end end end describe '.all_uncached' do let(:base_model) do Class.new do def self.uncached @uncached = true yield end end end let(:model1) { Class.new(base_model) } let(:model2) { Class.new(base_model) } before do allow(described_class).to receive(:database_base_models) .and_return({ model1: model1, model2: model2 }.with_indifferent_access) end it 'wraps the given block in uncached calls for each primary connection', :aggregate_failures do expect(model1.instance_variable_get(:@uncached)).to be_nil expect(model2.instance_variable_get(:@uncached)).to be_nil expect(Gitlab::Database::LoadBalancing::Session.current).to receive(:use_primary).and_yield expect(model2).to receive(:uncached).and_call_original expect(model1).to receive(:uncached).and_call_original yielded_to_block = false described_class.all_uncached do expect(model1.instance_variable_get(:@uncached)).to be(true) expect(model2.instance_variable_get(:@uncached)).to be(true) yielded_to_block = true end expect(yielded_to_block).to be(true) end end describe '.read_only?' do it 'returns false' do expect(described_class.read_only?).to eq(false) end end describe '.read_write' do it 'returns true' do expect(described_class.read_write?).to eq(true) end end describe 'ActiveRecordBaseTransactionMetrics' do def subscribe_events events = [] begin subscriber = ActiveSupport::Notifications.subscribe('transaction.active_record') do |e| events << e end yield ensure ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber end events end context 'without a transaction block' do it 'does not publish a transaction event' do events = subscribe_events do User.first end expect(events).to be_empty end end context 'within a transaction block' do it 'publishes a transaction event' do events = subscribe_events do ActiveRecord::Base.transaction do User.first end end expect(events.length).to be(1) event = events.first expect(event).not_to be_nil expect(event.duration).to be > 0.0 expect(event.payload).to a_hash_including( connection: be_a(Gitlab::Database::LoadBalancing::ConnectionProxy) ) end end context 'within an empty transaction block' do it 'publishes a transaction event' do events = subscribe_events do ActiveRecord::Base.transaction {} end expect(events.length).to be(1) event = events.first expect(event).not_to be_nil expect(event.duration).to be > 0.0 expect(event.payload).to a_hash_including( connection: be_a(Gitlab::Database::LoadBalancing::ConnectionProxy) ) end end context 'within a nested transaction block' do it 'publishes multiple transaction events' do events = subscribe_events do ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do User.first end end end end expect(events.length).to be(3) events.each do |event| expect(event).not_to be_nil expect(event.duration).to be > 0.0 expect(event.payload).to a_hash_including( connection: be_a(Gitlab::Database::LoadBalancing::ConnectionProxy) ) end end end context 'within a cancelled transaction block' do it 'publishes multiple transaction events' do events = subscribe_events do ActiveRecord::Base.transaction do User.first raise ActiveRecord::Rollback end end expect(events.length).to be(1) event = events.first expect(event).not_to be_nil expect(event.duration).to be > 0.0 expect(event.payload).to a_hash_including( connection: be_a(Gitlab::Database::LoadBalancing::ConnectionProxy) ) end end end end