using System;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text;
using Npgsql.Internal.Postgres;
using Npgsql.NameTranslation;
using Npgsql.PostgresTypes;
namespace Npgsql.Internal;
[Experimental(NpgsqlDiagnostics.ConvertersExperimental)]
public sealed class PgSerializerOptions
{
///
/// Used by GetSchema to be able to attempt to resolve all type catalog types without exceptions.
///
[field: ThreadStatic]
internal static bool IntrospectionCaller { get; set; }
readonly PgTypeInfoResolverChain _resolverChain;
readonly Func? _timeZoneProvider;
IPgTypeInfoResolver? _typeInfoResolver;
object? _typeInfoCache;
internal PgSerializerOptions(NpgsqlDatabaseInfo databaseInfo, PgTypeInfoResolverChain? resolverChain = null, Func? timeZoneProvider = null)
{
_resolverChain = resolverChain ?? new();
_timeZoneProvider = timeZoneProvider;
DatabaseInfo = databaseInfo;
UnspecifiedDBNullTypeInfo = new(this, new Converters.Internal.VoidConverter(), DataTypeName.Unspecified, unboxedType: typeof(DBNull));
}
internal PgTypeInfo UnspecifiedDBNullTypeInfo { get; }
PostgresType? _textPgType;
internal PgTypeId TextPgTypeId => ToCanonicalTypeId(_textPgType ??= DatabaseInfo.GetPostgresType(DataTypeNames.Text));
// Used purely for type mapping, where we don't have a full set of types but resolvers might know enough.
readonly bool _introspectionInstance;
internal bool IntrospectionMode
{
get => _introspectionInstance || IntrospectionCaller;
init => _introspectionInstance = value;
}
/// Whether options should return a portable identifier (data type name) to prevent any generated id (oid) confusion across backends, this comes with a perf penalty.
internal bool PortableTypeIds { get; init; }
internal NpgsqlDatabaseInfo DatabaseInfo { get; }
public string TimeZone => _timeZoneProvider?.Invoke() ?? throw new NotSupportedException("TimeZone was not configured.");
public Encoding TextEncoding { get; init; } = Encoding.UTF8;
public IPgTypeInfoResolver TypeInfoResolver
{
get => _typeInfoResolver ??= new ChainTypeInfoResolver(_resolverChain);
internal init => _typeInfoResolver = value;
}
public bool EnableDateTimeInfinityConversions { get; init; } = true;
public ArrayNullabilityMode ArrayNullabilityMode { get; init; } = ArrayNullabilityMode.Never;
public INpgsqlNameTranslator DefaultNameTranslator { get; init; } = NpgsqlSnakeCaseNameTranslator.Instance;
public static bool IsWellKnownTextType(Type type)
{
type = type.IsValueType ? Nullable.GetUnderlyingType(type) ?? type : type;
return Array.IndexOf([
typeof(string), typeof(char),
typeof(char[]), typeof(ReadOnlyMemory), typeof(ArraySegment),
typeof(byte[]), typeof(ReadOnlyMemory)
], type) != -1 || typeof(Stream).IsAssignableFrom(type);
}
internal bool RangesEnabled => _resolverChain.RangesEnabled;
internal bool MultirangesEnabled => _resolverChain.MultirangesEnabled;
internal bool ArraysEnabled => _resolverChain.ArraysEnabled;
// We don't verify the kind of pgTypeId we get, it'll throw if it's incorrect.
// It's up to the caller to call GetCanonicalTypeId if they want to use an oid instead of a DataTypeName.
// This also makes it easier to realize it should be a cached value if infos for different CLR types are requested for the same
// pgTypeId. Effectively it should be 'impossible' to get the wrong kind via any PgConverterOptions api which is what this is mainly
// for.
PgTypeInfo? GetTypeInfoCore(Type? type, PgTypeId? pgTypeId)
=> PortableTypeIds
? ((TypeInfoCache)(_typeInfoCache ??= new TypeInfoCache(this))).GetOrAddInfo(type, pgTypeId?.DataTypeName)
: ((TypeInfoCache)(_typeInfoCache ??= new TypeInfoCache(this))).GetOrAddInfo(type, pgTypeId?.Oid);
internal PgTypeInfo? GetTypeInfoInternal(Type? type, PgTypeId? pgTypeId)
=> GetTypeInfoCore(type, pgTypeId);
public PgTypeInfo? GetDefaultTypeInfo(Type type)
=> GetTypeInfoCore(type, null);
public PgTypeInfo? GetDefaultTypeInfo(PgTypeId pgTypeId)
=> GetTypeInfoCore(null, GetCanonicalTypeId(pgTypeId));
public PgTypeInfo? GetTypeInfo(Type type, PgTypeId pgTypeId)
=> GetTypeInfoCore(type, GetCanonicalTypeId(pgTypeId));
// If a given type id is in the opposite form than what was expected it will be mapped according to the requirement.
internal PgTypeId GetCanonicalTypeId(PgTypeId pgTypeId)
=> PortableTypeIds ? DatabaseInfo.GetDataTypeName(pgTypeId) : DatabaseInfo.GetOid(pgTypeId);
// If a given type id is in the opposite form than what was expected it will be mapped according to the requirement.
internal PgTypeId ToCanonicalTypeId(PostgresType pgType)
=> PortableTypeIds ? pgType.DataTypeName : (Oid)pgType.OID;
public PgTypeId GetArrayTypeId(PgTypeId elementTypeId)
{
// Static affordance to help the global type mapper.
if (PortableTypeIds && elementTypeId.IsDataTypeName)
return elementTypeId.DataTypeName.ToArrayName();
return ToCanonicalTypeId(DatabaseInfo.GetPostgresType(elementTypeId).Array
?? throw new NotSupportedException("Cannot resolve array type id"));
}
public PgTypeId GetArrayElementTypeId(PgTypeId arrayTypeId)
{
// Static affordance to help the global type mapper.
if (PortableTypeIds && arrayTypeId.IsDataTypeName && arrayTypeId.DataTypeName.UnqualifiedNameSpan.StartsWith("_".AsSpan(), StringComparison.Ordinal))
return new DataTypeName(arrayTypeId.DataTypeName.Schema + arrayTypeId.DataTypeName.UnqualifiedNameSpan.Slice(1).ToString());
return ToCanonicalTypeId((DatabaseInfo.GetPostgresType(arrayTypeId) as PostgresArrayType)?.Element
?? throw new NotSupportedException("Cannot resolve array element type id"));
}
public PgTypeId GetRangeTypeId(PgTypeId subtypeTypeId) =>
ToCanonicalTypeId(DatabaseInfo.GetPostgresType(subtypeTypeId).Range
?? throw new NotSupportedException("Cannot resolve range type id"));
public PgTypeId GetRangeSubtypeTypeId(PgTypeId rangeTypeId) =>
ToCanonicalTypeId((DatabaseInfo.GetPostgresType(rangeTypeId) as PostgresRangeType)?.Subtype
?? throw new NotSupportedException("Cannot resolve range subtype type id"));
public PgTypeId GetMultirangeTypeId(PgTypeId rangeTypeId) =>
ToCanonicalTypeId((DatabaseInfo.GetPostgresType(rangeTypeId) as PostgresRangeType)?.Multirange
?? throw new NotSupportedException("Cannot resolve multirange type id"));
public PgTypeId GetMultirangeElementTypeId(PgTypeId multirangeTypeId) =>
ToCanonicalTypeId((DatabaseInfo.GetPostgresType(multirangeTypeId) as PostgresMultirangeType)?.Subrange
?? throw new NotSupportedException("Cannot resolve multirange element type id"));
public bool TryGetDataTypeName(PgTypeId pgTypeId, out DataTypeName dataTypeName)
{
if (DatabaseInfo.FindPostgresType(pgTypeId) is { } pgType)
{
dataTypeName = pgType.DataTypeName;
return true;
}
dataTypeName = default;
return false;
}
public DataTypeName GetDataTypeName(PgTypeId pgTypeId)
=> !TryGetDataTypeName(pgTypeId, out var name)
? throw new ArgumentException("Unknown type id", nameof(pgTypeId))
: name;
}