149 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			Ruby
		
	
	
	
			
		
		
	
	
			149 lines
		
	
	
		
			5.3 KiB
		
	
	
	
		
			Ruby
		
	
	
	
| # 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'" },
 | |
|           # Based on
 | |
|           #   - https://cloud.google.com/sql/docs/postgres/flags
 | |
|           # running a query to detect flag names that begin with 'alloydb
 | |
|           'AlloyDB for PostgreSQL' => { statement: "SELECT name FROM pg_settings WHERE name LIKE 'alloydb%'" }
 | |
|         }.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
 |