1# frozen_string_literal: true
2
3module Gitlab
4  module Database
5    DATABASE_NAMES = %w[main ci].freeze
6
7    MAIN_DATABASE_NAME = 'main'
8    CI_DATABASE_NAME = 'ci'
9    DEFAULT_POOL_HEADROOM = 10
10
11    # This constant is used when renaming tables concurrently.
12    # If you plan to rename a table using the `rename_table_safely` method, add your table here one milestone before the rename.
13    # Example:
14    # TABLES_TO_BE_RENAMED = {
15    #   'old_name' => 'new_name'
16    # }.freeze
17    TABLES_TO_BE_RENAMED = {}.freeze
18
19    # Minimum PostgreSQL version requirement per documentation:
20    # https://docs.gitlab.com/ee/install/requirements.html#postgresql-requirements
21    MINIMUM_POSTGRES_VERSION = 12
22
23    # https://www.postgresql.org/docs/9.2/static/datatype-numeric.html
24    MAX_INT_VALUE = 2147483647
25    MIN_INT_VALUE = -2147483648
26
27    # The max value between MySQL's TIMESTAMP and PostgreSQL's timestampz:
28    # https://www.postgresql.org/docs/9.1/static/datatype-datetime.html
29    # https://dev.mysql.com/doc/refman/5.7/en/datetime.html
30    # FIXME: this should just be the max value of timestampz
31    MAX_TIMESTAMP_VALUE = Time.at((1 << 31) - 1).freeze
32
33    # The maximum number of characters for text fields, to avoid DoS attacks via parsing huge text fields
34    # https://gitlab.com/gitlab-org/gitlab-foss/issues/61974
35    MAX_TEXT_SIZE_LIMIT = 1_000_000
36
37    # Minimum schema version from which migrations are supported
38    # Migrations before this version may have been removed
39    MIN_SCHEMA_VERSION = 20190506135400
40    MIN_SCHEMA_GITLAB_VERSION = '11.11.0'
41
42    # Schema we store dynamically managed partitions in (e.g. for time partitioning)
43    DYNAMIC_PARTITIONS_SCHEMA = :gitlab_partitions_dynamic
44
45    # Schema we store static partitions in (e.g. for hash partitioning)
46    STATIC_PARTITIONS_SCHEMA = :gitlab_partitions_static
47
48    # This is an extensive list of postgres schemas owned by GitLab
49    # It does not include the default public schema
50    EXTRA_SCHEMAS = [DYNAMIC_PARTITIONS_SCHEMA, STATIC_PARTITIONS_SCHEMA].freeze
51
52    PRIMARY_DATABASE_NAME = ActiveRecord::Base.connection_db_config.name.to_sym
53
54    def self.database_base_models
55      @database_base_models ||= {
56        # Note that we use ActiveRecord::Base here and not ApplicationRecord.
57        # This is deliberate, as we also use these classes to apply load
58        # balancing to, and the load balancer must be enabled for _all_ models
59        # that inher from ActiveRecord::Base; not just our own models that
60        # inherit from ApplicationRecord.
61        main: ::ActiveRecord::Base,
62        ci: ::Ci::ApplicationRecord.connection_class? ? ::Ci::ApplicationRecord : nil
63      }.compact.with_indifferent_access.freeze
64    end
65
66    # This returns a list of base models with connection associated for a given gitlab_schema
67    def self.schemas_to_base_models
68      @schemas_to_base_models ||= {
69        gitlab_main: [self.database_base_models.fetch(:main)],
70        gitlab_ci: [self.database_base_models[:ci] || self.database_base_models.fetch(:main)], # use CI or fallback to main
71        gitlab_shared: self.database_base_models.values # all models
72      }.with_indifferent_access.freeze
73    end
74
75    def self.all_database_names
76      DATABASE_NAMES
77    end
78
79    # We configure the database connection pool size automatically based on the
80    # configured concurrency. We also add some headroom, to make sure we don't
81    # run out of connections when more threads besides the 'user-facing' ones
82    # are running.
83    #
84    # Read more about this in
85    # doc/development/database/client_side_connection_pool.md
86    def self.default_pool_size
87      headroom =
88        (ENV["DB_POOL_HEADROOM"].presence || DEFAULT_POOL_HEADROOM).to_i
89
90      Gitlab::Runtime.max_threads + headroom
91    end
92
93    def self.has_config?(database_name)
94      Gitlab::Application.config.database_configuration[Rails.env].include?(database_name.to_s)
95    end
96
97    def self.main_database?(name)
98      # The database is `main` if it is a first entry in `database.yml`
99      # Rails internally names them `primary` to avoid confusion
100      # with broad `primary` usage we use `main` instead
101      #
102      # TODO: The explicit `== 'main'` is needed in a transition period till
103      # the `database.yml` is not migrated into `main:` syntax
104      # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65243
105      ActiveRecord::Base.configurations.primary?(name.to_s) || name.to_s == 'main'
106    end
107
108    def self.ci_database?(name)
109      name.to_s == CI_DATABASE_NAME
110    end
111
112    def self.check_postgres_version_and_print_warning
113      return if Gitlab::Runtime.rails_runner?
114
115      database_base_models.each do |name, model|
116        database = Gitlab::Database::Reflection.new(model)
117
118        next if database.postgresql_minimum_supported_version?
119
120        Kernel.warn ERB.new(Rainbow.new.wrap(<<~EOS).red).result
121
122                    ██     ██  █████  ██████  ███    ██ ██ ███    ██  ██████ 
123                    ██     ██ ██   ██ ██   ██ ████   ██ ██ ████   ██ ██      
124                    ██  █  ██ ███████ ██████  ██ ██  ██ ██ ██ ██  ██ ██   ███ 
125                    ██ ███ ██ ██   ██ ██   ██ ██  ██ ██ ██ ██  ██ ██ ██    ██ 
126                     ███ ███  ██   ██ ██   ██ ██   ████ ██ ██   ████  ██████  
127
128          ******************************************************************************
129            You are using PostgreSQL #{database.version} for the #{name} database, but PostgreSQL >= <%= Gitlab::Database::MINIMUM_POSTGRES_VERSION %>
130            is required for this version of GitLab.
131            <% if Rails.env.development? || Rails.env.test? %>
132            If using gitlab-development-kit, please find the relevant steps here:
133              https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/main/doc/howto/postgresql.md#upgrade-postgresql
134            <% end %>
135            Please upgrade your environment to a supported PostgreSQL version, see
136            https://docs.gitlab.com/ee/install/requirements.html#database for details.
137          ******************************************************************************
138        EOS
139      rescue ActiveRecord::ActiveRecordError, PG::Error
140        # ignore - happens when Rake tasks yet have to create a database, e.g. for testing
141      end
142    end
143
144    def self.nulls_order(field, direction = :asc, nulls_order = :nulls_last)
145      raise ArgumentError unless [:nulls_last, :nulls_first].include?(nulls_order)
146      raise ArgumentError unless [:asc, :desc].include?(direction)
147
148      case nulls_order
149      when :nulls_last then nulls_last_order(field, direction)
150      when :nulls_first then nulls_first_order(field, direction)
151      end
152    end
153
154    def self.nulls_last_order(field, direction = 'ASC')
155      Arel.sql("#{field} #{direction} NULLS LAST")
156    end
157
158    def self.nulls_first_order(field, direction = 'ASC')
159      Arel.sql("#{field} #{direction} NULLS FIRST")
160    end
161
162    def self.random
163      "RANDOM()"
164    end
165
166    def self.true_value
167      "'t'"
168    end
169
170    def self.false_value
171      "'f'"
172    end
173
174    def self.sanitize_timestamp(timestamp)
175      MAX_TIMESTAMP_VALUE > timestamp ? timestamp : MAX_TIMESTAMP_VALUE.dup
176    end
177
178    def self.allow_cross_joins_across_databases(url:)
179      # this method is implemented in:
180      # spec/support/database/prevent_cross_joins.rb
181      yield
182    end
183
184    def self.add_post_migrate_path_to_rails(force: false)
185      return if ENV['SKIP_POST_DEPLOYMENT_MIGRATIONS'] && !force
186
187      Rails.application.config.paths['db'].each do |db_path|
188        path = Rails.root.join(db_path, 'post_migrate').to_s
189
190        unless Rails.application.config.paths['db/migrate'].include? path
191          Rails.application.config.paths['db/migrate'] << path
192
193          # Rails memoizes migrations at certain points where it won't read the above
194          # path just yet. As such we must also update the following list of paths.
195          ActiveRecord::Migrator.migrations_paths << path
196        end
197      end
198    end
199
200    def self.db_config_names
201      ::ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).map(&:name)
202    end
203
204    def self.db_config_for_connection(connection)
205      return unless connection
206
207      # The LB connection proxy does not have a direct db_config
208      # that can be referenced
209      return if connection.is_a?(::Gitlab::Database::LoadBalancing::ConnectionProxy)
210
211      # During application init we might receive `NullPool`
212      return unless connection.respond_to?(:pool) &&
213        connection.pool.respond_to?(:db_config)
214
215      connection.pool.db_config
216    end
217
218    # At the moment, the connection can only be retrieved by
219    # Gitlab::Database::LoadBalancer#read or #read_write or from the
220    # ActiveRecord directly. Therefore, if the load balancer doesn't
221    # recognize the connection, this method returns the primary role
222    # directly. In future, we may need to check for other sources.
223    # Expected returned names:
224    # main, main_replica, ci, ci_replica, unknown
225    def self.db_config_name(connection)
226      db_config = db_config_for_connection(connection)
227      db_config&.name || 'unknown'
228    end
229
230    def self.read_only?
231      false
232    end
233
234    def self.read_write?
235      !read_only?
236    end
237
238    # Monkeypatch rails with upgraded database observability
239    def self.install_transaction_metrics_patches!
240      ActiveRecord::Base.prepend(ActiveRecordBaseTransactionMetrics)
241    end
242
243    def self.install_transaction_context_patches!
244      ActiveRecord::ConnectionAdapters::TransactionManager
245        .prepend(TransactionManagerContext)
246      ActiveRecord::ConnectionAdapters::RealTransaction
247        .prepend(RealTransactionContext)
248    end
249
250    # MonkeyPatch for ActiveRecord::Base for adding observability
251    module ActiveRecordBaseTransactionMetrics
252      extend ActiveSupport::Concern
253
254      class_methods do
255        # A patch over ActiveRecord::Base.transaction that provides
256        # observability into transactional methods.
257        def transaction(**options, &block)
258          transaction_type = get_transaction_type(connection.transaction_open?, options[:requires_new])
259
260          ::Gitlab::Database::Metrics.subtransactions_increment(self.name) if transaction_type == :sub_transaction
261
262          payload = { connection: connection, transaction_type: transaction_type }
263
264          ActiveSupport::Notifications.instrument('transaction.active_record', payload) do
265            super(**options, &block)
266          end
267        end
268
269        private
270
271        def get_transaction_type(transaction_open, requires_new_flag)
272          if transaction_open
273            return :sub_transaction if requires_new_flag
274
275            return :fake_transaction
276          end
277
278          :real_transaction
279        end
280      end
281    end
282
283    # rubocop:disable Gitlab/ModuleWithInstanceVariables
284    module TransactionManagerContext
285      def transaction_context
286        @stack.first.try(:gitlab_transaction_context)
287      end
288    end
289
290    module RealTransactionContext
291      def gitlab_transaction_context
292        @gitlab_transaction_context ||= ::Gitlab::Database::Transaction::Context.new
293      end
294
295      def commit
296        gitlab_transaction_context.commit
297
298        super
299      end
300
301      def rollback
302        gitlab_transaction_context.rollback
303
304        super
305      end
306    end
307    # rubocop:enable Gitlab/ModuleWithInstanceVariables
308  end
309end
310
311Gitlab::Database.prepend_mod_with('Gitlab::Database')
312