1111
1212namespace Npgsql . BackendMessages ;
1313
14- readonly struct ColumnInfo ( PgConverterInfo converterInfo , DataFormat dataFormat , bool asObject )
14+ readonly struct ReadConversionContext ( PgConcreteTypeInfo typeInfo , PgFieldBinding binding )
1515{
16- public PgConverterInfo ConverterInfo { get ; } = converterInfo ;
17- public DataFormat DataFormat { get ; } = dataFormat ;
18- public bool AsObject { get ; } = asObject ;
16+ public bool IsDefault => TypeInfo is null ;
17+ public PgConcreteTypeInfo TypeInfo { get ; } = typeInfo ;
18+ public PgFieldBinding Binding { get ; } = binding ;
1919}
2020
2121/// <summary>
@@ -36,7 +36,7 @@ sealed class RowDescriptionMessage : IBackendMessage
3636 FieldDescription ? [ ] _fields ;
3737 readonly Dictionary < string , int > _nameIndex ;
3838 Dictionary < string , int > ? _insensitiveIndex ;
39- ColumnInfo [ ] ? _lastConverterInfoCache ;
39+ ReadConversionContext [ ] ? _lastConverterInfoCache ;
4040
4141 internal RowDescriptionMessage ( bool connectorOwned , int numFields = 10 )
4242 {
@@ -135,14 +135,18 @@ public FieldDescription this[int ordinal]
135135 }
136136 }
137137
138- internal void SetColumnInfoCache ( ReadOnlySpan < ColumnInfo > values )
138+ [ MethodImpl ( MethodImplOptions . NoInlining ) ]
139+ internal void GetConversionContext ( int ordinal , Type type , ref ReadConversionContext result )
140+ => this [ ordinal ] . GetConversionContext ( type , ref result ) ;
141+
142+ internal void SetColumnInfoCache ( ReadOnlySpan < ReadConversionContext > values )
139143 {
140144 if ( _connectorOwned || _lastConverterInfoCache is not null )
141145 return ;
142146 Interlocked . CompareExchange ( ref _lastConverterInfoCache , values . ToArray ( ) , null ) ;
143147 }
144148
145- internal void LoadColumnInfoCache ( PgSerializerOptions options , ColumnInfo [ ] values )
149+ internal void LoadColumnInfoCache ( PgSerializerOptions options , ReadConversionContext [ ] values )
146150 {
147151 if ( _lastConverterInfoCache is not { } cache )
148152 return ;
@@ -232,7 +236,7 @@ internal FieldDescription(FieldDescription source)
232236 DataFormat = source . DataFormat ;
233237 PostgresType = source . PostgresType ;
234238 Field = source . Field ;
235- _objectInfo = source . _objectInfo ;
239+ _objectConversionContext = source . _objectConversionContext ;
236240 }
237241
238242 internal void Populate (
@@ -250,7 +254,7 @@ internal void Populate(
250254 DataFormat = dataFormat ;
251255 PostgresType = _serializerOptions . DatabaseInfo . FindPostgresType ( ( Oid ) TypeOID ) ? . GetRepresentationalType ( ) ?? UnknownBackendType . Instance ;
252256 Field = new ( Name , _serializerOptions . ToCanonicalTypeId ( PostgresType ) , TypeModifier ) ;
253- _objectInfo = default ;
257+ _objectConversionContext = default ;
254258 }
255259
256260 /// <summary>
@@ -290,25 +294,35 @@ internal void Populate(
290294 /// </summary>
291295 internal DataFormat DataFormat { get ; set ; }
292296
297+ /// <summary>
298+ /// Whether this field's data was requested in text format because the user opted into UnknownResultType
299+ /// (via NpgsqlCommand.UnknownResultTypeList or AllResultTypesAreUnknown). Bindings for such fields are
300+ /// expected to reinterpret the text bytes through a converter that could potentially only support binary formats.
301+ /// </summary>
302+ /// <remarks>
303+ /// DataFormat.Text today exclusively signals that we executed with an UnknownResultTypeList.
304+ /// If we ever want to fully support DataFormat.Text we'll need to flow UnknownResultType status separately.
305+ /// </remarks>
306+ internal bool IsUnknownResultType => DataFormat is DataFormat . Text ;
307+
293308 internal Field Field { get ; private set ; }
294309
295310 internal string TypeDisplayName => PostgresType . GetDisplayNameWithFacets ( TypeModifier ) ;
296311
297312 internal PostgresType PostgresType { get ; private set ; }
298313
299- internal Type FieldType => ObjectInfo . TypeToConvert ;
314+ internal Type FieldType => ObjectConversionContext . TypeInfo . Type ;
300315
301- ColumnInfo _objectInfo ;
302- internal PgConverterInfo ObjectInfo
316+ ReadConversionContext _objectConversionContext ;
317+ internal ReadConversionContext ObjectConversionContext
303318 {
304319 get
305320 {
306- if ( ! _objectInfo . ConverterInfo . IsDefault )
307- return _objectInfo . ConverterInfo ;
321+ if ( ! _objectConversionContext . IsDefault )
322+ return _objectConversionContext ;
308323
309- ref var info = ref _objectInfo ;
310- GetInfoCore ( null , ref _objectInfo ) ;
311- return info . ConverterInfo ;
324+ GetInfoAndBind ( null , ref _objectConversionContext ) ;
325+ return _objectConversionContext ;
312326 }
313327 }
314328
@@ -320,84 +334,74 @@ internal FieldDescription Clone()
320334 return field ;
321335 }
322336
323- internal void GetInfo ( Type type , ref ColumnInfo lastColumnInfo ) => GetInfoCore ( type , ref lastColumnInfo ) ;
324- void GetInfoCore ( Type ? type , ref ColumnInfo lastColumnInfo )
337+ internal void GetConversionContext ( Type type , ref ReadConversionContext result ) => GetInfoAndBind ( type , ref result ) ;
338+ void GetInfoAndBind ( Type ? type , ref ReadConversionContext result )
325339 {
326- Debug . Assert ( lastColumnInfo . ConverterInfo . IsDefault || (
327- ReferenceEquals ( _serializerOptions , lastColumnInfo . ConverterInfo . TypeInfo . Options ) && (
328- IsUnknownResultType ( ) && lastColumnInfo . ConverterInfo . TypeInfo . PgTypeId == _serializerOptions . TextPgTypeId ||
340+ Debug . Assert ( result . IsDefault || (
341+ ReferenceEquals ( _serializerOptions , result . TypeInfo . Options ) && (
342+ IsUnknownResultType && result . TypeInfo . PgTypeId == _serializerOptions . TextPgTypeId ||
329343 // Normal resolution
330- lastColumnInfo . ConverterInfo . TypeInfo . PgTypeId == _serializerOptions . ToCanonicalTypeId ( PostgresType ) )
344+ result . TypeInfo . PgTypeId == _serializerOptions . ToCanonicalTypeId ( PostgresType ) )
331345 ) , "Cache is bleeding over" ) ;
332346
333- if ( ! lastColumnInfo . ConverterInfo . IsDefault && lastColumnInfo . ConverterInfo . TypeToConvert == type )
347+ if ( result is { IsDefault : false , TypeInfo . Type : var typeToConvert } && typeToConvert == type )
334348 return ;
335349
336- var objectInfo = DataFormat is DataFormat . Text && type is not null ? ObjectInfo : _objectInfo . ConverterInfo ;
337- if ( objectInfo is { IsDefault : false } )
350+ var objectInfo = DataFormat is DataFormat . Text && type is not null ? ObjectConversionContext : _objectConversionContext ;
351+ if ( objectInfo . TypeInfo is not null && ( typeof ( object ) == type || objectInfo . TypeInfo . Type == type ) )
338352 {
339- if ( typeof ( object ) == type )
340- {
341- lastColumnInfo = new ( objectInfo , DataFormat , true ) ;
342- return ;
343- }
344- if ( objectInfo . TypeToConvert == type )
345- {
346- // As TypeInfoMappingCollection is always adding object mappings for
347- // default/datatypename mappings, we'll also check Converter.TypeToConvert.
348- // If we have an exact match we are still able to use e.g. a converter for ints in an unboxed fashion.
349- lastColumnInfo = new ( objectInfo , DataFormat , objectInfo . IsBoxingConverter && objectInfo . Converter . TypeToConvert != type ) ;
350- return ;
351- }
353+ result = objectInfo ;
354+ return ;
352355 }
353356
354- GetInfoSlow ( type , out lastColumnInfo ) ;
357+ Core ( type , out result ) ;
358+ if ( ! result . IsDefault && result . Binding . DataFormat != DataFormat )
359+ ThrowHelper . ThrowInvalidOperationException (
360+ $ "Binding for column '{ Name } ' produced format '{ result . Binding . DataFormat } ' but the field format is '{ DataFormat } '.") ;
355361
356362 [ MethodImpl ( MethodImplOptions . NoInlining ) ]
357- void GetInfoSlow ( Type ? type , out ColumnInfo lastColumnInfo )
363+ void Core ( Type ? type , out ReadConversionContext lastReadConversionContext )
358364 {
359- PgConverterInfo converterInfo ;
365+ PgFieldBinding binding ;
360366 switch ( DataFormat )
361367 {
362- case DataFormat . Text when IsUnknownResultType ( ) :
368+ case DataFormat . Text when IsUnknownResultType :
363369 {
364- // Try to resolve some 'pg_catalog.text' type info for the expected clr type.
370+ // Resolve the converter against pg_catalog.text, UnknownResultType reads text bytes
371+ // for any column type. Every pg_catalog.text mapping we own declares text-format support, so a converter that
372+ // can't bind to text here throws and surfaces as a missing mapping rather than getting silently reinterpreted.
365373 var typeInfo = AdoSerializerHelpers . GetTypeInfoForReading ( type ?? typeof ( string ) , _serializerOptions . TextPgTypeId , _serializerOptions ) ;
374+ var concreteTypeInfo = typeInfo . MakeConcreteForField ( Field ) ;
375+ if ( ! concreteTypeInfo . SupportsReading )
376+ AdoSerializerHelpers . ThrowReadingNotSupported ( type , _serializerOptions , _serializerOptions . TextPgTypeId , resolved : true ) ;
366377
367- // We start binding to DataFormat.Binary as it's the broadest supported format.
368- // The format however is irrelevant as 'pg_catalog.text' data is identical across either.
369- // Given we did a resolution against 'pg_catalog.text' and not the actual field type we're in reinterpretation territory anyway.
370- if ( ! typeInfo . TryBind ( Field , DataFormat . Binary , out converterInfo ) )
371- converterInfo = typeInfo . Bind ( Field , DataFormat . Text ) ;
372-
373- lastColumnInfo = new ( converterInfo , DataFormat , type != converterInfo . TypeToConvert || converterInfo . IsBoxingConverter ) ;
374-
378+ binding = concreteTypeInfo . BindField ( DataFormat . Text ) ;
379+ lastReadConversionContext = new ( concreteTypeInfo , binding ) ;
375380 break ;
376381 }
377382 case DataFormat . Binary or DataFormat . Text :
378383 {
379384 var typeInfo = AdoSerializerHelpers . GetTypeInfoForReading ( type ?? typeof ( object ) , _serializerOptions . ToCanonicalTypeId ( PostgresType ) , _serializerOptions ) ;
385+ var concreteTypeInfo = typeInfo . MakeConcreteForField ( Field ) ;
386+ if ( ! concreteTypeInfo . SupportsReading )
387+ AdoSerializerHelpers . ThrowReadingNotSupported ( type , _serializerOptions , _serializerOptions . ToCanonicalTypeId ( PostgresType ) , resolved : true ) ;
380388
381389 // If we don't support the DataFormat we'll just throw.
382- converterInfo = typeInfo . Bind ( Field , DataFormat ) ;
383- lastColumnInfo = new ( converterInfo , DataFormat , typeof ( object ) == type || converterInfo . IsBoxingConverter ) ;
390+ binding = concreteTypeInfo . BindField ( DataFormat ) ;
391+ lastReadConversionContext = new ( concreteTypeInfo , binding ) ;
384392 break ;
385393 }
386394 default :
387395 ThrowHelper . ThrowUnreachableException ( "Unknown data format {0}" , DataFormat ) ;
388- lastColumnInfo = default ;
396+ lastReadConversionContext = default ;
389397 break ;
390398 }
391399
392400 // We delay initializing ObjectOrDefaultInfo until after the first lookup (unless it is itself the first lookup).
393401 // When passed in an unsupported type it allows the error to be more specific, instead of just having object/null to deal with.
394- if ( _objectInfo . ConverterInfo . IsDefault && type is not null )
395- _ = ObjectInfo ;
402+ if ( _objectConversionContext . TypeInfo is null && type is not null )
403+ _ = ObjectConversionContext ;
396404 }
397-
398- // DataFormat.Text today exclusively signals that we executed with an UnknownResultTypeList.
399- // If we ever want to fully support DataFormat.Text we'll need to flow UnknownResultType status separately.
400- bool IsUnknownResultType ( ) => DataFormat is DataFormat . Text ;
401405 }
402406
403407 /// <summary>
0 commit comments