Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/Npgsql/Internal/AdoSerializerHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ public static PgTypeInfo GetTypeInfoForReading(Type type, PgTypeId pgTypeId, PgS
try
{
typeInfo = options.GetTypeInfoInternal(type, pgTypeId);
if (typeInfo is { SupportsReading: false })
typeInfo = null;
}
catch (Exception ex)
{
Expand All @@ -41,6 +43,8 @@ public static PgTypeInfo GetTypeInfoForWriting(Type? type, PgTypeId? pgTypeId, P
try
{
typeInfo = options.GetTypeInfoInternal(type, pgTypeId);
if (typeInfo is { SupportsWriting: false })
typeInfo = null;
}
catch (Exception ex)
{
Expand Down
7 changes: 7 additions & 0 deletions src/Npgsql/Internal/PgTypeInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ public class PgTypeInfo
Options = options;
IsBoxing = unboxedType is not null;
Type = unboxedType ?? type;
SupportsReading = GetDefaultSupportsReading(type, unboxedType);
SupportsWriting = true;
}

Expand Down Expand Up @@ -54,6 +55,7 @@ private protected PgTypeInfo(PgSerializerOptions options, Type type, PgConverter
public Type Type { get; }
public PgSerializerOptions Options { get; }

public bool SupportsReading { get; init; }
public bool SupportsWriting { get; init; }
public DataFormat? PreferredFormat { get; init; }

Expand Down Expand Up @@ -240,6 +242,11 @@ DataFormat ResolveFormat(PgConverter converter, out BufferRequirements bufferReq
return default;
}
}

// We assume a boxing type info does not support reading as the converter won't be able to produce the derived type statically.
// Cases like Array converters unboxing to int[], int[,] etc. are the exception and the reason why SupportsReading is a settable property.
internal static bool GetDefaultSupportsReading(Type type, Type? unboxedType)
=> unboxedType is null || unboxedType == type;
}

public sealed class PgResolverTypeInfo(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,10 @@ static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings)
mapping => mapping with { MatchRequirement = MatchRequirement.DataTypeName, TypeMatchPredicate = type => typeof(Stream).IsAssignableFrom(type) });
//Special mappings, these have no corresponding array mapping.
mappings.AddType<TextReader>(DataTypeNames.Text,
static (options, mapping, _) => mapping.CreateInfo(options, new TextReaderTextConverter(options.TextEncoding), supportsWriting: false, preferredFormat: DataFormat.Text),
static (options, mapping, _) => mapping.CreateInfo(options, new TextReaderTextConverter(options.TextEncoding), preferredFormat: DataFormat.Text, supportsWriting: false),
MatchRequirement.DataTypeName);
mappings.AddStructType<GetChars>(DataTypeNames.Text,
static (options, mapping, _) => mapping.CreateInfo(options, new GetCharsTextConverter(options.TextEncoding), supportsWriting: false, preferredFormat: DataFormat.Text),
static (options, mapping, _) => mapping.CreateInfo(options, new GetCharsTextConverter(options.TextEncoding), preferredFormat: DataFormat.Text, supportsWriting: false),
MatchRequirement.DataTypeName);

// Alternative text types
Expand All @@ -118,10 +118,10 @@ static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings)
mapping => mapping with { MatchRequirement = MatchRequirement.DataTypeName, TypeMatchPredicate = type => typeof(Stream).IsAssignableFrom(type) });
//Special mappings, these have no corresponding array mapping.
mappings.AddType<TextReader>(dataTypeName,
static (options, mapping, _) => mapping.CreateInfo(options, new TextReaderTextConverter(options.TextEncoding), supportsWriting: false, preferredFormat: DataFormat.Text),
static (options, mapping, _) => mapping.CreateInfo(options, new TextReaderTextConverter(options.TextEncoding), preferredFormat: DataFormat.Text, supportsWriting: false),
MatchRequirement.DataTypeName);
mappings.AddStructType<GetChars>(dataTypeName,
static (options, mapping, _) => mapping.CreateInfo(options, new GetCharsTextConverter(options.TextEncoding), supportsWriting: false, preferredFormat: DataFormat.Text),
static (options, mapping, _) => mapping.CreateInfo(options, new GetCharsTextConverter(options.TextEncoding), preferredFormat: DataFormat.Text, supportsWriting: false),
MatchRequirement.DataTypeName);
}

Expand All @@ -142,10 +142,10 @@ static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings)
mapping => mapping with { MatchRequirement = MatchRequirement.DataTypeName, TypeMatchPredicate = type => typeof(Stream).IsAssignableFrom(type) });
//Special mappings, these have no corresponding array mapping.
mappings.AddType<TextReader>(DataTypeNames.Jsonb,
static (options, mapping, _) => mapping.CreateInfo(options, new VersionPrefixedTextConverter<TextReader>(jsonbVersion, new TextReaderTextConverter(options.TextEncoding)), supportsWriting: false, preferredFormat: DataFormat.Text),
static (options, mapping, _) => mapping.CreateInfo(options, new VersionPrefixedTextConverter<TextReader>(jsonbVersion, new TextReaderTextConverter(options.TextEncoding)), preferredFormat: DataFormat.Text, supportsWriting: false),
MatchRequirement.DataTypeName);
mappings.AddStructType<GetChars>(DataTypeNames.Jsonb,
static (options, mapping, _) => mapping.CreateInfo(options, new VersionPrefixedTextConverter<GetChars>(jsonbVersion, new GetCharsTextConverter(options.TextEncoding)), supportsWriting: false, preferredFormat: DataFormat.Text),
static (options, mapping, _) => mapping.CreateInfo(options, new VersionPrefixedTextConverter<GetChars>(jsonbVersion, new GetCharsTextConverter(options.TextEncoding)), preferredFormat: DataFormat.Text, supportsWriting: false),
MatchRequirement.DataTypeName);

// Jsonpath
Expand All @@ -154,10 +154,10 @@ static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings)
static (options, mapping, _) => mapping.CreateInfo(options, new VersionPrefixedTextConverter<string>(jsonpathVersion, new StringTextConverter(options.TextEncoding))), isDefault: true);
//Special mappings, these have no corresponding array mapping.
mappings.AddType<TextReader>(DataTypeNames.Jsonpath,
static (options, mapping, _) => mapping.CreateInfo(options, new VersionPrefixedTextConverter<TextReader>(jsonpathVersion, new TextReaderTextConverter(options.TextEncoding)), supportsWriting: false, preferredFormat: DataFormat.Text),
static (options, mapping, _) => mapping.CreateInfo(options, new VersionPrefixedTextConverter<TextReader>(jsonpathVersion, new TextReaderTextConverter(options.TextEncoding)), preferredFormat: DataFormat.Text, supportsWriting: false),
MatchRequirement.DataTypeName);
mappings.AddStructType<GetChars>(DataTypeNames.Jsonpath,
static (options, mapping, _) => mapping.CreateInfo(options, new VersionPrefixedTextConverter<GetChars>(jsonpathVersion, new GetCharsTextConverter(options.TextEncoding)), supportsWriting: false, preferredFormat: DataFormat.Text),
static (options, mapping, _) => mapping.CreateInfo(options, new VersionPrefixedTextConverter<GetChars>(jsonpathVersion, new GetCharsTextConverter(options.TextEncoding)), preferredFormat: DataFormat.Text, supportsWriting: false),
MatchRequirement.DataTypeName);

// Bytea
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,9 @@ static TypeInfoMappingCollection AddMappings(TypeInfoMappingCollection mappings)

// inet
// There are certain IPAddress values like Loopback or Any that return a *private* derived type (see https://github.com/dotnet/runtime/issues/27870).
// However we still need to be able to resolve some typed converter for those values.
// We do so by returning a boxing info when we deal with a derived type, as a result we don't need an exact typed converter.
// For arrays users can't actually reference the private type so we'll only see some version of ArrayType<IPAddress>.
// For reads we'll only see the public type so we never surface an InvalidCastException trying to cast IPAddress to ReadOnlyIPAddress.
// Finally we add a custom predicate to be able to match any type which values are assignable to IPAddress.
mappings.AddType<IPAddress>(DataTypeNames.Inet,
static (options, mapping, _) => new PgTypeInfo(options, new IPAddressConverter(),
new DataTypeName(mapping.DataTypeName), unboxedType: mapping.Type == typeof(IPAddress) ? null : mapping.Type),
static (options, mapping, _) => new PgTypeInfo(options, new IPAddressConverter(), new DataTypeName(mapping.DataTypeName),
unboxedType: mapping.Type != typeof(IPAddress) ? mapping.Type : null),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Super minor nit: FWIW I prefer the the previous ternary to avoid double negation

mapping => mapping with
{
MatchRequirement = MatchRequirement.Single,
Expand Down
31 changes: 17 additions & 14 deletions src/Npgsql/Internal/TypeInfoMapping.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public readonly struct TypeInfoMapping(Type type, string dataTypeName, TypeInfoF

public bool TypeEquals(Type type) => TypeMatchPredicate?.Invoke(type) ?? Type == type;

private bool DataTypeNameEqualsCore(string dataTypeName)
bool DataTypeNameEqualsCore(string dataTypeName)
{
var span = DataTypeName.AsSpan();
return Postgres.DataTypeName.IsFullyQualified(span)
Expand Down Expand Up @@ -196,7 +196,7 @@ TypeInfoMapping GetMapping(Type type, string dataTypeName)
=> TryGetMapping(type, dataTypeName, out var info) ? info : throw new InvalidOperationException($"Could not find mapping for {type} <-> {dataTypeName}");

// Helper to eliminate generic display class duplication.
static TypeInfoFactory CreateComposedFactory(Type mappingType, TypeInfoMapping innerMapping, Func<TypeInfoMapping, PgTypeInfo, PgConverter> mapper, bool copyPreferredFormat = false, bool supportsWriting = true)
static TypeInfoFactory CreateComposedFactory(Type mappingType, TypeInfoMapping innerMapping, Func<TypeInfoMapping, PgTypeInfo, PgConverter> mapper, bool copyPreferredFormat = false, bool? supportsReading = null, bool? supportsWriting = null)
=> (options, mapping, requiresDataTypeName) =>
{
var resolvedInnerMapping = innerMapping;
Expand All @@ -206,18 +206,20 @@ static TypeInfoFactory CreateComposedFactory(Type mappingType, TypeInfoMapping i
var innerInfo = innerMapping.Factory(options, resolvedInnerMapping, requiresDataTypeName);
var converter = mapper(mapping, innerInfo);
var preferredFormat = copyPreferredFormat ? innerInfo.PreferredFormat : null;
var writingSupported = supportsWriting && innerInfo.SupportsWriting;
var unboxedType = ComputeUnboxedType(defaultType: mappingType, converter.TypeToConvert, mapping.Type);
var readingSupported = innerInfo.SupportsReading && (supportsReading ?? PgTypeInfo.GetDefaultSupportsReading(converter.TypeToConvert, unboxedType));
var writingSupported = innerInfo.SupportsWriting && (supportsWriting ?? true);

return new PgTypeInfo(options, converter, options.GetCanonicalTypeId(new DataTypeName(mapping.DataTypeName)), unboxedType)
{
PreferredFormat = preferredFormat,
SupportsReading = readingSupported,
SupportsWriting = writingSupported
};
};

// Helper to eliminate generic display class duplication.
static TypeInfoFactory CreateComposedFactory(Type mappingType, TypeInfoMapping innerMapping, Func<TypeInfoMapping, PgResolverTypeInfo, PgConverterResolver> mapper, bool copyPreferredFormat = false, bool supportsWriting = true)
static TypeInfoFactory CreateComposedFactory(Type mappingType, TypeInfoMapping innerMapping, Func<TypeInfoMapping, PgResolverTypeInfo, PgConverterResolver> mapper, bool copyPreferredFormat = false, bool? supportsReading = null, bool? supportsWriting = null)
=> (options, mapping, requiresDataTypeName) =>
{
var resolvedInnerMapping = innerMapping;
Expand All @@ -227,8 +229,9 @@ static TypeInfoFactory CreateComposedFactory(Type mappingType, TypeInfoMapping i
var innerInfo = (PgResolverTypeInfo)innerMapping.Factory(options, resolvedInnerMapping, requiresDataTypeName);
var resolver = mapper(mapping, innerInfo);
var preferredFormat = copyPreferredFormat ? innerInfo.PreferredFormat : null;
var writingSupported = supportsWriting && innerInfo.SupportsWriting;
var unboxedType = ComputeUnboxedType(defaultType: mappingType, resolver.TypeToConvert, mapping.Type);
var readingSupported = innerInfo.SupportsReading && (supportsReading ?? PgTypeInfo.GetDefaultSupportsReading(resolver.TypeToConvert, unboxedType));
var writingSupported = innerInfo.SupportsWriting && (supportsWriting ?? true);
// We include the data type name if the inner info did so as well.
// This way we can rely on its logic around resolvedDataTypeName, including when it ignores that flag.
PgTypeId? pgTypeId = innerInfo.PgTypeId is not null
Expand All @@ -237,6 +240,7 @@ static TypeInfoFactory CreateComposedFactory(Type mappingType, TypeInfoMapping i
return new PgResolverTypeInfo(options, resolver, pgTypeId, unboxedType)
{
PreferredFormat = preferredFormat,
SupportsReading = readingSupported,
SupportsWriting = writingSupported
};
};
Expand Down Expand Up @@ -351,7 +355,7 @@ public void AddArrayType<TElement>(TypeInfoMapping elementMapping, bool suppress

void AddArrayType(TypeInfoMapping elementMapping, Type type, Func<TypeInfoMapping, PgTypeInfo, PgConverter> converter, Func<Type?, bool>? typeMatchPredicate = null, bool suppressObjectMapping = false)
{
var arrayMapping = new TypeInfoMapping(type, arrayDataTypeName, CreateComposedFactory(type, elementMapping, converter))
var arrayMapping = new TypeInfoMapping(type, arrayDataTypeName, CreateComposedFactory(type, elementMapping, converter, supportsReading: true))
{
MatchRequirement = elementMapping.MatchRequirement,
TypeMatchPredicate = typeMatchPredicate
Expand Down Expand Up @@ -391,7 +395,7 @@ public void AddResolverArrayType<TElement>(TypeInfoMapping elementMapping, bool

void AddResolverArrayType(TypeInfoMapping elementMapping, Type type, Func<TypeInfoMapping, PgResolverTypeInfo, PgConverterResolver> converter, Func<Type?, bool>? typeMatchPredicate = null, bool suppressObjectMapping = false)
{
var arrayMapping = new TypeInfoMapping(type, arrayDataTypeName, CreateComposedFactory(type, elementMapping, converter))
var arrayMapping = new TypeInfoMapping(type, arrayDataTypeName, CreateComposedFactory(type, elementMapping, converter, supportsReading: true))
{
MatchRequirement = elementMapping.MatchRequirement,
TypeMatchPredicate = typeMatchPredicate
Expand Down Expand Up @@ -483,12 +487,12 @@ void AddStructArrayType(TypeInfoMapping elementMapping, TypeInfoMapping nullable
Func<Type?, bool>? typeMatchPredicate, Func<Type?, bool>? nullableTypeMatchPredicate, bool suppressObjectMapping)
{
var arrayDataTypeName = GetArrayDataTypeName(elementMapping.DataTypeName);
var arrayMapping = new TypeInfoMapping(type, arrayDataTypeName, CreateComposedFactory(type, elementMapping, converter))
var arrayMapping = new TypeInfoMapping(type, arrayDataTypeName, CreateComposedFactory(type, elementMapping, converter, supportsReading: true))
{
MatchRequirement = elementMapping.MatchRequirement,
TypeMatchPredicate = typeMatchPredicate
};
var nullableArrayMapping = new TypeInfoMapping(nullableType, arrayDataTypeName, CreateComposedFactory(nullableType, nullableElementMapping, nullableConverter))
var nullableArrayMapping = new TypeInfoMapping(nullableType, arrayDataTypeName, CreateComposedFactory(nullableType, nullableElementMapping, nullableConverter, supportsReading: true))
{
MatchRequirement = arrayMapping.MatchRequirement,
TypeMatchPredicate = nullableTypeMatchPredicate
Expand Down Expand Up @@ -601,12 +605,12 @@ void AddResolverStructArrayType(TypeInfoMapping elementMapping, TypeInfoMapping
{
var arrayDataTypeName = GetArrayDataTypeName(elementMapping.DataTypeName);

var arrayMapping = new TypeInfoMapping(type, arrayDataTypeName, CreateComposedFactory(type, elementMapping, converter))
var arrayMapping = new TypeInfoMapping(type, arrayDataTypeName, CreateComposedFactory(type, elementMapping, converter, supportsReading: true))
{
MatchRequirement = elementMapping.MatchRequirement,
TypeMatchPredicate = typeMatchPredicate
};
var nullableArrayMapping = new TypeInfoMapping(nullableType, arrayDataTypeName, CreateComposedFactory(nullableType, nullableElementMapping, nullableConverter))
var nullableArrayMapping = new TypeInfoMapping(nullableType, arrayDataTypeName, CreateComposedFactory(nullableType, nullableElementMapping, nullableConverter, supportsReading: true))
{
MatchRequirement = elementMapping.MatchRequirement,
TypeMatchPredicate = nullableTypeMatchPredicate
Expand Down Expand Up @@ -654,7 +658,7 @@ void AddPolymorphicResolverArrayType(TypeInfoMapping elementMapping, Type type,
{
var arrayDataTypeName = GetArrayDataTypeName(elementMapping.DataTypeName);
var mapping = new TypeInfoMapping(type, arrayDataTypeName,
CreateComposedFactory(typeof(Array), elementMapping, converter, supportsWriting: false))
CreateComposedFactory(typeof(Array), elementMapping, converter, supportsReading: true, supportsWriting: false))
{
MatchRequirement = elementMapping.MatchRequirement,
TypeMatchPredicate = typeMatchPredicate
Expand Down Expand Up @@ -811,8 +815,7 @@ public static PgTypeInfo CreateInfo(this TypeInfoMapping mapping, PgSerializerOp
public static PgResolverTypeInfo CreateInfo(this TypeInfoMapping mapping, PgSerializerOptions options, PgConverterResolver resolver, bool includeDataTypeName)
=> new(options, resolver, includeDataTypeName ? new DataTypeName(mapping.DataTypeName) : null)
{
PreferredFormat = null,
SupportsWriting = true
PreferredFormat = null
};

/// <summary>
Expand Down
3 changes: 0 additions & 3 deletions src/Npgsql/NpgsqlParameter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -618,9 +618,6 @@ internal void Bind(out DataFormat format, out Size size, DataFormat? requiredFor
if (TypeInfo is null)
ThrowHelper.ThrowInvalidOperationException($"Missing type info, {nameof(ResolveTypeInfo)} needs to be called before {nameof(Bind)}.");

if (!TypeInfo.SupportsWriting)
ThrowHelper.ThrowNotSupportedException($"Cannot write values for parameters of type '{TypeInfo.Type}' and postgres type '{TypeInfo.Options.DatabaseInfo.GetDataTypeName(PgTypeId).DisplayName}'.");

// We might call this twice, once during validation and once during WriteBind, only compute things once.
if (WriteSize is null)
{
Expand Down
Loading