# 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 '.config' do it 'returns a Hash' do expect(described_class.config).to be_an_instance_of(Hash) end end describe '.adapter_name' do it 'returns the name of the adapter' do expect(described_class.adapter_name).to be_an_instance_of(String) end it 'returns Unknown when using anything else' do allow(described_class).to receive(:postgresql?).and_return(false) expect(described_class.human_adapter_name).to eq('Unknown') end end describe '.human_adapter_name' do it 'returns PostgreSQL when using PostgreSQL' do expect(described_class.human_adapter_name).to eq('PostgreSQL') end end describe '.postgresql?' do subject { described_class.postgresql? } it { is_expected.to satisfy { |val| val == true || val == false } } end describe '.version' do around do |example| described_class.instance_variable_set(:@version, nil) example.run described_class.instance_variable_set(:@version, nil) end context "on postgresql" do it "extracts the version number" do allow(described_class).to receive(:database_version) .and_return("PostgreSQL 9.4.4 on x86_64-apple-darwin14.3.0") expect(described_class.version).to eq '9.4.4' end end it 'memoizes the result' do count = ActiveRecord::QueryRecorder .new { 2.times { described_class.version } } .count expect(count).to eq(1) end end describe '.postgresql_9_or_less?' do it 'returns true when using postgresql 8.4' do allow(described_class).to receive(:version).and_return('8.4') expect(described_class.postgresql_9_or_less?).to eq(true) end it 'returns true when using PostgreSQL 9.6' do allow(described_class).to receive(:version).and_return('9.6') expect(described_class.postgresql_9_or_less?).to eq(true) end it 'returns false when using PostgreSQL 10 or newer' do allow(described_class).to receive(:version).and_return('10') expect(described_class.postgresql_9_or_less?).to eq(false) end end describe '.postgresql_minimum_supported_version?' do it 'returns false when using PostgreSQL 10' do allow(described_class).to receive(:version).and_return('10') expect(described_class.postgresql_minimum_supported_version?).to eq(false) end it 'returns true when using PostgreSQL 11' do allow(described_class).to receive(:version).and_return('11') expect(described_class.postgresql_minimum_supported_version?).to eq(true) end it 'returns true when using PostgreSQL 12' do allow(described_class).to receive(:version).and_return('12') expect(described_class.postgresql_minimum_supported_version?).to eq(true) end end describe '.postgresql_upcoming_deprecation?' do it 'returns true when database version is lower than the upcoming minimum' do allow(described_class).to receive(:version).and_return('11') expect(described_class.postgresql_upcoming_deprecation?).to eq(true) end it 'returns false when database version equals the upcoming minimum' do allow(described_class).to receive(:version).and_return('12') expect(described_class.postgresql_upcoming_deprecation?).to eq(false) end it 'returns false when database version is greater the upcoming minimum' do allow(described_class).to receive(:version).and_return('13') expect(described_class.postgresql_upcoming_deprecation?).to eq(false) end end describe '.within_deprecation_notice_window?' do using RSpec::Parameterized::TableSyntax where(:case_name, :days, :result) do 'outside window' | Gitlab::Database::DEPRECATION_WINDOW_DAYS + 1 | false 'equal to window' | Gitlab::Database::DEPRECATION_WINDOW_DAYS | true 'within window' | Gitlab::Database::DEPRECATION_WINDOW_DAYS - 1 | true end with_them do it "returns #{params[:result]} when #{params[:case_name]}" do allow(Date) .to receive(:today) .and_return Date.parse(Gitlab::Database::UPCOMING_POSTGRES_VERSION_DETAILS[:gl_version_date]) - days expect(described_class.within_deprecation_notice_window?).to eq(result) end end end describe '.check_postgres_version_and_print_warning' do subject { described_class.check_postgres_version_and_print_warning } it 'prints a warning if not compliant with minimum postgres version' do allow(described_class).to receive(:postgresql_minimum_supported_version?).and_return(false) expect(Kernel).to receive(:warn).with(/You are using PostgreSQL/) subject end it 'doesnt print a warning if compliant with minimum postgres version' do allow(described_class).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(described_class).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(described_class).to receive(:postgresql_minimum_supported_version?).and_raise(ActiveRecord::ActiveRecordError) expect { subject }.not_to raise_error end it 'ignores Postgres errors' do allow(described_class).to receive(:postgresql_minimum_supported_version?).and_raise(PG::Error) expect { subject }.not_to raise_error end end describe '.pg_wal_lsn_diff' do it 'returns old name when using PostgreSQL 9.6' do allow(described_class).to receive(:version).and_return('9.6') expect(described_class.pg_wal_lsn_diff).to eq('pg_xlog_location_diff') end it 'returns new name when using PostgreSQL 10 or newer' do allow(described_class).to receive(:version).and_return('10') expect(described_class.pg_wal_lsn_diff).to eq('pg_wal_lsn_diff') end end describe '.pg_current_wal_insert_lsn' do it 'returns old name when using PostgreSQL 9.6' do allow(described_class).to receive(:version).and_return('9.6') expect(described_class.pg_current_wal_insert_lsn).to eq('pg_current_xlog_insert_location') end it 'returns new name when using PostgreSQL 10 or newer' do allow(described_class).to receive(:version).and_return('10') expect(described_class.pg_current_wal_insert_lsn).to eq('pg_current_wal_insert_lsn') end end describe '.pg_last_wal_receive_lsn' do it 'returns old name when using PostgreSQL 9.6' do allow(described_class).to receive(:version).and_return('9.6') expect(described_class.pg_last_wal_receive_lsn).to eq('pg_last_xlog_receive_location') end it 'returns new name when using PostgreSQL 10 or newer' do allow(described_class).to receive(:version).and_return('10') expect(described_class.pg_last_wal_receive_lsn).to eq('pg_last_wal_receive_lsn') end end describe '.pg_last_wal_replay_lsn' do it 'returns old name when using PostgreSQL 9.6' do allow(described_class).to receive(:version).and_return('9.6') expect(described_class.pg_last_wal_replay_lsn).to eq('pg_last_xlog_replay_location') end it 'returns new name when using PostgreSQL 10 or newer' do allow(described_class).to receive(:version).and_return('10') expect(described_class.pg_last_wal_replay_lsn).to eq('pg_last_wal_replay_lsn') end end describe '.pg_last_xact_replay_timestamp' do it 'returns pg_last_xact_replay_timestamp' do expect(described_class.pg_last_xact_replay_timestamp).to eq('pg_last_xact_replay_timestamp') 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 '.with_connection_pool' do it 'creates a new connection pool and disconnect it after used' do closed_pool = nil described_class.with_connection_pool(1) do |pool| pool.with_connection do |connection| connection.execute('SELECT 1 AS value') end expect(pool).to be_connected closed_pool = pool end expect(closed_pool).not_to be_connected end it 'disconnects the pool even an exception was raised' do error = Class.new(RuntimeError) closed_pool = nil begin described_class.with_connection_pool(1) do |pool| pool.with_connection do |connection| connection.execute('SELECT 1 AS value') end closed_pool = pool raise error.new('boom') end rescue error end expect(closed_pool).not_to be_connected end end describe '.bulk_insert' do before do allow(described_class).to receive(:connection).and_return(connection) allow(connection).to receive(:quote_column_name, &:itself) allow(connection).to receive(:quote, &:itself) allow(connection).to receive(:execute) end let(: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(connection).not_to receive(:execute) described_class.bulk_insert('test', []) end it 'uses the ordering from the first row' do expect(connection).to receive(:execute) do |sql| expect(sql).to include('(1, 2, 3)') expect(sql).to include('(4, 5, 6)') end described_class.bulk_insert('test', rows) end it 'quotes column names' do expect(connection).to receive(:quote_column_name).with(:a) expect(connection).to receive(:quote_column_name).with(:b) expect(connection).to receive(:quote_column_name).with(:c) described_class.bulk_insert('test', rows) end it 'quotes values' do 1.upto(6) do |i| expect(connection).to receive(:quote).with(i) end described_class.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(connection).to receive(:quote).with(i) end described_class.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(connection).to receive(:quote).with(i) end described_class.bulk_insert('test', rows, disable_quote: [:a, :c]) end it 'handles non-UTF-8 data' do expect { described_class.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(connection) .to receive(:execute) .with(/RETURNING id/) .and_return(result) ids = described_class .bulk_insert('test', [{ number: 10 }], return_ids: true) expect(ids).to eq([10]) end it 'allows setting the upsert to do nothing' do expect(connection) .to receive(:execute) .with(/ON CONFLICT DO NOTHING/) described_class .bulk_insert('test', [{ number: 10 }], on_conflict: :do_nothing) end end end describe '.create_connection_pool' do it 'creates a new connection pool with specific pool size' do pool = described_class.create_connection_pool(5) begin expect(pool) .to be_kind_of(ActiveRecord::ConnectionAdapters::ConnectionPool) expect(pool.spec.config[:pool]).to eq(5) ensure pool.disconnect! end end it 'allows setting of a custom hostname' do pool = described_class.create_connection_pool(5, '127.0.0.1') begin expect(pool.spec.config[:host]).to eq('127.0.0.1') ensure pool.disconnect! end end it 'allows setting of a custom hostname and port' do pool = described_class.create_connection_pool(5, '127.0.0.1', 5432) begin expect(pool.spec.config[:host]).to eq('127.0.0.1') expect(pool.spec.config[:port]).to eq(5432) ensure pool.disconnect! end end end describe '.cached_column_exists?' do it 'only retrieves data once' do expect(ActiveRecord::Base.connection).to receive(:columns).once.and_call_original 2.times do expect(described_class.cached_column_exists?(:projects, :id)).to be_truthy expect(described_class.cached_column_exists?(:projects, :bogus_column)).to be_falsey end end end describe '.cached_table_exists?' do it 'only retrieves data once per table' do expect(ActiveRecord::Base.connection).to receive(:data_source_exists?).with(:projects).once.and_call_original expect(ActiveRecord::Base.connection).to receive(:data_source_exists?).with(:bogus_table_name).once.and_call_original 2.times do expect(described_class.cached_table_exists?(:projects)).to be_truthy expect(described_class.cached_table_exists?(:bogus_table_name)).to be_falsey end end it 'returns false when database does not exist' do expect(ActiveRecord::Base).to receive(:connection) { raise ActiveRecord::NoDatabaseError, 'broken' } expect(described_class.cached_table_exists?(:projects)).to be(false) end end describe '.exists?' do it 'returns true if `ActiveRecord::Base.connection` succeeds' do expect(ActiveRecord::Base).to receive(:connection) expect(described_class.exists?).to be(true) end it 'returns false if `ActiveRecord::Base.connection` fails' do expect(ActiveRecord::Base).to receive(:connection) { raise ActiveRecord::NoDatabaseError, 'broken' } expect(described_class.exists?).to be(false) 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 '.read_only?' do it 'returns false' do expect(described_class.read_only?).to be_falsey end end describe '.db_read_only?' do before do allow(ActiveRecord::Base.connection).to receive(:execute).and_call_original end it 'detects a read only database' do allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => "t" }]) expect(described_class.db_read_only?).to be_truthy end it 'detects a read only database' do allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => true }]) expect(described_class.db_read_only?).to be_truthy end it 'detects a read write database' do allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => "f" }]) expect(described_class.db_read_only?).to be_falsey end it 'detects a read write database' do allow(ActiveRecord::Base.connection).to receive(:execute).with('SELECT pg_is_in_recovery()').and_return([{ "pg_is_in_recovery" => false }]) expect(described_class.db_read_only?).to be_falsey 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 end