summaryrefslogtreecommitdiff
path: root/spec/support/helpers/database/multiple_databases_helpers.rb
blob: 3c9a5762c47573bf6ca585cb6c52c20f7223f99c (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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# frozen_string_literal: true

module Database
  module MultipleDatabasesHelpers
    EXTRA_DBS = ::Gitlab::Database::DATABASE_NAMES.map(&:to_sym) - [:main]

    def database_exists?(database_name)
      ::Gitlab::Database.has_database?(database_name)
    end

    def skip_if_shared_database(database_name)
      skip "Skipping because #{database_name} is shared or doesn't not exist" unless database_exists?(database_name)
    end

    def skip_if_database_exists(database_name)
      skip "Skipping because database #{database_name} exists" if database_exists?(database_name)
    end

    def execute_on_each_database(query, databases: %I[main ci])
      databases = databases.select { |database_name| database_exists?(database_name) }

      Gitlab::Database::EachDatabase.each_database_connection(only: databases, include_shared: false) do |connection, _|
        next unless Gitlab::Database.gitlab_schemas_for_connection(connection).include?(:gitlab_shared)

        connection.execute(query)
      end
    end

    def skip_if_multiple_databases_not_setup(*databases)
      unless (databases - EXTRA_DBS).empty?
        raise "Unsupported database in #{databases}. It must be one of #{EXTRA_DBS}."
      end

      databases = EXTRA_DBS if databases.empty?
      return if databases.any? { |db| Gitlab::Database.has_config?(db) }

      skip "Skipping because none of the extra databases #{databases} are setup"
    end

    def skip_if_multiple_databases_are_setup(*databases)
      unless (databases - EXTRA_DBS).empty?
        raise "Unsupported database in #{databases}. It must be one of #{EXTRA_DBS}."
      end

      databases = EXTRA_DBS if databases.empty?
      return if databases.none? { |db| Gitlab::Database.has_config?(db) }

      skip "Skipping because some of the extra databases #{databases} are setup"
    end

    def reconfigure_db_connection(name: nil, config_hash: {}, model: ActiveRecord::Base, config_model: nil)
      db_config = (config_model || model).connection_db_config

      new_db_config = ActiveRecord::DatabaseConfigurations::HashConfig.new(
        db_config.env_name,
        name ? name.to_s : db_config.name,
        db_config.configuration_hash.merge(config_hash)
      )

      model.establish_connection(new_db_config)
    end

    def ensure_schema_and_empty_tables
      # Ensure all schemas for both databases are migrated back
      Gitlab::Database.database_base_models.each do |_, base_model|
        with_reestablished_active_record_base do
          reconfigure_db_connection(
            model: ActiveRecord::Base,
            config_model: base_model
          )

          delete_from_all_tables!(except: deletion_except_tables)
          schema_migrate_up!
        end
      end

      # ActiveRecord::Base.clear_all_connections! disconnects and clears attribute methods
      # Force a refresh to avoid schema failures.
      reset_column_in_all_models
      refresh_attribute_methods
    end

    # The usage of this method switches temporarily used `connection_handler`
    # allowing full manipulation of ActiveRecord::Base connections without
    # having side effects like:
    # - misaligned transactions since this is managed by `BeforeAllAdapter`
    # - removal of primary connections
    #
    # The execution within a block ensures safe cleanup of all allocated resources.
    #
    # rubocop:disable Database/MultipleDatabases
    def with_reestablished_active_record_base(reconnect: true)
      connection_classes = ActiveRecord::Base
        .connection_handler
        .connection_pool_names
        .map(&:constantize)
        .index_with(&:connection_db_config)

      original_handler = ActiveRecord::Base.connection_handler
      new_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
      ActiveRecord::Base.connection_handler = new_handler

      connection_classes.each { |klass, db_config| klass.establish_connection(db_config) } if reconnect

      yield
    ensure
      ActiveRecord::Base.connection_handler = original_handler
      new_handler&.clear_all_connections!
    end
    # rubocop:enable Database/MultipleDatabases

    def with_db_configs(test: test_config)
      current_configurations = ActiveRecord::Base.configurations # rubocop:disable Database/MultipleDatabases
      ActiveRecord::Base.configurations = { test: test_config }
      yield
    ensure
      ActiveRecord::Base.configurations = current_configurations
    end

    def with_added_ci_connection
      if Gitlab::Database.has_config?(:ci)
        # No need to add a ci: connection if we already have one
        yield
      else
        with_reestablished_active_record_base(reconnect: true) do
          reconfigure_db_connection(
            name: :ci,
            model: Ci::ApplicationRecord,
            config_model: ActiveRecord::Base
          )

          yield

          # Cleanup connection_specification_name for Ci::ApplicationRecord
          Ci::ApplicationRecord.remove_connection
        end
      end
    end
  end

  module ActiveRecordBaseEstablishConnection
    def establish_connection(*args)
      # rubocop:disable Database/MultipleDatabases
      if connected? &&
          connection&.transaction_open? &&
          ActiveRecord::Base.connection_handler == ActiveRecord::Base.default_connection_handler
        raise "Cannot re-establish '#{self}.establish_connection' within an open transaction " \
          "(#{connection&.open_transactions.to_i}). Use `with_reestablished_active_record_base` " \
          "instead or add `:reestablished_active_record_base` to rspec context."
      end
      # rubocop:enable Database/MultipleDatabases

      super
    end
  end
end

ActiveRecord::Base.singleton_class.prepend(::Database::ActiveRecordBaseEstablishConnection) # rubocop:disable Database/MultipleDatabases