-
Notifications
You must be signed in to change notification settings - Fork 818
/
PgTypeInfo.cs
329 lines (274 loc) · 13.9 KB
/
PgTypeInfo.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
using System;
using System.Diagnostics.CodeAnalysis;
using Npgsql.Internal.Postgres;
namespace Npgsql.Internal;
public class PgTypeInfo
{
readonly bool _canBinaryConvert;
readonly BufferRequirements _binaryBufferRequirements;
readonly bool _canTextConvert;
readonly BufferRequirements _textBufferRequirements;
PgTypeInfo(PgSerializerOptions options, Type type, Type? unboxedType)
{
if (unboxedType is not null && !type.IsAssignableFrom(unboxedType))
throw new ArgumentException("A value of unboxed type is not assignable to converter type", nameof(unboxedType));
Options = options;
IsBoxing = unboxedType is not null;
Type = unboxedType ?? type;
SupportsWriting = true;
}
public PgTypeInfo(PgSerializerOptions options, PgConverter converter, PgTypeId pgTypeId, Type? unboxedType = null)
: this(options, converter.TypeToConvert, unboxedType)
{
Converter = converter;
PgTypeId = options.GetCanonicalTypeId(pgTypeId);
_canBinaryConvert = converter.CanConvert(DataFormat.Binary, out _binaryBufferRequirements);
_canTextConvert = converter.CanConvert(DataFormat.Text, out _textBufferRequirements);
}
private protected PgTypeInfo(PgSerializerOptions options, Type type, PgConverterResolution? resolution, Type? unboxedType = null)
: this(options, type, unboxedType)
{
if (resolution is { } res)
{
// Resolutions should always be in canonical form already.
if (options.PortableTypeIds && res.PgTypeId.IsOid || !options.PortableTypeIds && res.PgTypeId.IsDataTypeName)
throw new ArgumentException("Given type id is not in canonical form. Make sure ConverterResolver implementations close over canonical ids, e.g. by calling options.GetCanonicalTypeId(pgTypeId) on the constructor arguments.", nameof(PgTypeId));
PgTypeId = res.PgTypeId;
Converter = res.Converter;
_canBinaryConvert = res.Converter.CanConvert(DataFormat.Binary, out _binaryBufferRequirements);
_canTextConvert = res.Converter.CanConvert(DataFormat.Text, out _textBufferRequirements);
}
}
bool HasCachedInfo(PgConverter converter) => ReferenceEquals(Converter, converter);
public Type Type { get; }
public PgSerializerOptions Options { get; }
public bool SupportsWriting { get; init; }
public DataFormat? PreferredFormat { get; init; }
// Doubles as the storage for the converter coming from a default resolution (used to confirm whether we can use cached info).
PgConverter? Converter { get; }
[MemberNotNullWhen(false, nameof(Converter))]
[MemberNotNullWhen(false, nameof(PgTypeId))]
internal bool IsResolverInfo => GetType() == typeof(PgResolverTypeInfo);
// TODO pull validate from options + internal exempt for perf?
internal bool ValidateResolution => true;
// Used for internal converters to save on binary bloat.
internal bool IsBoxing { get; }
public PgTypeId? PgTypeId { get; }
// Having it here so we can easily extend any behavior.
internal void DisposeWriteState(object writeState)
{
if (writeState is IDisposable disposable)
disposable.Dispose();
}
public PgConverterResolution GetResolution<T>(T? value)
{
if (this is not PgResolverTypeInfo resolverInfo)
return new(Converter!, PgTypeId.GetValueOrDefault());
var resolution = resolverInfo.GetResolution(value, null);
return resolution ?? resolverInfo.GetDefaultResolution(null);
}
// Note: this api is not called GetResolutionAsObject as the semantics are extended, DBNull is a NULL value for all object values.
public PgConverterResolution GetObjectResolution(object? value)
{
switch (this)
{
case { IsResolverInfo: false }:
return new(Converter, PgTypeId.GetValueOrDefault());
case PgResolverTypeInfo resolverInfo:
PgConverterResolution? resolution = null;
if (value is not DBNull)
resolution = resolverInfo.GetResolutionAsObject(value, null);
return resolution ?? resolverInfo.GetDefaultResolution(null);
default:
return ThrowNotSupported();
}
static PgConverterResolution ThrowNotSupported()
=> throw new NotSupportedException("Should not happen, please file a bug.");
}
/// Throws if the instance is a PgResolverTypeInfo.
internal PgConverterResolution GetResolution()
{
if (IsResolverInfo)
ThrowHelper.ThrowInvalidOperationException("Instance is a PgResolverTypeInfo.");
return new(Converter, PgTypeId.GetValueOrDefault());
}
bool CachedCanConvert(DataFormat format, out BufferRequirements bufferRequirements)
{
if (format is DataFormat.Binary)
{
bufferRequirements = _binaryBufferRequirements;
return _canBinaryConvert;
}
bufferRequirements = _textBufferRequirements;
return _canTextConvert;
}
public BufferRequirements? GetBufferRequirements(PgConverter converter, DataFormat format)
{
var success = HasCachedInfo(converter)
? CachedCanConvert(format, out var bufferRequirements)
: converter.CanConvert(format, out bufferRequirements);
return success ? bufferRequirements : null;
}
// TryBind for reading.
internal bool TryBind(Field field, DataFormat format, out PgConverterInfo info)
{
switch (this)
{
case { IsResolverInfo: false }:
if (!CachedCanConvert(format, out var bufferRequirements))
{
info = default;
return false;
}
info = new(this, Converter, bufferRequirements.Read);
return true;
case PgResolverTypeInfo resolverInfo:
var resolution = resolverInfo.GetResolution(field);
if (!HasCachedInfo(resolution.Converter)
? !CachedCanConvert(format, out bufferRequirements)
: !resolution.Converter.CanConvert(format, out bufferRequirements))
{
info = default;
return false;
}
info = new(this, resolution.Converter, bufferRequirements.Read);
return true;
default:
throw new NotSupportedException("Should not happen, please file a bug.");
}
}
// Bind for reading.
internal PgConverterInfo Bind(Field field, DataFormat format)
{
if (!TryBind(field, format, out var info))
ThrowHelper.ThrowInvalidOperationException($"Resolved converter does not support {format} format.");
return info;
}
// Bind for writing.
/// When result is null, the value was interpreted to be a SQL NULL.
internal PgConverterInfo? Bind<T>(PgConverter<T> converter, T? value, out Size size, out object? writeState, out DataFormat format, DataFormat? formatPreference = null)
{
// Basically exists to catch cases like object[] resolving a polymorphic read converter, better to fail during binding than writing.
if (!SupportsWriting)
ThrowHelper.ThrowNotSupportedException($"Writing {Type} is not supported for this type info.");
format = ResolveFormat(converter, out var bufferRequirements, formatPreference ?? PreferredFormat);
writeState = null;
if (converter.GetSizeOrDbNull(format, bufferRequirements.Write, value, ref writeState) is not { } sizeOrDbNull)
{
size = default;
return null;
}
size = sizeOrDbNull;
return new(this, converter, bufferRequirements.Write);
}
// Bind for writing.
// Note: this api is not called BindAsObject as the semantics are extended, DBNull is a NULL value for all object values.
/// When result is null or DBNull, the value was interpreted to be a SQL NULL.
internal PgConverterInfo? BindObject(PgConverter converter, object? value, out Size size, out object? writeState, out DataFormat format, DataFormat? formatPreference = null)
{
// Basically exists to catch cases like object[] resolving a polymorphic read converter, better to fail during binding than writing.
if (!SupportsWriting)
throw new NotSupportedException($"Writing {Type} is not supported for this type info.");
format = ResolveFormat(converter, out var bufferRequirements, formatPreference ?? PreferredFormat);
// Given SQL values are effectively a union of T | NULL we support DBNull.Value to signify a NULL value for all types except DBNull in this api.
writeState = null;
if (value is DBNull && Type != typeof(DBNull) || converter.GetSizeOrDbNullAsObject(format, bufferRequirements.Write, value, ref writeState) is not { } sizeOrDbNull)
{
size = default;
return null;
}
size = sizeOrDbNull;
return new(this, converter, bufferRequirements.Write);
}
// If we don't have a converter stored we must ask the retrieved one.
DataFormat ResolveFormat(PgConverter converter, out BufferRequirements bufferRequirements, DataFormat? formatPreference = null)
{
switch (formatPreference)
{
// The common case, no preference means we default to binary if supported.
case null or DataFormat.Binary when HasCachedInfo(converter) ? CachedCanConvert(DataFormat.Binary, out bufferRequirements) : converter.CanConvert(DataFormat.Binary, out bufferRequirements):
return DataFormat.Binary;
// In this case we either prefer text or we have no preference and our converter doesn't support binary.
case null or DataFormat.Text:
var canTextConvert = HasCachedInfo(converter) ? CachedCanConvert(DataFormat.Text, out bufferRequirements) : converter.CanConvert(DataFormat.Text, out bufferRequirements);
if (!canTextConvert)
{
if (formatPreference is null)
throw new InvalidOperationException("Converter doesn't support any data format.");
// Rerun without preference.
return ResolveFormat(converter, out bufferRequirements);
}
return DataFormat.Text;
default:
throw new ArgumentOutOfRangeException();
}
}
}
public sealed class PgResolverTypeInfo : PgTypeInfo
{
internal readonly PgConverterResolver _converterResolver;
public PgResolverTypeInfo(PgSerializerOptions options, PgConverterResolver converterResolver, PgTypeId? pgTypeId, Type? unboxedType = null)
: base(options,
converterResolver.TypeToConvert,
pgTypeId is { } typeId ? ResolveDefaultId(options, converterResolver, typeId) : null,
// We always mark resolvers with type object as boxing, as they may freely return converters for any type (see PgConverterResolver.Validate).
unboxedType ?? (converterResolver.TypeToConvert == typeof(object) ? typeof(object) : null))
=> _converterResolver = converterResolver;
// We'll always validate the default resolution, the info will be re-used so there is no real downside.
static PgConverterResolution ResolveDefaultId(PgSerializerOptions options, PgConverterResolver converterResolver, PgTypeId typeId)
=> converterResolver.GetDefaultInternal(validate: true, options.PortableTypeIds, options.GetCanonicalTypeId(typeId));
public PgConverterResolution? GetResolution<T>(T? value, PgTypeId? expectedPgTypeId)
{
return _converterResolver is PgConverterResolver<T> resolverT
? resolverT.GetInternal(this, value, expectedPgTypeId ?? PgTypeId)
: ThrowNotSupportedType(typeof(T));
PgConverterResolution ThrowNotSupportedType(Type? type)
=> throw new NotSupportedException(IsBoxing
? "TypeInfo only supports boxing conversions, call GetResolutionAsObject instead."
: $"TypeInfo is not of type {type}");
}
public PgConverterResolution? GetResolutionAsObject(object? value, PgTypeId? expectedPgTypeId)
=> _converterResolver.GetAsObjectInternal(this, value, expectedPgTypeId ?? PgTypeId);
public PgConverterResolution GetResolution(Field field)
=> _converterResolver.GetInternal(this, field);
public PgConverterResolution GetDefaultResolution(PgTypeId? pgTypeId)
=> _converterResolver.GetDefaultInternal(ValidateResolution, Options.PortableTypeIds, pgTypeId ?? PgTypeId);
}
public readonly struct PgConverterResolution
{
public PgConverterResolution(PgConverter converter, PgTypeId pgTypeId)
{
Converter = converter;
PgTypeId = pgTypeId;
}
public PgConverter Converter { get; }
public PgTypeId PgTypeId { get; }
public PgConverter<T> GetConverter<T>() => (PgConverter<T>)Converter;
}
readonly struct PgConverterInfo
{
public PgConverterInfo(PgTypeInfo pgTypeInfo, PgConverter converter, Size bufferRequirement)
{
TypeInfo = pgTypeInfo;
Converter = converter;
BufferRequirement = bufferRequirement;
}
public bool IsDefault => TypeInfo is null;
public Type TypeToConvert
{
get
{
// Object typed resolvers can return any type of converter, so we check the type of the converter instead.
// We cannot do this in general as we should respect the 'unboxed type' of infos, which can differ from the converter type.
if (TypeInfo.IsResolverInfo && TypeInfo.Type == typeof(object))
return Converter.TypeToConvert;
return TypeInfo.Type;
}
}
public PgTypeInfo TypeInfo { get; }
public PgConverter Converter { get; }
public Size BufferRequirement { get; }
/// Whether Converter.TypeToConvert matches PgTypeInfo.Type, if it doesn't object apis should be used.
public bool IsBoxingConverter => TypeInfo.IsBoxing;
public PgConverter<T> GetConverter<T>() => (PgConverter<T>)Converter;
}