{{ message }}
Repository navigation
CLR enum underlying type mapping #6581
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
NinoFloris
wants to merge
11
commits into
npgsql:main
Choose a base branch
from
NinoFloris:enum-underlying-type-mapping
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+949
−305
Open
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
5c79310
Rename Enum handling to PgEnum
NinoFloris a37a6d3
Add unmapped enum reading and writing through underlying type
NinoFloris 9e0e6a6
Add tests
NinoFloris a0dbacc
Fix unsigned support
NinoFloris 6bac631
Don't infer enum mappings
NinoFloris 913a8b6
Add nullable enum support
NinoFloris 56fb930
Support nullable enums in arrays
NinoFloris 6ff3d2c
Address feedback
NinoFloris b7cc9fa
Fix signed-byte enum conversion and nullable enum object writes
NinoFloris 740e716
Add typed nullable enum write dispatch on JIT runtimes
NinoFloris 7527c19
Test enum underlying mappings in composite fields
NinoFloris File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
152 changes: 152 additions & 0 deletions
152
src/Npgsql/Internal/Converters/EnumUnderlyingConverter.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| using System; | ||
| using System.Numerics; | ||
| using System.Runtime.CompilerServices; | ||
| using System.Threading; | ||
| using System.Threading.Tasks; | ||
|
|
||
| namespace Npgsql.Internal.Converters; | ||
|
|
||
| /// <summary> | ||
| /// Non-generic marker for <see cref="EnumUnderlyingConverter{T}"/>. Used by <see cref="PgConcreteTypeInfo"/> at | ||
| /// construction time to enforce that only this converter may participate in enum reported-type widening. | ||
| /// </summary> | ||
| interface IEnumUnderlyingConverter | ||
| { | ||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| static bool IsEnumUnderlyingConversion<T>(PgConverter converter) | ||
| => typeof(T).IsEnum && Enum.GetUnderlyingType(typeof(T)) == converter.TypeToConvert && converter is IEnumUnderlyingConverter; | ||
|
|
||
| // Typed enum-underlying helpers. The enum-typed instantiations are reached only via the dispatch sites' | ||
| // RuntimeFeature.IsDynamicCodeSupported gate, which ILC discharges to false under NAOT — so they don't get | ||
| // instantiated per enum-T (no viral bloat). Under JIT, RyuJIT folds the typecode switch per concrete T at | ||
| // codegen time, leaving only the matching case live. T is the enum or (via the converter's own Read/Write) | ||
| // its underlying primitive; Unsafe.BitCast reinterprets the layout-identical bits — signed and unsigned | ||
| // alike — without boxing. | ||
| [MethodImpl(MethodImplOptions.NoInlining)] | ||
| static T ReadAsEnumUnderlying<T>(PgReader reader) | ||
| => Type.GetTypeCode(typeof(T)) switch | ||
| { | ||
| TypeCode.Int32 or TypeCode.UInt32 => Unsafe.BitCast<int, T>(reader.ReadInt32()), | ||
| TypeCode.Int64 or TypeCode.UInt64 => Unsafe.BitCast<long, T>(reader.ReadInt64()), | ||
| TypeCode.Int16 or TypeCode.UInt16 => Unsafe.BitCast<short, T>(reader.ReadInt16()), | ||
| TypeCode.Byte => Unsafe.BitCast<byte, T>(checked((byte)reader.ReadInt16())), | ||
| TypeCode.SByte => Unsafe.BitCast<sbyte, T>(checked((sbyte)reader.ReadInt16())), | ||
| _ => throw new NotSupportedException() | ||
| }; | ||
|
NinoFloris marked this conversation as resolved.
|
||
|
|
||
| static bool IsDbNullAsEnumUnderlying<T>(T? value, object? writeState) => false; | ||
|
|
||
| [MethodImpl(MethodImplOptions.NoInlining)] | ||
| static Size BindAsEnumUnderlying<T>(in BindContext context, T value, ref object? writeState) | ||
| { | ||
| if (context.IsBindOptional) | ||
| return context.BufferRequirement; | ||
|
|
||
| throw new NotSupportedException(); | ||
| } | ||
|
|
||
| [MethodImpl(MethodImplOptions.NoInlining)] | ||
| static void WriteAsEnumUnderlying<T>(PgWriter writer, T value) | ||
| { | ||
| switch (Type.GetTypeCode(typeof(T))) | ||
|
NinoFloris marked this conversation as resolved.
|
||
| { | ||
| case TypeCode.Int32 or TypeCode.UInt32: | ||
| writer.WriteInt32(Unsafe.BitCast<T, int>(value)); | ||
| break; | ||
| case TypeCode.Int64 or TypeCode.UInt64: | ||
| writer.WriteInt64(Unsafe.BitCast<T, long>(value)); | ||
| break; | ||
| case TypeCode.Int16 or TypeCode.UInt16: | ||
| writer.WriteInt16(Unsafe.BitCast<T, short>(value)); | ||
| break; | ||
| case TypeCode.Byte: | ||
| writer.WriteInt16(Unsafe.BitCast<T, byte>(value)); | ||
| break; | ||
| case TypeCode.SByte: | ||
| writer.WriteInt16(Unsafe.BitCast<T, sbyte>(value)); | ||
| break; | ||
| default: throw new NotSupportedException(); | ||
| } | ||
| } | ||
|
|
||
| // Known PG wire sizes per .NET underlying type — matches the default Ado mappings. byte/sbyte/short/ushort | ||
| // → Int2 (2 bytes), int/uint → Int4 (4 bytes), long/ulong → Int8 (8 bytes). PG has no unsigned types, so | ||
| // unsigned .NET underlyings are bit-reinterpreted onto the signed wire format of the same width. | ||
| static int WireSize<T>() | ||
| => Type.GetTypeCode(typeof(T)) switch | ||
| { | ||
| TypeCode.Int32 or TypeCode.UInt32 => sizeof(int), | ||
| TypeCode.Int64 or TypeCode.UInt64 => sizeof(long), | ||
| TypeCode.Int16 or TypeCode.UInt16 or TypeCode.Byte or TypeCode.SByte => sizeof(short), | ||
| _ => throw new NotSupportedException() | ||
| }; | ||
|
|
||
| // Typed Enum.ToObject overload — one allocation (the boxed enum), no intermediate boxed underlying. | ||
| // Used by both the value and nullable converter's AsObject paths. | ||
| [MethodImpl(MethodImplOptions.NoInlining)] | ||
| static object ReadAsBoxedEnum<T>(PgReader reader, Type enumType) | ||
| => Type.GetTypeCode(typeof(T)) switch | ||
| { | ||
| TypeCode.Int32 or TypeCode.UInt32 => Enum.ToObject(enumType, reader.ReadInt32()), | ||
| TypeCode.Int64 or TypeCode.UInt64 => Enum.ToObject(enumType, reader.ReadInt64()), | ||
| TypeCode.Int16 or TypeCode.UInt16 => Enum.ToObject(enumType, reader.ReadInt16()), | ||
| TypeCode.Byte => Enum.ToObject(enumType, checked((byte)reader.ReadInt16())), | ||
| TypeCode.SByte => Enum.ToObject(enumType, checked((sbyte)reader.ReadInt16())), | ||
| _ => throw new NotSupportedException() | ||
| }; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Converter for a CLR enum's underlying primitive. Reads/writes the primitive directly using the PG wire format | ||
| /// Npgsql's default mappings use for the corresponding .NET type (byte/sbyte/short → smallint, int → integer, | ||
| /// long → bigint), and re-materializes the boxed value as the enum on the AsObject path via | ||
| /// <see cref="Enum.ToObject(Type, object)"/>. Inherits <see cref="PgBufferedConverter{T}"/> so all per-T async | ||
| /// accessor codegen is provided by the base — only the AsObject read needs an override here. Carrying the enum | ||
| /// type here lets callers holding a non-generic <see cref="PgConverter"/> reference get a correctly-boxed enum. | ||
| /// </summary> | ||
| sealed class EnumUnderlyingConverter<T>(Type enumType) : PgBufferedConverter<T>, IEnumUnderlyingConverter | ||
| where T : unmanaged, IBinaryInteger<T> | ||
| { | ||
| public override ConverterDescriptor GetDescriptor(in DescriptorContext context) | ||
| => ConverterDescriptor.Invariant with { BufferRequirements = BufferRequirements.CreateFixedSize(IEnumUnderlyingConverter.WireSize<T>()) }; | ||
|
|
||
| public override T Read(PgReader reader) | ||
| => IEnumUnderlyingConverter.ReadAsEnumUnderlying<T>(reader); | ||
|
|
||
| public override void Write(PgWriter writer, T value) | ||
| => IEnumUnderlyingConverter.WriteAsEnumUnderlying(writer, value); | ||
|
|
||
|
NinoFloris marked this conversation as resolved.
|
||
| // Typed Enum.ToObject overload — exactly one allocation per call (the boxed enum), no intermediate boxed | ||
| // underlying. No async machinery: reads are buffered, result is synchronously available. | ||
| internal override ValueTask<object?> ReadAsObject(bool async, PgReader reader, CancellationToken cancellationToken) | ||
| => new(IEnumUnderlyingConverter.ReadAsBoxedEnum<T>(reader, enumType)); | ||
| } | ||
|
NinoFloris marked this conversation as resolved.
|
||
|
|
||
| /// <summary> | ||
| /// Nullable sibling of <see cref="EnumUnderlyingConverter{T}"/>. Same wire format, same boxing behavior on the | ||
| /// AsObject path (boxed <see cref="Nullable{T}"/> strips the wrapper, so a boxed enum unboxes to TEnum? at the | ||
| /// caller). Read/Write delegate to the shared <see cref="IEnumUnderlyingConverter"/> helpers; the framework's | ||
| /// null-handling stops at the wire-length check, so Read/Write only see non-null values here. | ||
| /// </summary> | ||
| sealed class EnumUnderlyingNullableConverter<T>(Type enumType) : PgBufferedConverter<T?>, IEnumUnderlyingConverter | ||
| where T : unmanaged, IBinaryInteger<T> | ||
| { | ||
| public override ConverterDescriptor GetDescriptor(in DescriptorContext context) | ||
| => ConverterDescriptor.Invariant with { BufferRequirements = BufferRequirements.CreateFixedSize(IEnumUnderlyingConverter.WireSize<T>()) }; | ||
|
|
||
| public override T? Read(PgReader reader) | ||
| => IEnumUnderlyingConverter.ReadAsEnumUnderlying<T>(reader); | ||
|
|
||
| public override void Write(PgWriter writer, T? value) | ||
| => IEnumUnderlyingConverter.WriteAsEnumUnderlying(writer, value.GetValueOrDefault()); | ||
|
|
||
| internal override ValueTask WriteAsObject(bool async, PgWriter writer, object? value, CancellationToken cancellationToken) | ||
| { | ||
| // A boxed enum can unbox as its underlying T, but not directly as Nullable<T>. | ||
| Write(writer, value is null ? null : (T)value); | ||
| return default; | ||
| } | ||
|
|
||
| internal override ValueTask<object?> ReadAsObject(bool async, PgReader reader, CancellationToken cancellationToken) | ||
| => new(IEnumUnderlyingConverter.ReadAsBoxedEnum<T>(reader, enumType)); | ||
| } | ||
|
NinoFloris marked this conversation as resolved.
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
You can’t perform that action at this time.

Uh oh!
There was an error while loading. Please reload this page.