X Tutup
using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Npgsql.Internal.Postgres; using Npgsql.PostgresTypes; using Npgsql.Util; namespace Npgsql.Internal; /// /// Base class for implementations which provide information about PostgreSQL and PostgreSQL-like databases /// (e.g. type definitions, capabilities...). /// [Experimental(NpgsqlDiagnostics.DatabaseInfoExperimental)] public abstract class NpgsqlDatabaseInfo { #region Fields static volatile INpgsqlDatabaseInfoFactory[] Factories = [ new PostgresMinimalDatabaseInfoFactory(), new PostgresDatabaseInfoFactory() ]; #endregion Fields #region General database info /// /// The hostname of IP address of the database. /// public string Host { get; } /// /// The TCP port of the database. /// public int Port { get; } /// /// The database name. /// public string Name { get; } /// /// The version of the PostgreSQL database we're connected to, as reported in the "server_version" parameter. /// Exposed via . /// public Version Version { get; } /// /// The PostgreSQL version string as returned by the server_version option. Populated during loading. /// public string ServerVersion { get; } #endregion General database info #region Supported capabilities and features /// /// Whether the backend supports range types. /// public virtual bool SupportsRangeTypes => Version.IsGreaterOrEqual(9, 2); /// /// Whether the backend supports multirange types. /// public virtual bool SupportsMultirangeTypes => Version.IsGreaterOrEqual(14); /// /// Whether the backend supports enum types. /// public virtual bool SupportsEnumTypes => Version.IsGreaterOrEqual(8, 3); /// /// Whether the backend supports the CLOSE ALL statement. /// public virtual bool SupportsCloseAll => Version.IsGreaterOrEqual(8, 3); /// /// Whether the backend supports advisory locks. /// public virtual bool SupportsAdvisoryLocks => Version.IsGreaterOrEqual(8, 2); /// /// Whether the backend supports the DISCARD SEQUENCES statement. /// public virtual bool SupportsDiscardSequences => Version.IsGreaterOrEqual(9, 4); /// /// Whether the backend supports the UNLISTEN statement. /// public virtual bool SupportsUnlisten => Version.IsGreaterOrEqual(6, 4); // overridden by PostgresDatabase /// /// Whether the backend supports the DISCARD TEMP statement. /// public virtual bool SupportsDiscardTemp => Version.IsGreaterOrEqual(8, 3); /// /// Whether the backend supports the DISCARD statement. /// public virtual bool SupportsDiscard => Version.IsGreaterOrEqual(8, 3); /// /// Reports whether the backend uses the newer integer timestamp representation. /// public virtual bool HasIntegerDateTimes { get; protected set; } = true; /// /// Whether the database supports transactions. /// public virtual bool SupportsTransactions { get; protected set; } = true; #endregion Supported capabilities and features #region Types readonly List _baseTypesMutable = []; readonly List _arrayTypesMutable = []; readonly List _rangeTypesMutable = []; readonly List _multirangeTypesMutable = []; readonly List _enumTypesMutable = []; readonly List _compositeTypesMutable = []; readonly List _domainTypesMutable = []; internal IReadOnlyList BaseTypes => _baseTypesMutable; internal IReadOnlyList ArrayTypes => _arrayTypesMutable; internal IReadOnlyList RangeTypes => _rangeTypesMutable; internal IReadOnlyList MultirangeTypes => _multirangeTypesMutable; internal IReadOnlyList EnumTypes => _enumTypesMutable; internal IReadOnlyList CompositeTypes => _compositeTypesMutable; internal IReadOnlyList DomainTypes => _domainTypesMutable; /// /// Indexes backend types by their type OID. /// internal Dictionary ByOID { get; } = new(); /// /// Indexes backend types by their PostgreSQL internal name, including namespace (e.g. pg_catalog.int4). /// Only used for enums and composites. /// internal Dictionary ByFullName { get; } = new(); /// /// Indexes backend types by their PostgreSQL name, not including namespace. /// If more than one type exists with the same name (i.e. in different namespaces) this /// table will contain an entry with a null value. /// Only used for enums and composites. /// internal Dictionary ByName { get; } = new(); /// /// Initializes the instance of . /// protected NpgsqlDatabaseInfo(string host, int port, string databaseName, Version version) : this(host, port, databaseName, version, version.ToString()) { } /// /// Initializes the instance of . /// protected NpgsqlDatabaseInfo(string host, int port, string databaseName, Version version, string serverVersion) { Host = host; Port = port; Name = databaseName; Version = version; ServerVersion = serverVersion; } private protected NpgsqlDatabaseInfo(string host, int port, string databaseName, string serverVersion) { Host = host; Port = port; Name = databaseName; ServerVersion = serverVersion; Version = ParseServerVersion(serverVersion); } internal PostgresType GetPostgresType(Oid oid) => GetPostgresType(oid.Value); public PostgresType GetPostgresType(uint oid) => ByOID.TryGetValue(oid, out var pgType) ? pgType : throw new ArgumentException($"A PostgreSQL type with the oid '{oid}' was not found in the current database info"); internal PostgresType GetPostgresType(DataTypeName dataTypeName) => ByFullName.TryGetValue(dataTypeName.Value, out var value) ? value : throw new ArgumentException($"A PostgreSQL type with the name '{dataTypeName}' was not found in the current database info"); public PostgresType GetPostgresType(string pgName) => TryGetPostgresTypeByName(pgName, out var pgType) ? pgType : throw new ArgumentException($"A PostgreSQL type with the name '{pgName}' was not found in the current database info"); public bool TryGetPostgresTypeByName(string pgName, [NotNullWhen(true)] out PostgresType? pgType) { // Full type name with namespace if (pgName.IndexOf('.') > -1) { if (ByFullName.TryGetValue(pgName, out pgType)) return true; } // No dot, partial type name else if (ByName.TryGetValue(pgName, out pgType)) { if (pgType is not null) return true; // If the name was found but the value is null, that means that there are // two db types with the same name (different schemas). // Try to fall back to pg_catalog, otherwise fail. if (ByFullName.TryGetValue($"pg_catalog.{pgName}", out pgType)) return true; var ambiguousTypes = new List(); foreach (var key in ByFullName.Keys) if (key.EndsWith($".{pgName}", StringComparison.Ordinal)) ambiguousTypes.Add(key); throw new ArgumentException($"More than one PostgreSQL type was found with the name {pgName}, " + $"please specify a full name including schema: {string.Join(", ", ambiguousTypes)}"); } return false; } internal void ProcessTypes() { var unspecified = new PostgresBaseType(DataTypeName.Unspecified, Oid.Unspecified); ByOID[Oid.Unspecified.Value] = unspecified; ByFullName[unspecified.DataTypeName.Value] = unspecified; ByName[unspecified.InternalName] = unspecified; foreach (var type in GetTypes()) { ByOID[type.OID] = type; ByFullName[type.DataTypeName.Value] = type; // If more than one type exists with the same partial name, we place a null value. // This allows us to detect this case later and force the user to use full names only. var typeInternalName = type.InternalName; if (!ByName.TryAdd(typeInternalName, type)) ByName[typeInternalName] = null; switch (type) { case PostgresBaseType baseType: _baseTypesMutable.Add(baseType); continue; case PostgresArrayType arrayType: _arrayTypesMutable.Add(arrayType); continue; case PostgresRangeType rangeType: _rangeTypesMutable.Add(rangeType); continue; case PostgresMultirangeType multirangeType: _multirangeTypesMutable.Add(multirangeType); continue; case PostgresEnumType enumType: _enumTypesMutable.Add(enumType); continue; case PostgresCompositeType compositeType: _compositeTypesMutable.Add(compositeType); continue; case PostgresDomainType domainType: _domainTypesMutable.Add(domainType); continue; default: throw new ArgumentOutOfRangeException(); } } } /// /// Provides all PostgreSQL types detected in this database. /// /// protected abstract IEnumerable GetTypes(); #endregion Types #region Misc /// /// Parses a PostgreSQL server version (e.g. 10.1, 9.6.3) and returns a CLR Version. /// protected static Version ParseServerVersion(string value) { var versionString = value.TrimStart(); for (var idx = 0; idx != versionString.Length; ++idx) { var c = versionString[idx]; if (!char.IsDigit(c) && c != '.') { versionString = versionString.Substring(0, idx); break; } } if (!versionString.Contains(".")) versionString += ".0"; return new Version(versionString); } #endregion Misc #region Factory management /// /// Registers a new database info factory, which is used to load information about databases. /// public static void RegisterFactory(INpgsqlDatabaseInfoFactory factory) { ArgumentNullException.ThrowIfNull(factory); var factories = new INpgsqlDatabaseInfoFactory[Factories.Length + 1]; factories[0] = factory; Array.Copy(Factories, 0, factories, 1, Factories.Length); Factories = factories; } internal static async Task Load(NpgsqlConnector conn, NpgsqlTimeout timeout, bool async) { foreach (var factory in Factories) { var dbInfo = await factory.Load(conn, timeout, async).ConfigureAwait(false); if (dbInfo != null) { dbInfo.ProcessTypes(); return dbInfo; } } // Should never be here throw new NpgsqlException("No DatabaseInfoFactory could be found for this connection"); } // For tests internal static void ResetFactories() => Factories = [ new PostgresMinimalDatabaseInfoFactory(), new PostgresDatabaseInfoFactory() ]; #endregion Factory management internal Oid GetOid(PgTypeId pgTypeId, bool validate = false) => pgTypeId.IsOid ? validate ? GetPostgresType(pgTypeId.Oid).OID : pgTypeId.Oid : GetPostgresType(pgTypeId.DataTypeName).OID; internal DataTypeName GetDataTypeName(PgTypeId pgTypeId, bool validate = false) => pgTypeId.IsDataTypeName ? validate ? GetPostgresType(pgTypeId.DataTypeName).DataTypeName : pgTypeId.DataTypeName : GetPostgresType(pgTypeId.Oid).DataTypeName; internal PostgresType GetPostgresType(PgTypeId pgTypeId) => pgTypeId.IsOid ? GetPostgresType(pgTypeId.Oid.Value) : GetPostgresType(pgTypeId.DataTypeName.Value); internal PostgresType? FindPostgresType(PgTypeId pgTypeId) => pgTypeId.IsOid ? ByOID.TryGetValue(pgTypeId.Oid.Value, out var pgType) ? pgType : null : TryGetPostgresTypeByName(pgTypeId.DataTypeName.Value, out pgType) ? pgType : null; }
X Tutup