using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Npgsql.Logging;
using Npgsql.PostgresTypes;
using Npgsql.Util;
// ReSharper disable StringLiteralTypo
// ReSharper disable CommentTypo
namespace Npgsql
{
///
/// The default implementation of , for standard PostgreSQL databases..
///
class PostgresDatabaseInfoFactory : INpgsqlDatabaseInfoFactory
{
///
public async Task Load(NpgsqlConnection conn, NpgsqlTimeout timeout, bool async)
{
var db = new PostgresDatabaseInfo(conn);
await db.LoadPostgresInfo(conn, timeout, async);
return db;
}
}
///
/// The default implementation of NpgsqlDatabase, for standard PostgreSQL databases.
///
class PostgresDatabaseInfo : NpgsqlDatabaseInfo
{
///
/// The Npgsql logger instance.
///
static readonly NpgsqlLogger Log = NpgsqlLogManager.CreateLogger(nameof(PostgresDatabaseInfo));
///
/// The PostgreSQL types detected in the database.
///
List? _types;
///
protected override IEnumerable GetTypes() => _types ?? Enumerable.Empty();
///
/// True if the backend is Amazon Redshift; otherwise, false.
///
public bool IsRedshift { get; private set; }
///
public override bool SupportsUnlisten => Version >= new Version(6, 4, 0) && !IsRedshift;
///
/// True if the 'pg_enum' table includes the 'enumsortorder' column; otherwise, false.
///
public virtual bool HasEnumSortOrder => Version >= new Version(9, 1, 0);
///
/// True if the 'pg_type' table includes the 'typcategory' column; otherwise, false.
///
///
/// pg_type.typcategory is added after 8.4.
/// see: https://www.postgresql.org/docs/8.4/static/catalog-pg-type.html#CATALOG-TYPCATEGORY-TABLE
///
public virtual bool HasTypeCategory => Version >= new Version(8, 4, 0);
internal PostgresDatabaseInfo(NpgsqlConnection conn)
: base(conn.Host!, conn.Port, conn.Database!, ParseServerVersion(conn.PostgresParameters["server_version"]))
{
}
///
/// Loads database information from the PostgreSQL database specified by .
///
/// The database connection.
/// The timeout while loading types from the backend.
/// True to load types asynchronously.
///
/// A task representing the asynchronous operation.
///
internal async Task LoadPostgresInfo(NpgsqlConnection conn, NpgsqlTimeout timeout, bool async)
{
HasIntegerDateTimes =
conn.PostgresParameters.TryGetValue("integer_datetimes", out var intDateTimes) &&
intDateTimes == "on";
IsRedshift = conn.Settings.ServerCompatibilityMode == ServerCompatibilityMode.Redshift;
_types = await LoadBackendTypes(conn, timeout, async);
}
///
/// Generates a raw SQL query string to select type information.
///
/// True to load range types.
/// True to load enum types.
///
/// True to load table composites.
/// True if the 'pg_type' table includes the 'typcategory' column
///
/// A raw SQL query string that selects type information.
///
///
/// Select all types (base, array which is also base, enum, range, composite).
/// Note that arrays are distinguished from primitive types through them having typreceive=array_recv.
/// Order by primitives first, container later.
/// For arrays and ranges, join in the element OID and type (to filter out arrays of unhandled
/// types).
///
static string GenerateTypesQuery(bool withRange, bool withEnum, bool withEnumSortOrder, bool loadTableComposites, bool withTypeCategory)
=> $@"
/*** Load all supported types ***/
SELECT ns.nspname, a.typname, a.oid, a.typbasetype, a.typnotnull,
CASE WHEN pg_proc.proname='array_recv' THEN 'a' ELSE a.typtype END AS typtype,
CASE
WHEN pg_proc.proname='array_recv' THEN a.typelem
{(withRange ? "WHEN a.typtype='r' THEN rngsubtype" : "")}
ELSE 0
END AS typelem,
CASE
{(withTypeCategory ? "WHEN a.typtype='d' AND a.typcategory='A' THEN 4 /* Domains over arrays last */" : "")}
WHEN pg_proc.proname IN ('array_recv','oidvectorrecv') THEN 3 /* Arrays before */
WHEN a.typtype='r' THEN 2 /* Ranges before */
WHEN a.typtype='d' THEN 1 /* Domains before */
ELSE 0 /* Base types first */
END AS ord
FROM pg_type AS a
JOIN pg_namespace AS ns ON (ns.oid = a.typnamespace)
JOIN pg_proc ON pg_proc.oid = a.typreceive
LEFT OUTER JOIN pg_class AS cls ON (cls.oid = a.typrelid)
LEFT OUTER JOIN pg_type AS b ON (b.oid = a.typelem)
LEFT OUTER JOIN pg_class AS elemcls ON (elemcls.oid = b.typrelid)
{(withRange ? "LEFT OUTER JOIN pg_range ON (pg_range.rngtypid = a.oid) " : "")}
WHERE
a.typtype IN ('b', 'r', 'e', 'd') OR /* Base, range, enum, domain */
(a.typtype = 'c' AND {(loadTableComposites ? "ns.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')" : "cls.relkind='c'")}) OR /* User-defined free-standing composites (not table composites) by default */
(pg_proc.proname='array_recv' AND (
b.typtype IN ('b', 'r', 'e', 'd') OR /* Array of base, range, enum, domain */
(b.typtype = 'p' AND b.typname IN ('record', 'void')) OR /* Arrays of special supported pseudo-types */
(b.typtype = 'c' AND elemcls.relkind='c') /* Array of user-defined free-standing composites (not table composites) */
)) OR
(a.typtype = 'p' AND a.typname IN ('record', 'void')) /* Some special supported pseudo-types */
ORDER BY ord;
/*** Load field definitions for (free-standing) composite types ***/
SELECT typ.oid, att.attname, att.atttypid
FROM pg_type AS typ
JOIN pg_namespace AS ns ON (ns.oid = typ.typnamespace)
JOIN pg_class AS cls ON (cls.oid = typ.typrelid)
JOIN pg_attribute AS att ON (att.attrelid = typ.typrelid)
WHERE
(typ.typtype = 'c' AND {(loadTableComposites ? "ns.nspname NOT IN ('pg_catalog', 'information_schema', 'pg_toast')" : "cls.relkind='c'")}) AND
attnum > 0 AND /* Don't load system attributes */
NOT attisdropped
ORDER BY typ.oid, att.attnum;
{(withEnum ? $@"
/*** Load enum fields ***/
SELECT pg_type.oid, enumlabel
FROM pg_enum
JOIN pg_type ON pg_type.oid=enumtypid
ORDER BY oid{(withEnumSortOrder ? ", enumsortorder" : "")};" : "")}
";
///
/// Loads type information from the backend specified by .
///
/// The database connection.
/// The timeout while loading types from the backend.
/// True to load types asynchronously.
///
/// A collection of types loaded from the backend.
///
///
/// Unknown typtype for type '{internalName}' in pg_type: {typeChar}.
internal async Task> LoadBackendTypes(NpgsqlConnection conn, NpgsqlTimeout timeout, bool async)
{
var commandTimeout = 0; // Default to infinity
if (timeout.IsSet)
{
commandTimeout = (int)timeout.TimeLeft.TotalSeconds;
if (commandTimeout <= 0)
throw new TimeoutException();
}
var typeLoadingQuery = GenerateTypesQuery(SupportsRangeTypes, SupportsEnumTypes, HasEnumSortOrder, conn.Settings.LoadTableComposites, HasTypeCategory);
using var command = new NpgsqlCommand(typeLoadingQuery, conn)
{
CommandTimeout = commandTimeout,
AllResultTypesAreUnknown = true
};
using var reader = async ? await command.ExecuteReaderAsync() : command.ExecuteReader();
var byOID = new Dictionary();
// First load the types themselves
while (async ? await reader.ReadAsync() : reader.Read())
{
timeout.Check();
var ns = reader.GetString("nspname");
var internalName = reader.GetString("typname");
var oid = uint.Parse(reader.GetString("oid"), NumberFormatInfo.InvariantInfo);
Debug.Assert(oid != 0);
var typeChar = reader.GetChar("typtype");
switch (typeChar)
{
case 'b': // Normal base type
var baseType = new PostgresBaseType(ns, internalName, oid);
byOID[baseType.OID] = baseType;
continue;
case 'a': // Array
{
var elementOID = uint.Parse(reader.GetString("typelem"), NumberFormatInfo.InvariantInfo);
Debug.Assert(elementOID > 0);
if (!byOID.TryGetValue(elementOID, out var elementPostgresType))
{
Log.Trace($"Array type '{internalName}' refers to unknown element with OID {elementOID}, skipping", conn.ProcessID);
continue;
}
var arrayType = new PostgresArrayType(ns, internalName, oid, elementPostgresType);
byOID[arrayType.OID] = arrayType;
continue;
}
case 'r': // Range
{
var elementOID = uint.Parse(reader.GetString("typelem"), NumberFormatInfo.InvariantInfo);
Debug.Assert(elementOID > 0);
if (!byOID.TryGetValue(elementOID, out var subtypePostgresType))
{
Log.Trace($"Range type '{internalName}' refers to unknown subtype with OID {elementOID}, skipping", conn.ProcessID);
continue;
}
var rangeType = new PostgresRangeType(ns, internalName, oid, subtypePostgresType);
byOID[rangeType.OID] = rangeType;
continue;
}
case 'e': // Enum
var enumType = new PostgresEnumType(ns, internalName, oid);
byOID[enumType.OID] = enumType;
continue;
case 'c': // Composite
// Unlike other types, we don't
var compositeType = new PostgresCompositeType(ns, internalName, oid);
byOID[compositeType.OID] = compositeType;
continue;
case 'd': // Domain
var baseTypeOID = uint.Parse(reader.GetString("typbasetype"), NumberFormatInfo.InvariantInfo);
Debug.Assert(baseTypeOID > 0);
if (!byOID.TryGetValue(baseTypeOID, out var basePostgresType))
{
Log.Trace($"Domain type '{internalName}' refers to unknown base type with OID {baseTypeOID}, skipping", conn.ProcessID);
continue;
}
var domainType = new PostgresDomainType(ns, internalName, oid, basePostgresType, reader.GetString("typnotnull") == "t");
byOID[domainType.OID] = domainType;
continue;
case 'p': // pseudo-type (record, void)
// Hack this as a base type
goto case 'b';
default:
throw new ArgumentOutOfRangeException($"Unknown typtype for type '{internalName}' in pg_type: {typeChar}");
}
}
if (async)
await reader.NextResultAsync();
else
reader.NextResult();
LoadCompositeFields(reader, byOID);
if (SupportsEnumTypes)
{
if (async)
await reader.NextResultAsync();
else
reader.NextResult();
LoadEnumLabels(reader, byOID);
}
return byOID.Values.ToList();
}
///
/// Loads composite fields for the composite type specified by the OID.
///
/// The reader from which to read composite fields.
/// The OID of the composite type for which fields are read.
static void LoadCompositeFields(NpgsqlDataReader reader, Dictionary byOID)
{
var currentOID = uint.MaxValue;
PostgresCompositeType? currentComposite = null;
var skipCurrent = false;
while (reader.Read())
{
var oid = uint.Parse(reader.GetString("oid"), NumberFormatInfo.InvariantInfo);
if (oid != currentOID)
{
currentOID = oid;
if (!byOID.TryGetValue(oid, out var type)) // See #2020
{
Log.Warn($"Skipping composite type with OID {oid} which was not found in pg_type");
byOID.Remove(oid);
skipCurrent = true;
continue;
}
currentComposite = type as PostgresCompositeType;
if (currentComposite == null)
{
Log.Warn($"Type {type.Name} was referenced as a composite type but is a {type.GetType()}");
byOID.Remove(oid);
skipCurrent = true;
continue;
}
skipCurrent = false;
}
if (skipCurrent)
continue;
var fieldName = reader.GetString("attname");
var fieldTypeOID = uint.Parse(reader.GetString("atttypid"), NumberFormatInfo.InvariantInfo);
if (!byOID.TryGetValue(fieldTypeOID, out var fieldType)) // See #2020
{
Log.Warn($"Skipping composite type {currentComposite!.DisplayName} with field {fieldName} with type OID {fieldTypeOID}, which could not be resolved to a PostgreSQL type.");
byOID.Remove(oid);
skipCurrent = true;
continue;
}
currentComposite!.MutableFields.Add(new PostgresCompositeType.Field(fieldName, fieldType));
}
}
///
/// Loads enum labels for the enum type specified by the OID.
///
/// The reader from which to read enum labels.
/// The OID of the enum type for which labels are read.
static void LoadEnumLabels(NpgsqlDataReader reader, Dictionary byOID)
{
var currentOID = uint.MaxValue;
PostgresEnumType? currentEnum = null;
var skipCurrent = false;
while (reader.Read())
{
var oid = uint.Parse(reader.GetString("oid"), NumberFormatInfo.InvariantInfo);
if (oid != currentOID)
{
currentOID = oid;
if (!byOID.TryGetValue(oid, out var type)) // See #2020
{
Log.Warn($"Skipping enum type with OID {oid} which was not found in pg_type");
byOID.Remove(oid);
skipCurrent = true;
continue;
}
currentEnum = type as PostgresEnumType;
if (currentEnum == null)
{
Log.Warn($"Type {type.Name} was referenced as an enum type but is a {type.GetType()}");
byOID.Remove(oid);
skipCurrent = true;
continue;
}
skipCurrent = false;
}
if (skipCurrent)
continue;
currentEnum!.MutableLabels.Add(reader.GetString("enumlabel"));
}
}
}
}