summaryrefslogtreecommitdiff
path: root/spec/lib/gitlab/database/loose_foreign_keys_spec.rb
blob: ff99f681b0cac95a1b2eeee6b0671c9d47437edc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Gitlab::Database::LooseForeignKeys do
  describe 'verify all definitions' do
    subject(:definitions) { described_class.definitions }

    it 'all definitions have assigned a known gitlab_schema and on_delete' do
      is_expected.to all(have_attributes(
                           options: a_hash_including(
                             column: be_a(String),
                             gitlab_schema: be_in(Gitlab::Database.schemas_to_base_models.symbolize_keys.keys),
                             on_delete: be_in([:async_delete, :async_nullify])
                           ),
                           from_table: be_a(String),
                           to_table: be_a(String)
                         ))
    end

    context 'ensure keys are sorted' do
      it 'does not have any keys that are out of order' do
        parsed = YAML.parse_file(described_class.loose_foreign_keys_yaml_path)
        mapping = parsed.children.first
        table_names = mapping.children.select(&:scalar?).map(&:value)
        expect(table_names).to eq(table_names.sort), "expected sorted table names in the YAML file"
      end
    end

    context 'ensure no duplicates are found' do
      it 'does not have duplicate tables defined' do
        # since we use hash to detect duplicate hash keys we need to parse YAML document
        parsed = YAML.parse_file(described_class.loose_foreign_keys_yaml_path)
        expect(parsed).to be_document
        expect(parsed.children).to be_one, "YAML has a single document"

        # require hash
        mapping = parsed.children.first
        expect(mapping).to be_mapping, "YAML has a top-level hash"

        # find all scalars with names
        table_names = mapping.children.select(&:scalar?).map(&:value)
        expect(table_names).not_to be_empty, "YAML has a non-zero tables defined"

        # expect to not have duplicates
        expect(table_names).to contain_exactly(*table_names.uniq)
      end

      it 'does not have duplicate column definitions' do
        # ignore other modifiers
        all_definitions = definitions.map do |definition|
          { from_table: definition.from_table, to_table: definition.to_table, column: definition.column }
        end

        # expect to not have duplicates
        expect(all_definitions).to contain_exactly(*all_definitions.uniq)
      end
    end

    describe 'ensuring database integrity' do
      def base_models_for(table)
        parent_table_schema = Gitlab::Database::GitlabSchema.table_schema(table)
        Gitlab::Database.schemas_to_base_models.fetch(parent_table_schema)
      end

      it 'all `to_table` tables are present', :aggregate_failures do
        definitions.each do |definition|
          base_models_for(definition.to_table).each do |model|
            expect(model.connection).to be_table_exist(definition.to_table),
              "Table #{definition.from_table} does not exist"
          end
        end
      end

      it 'all `from_table` tables are present', :aggregate_failures do
        definitions.each do |definition|
          base_models_for(definition.from_table).each do |model|
            expect(model.connection).to be_table_exist(definition.from_table),
              "Table #{definition.from_table} does not exist"
            expect(model.connection).to be_column_exist(definition.from_table, definition.column),
              "Column #{definition.column} in #{definition.from_table} does not exist"
          end
        end
      end
    end
  end

  describe '.definitions' do
    subject(:definitions) { described_class.definitions }

    it 'contains at least all parent tables that have triggers' do
      all_definition_parent_tables = definitions.map { |d| d.to_table }.to_set

      triggers_query = <<~SQL
        SELECT event_object_table, trigger_name
        FROM information_schema.triggers
        WHERE trigger_name LIKE '%_loose_fk_trigger'
        GROUP BY event_object_table, trigger_name
      SQL

      all_triggers = ApplicationRecord.connection.execute(triggers_query)

      all_triggers.each do |trigger|
        table = trigger['event_object_table']
        trigger_name = trigger['trigger_name']
        error_message = <<~END
          Missing a loose foreign key definition for parent table: #{table} with trigger: #{trigger_name}.
          Loose foreign key definitions must be added before triggers are added and triggers must be removed before removing the loose foreign key definition.
          Read more at https://docs.gitlab.com/ee/development/database/loose_foreign_keys.html ."
        END
        expect(all_definition_parent_tables).to include(table), error_message
      end
    end
  end
end