-
Notifications
You must be signed in to change notification settings - Fork 877
Expand file tree
/
Copy pathPgConverter.cs
More file actions
245 lines (201 loc) · 11 KB
/
PgConverter.cs
File metadata and controls
245 lines (201 loc) · 11 KB
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
using System;
using System.Buffers;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace Npgsql.Internal;
[Experimental(NpgsqlDiagnostics.ConvertersExperimental)]
public abstract class PgConverter
{
internal DbNullPredicate DbNullPredicateKind { get; }
public bool IsDbNullable => DbNullPredicateKind is not DbNullPredicate.None;
private protected PgConverter(Type type, bool isNullDefaultValue, bool customDbNullPredicate = false)
=> DbNullPredicateKind = customDbNullPredicate ? DbNullPredicate.Custom : InferDbNullPredicate(type, isNullDefaultValue);
/// <summary>
/// Whether this converter can handle the given format and with which buffer requirements.
/// </summary>
/// <param name="format">The data format.</param>
/// <param name="bufferRequirements">Returns the buffer requirements.</param>
/// <returns>Returns true if the given data format is supported.</returns>
/// <remarks>The buffer requirements should not cover database NULL reads or writes, these are handled by the caller.</remarks>
public abstract bool CanConvert(DataFormat format, out BufferRequirements bufferRequirements);
internal abstract Type TypeToConvert { get; }
internal bool IsDbNullAsObject([NotNullWhen(false)] object? value, object? writeState)
=> DbNullPredicateKind switch
{
DbNullPredicate.Null => value is null,
DbNullPredicate.None => false,
DbNullPredicate.PolymorphicNull => value is null or DBNull,
// We do the null check to keep the NotNullWhen(false) invariant.
DbNullPredicate.Custom => IsDbNullValueAsObject(value, writeState) || (value is null && ThrowInvalidNullValue()),
_ => ThrowDbNullPredicateOutOfRange()
};
[Obsolete("Use the overload without ref.")]
internal bool IsDbNullAsObject([NotNullWhen(false)] object? value, ref object? writeState)
=> IsDbNullAsObject(value, writeState);
private protected abstract bool IsDbNullValueAsObject(object? value, object? writeState);
[Obsolete("Use the overload without ref.")]
private protected bool IsDbNullValueAsObject(object? value, ref object? writeState)
=> IsDbNullValueAsObject(value, writeState);
internal abstract Size GetSizeAsObject(SizeContext context, object value, ref object? writeState);
internal object ReadAsObject(PgReader reader)
=> ReadAsObject(async: false, reader, CancellationToken.None).GetAwaiter().GetResult();
internal ValueTask<object> ReadAsObjectAsync(PgReader reader, CancellationToken cancellationToken = default)
=> ReadAsObject(async: true, reader, cancellationToken);
// Shared sync/async abstract to reduce virtual method table size overhead and code size for each NpgsqlConverter<T> instantiation.
internal abstract ValueTask<object> ReadAsObject(bool async, PgReader reader, CancellationToken cancellationToken);
internal void WriteAsObject(PgWriter writer, object value)
=> WriteAsObject(async: false, writer, value, CancellationToken.None).GetAwaiter().GetResult();
internal ValueTask WriteAsObjectAsync(PgWriter writer, object value, CancellationToken cancellationToken = default)
=> WriteAsObject(async: true, writer, value, cancellationToken);
// Shared sync/async abstract to reduce virtual method table size overhead and code size for each NpgsqlConverter<T> instantiation.
internal abstract ValueTask WriteAsObject(bool async, PgWriter writer, object value, CancellationToken cancellationToken);
static DbNullPredicate InferDbNullPredicate(Type type, bool isNullDefaultValue)
=> type == typeof(object) || type == typeof(DBNull)
? DbNullPredicate.PolymorphicNull
: isNullDefaultValue
? DbNullPredicate.Null
: DbNullPredicate.None;
internal enum DbNullPredicate : byte
{
/// Never DbNull (struct types)
None,
/// DbNull when *user code*
Custom,
/// DbNull when value is null
Null,
/// DbNull when value is null or DBNull
PolymorphicNull
}
[DoesNotReturn]
private protected void ThrowIORequired(Size bufferRequirement)
=> throw new InvalidOperationException($"Buffer requirement '{bufferRequirement}' not respected for converter '{GetType().FullName}', expected no IO to be required.");
private protected static bool ThrowInvalidNullValue()
=> throw new ArgumentNullException("value", "Null value given for non-nullable type converter");
private protected bool ThrowDbNullPredicateOutOfRange()
=> throw new UnreachableException($"Unknown case {DbNullPredicateKind.ToString()}");
}
public abstract class PgConverter<T> : PgConverter
{
private protected PgConverter(bool customDbNullPredicate)
: base(typeof(T), default(T) is null, customDbNullPredicate) { }
#pragma warning disable CS0618 // Obsolete - delegates to ref overload for binary compat with existing overrides
protected virtual bool IsDbNullValue(T? value, object? writeState)
{
// The obsolete ref overload is kept around for binary compatibility on the signature, but
// mutating writeState during a null probe is no longer a supported behaviour. Detect the
// mutation via a local captured before the forward and throw — a violating override is a
// bug in the derived converter, not something to defend against here.
var originalWriteState = writeState;
var isDbNull = IsDbNullValue(value, ref writeState);
if (!ReferenceEquals(writeState, originalWriteState))
ThrowHelper.ThrowInvalidOperationException(
$"{GetType().FullName} mutated writeState from its IsDbNullValue override. Override the overload without ref and produce write state only in GetSize.");
return isDbNull;
}
#pragma warning restore CS0618
[Obsolete("Use the overload without ref.")]
[EditorBrowsable(EditorBrowsableState.Never)]
protected virtual bool IsDbNullValue(T? value, ref object? writeState) => throw new NotSupportedException();
// Object null semantics as follows, if T is a struct (so excluding nullable) report false for null values, don't throw on the cast.
// As a result this creates symmetry with IsDbNull when we're dealing with a struct T, as it cannot be passed null at all.
private protected override bool IsDbNullValueAsObject(object? value, object? writeState)
=> (default(T) is null || value is not null) && IsDbNullValue((T?)value, writeState);
public bool IsDbNull([NotNullWhen(false)] T? value, object? writeState)
=> DbNullPredicateKind switch
{
DbNullPredicate.Null => value is null,
DbNullPredicate.None => false,
DbNullPredicate.PolymorphicNull => value is null or DBNull,
// We do the null check to keep the NotNullWhen(false) invariant.
DbNullPredicate.Custom => IsDbNullValue(value, writeState) || (value is null && ThrowInvalidNullValue()),
_ => ThrowDbNullPredicateOutOfRange()
};
[Obsolete("Use the overload without ref.")]
[EditorBrowsable(EditorBrowsableState.Never)]
public bool IsDbNull([NotNullWhen(false)] T? value, ref object? writeState)
=> IsDbNull(value, writeState);
public abstract T Read(PgReader reader);
public abstract ValueTask<T> ReadAsync(PgReader reader, CancellationToken cancellationToken = default);
public abstract Size GetSize(SizeContext context, [DisallowNull]T value, ref object? writeState);
public abstract void Write(PgWriter writer, [DisallowNull] T value);
public abstract ValueTask WriteAsync(PgWriter writer, [DisallowNull] T value, CancellationToken cancellationToken = default);
internal sealed override Type TypeToConvert => typeof(T);
internal sealed override Size GetSizeAsObject(SizeContext context, object value, ref object? writeState)
=> GetSize(context, (T)value, ref writeState);
}
static class PgConverterExtensions
{
public static Size? GetSizeOrDbNull<T>(this PgConverter<T> converter, DataFormat format, Size writeRequirement, T? value, ref object? writeState)
{
if (converter.IsDbNull(value, writeState))
return null;
if (writeRequirement is { Kind: SizeKind.Exact, Value: var byteCount })
return byteCount;
var size = converter.GetSize(new(format, writeRequirement), value, ref writeState);
switch (size.Kind)
{
case SizeKind.UpperBound:
ThrowHelper.ThrowInvalidOperationException($"{nameof(SizeKind.UpperBound)} is not a valid return value for GetSize.");
break;
case SizeKind.Unknown:
// Not valid yet.
ThrowHelper.ThrowInvalidOperationException($"{nameof(SizeKind.Unknown)} is not a valid return value for GetSize.");
break;
}
return size;
}
public static Size? GetSizeOrDbNullAsObject(this PgConverter converter, DataFormat format, Size writeRequirement, object? value, ref object? writeState)
{
if (converter.IsDbNullAsObject(value, writeState))
return null;
if (writeRequirement is { Kind: SizeKind.Exact, Value: var byteCount })
return byteCount;
var size = converter.GetSizeAsObject(new(format, writeRequirement), value, ref writeState);
switch (size.Kind)
{
case SizeKind.UpperBound:
ThrowHelper.ThrowInvalidOperationException($"{nameof(SizeKind.UpperBound)} is not a valid return value for GetSize.");
break;
case SizeKind.Unknown:
// Not valid yet.
ThrowHelper.ThrowInvalidOperationException($"{nameof(SizeKind.Unknown)} is not a valid return value for GetSize.");
break;
}
return size;
}
internal static PgConverter<T> UnsafeDowncast<T>(this PgConverter converter)
{
// Justification: avoid perf cost of casting to a known base class type per read/write, see callers.
Debug.Assert(converter is PgConverter<T>);
return Unsafe.As<PgConverter<T>>(converter);
}
}
[method: SetsRequiredMembers]
public readonly struct SizeContext(DataFormat format, Size bufferRequirement)
{
public required Size BufferRequirement { get; init; } = bufferRequirement;
public DataFormat Format { get; } = format;
}
class MultiWriteState : IDisposable
{
public ArrayPool<(Size Size, object? WriteState)>? ArrayPool { get; set; }
public ArraySegment<(Size Size, object? WriteState)> Data { get; set; }
public bool AnyWriteState { get; set; }
public void Dispose()
{
if (Data.Array is not { } array)
return;
if (AnyWriteState)
{
for (var i = Data.Offset; i < Data.Offset + Data.Count; i++)
if (array[i].WriteState is IDisposable disposable)
disposable.Dispose();
Array.Clear(Data.Array, Data.Offset, Data.Count);
}
ArrayPool?.Return(Data.Array);
}
}