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
|