summaryrefslogtreecommitdiff
path: root/lib/gitlab/database/reflection.rb
blob: 33c965cb150842cf02908f58ea9762264820023b (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
# frozen_string_literal: true

module Gitlab
  module Database
    # A class for reflecting upon a database and its settings, such as the
    # adapter name, PostgreSQL version, and the presence of tables or columns.
    class Reflection
      attr_reader :model

      def initialize(model)
        @model = model
        @version = nil
      end

      def config
        # The result of this method must not be cached, as other methods may use
        # it after making configuration changes and expect those changes to be
        # present. For example, `disable_prepared_statements` expects the
        # configuration settings to always be up to date.
        #
        # See the following for more information:
        #
        # - https://gitlab.com/gitlab-org/release/retrospectives/-/issues/39
        # - https://gitlab.com/gitlab-com/gl-infra/production/-/issues/5238
        model.connection_db_config.configuration_hash.with_indifferent_access
      end

      def username
        config[:username] || ENV['USER']
      end

      def database_name
        config[:database]
      end

      def adapter_name
        config[:adapter]
      end

      def human_adapter_name
        if postgresql?
          'PostgreSQL'
        else
          'Unknown'
        end
      end

      def postgresql?
        adapter_name.casecmp('postgresql') == 0
      end

      # Check whether the underlying database is in read-only mode
      def db_read_only?
        pg_is_in_recovery =
          connection
            .execute('SELECT pg_is_in_recovery()')
            .first
            .fetch('pg_is_in_recovery')

        Gitlab::Utils.to_boolean(pg_is_in_recovery)
      end

      def db_read_write?
        !db_read_only?
      end

      def version
        @version ||= database_version.match(/\A(?:PostgreSQL |)([^\s]+).*\z/)[1]
      end

      def database_version
        connection.execute("SELECT VERSION()").first['version']
      end

      def postgresql_minimum_supported_version?
        version.to_f >= MINIMUM_POSTGRES_VERSION
      end

      def cached_column_exists?(column_name)
        connection
          .schema_cache.columns_hash(model.table_name)
          .has_key?(column_name.to_s)
      end

      def cached_table_exists?
        exists? && connection.schema_cache.data_source_exists?(model.table_name)
      end

      def exists?
        # We can't _just_ check if `connection` raises an error, as it will
        # point to a `ConnectionProxy`, and obtaining those doesn't involve any
        # database queries. So instead we obtain the database version, which is
        # cached after the first call.
        connection.schema_cache.database_version
        true
      rescue StandardError
        false
      end

      def system_id
        row = connection
          .execute('SELECT system_identifier FROM pg_control_system()')
          .first

        row['system_identifier']
      end

      def flavor
        {
          # Based on https://aws.amazon.com/premiumsupport/knowledge-center/aurora-version-number/
          'Amazon Aurora PostgreSQL' => { statement: 'SELECT AURORA_VERSION()', error: /PG::UndefinedFunction/ },
          # Based on https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_PostgreSQL.html#PostgreSQL.Concepts.General.FeatureSupport.Extensions,
          # this is also available for both Aurora and RDS, so we need to check for the former first.
          'PostgreSQL on Amazon RDS' => { statement: 'SHOW rds.extensions', error: /PG::UndefinedObject/ },
          # Based on https://cloud.google.com/sql/docs/postgres/flags#postgres-c this should be specific
          # to Cloud SQL for PostgreSQL
          'Cloud SQL for PostgreSQL' => { statement: 'SHOW cloudsql.iam_authentication', error: /PG::UndefinedObject/ },
          # Based on
          #   - https://docs.microsoft.com/en-us/azure/postgresql/flexible-server/concepts-extensions
          #   - https://docs.microsoft.com/en-us/azure/postgresql/concepts-extensions
          # this should be available only for Azure Database for PostgreSQL - Flexible Server.
          'Azure Database for PostgreSQL - Flexible Server' => { statement: 'SHOW azure.extensions', error: /PG::UndefinedObject/ },
          # Based on
          #   - https://docs.microsoft.com/en-us/azure/postgresql/flexible-server/concepts-servers
          #   - https://docs.microsoft.com/en-us/azure/postgresql/concepts-servers#managing-your-server
          # this database is present on both Flexible and Single server, so we should check the former first.
          'Azure Database for PostgreSQL - Single Server' => { statement: "SELECT datname FROM pg_database WHERE datname = 'azure_maintenance'" }
        }.each do |flavor, conditions|
          return flavor if connection.execute(conditions[:statement]).to_a.present?
        rescue ActiveRecord::StatementInvalid => e
          raise if conditions[:error] && !e.message.match?(conditions[:error])
        end

        nil
      end

      private

      def connection
        model.connection
      end
    end
  end
end