-
Notifications
You must be signed in to change notification settings - Fork 818
/
PgConverter.cs
225 lines (182 loc) · 9.36 KB
/
PgConverter.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
using System;
using System.Buffers;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
namespace Npgsql.Internal;
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, ref 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.
_ => IsDbNullValueAsObject(value, ref writeState) || (value is null && ThrowInvalidNullValue())
};
private protected abstract bool IsDbNullValueAsObject(object? value, ref object? 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 static void ThrowIORequired()
=> throw new InvalidOperationException("Buffer requirements for format not respected, expected no IO to be required.");
private protected static bool ThrowInvalidNullValue()
=> throw new ArgumentNullException("value", "Null value given for non-nullable type converter");
protected bool CanConvertBufferedDefault(DataFormat format, out BufferRequirements bufferRequirements)
{
bufferRequirements = BufferRequirements.Value;
return format is DataFormat.Binary;
}
}
public abstract class PgConverter<T> : PgConverter
{
private protected PgConverter(bool customDbNullPredicate)
: base(typeof(T), default(T) is null, customDbNullPredicate) { }
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, ref object? writeState)
=> (default(T) is null || value is not null) && IsDbNullValue(Downcast(value), ref writeState);
public bool IsDbNull([NotNullWhen(false)] T? value, ref object? writeState)
{
return 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, ref writeState) || (value is null && ThrowInvalidNullValue()),
_ => ThrowOutOfRange()
};
bool ThrowOutOfRange() => throw new ArgumentOutOfRangeException(nameof(DbNullPredicateKind), "Unknown case", DbNullPredicateKind.ToString());
}
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, Downcast(value), ref writeState);
[MethodImpl(MethodImplOptions.NoInlining)]
[return: NotNullIfNotNull(nameof(value))]
static T? Downcast(object? value) => (T?)value;
}
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, ref 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, ref 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;
}
}
interface IResumableRead
{
bool Supported { get; }
}
public readonly struct SizeContext
{
[SetsRequiredMembers]
public SizeContext(DataFormat format, Size bufferRequirement)
{
Format = format;
BufferRequirement = bufferRequirement;
}
public required Size BufferRequirement { get; init; }
public DataFormat Format { get; }
}
class MultiWriteState : IDisposable
{
public required ArrayPool<(Size Size, object? WriteState)>? ArrayPool { get; init; }
public required ArraySegment<(Size Size, object? WriteState)> Data { get; init; }
public required bool AnyWriteState { get; init; }
public void Dispose()
{
if (Data.Array is not { } array)
return;
if (AnyWriteState)
{
for (var i = Data.Offset; i < array.Length; i++)
if (array[i].WriteState is IDisposable disposable)
disposable.Dispose();
Array.Clear(Data.Array, Data.Offset, Data.Count);
}
ArrayPool?.Return(Data.Array);
}
}