forked from npgsql/npgsql
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathNpgsqlTypeHandler`.cs
More file actions
286 lines (243 loc) · 14.9 KB
/
NpgsqlTypeHandler`.cs
File metadata and controls
286 lines (243 loc) · 14.9 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
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
using System;
using System.Collections.Concurrent;
using System.Data.Common;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Threading.Tasks;
using Npgsql.BackendMessages;
using Npgsql.PostgresTypes;
using Npgsql.TypeHandlers;
namespace Npgsql.TypeHandling
{
/// <summary>
/// Base class for all type handlers, which read and write CLR types into their PostgreSQL
/// binary representation. Unless your type is arbitrary-length, consider inheriting from
/// <see cref="NpgsqlSimpleTypeHandler{T}"/> instead.
/// </summary>
/// <typeparam name="TDefault">
/// The default CLR type that this handler will read and write. For example, calling <see cref="DbDataReader.GetValue"/>
/// on a column with this handler will return a value with type <typeparamref name="TDefault"/>.
/// Type handlers can support additional types by implementing <see cref="INpgsqlTypeHandler{T}"/>.
/// </typeparam>
public abstract class NpgsqlTypeHandler<TDefault> : NpgsqlTypeHandler, INpgsqlTypeHandler<TDefault>
{
delegate int NonGenericValidateAndGetLength(NpgsqlTypeHandler handler, object value, ref NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter);
readonly NonGenericValidateAndGetLength _nonGenericValidateAndGetLength;
readonly NonGenericWriteWithLength _nonGenericWriteWithLength;
#pragma warning disable CA1823
static readonly ConcurrentDictionary<Type, (NonGenericValidateAndGetLength, NonGenericWriteWithLength)>
NonGenericDelegateCache = new ConcurrentDictionary<Type, (NonGenericValidateAndGetLength, NonGenericWriteWithLength)>();
#pragma warning restore CA1823
/// <summary>
/// Constructs an <see cref="NpgsqlTypeHandler{TDefault}"/>.
/// </summary>
protected NpgsqlTypeHandler(PostgresType postgresType)
: base(postgresType)
// Get code-generated delegates for non-generic ValidateAndGetLength/WriteWithLengthInternal
=>
(_nonGenericValidateAndGetLength, _nonGenericWriteWithLength) =
NonGenericDelegateCache.GetOrAdd(GetType(), t => (
GenerateNonGenericValidationMethod(GetType()),
GenerateNonGenericWriteMethod(GetType(), typeof(INpgsqlTypeHandler<>)))
);
#region Read
/// <summary>
/// Reads a value of type <typeparamref name="TDefault"/> with the given length from the provided buffer,
/// using either sync or async I/O.
/// </summary>
/// <param name="buf">The buffer from which to read.</param>
/// <param name="len">The byte length of the value. The buffer might not contain the full length, requiring I/O to be performed.</param>
/// <param name="async">If I/O is required to read the full length of the value, whether it should be performed synchronously or asynchronously.</param>
/// <param name="fieldDescription">Additional PostgreSQL information about the type, such as the length in varchar(30).</param>
/// <returns>The fully-read value.</returns>
public abstract ValueTask<TDefault> Read(NpgsqlReadBuffer buf, int len, bool async, FieldDescription? fieldDescription = null);
/// <summary>
/// Reads a value of type <typeparamref name="TDefault"/> with the given length from the provided buffer,
/// using either sync or async I/O. Type handlers typically don't need to override this -
/// override <see cref="Read(NpgsqlReadBuffer, int, bool, FieldDescription)"/> - but may do
/// so in exceptional cases where reading of arbitrary types is required.
/// </summary>
/// <param name="buf">The buffer from which to read.</param>
/// <param name="len">The byte length of the value. The buffer might not contain the full length, requiring I/O to be performed.</param>
/// <param name="async">If I/O is required to read the full length of the value, whether it should be performed synchronously or asynchronously.</param>
/// <param name="fieldDescription">Additional PostgreSQL information about the type, such as the length in varchar(30).</param>
/// <returns>The fully-read value.</returns>
protected internal override ValueTask<TAny> Read<TAny>(NpgsqlReadBuffer buf, int len, bool async, FieldDescription? fieldDescription = null)
{
var asTypedHandler = this as INpgsqlTypeHandler<TAny>;
if (asTypedHandler == null)
{
buf.Skip(len); // Perform this in sync for performance
throw new NpgsqlSafeReadException(new InvalidCastException(fieldDescription == null
? $"Can't cast database type to {typeof(TAny).Name}"
: $"Can't cast database type {fieldDescription.Handler.PgDisplayName} to {typeof(TAny).Name}"
));
}
return asTypedHandler.Read(buf, len, async, fieldDescription);
}
internal override TAny Read<TAny>(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription = null)
=> Read<TAny>(buf, len, false, fieldDescription).Result;
// Since TAny isn't constrained to class? or struct (C# doesn't have a non-nullable constraint that doesn't limit us to either struct or class),
// we must use the bang operator here to tell the compiler that a null value will never returned.
internal override async ValueTask<object> ReadAsObject(NpgsqlReadBuffer buf, int len, bool async, FieldDescription? fieldDescription = null)
=> (await Read<TDefault>(buf, len, async, fieldDescription))!;
internal override object ReadAsObject(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription = null)
=> Read<TDefault>(buf, len, fieldDescription)!;
#endregion Read
#region Write
/// <summary>
/// Called to validate and get the length of a value of a generic <see cref="NpgsqlParameter{T}"/>.
/// </summary>
public abstract int ValidateAndGetLength(TDefault value, ref NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter);
/// <summary>
/// Called to write the value of a generic <see cref="NpgsqlParameter{T}"/>.
/// </summary>
public abstract Task Write(TDefault value, NpgsqlWriteBuffer buf, NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter, bool async);
/// <summary>
/// Called to validate and get the length of a value of an arbitrary type.
/// Checks that the current handler supports that type and throws an exception otherwise.
/// </summary>
protected internal override int ValidateAndGetLength<TAny>(TAny value, ref NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter)
=> this is INpgsqlTypeHandler<TAny> typedHandler
? typedHandler.ValidateAndGetLength(value, ref lengthCache, parameter)
: throw new InvalidCastException($"Can't write CLR type {typeof(TAny)} to database type {PgDisplayName}");
/// <summary>
/// In the vast majority of cases writing a parameter to the buffer won't need to perform I/O.
/// </summary>
internal override Task WriteWithLengthInternal<TAny>([AllowNull] TAny value, NpgsqlWriteBuffer buf, NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter, bool async)
{
if (buf.WriteSpaceLeft < 4)
return WriteWithLengthLong();
if (value == null || typeof(TAny) == typeof(DBNull))
{
buf.WriteInt32(-1);
return Task.CompletedTask;
}
return WriteWithLength(value, buf, lengthCache, parameter, async);
async Task WriteWithLengthLong()
{
if (buf.WriteSpaceLeft < 4)
await buf.Flush(async);
if (value == null || typeof(TAny) == typeof(DBNull))
{
buf.WriteInt32(-1);
return;
}
await WriteWithLength(value, buf, lengthCache, parameter, async);
}
}
/// <summary>
/// Typically does not need to be overridden by type handlers, but may be needed in some
/// cases (e.g. <see cref="ArrayHandler"/>.
/// Note that this method assumes it can write 4 bytes of length (already verified by
/// <see cref="WriteWithLengthInternal{TAny}"/>).
/// </summary>
protected virtual Task WriteWithLength<TAny>(TAny value, NpgsqlWriteBuffer buf, NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter, bool async)
{
Debug.Assert(this is INpgsqlTypeHandler<TAny>);
var typedHandler = (INpgsqlTypeHandler<TAny>)this;
buf.WriteInt32(typedHandler.ValidateAndGetLength(value, ref lengthCache, parameter));
return typedHandler.Write(value, buf, lengthCache, parameter, async);
}
// Object overloads for non-generic NpgsqlParameter
/// <summary>
/// Called to validate and get the length of a value of a non-generic <see cref="NpgsqlParameter"/>.
/// Type handlers generally don't need to override this.
/// </summary>
protected internal override int ValidateObjectAndGetLength(object value, ref NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter)
=> value == null || value is DBNull
? -1
: _nonGenericValidateAndGetLength(this, value, ref lengthCache, parameter);
/// <summary>
/// Called to write the value of a non-generic <see cref="NpgsqlParameter"/>.
/// Type handlers generally don't need to override this.
/// </summary>
protected internal override Task WriteObjectWithLength(object value, NpgsqlWriteBuffer buf, NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter, bool async)
=> value is DBNull
? WriteWithLengthInternal(DBNull.Value, buf, lengthCache, parameter, async)
: _nonGenericWriteWithLength(this, value, buf, lengthCache, parameter, async);
#endregion Write
#region Code generation for non-generic writing
// We need to support writing via non-generic NpgsqlParameter, which means we get requests
// to write some object with no generic typing information.
// We need to find out which INpgsqlTypeHandler interfaces our handler implements, and call
// the ValidateAndGetLength/WriteWithLengthInternal methods on the interface which corresponds to the
// value type.
// Since doing this with reflection every time is slow, we generate delegates to do this for us
// for each type handler.
static NonGenericValidateAndGetLength GenerateNonGenericValidationMethod(Type handlerType)
{
var interfaces = handlerType.GetInterfaces().Where(i =>
i.GetTypeInfo().IsGenericType &&
i.GetGenericTypeDefinition() == typeof(INpgsqlTypeHandler<>)
).Reverse().ToList();
Expression? ifElseExpression = null;
var handlerParam = Expression.Parameter(typeof(NpgsqlTypeHandler), "handler");
var valueParam = Expression.Parameter(typeof(object), "value");
var lengthCacheParam = Expression.Parameter(typeof(NpgsqlLengthCache).MakeByRefType(), "lengthCache");
var parameterParam = Expression.Parameter(typeof(NpgsqlParameter), "parameter");
var resultVariable = Expression.Variable(typeof(int), "result");
foreach (var i in interfaces)
{
var handledType = i.GenericTypeArguments[0];
ifElseExpression = Expression.IfThenElse(
// Test whether the type of the value given to the delegate corresponds
// to our current interface's handled type (i.e. the T in INpgsqlTypeHandler<T>)
Expression.TypeEqual(valueParam, handledType),
// If it corresponds, cast the handler type (this) to INpgsqlTypeHandler<T>
// and call its ValidateAndGetLength method
Expression.Assign(
resultVariable,
Expression.Call(
Expression.Convert(handlerParam, i),
i.GetMethod(nameof(INpgsqlTypeHandler<TDefault>.ValidateAndGetLength)),
// Cast the value from object down to the interface's T
Expression.Convert(valueParam, handledType),
lengthCacheParam,
parameterParam
)
),
// If this is the first interface we're looking at, the else clause throws.
// Otherwise we stick the previous interface's IfThenElse in our else clause
ifElseExpression ?? Expression.Throw(
Expression.New(
typeof(InvalidCastException).GetConstructor(new[] { typeof(string) }),
Expression.Call( // Call string.Format to generate a nice informative exception message
typeof(string).GetMethod(nameof(string.Format), new[] { typeof(string), typeof(object) }),
new Expression[]
{
Expression.Constant($"Can't write CLR type {{0}} with handler type {handlerType.Name}"),
Expression.Call( // GetType() on the value
valueParam,
typeof(object).GetMethod(nameof(string.GetType), new Type[0])
)
}
)
)
)
);
}
return Expression.Lambda<NonGenericValidateAndGetLength>(
Expression.Block(
new[] { resultVariable },
ifElseExpression, resultVariable
),
handlerParam, valueParam, lengthCacheParam, parameterParam
).Compile();
}
#endregion Code generation for non-generic writing
#region Misc
internal override Type GetFieldType(FieldDescription? fieldDescription = null) => typeof(TDefault);
internal override Type GetProviderSpecificFieldType(FieldDescription? fieldDescription = null) => typeof(TDefault);
/// <inheritdoc />
public override ArrayHandler CreateArrayHandler(PostgresArrayType arrayBackendType)
=> new ArrayHandler<TDefault>(arrayBackendType, this);
/// <inheritdoc />
public override RangeHandler CreateRangeHandler(PostgresRangeType rangeBackendType)
=> new RangeHandler<TDefault>(rangeBackendType, this);
#endregion Misc
}
}