# frozen_string_literal: true require 'spec_helper' RSpec.describe Gitlab::Database::Connection do let(:connection) { described_class.new } describe '#config' do it 'returns a HashWithIndifferentAccess' do expect(connection.config).to be_an_instance_of(HashWithIndifferentAccess) end it 'returns a default pool size' do expect(connection.config) .to include(pool: Gitlab::Database.default_pool_size) end it 'does not cache its results' do a = connection.config b = connection.config expect(a).not_to equal(b) end end describe '#pool_size' do context 'when no explicit size is configured' do it 'returns the default pool size' do expect(connection).to receive(:config).and_return({ pool: nil }) expect(connection.pool_size).to eq(Gitlab::Database.default_pool_size) end end context 'when an explicit pool size is set' do it 'returns the pool size' do expect(connection).to receive(:config).and_return({ pool: 4 }) expect(connection.pool_size).to eq(4) end end end describe '#username' do context 'when a username is set' do it 'returns the username' do allow(connection).to receive(:config).and_return(username: 'bob') expect(connection.username).to eq('bob') end end context 'when a username is not set' do it 'returns the value of the USER environment variable' do allow(connection).to receive(:config).and_return(username: nil) allow(ENV).to receive(:[]).with('USER').and_return('bob') expect(connection.username).to eq('bob') end end end describe '#database_name' do it 'returns the name of the database' do allow(connection).to receive(:config).and_return(database: 'test') expect(connection.database_name).to eq('test') end end describe '#adapter_name' do it 'returns the database adapter name' do allow(connection).to receive(:config).and_return(adapter: 'test') expect(connection.adapter_name).to eq('test') end end describe '#human_adapter_name' do context 'when the adapter is PostgreSQL' do it 'returns PostgreSQL' do allow(connection).to receive(:config).and_return(adapter: 'postgresql') expect(connection.human_adapter_name).to eq('PostgreSQL') end end context 'when the adapter is not PostgreSQL' do it 'returns Unknown' do allow(connection).to receive(:config).and_return(adapter: 'kittens') expect(connection.human_adapter_name).to eq('Unknown') end end end describe '#postgresql?' do context 'when using PostgreSQL' do it 'returns true' do allow(connection).to receive(:adapter_name).and_return('PostgreSQL') expect(connection.postgresql?).to eq(true) end end context 'when not using PostgreSQL' do it 'returns false' do allow(connection).to receive(:adapter_name).and_return('MySQL') expect(connection.postgresql?).to eq(false) end end end describe '#db_config_with_default_pool_size' do it 'returns db_config with our default pool size' do allow(Gitlab::Database).to receive(:default_pool_size).and_return(9) expect(connection.db_config_with_default_pool_size.pool).to eq(9) end it 'returns db_config with the correct database name' do db_name = connection.scope.connection.pool.db_config.name expect(connection.db_config_with_default_pool_size.name).to eq(db_name) end end describe '#disable_prepared_statements', :reestablished_active_record_base do it 'disables prepared statements' do connection.scope.establish_connection( ::Gitlab::Database.main.config.merge(prepared_statements: true) ) expect(connection.scope.connection.prepared_statements).to eq(true) connection.disable_prepared_statements expect(connection.scope.connection.prepared_statements).to eq(false) end it 'retains the connection name' do connection.disable_prepared_statements expect(connection.scope.connection_db_config.name).to eq('main') end context 'with dynamic connection pool size' do before do connection.scope.establish_connection(connection.config.merge(pool: 7)) end it 'retains the set pool size' do connection.disable_prepared_statements expect(connection.scope.connection.prepared_statements).to eq(false) expect(connection.scope.connection.pool.size).to eq(7) end end end describe '#db_read_only?' do it 'detects a read-only database' do allow(connection.scope.connection) .to receive(:execute) .with('SELECT pg_is_in_recovery()') .and_return([{ "pg_is_in_recovery" => "t" }]) expect(connection.db_read_only?).to be_truthy end it 'detects a read-only database' do allow(connection.scope.connection) .to receive(:execute) .with('SELECT pg_is_in_recovery()') .and_return([{ "pg_is_in_recovery" => true }]) expect(connection.db_read_only?).to be_truthy end it 'detects a read-write database' do allow(connection.scope.connection) .to receive(:execute) .with('SELECT pg_is_in_recovery()') .and_return([{ "pg_is_in_recovery" => "f" }]) expect(connection.db_read_only?).to be_falsey end it 'detects a read-write database' do allow(connection.scope.connection) .to receive(:execute) .with('SELECT pg_is_in_recovery()') .and_return([{ "pg_is_in_recovery" => false }]) expect(connection.db_read_only?).to be_falsey end end describe '#db_read_write?' do it 'detects a read-only database' do allow(connection.scope.connection) .to receive(:execute) .with('SELECT pg_is_in_recovery()') .and_return([{ "pg_is_in_recovery" => "t" }]) expect(connection.db_read_write?).to eq(false) end it 'detects a read-only database' do allow(connection.scope.connection) .to receive(:execute) .with('SELECT pg_is_in_recovery()') .and_return([{ "pg_is_in_recovery" => true }]) expect(connection.db_read_write?).to eq(false) end it 'detects a read-write database' do allow(connection.scope.connection) .to receive(:execute) .with('SELECT pg_is_in_recovery()') .and_return([{ "pg_is_in_recovery" => "f" }]) expect(connection.db_read_write?).to eq(true) end it 'detects a read-write database' do allow(connection.scope.connection) .to receive(:execute) .with('SELECT pg_is_in_recovery()') .and_return([{ "pg_is_in_recovery" => false }]) expect(connection.db_read_write?).to eq(true) end end describe '#version' do around do |example| connection.instance_variable_set(:@version, nil) example.run connection.instance_variable_set(:@version, nil) end context "on postgresql" do it "extracts the version number" do allow(connection) .to receive(:database_version) .and_return("PostgreSQL 9.4.4 on x86_64-apple-darwin14.3.0") expect(connection.version).to eq '9.4.4' end end it 'memoizes the result' do count = ActiveRecord::QueryRecorder .new { 2.times { connection.version } } .count expect(count).to eq(1) end end describe '#postgresql_minimum_supported_version?' do it 'returns false when using PostgreSQL 10' do allow(connection).to receive(:version).and_return('10') expect(connection.postgresql_minimum_supported_version?).to eq(false) end it 'returns false when using PostgreSQL 11' do allow(connection).to receive(:version).and_return('11') expect(connection.postgresql_minimum_supported_version?).to eq(false) end it 'returns true when using PostgreSQL 12' do allow(connection).to receive(:version).and_return('12') expect(connection.postgresql_minimum_supported_version?).to eq(true) end end describe '#bulk_insert' do before do allow(connection).to receive(:connection).and_return(dummy_connection) allow(dummy_connection).to receive(:quote_column_name, &:itself) allow(dummy_connection).to receive(:quote, &:itself) allow(dummy_connection).to receive(:execute) end let(:dummy_connection) { double(:connection) } let(:rows) do [ { a: 1, b: 2, c: 3 }, { c: 6, a: 4, b: 5 } ] end it 'does nothing with empty rows' do expect(dummy_connection).not_to receive(:execute) connection.bulk_insert('test', []) end it 'uses the ordering from the first row' do expect(dummy_connection).to receive(:execute) do |sql| expect(sql).to include('(1, 2, 3)') expect(sql).to include('(4, 5, 6)') end connection.bulk_insert('test', rows) end it 'quotes column names' do expect(dummy_connection).to receive(:quote_column_name).with(:a) expect(dummy_connection).to receive(:quote_column_name).with(:b) expect(dummy_connection).to receive(:quote_column_name).with(:c) connection.bulk_insert('test', rows) end it 'quotes values' do 1.upto(6) do |i| expect(dummy_connection).to receive(:quote).with(i) end connection.bulk_insert('test', rows) end it 'does not quote values of a column in the disable_quote option' do [1, 2, 4, 5].each do |i| expect(dummy_connection).to receive(:quote).with(i) end connection.bulk_insert('test', rows, disable_quote: :c) end it 'does not quote values of columns in the disable_quote option' do [2, 5].each do |i| expect(dummy_connection).to receive(:quote).with(i) end connection.bulk_insert('test', rows, disable_quote: [:a, :c]) end it 'handles non-UTF-8 data' do expect { connection.bulk_insert('test', [{ a: "\255" }]) }.not_to raise_error end context 'when using PostgreSQL' do it 'allows the returning of the IDs of the inserted rows' do result = double(:result, values: [['10']]) expect(dummy_connection) .to receive(:execute) .with(/RETURNING id/) .and_return(result) ids = connection .bulk_insert('test', [{ number: 10 }], return_ids: true) expect(ids).to eq([10]) end it 'allows setting the upsert to do nothing' do expect(dummy_connection) .to receive(:execute) .with(/ON CONFLICT DO NOTHING/) connection .bulk_insert('test', [{ number: 10 }], on_conflict: :do_nothing) end end end describe '#cached_column_exists?' do it 'only retrieves the data from the schema cache' do queries = ActiveRecord::QueryRecorder.new do 2.times do expect(connection.cached_column_exists?(:projects, :id)).to be_truthy expect(connection.cached_column_exists?(:projects, :bogus_column)).to be_falsey end end expect(queries.count).to eq(0) end end describe '#cached_table_exists?' do it 'only retrieves the data from the schema cache' do queries = ActiveRecord::QueryRecorder.new do 2.times do expect(connection.cached_table_exists?(:projects)).to be_truthy expect(connection.cached_table_exists?(:bogus_table_name)).to be_falsey end end expect(queries.count).to eq(0) end it 'returns false when database does not exist' do expect(connection.scope).to receive(:connection) do raise ActiveRecord::NoDatabaseError, 'broken' end expect(connection.cached_table_exists?(:projects)).to be(false) end end describe '#exists?' do it 'returns true if the database exists' do expect(connection.exists?).to be(true) end it "returns false if the database doesn't exist" do expect(connection.scope.connection.schema_cache) .to receive(:database_version) .and_raise(ActiveRecord::NoDatabaseError) expect(connection.exists?).to be(false) end end describe '#system_id' do it 'returns the PostgreSQL system identifier' do expect(connection.system_id).to be_an_instance_of(Integer) end end describe '#get_write_location' do it 'returns a string' do expect(connection.get_write_location(connection.scope.connection)) .to be_a(String) end it 'returns nil if there are no results' do expect(connection.get_write_location(double(select_all: []))).to be_nil end end end