337 lines
12 KiB
C#
337 lines
12 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using Npgsql.PostgresTypes;
|
|
using Npgsql.Util;
|
|
|
|
namespace Npgsql.Internal;
|
|
|
|
/// <summary>
|
|
/// Base class for implementations which provide information about PostgreSQL and PostgreSQL-like databases
|
|
/// (e.g. type definitions, capabilities...).
|
|
/// </summary>
|
|
public abstract class NpgsqlDatabaseInfo
|
|
{
|
|
#region Fields
|
|
|
|
internal static readonly ConcurrentDictionary<NpgsqlDatabaseInfoCacheKey, NpgsqlDatabaseInfo> Cache
|
|
= new();
|
|
|
|
static volatile INpgsqlDatabaseInfoFactory[] Factories = new INpgsqlDatabaseInfoFactory[]
|
|
{
|
|
new PostgresMinimalDatabaseInfoFactory(),
|
|
new PostgresDatabaseInfoFactory()
|
|
};
|
|
|
|
#endregion Fields
|
|
|
|
#region General database info
|
|
|
|
/// <summary>
|
|
/// The hostname of IP address of the database.
|
|
/// </summary>
|
|
public string Host { get; }
|
|
|
|
/// <summary>
|
|
/// The TCP port of the database.
|
|
/// </summary>
|
|
public int Port { get; }
|
|
|
|
/// <summary>
|
|
/// The database name.
|
|
/// </summary>
|
|
public string Name { get; }
|
|
|
|
/// <summary>
|
|
/// The version of the PostgreSQL database we're connected to, as reported in the "server_version" parameter.
|
|
/// Exposed via <see cref="NpgsqlConnection.PostgreSqlVersion"/>.
|
|
/// </summary>
|
|
public Version Version { get; }
|
|
|
|
/// <summary>
|
|
/// The PostgreSQL version string as returned by the server_version option. Populated during loading.
|
|
/// </summary>
|
|
public string ServerVersion { get; }
|
|
|
|
#endregion General database info
|
|
|
|
#region Supported capabilities and features
|
|
|
|
/// <summary>
|
|
/// Whether the backend supports range types.
|
|
/// </summary>
|
|
public virtual bool SupportsRangeTypes => Version.IsGreaterOrEqual(9, 2);
|
|
|
|
/// <summary>
|
|
/// Whether the backend supports multirange types.
|
|
/// </summary>
|
|
public virtual bool SupportsMultirangeTypes => Version.IsGreaterOrEqual(14);
|
|
|
|
/// <summary>
|
|
/// Whether the backend supports enum types.
|
|
/// </summary>
|
|
public virtual bool SupportsEnumTypes => Version.IsGreaterOrEqual(8, 3);
|
|
|
|
/// <summary>
|
|
/// Whether the backend supports the CLOSE ALL statement.
|
|
/// </summary>
|
|
public virtual bool SupportsCloseAll => Version.IsGreaterOrEqual(8, 3);
|
|
|
|
/// <summary>
|
|
/// Whether the backend supports advisory locks.
|
|
/// </summary>
|
|
public virtual bool SupportsAdvisoryLocks => Version.IsGreaterOrEqual(8, 2);
|
|
|
|
/// <summary>
|
|
/// Whether the backend supports the DISCARD SEQUENCES statement.
|
|
/// </summary>
|
|
public virtual bool SupportsDiscardSequences => Version.IsGreaterOrEqual(9, 4);
|
|
|
|
/// <summary>
|
|
/// Whether the backend supports the UNLISTEN statement.
|
|
/// </summary>
|
|
public virtual bool SupportsUnlisten => Version.IsGreaterOrEqual(6, 4); // overridden by PostgresDatabase
|
|
|
|
/// <summary>
|
|
/// Whether the backend supports the DISCARD TEMP statement.
|
|
/// </summary>
|
|
public virtual bool SupportsDiscardTemp => Version.IsGreaterOrEqual(8, 3);
|
|
|
|
/// <summary>
|
|
/// Whether the backend supports the DISCARD statement.
|
|
/// </summary>
|
|
public virtual bool SupportsDiscard => Version.IsGreaterOrEqual(8, 3);
|
|
|
|
/// <summary>
|
|
/// Reports whether the backend uses the newer integer timestamp representation.
|
|
/// </summary>
|
|
public virtual bool HasIntegerDateTimes { get; protected set; } = true;
|
|
|
|
/// <summary>
|
|
/// Whether the database supports transactions.
|
|
/// </summary>
|
|
public virtual bool SupportsTransactions { get; protected set; } = true;
|
|
|
|
#endregion Supported capabilities and features
|
|
|
|
#region Types
|
|
|
|
readonly List<PostgresBaseType> _baseTypesMutable = new();
|
|
readonly List<PostgresArrayType> _arrayTypesMutable = new();
|
|
readonly List<PostgresRangeType> _rangeTypesMutable = new();
|
|
readonly List<PostgresMultirangeType> _multirangeTypesMutable = new();
|
|
readonly List<PostgresEnumType> _enumTypesMutable = new();
|
|
readonly List<PostgresCompositeType> _compositeTypesMutable = new();
|
|
readonly List<PostgresDomainType> _domainTypesMutable = new();
|
|
|
|
internal IReadOnlyList<PostgresBaseType> BaseTypes => _baseTypesMutable;
|
|
internal IReadOnlyList<PostgresArrayType> ArrayTypes => _arrayTypesMutable;
|
|
internal IReadOnlyList<PostgresRangeType> RangeTypes => _rangeTypesMutable;
|
|
internal IReadOnlyList<PostgresMultirangeType> MultirangeTypes => _multirangeTypesMutable;
|
|
internal IReadOnlyList<PostgresEnumType> EnumTypes => _enumTypesMutable;
|
|
internal IReadOnlyList<PostgresCompositeType> CompositeTypes => _compositeTypesMutable;
|
|
internal IReadOnlyList<PostgresDomainType> DomainTypes => _domainTypesMutable;
|
|
|
|
/// <summary>
|
|
/// Indexes backend types by their type OID.
|
|
/// </summary>
|
|
internal Dictionary<uint, PostgresType> ByOID { get; } = new();
|
|
|
|
/// <summary>
|
|
/// Indexes backend types by their PostgreSQL name, including namespace (e.g. pg_catalog.int4).
|
|
/// Only used for enums and composites.
|
|
/// </summary>
|
|
internal Dictionary<string, PostgresType> ByFullName { get; } = new();
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
internal Dictionary<string, PostgresType?> ByName { get; } = new();
|
|
|
|
/// <summary>
|
|
/// Initializes the instance of <see cref="NpgsqlDatabaseInfo"/>.
|
|
/// </summary>
|
|
protected NpgsqlDatabaseInfo(string host, int port, string databaseName, Version version)
|
|
: this(host, port, databaseName, version, version.ToString())
|
|
{ }
|
|
|
|
/// <summary>
|
|
/// Initializes the instance of <see cref="NpgsqlDatabaseInfo"/>.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
public PostgresType GetPostgresTypeByName(string pgName)
|
|
=> TryGetPostgresTypeByName(pgName, out var pgType)
|
|
? pgType
|
|
: throw new ArgumentException($"A PostgreSQL type with the name {pgName} was not found in the database");
|
|
|
|
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 = ByFullName.Keys.Where(n => n.EndsWith($".{pgName}", StringComparison.Ordinal));
|
|
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()
|
|
{
|
|
foreach (var type in GetTypes())
|
|
{
|
|
ByOID[type.OID] = type;
|
|
ByFullName[type.FullName] = 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.
|
|
ByName[type.Name] = ByName.ContainsKey(type.Name)
|
|
? null
|
|
: type;
|
|
|
|
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();
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Provides all PostgreSQL types detected in this database.
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
protected abstract IEnumerable<PostgresType> GetTypes();
|
|
|
|
#endregion Types
|
|
|
|
#region Misc
|
|
|
|
/// <summary>
|
|
/// Parses a PostgreSQL server version (e.g. 10.1, 9.6.3) and returns a CLR Version.
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Registers a new database info factory, which is used to load information about databases.
|
|
/// </summary>
|
|
public static void RegisterFactory(INpgsqlDatabaseInfoFactory factory)
|
|
{
|
|
if (factory == null)
|
|
throw new ArgumentNullException(nameof(factory));
|
|
|
|
var factories = new INpgsqlDatabaseInfoFactory[Factories.Length + 1];
|
|
factories[0] = factory;
|
|
Array.Copy(Factories, 0, factories, 1, Factories.Length);
|
|
Factories = factories;
|
|
|
|
Cache.Clear();
|
|
}
|
|
|
|
internal static async Task<NpgsqlDatabaseInfo> Load(NpgsqlConnector conn, NpgsqlTimeout timeout, bool async)
|
|
{
|
|
foreach (var factory in Factories)
|
|
{
|
|
var dbInfo = await factory.Load(conn, timeout, async);
|
|
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 INpgsqlDatabaseInfoFactory[]
|
|
{
|
|
new PostgresMinimalDatabaseInfoFactory(),
|
|
new PostgresDatabaseInfoFactory()
|
|
};
|
|
Cache.Clear();
|
|
}
|
|
|
|
#endregion Factory management
|
|
} |