-
Notifications
You must be signed in to change notification settings - Fork 891
Expand file tree
/
Copy pathPgSerializerOptions.cs
More file actions
187 lines (154 loc) · 9.04 KB
/
Copy pathPgSerializerOptions.cs
File metadata and controls
187 lines (154 loc) · 9.04 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
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text;
using Npgsql.Internal.Postgres;
using Npgsql.NameTranslation;
using Npgsql.PostgresTypes;
namespace Npgsql.Internal;
[Experimental(NpgsqlDiagnostics.ConvertersExperimental)]
public sealed class PgSerializerOptions
{
internal static UTF8Encoding DefaultUtf8Encoding => NpgsqlWriteBuffer.UTF8Encoding;
/// <summary>
/// Used by GetSchema to be able to attempt to resolve all type catalog types without exceptions.
/// </summary>
[field: ThreadStatic]
internal static bool IntrospectionCaller { get; set; }
readonly PgTypeInfoResolverChain _resolverChain;
readonly Func<string>? _timeZoneProvider;
IPgTypeInfoResolver? _typeInfoResolver;
TypeInfoCache<Oid>? _oidCache;
TypeInfoCache<DataTypeName>? _dataTypeNameCache;
internal PgSerializerOptions(NpgsqlDatabaseInfo databaseInfo, PgTypeInfoResolverChain? resolverChain = null, Func<string>? timeZoneProvider = null, Encoding? textEncoding = null)
{
_resolverChain = resolverChain ?? new();
_timeZoneProvider = timeZoneProvider;
DatabaseInfo = databaseInfo;
// ConversionContext must be in place before any PgConcreteTypeInfo construction probes via options.ConversionContext.
ConversionContext = new PgConversionContext { TextEncoding = textEncoding ?? NpgsqlWriteBuffer.RelaxedUTF8Encoding };
UnspecifiedDBNullTypeInfo = PgConcreteTypeInfo.Create(this, new Converters.Internal.VoidConverter(), DataTypeName.Unspecified, requestedType: typeof(DBNull));
}
internal PgConcreteTypeInfo UnspecifiedDBNullTypeInfo { get; }
PostgresType? _textPgType;
internal PgTypeId TextPgTypeId => ToCanonicalTypeId(_textPgType ??= DatabaseInfo.GetPostgresType(DataTypeNames.Text));
// Used purely for type mapping, where we don't have a full set of types but resolvers might know enough.
readonly bool _introspectionInstance;
internal bool IntrospectionMode
{
get => _introspectionInstance || IntrospectionCaller;
init => _introspectionInstance = value;
}
/// Whether options should return a portable identifier (data type name) to prevent any generated id (oid) confusion across backends, this comes with a perf penalty.
internal bool PortableTypeIds { get; init; }
internal NpgsqlDatabaseInfo DatabaseInfo { get; }
public string TimeZone => _timeZoneProvider?.Invoke() ?? throw new NotSupportedException("TimeZone was not configured.");
/// <summary>
/// Shared conversion context for this options instance. Carries connection/options-scoped state that
/// converters read at descriptor query / read / write / bind time. One instance is reused across all
/// converters in this options; lifetime moves to the connection once mid-session state (ParameterStatus
/// updates etc.) needs to be threaded.
/// </summary>
public PgConversionContext ConversionContext { get; }
public IPgTypeInfoResolver TypeInfoResolver
{
get => _typeInfoResolver ??= new ChainTypeInfoResolver(_resolverChain);
internal init => _typeInfoResolver = value;
}
public bool EnableDateTimeInfinityConversions { get; init; } = true;
public ArrayNullabilityMode ArrayNullabilityMode { get; init; } = ArrayNullabilityMode.Never;
public INpgsqlNameTranslator DefaultNameTranslator { get; init; } = NpgsqlSnakeCaseNameTranslator.Instance;
public static bool IsWellKnownTextType(Type type)
{
type = type.IsValueType ? Nullable.GetUnderlyingType(type) ?? type : type;
return Array.IndexOf([
typeof(string), typeof(char),
typeof(char[]), typeof(ReadOnlyMemory<char>), typeof(ArraySegment<char>),
typeof(byte[]), typeof(ReadOnlyMemory<byte>)
], type) != -1 || typeof(Stream).IsAssignableFrom(type);
}
internal bool RangesEnabled => _resolverChain.RangesEnabled;
internal bool MultirangesEnabled => _resolverChain.MultirangesEnabled;
internal bool ArraysEnabled => _resolverChain.ArraysEnabled;
// GetTypeInfoCore consumes the canonical pair: only the side matching PortableTypeIds is read, the other is
// expected to be default. Callers must canonicalize before calling — public entries (GetDefaultTypeInfo/GetTypeInfo)
// do this via GetCanonicalTypeId, which routes through DatabaseInfo.GetDataTypeName/GetOid (form-tolerant). The
// internal entry trusts callers to have canonicalized; passing a non-matching kind silently dispatches with a
// default opposite-side value.
PgTypeInfo? GetTypeInfoCore(Type? type, Oid? oid, DataTypeName? dataTypeName)
=> PortableTypeIds
? (_dataTypeNameCache ??= new TypeInfoCache<DataTypeName>(this)).GetOrAddInfo(type, dataTypeName)
: (_oidCache ??= new TypeInfoCache<Oid>(this)).GetOrAddInfo(type, oid);
internal PgTypeInfo? GetTypeInfoInternal(Type? type, PgTypeId? pgTypeId)
=> !pgTypeId.HasValue
? GetTypeInfoCore(type, null, null)
: GetTypeInfoInternal(type, Nullable.GetValueRefOrDefaultRef(in pgTypeId));
internal PgTypeInfo? GetTypeInfoInternal(Type? type, PgTypeId pgTypeId)
{
var (oid, dataTypeName) = pgTypeId;
Debug.Assert(PortableTypeIds ? oid == default : dataTypeName == default,
"PgTypeId form does not match PortableTypeIds; caller must canonicalize.");
return GetTypeInfoCore(type, oid, dataTypeName);
}
public PgTypeInfo? GetDefaultTypeInfo(Type type)
=> GetTypeInfoCore(type, null, null);
public PgTypeInfo? GetDefaultTypeInfo(PgTypeId pgTypeId)
{
var (oid, dataTypeName) = GetCanonicalTypeId(pgTypeId);
return GetTypeInfoCore(null, oid, dataTypeName);
}
public PgTypeInfo? GetTypeInfo(Type type, PgTypeId pgTypeId)
{
var (oid, dataTypeName) = GetCanonicalTypeId(pgTypeId);
return GetTypeInfoCore(type, oid, dataTypeName);
}
// If a given type id is in the opposite form than what was expected it will be mapped according to the requirement.
internal PgTypeId GetCanonicalTypeId(PgTypeId pgTypeId)
=> PortableTypeIds ? DatabaseInfo.GetDataTypeName(pgTypeId) : DatabaseInfo.GetOid(pgTypeId);
// If a given type id is in the opposite form than what was expected it will be mapped according to the requirement.
internal PgTypeId ToCanonicalTypeId(PostgresType pgType)
=> PortableTypeIds ? pgType.DataTypeName : (Oid)pgType.OID;
public PgTypeId GetArrayTypeId(PgTypeId elementTypeId)
{
// Static affordance to help the global type mapper.
if (PortableTypeIds && elementTypeId.IsDataTypeName)
return elementTypeId.DataTypeName.ToArrayName();
return ToCanonicalTypeId(DatabaseInfo.GetPostgresType(elementTypeId).Array
?? throw new NotSupportedException("Cannot resolve array type id"));
}
public PgTypeId GetArrayElementTypeId(PgTypeId arrayTypeId)
{
// Static affordance to help the global type mapper.
if (PortableTypeIds && arrayTypeId.IsDataTypeName && arrayTypeId.DataTypeName.UnqualifiedNameSpan.StartsWith("_".AsSpan(), StringComparison.Ordinal))
return new DataTypeName(arrayTypeId.DataTypeName.Schema + arrayTypeId.DataTypeName.UnqualifiedNameSpan.Slice(1).ToString());
return ToCanonicalTypeId((DatabaseInfo.GetPostgresType(arrayTypeId) as PostgresArrayType)?.Element
?? throw new NotSupportedException("Cannot resolve array element type id"));
}
public PgTypeId GetRangeTypeId(PgTypeId subtypeTypeId) =>
ToCanonicalTypeId(DatabaseInfo.GetPostgresType(subtypeTypeId).Range
?? throw new NotSupportedException("Cannot resolve range type id"));
public PgTypeId GetRangeSubtypeTypeId(PgTypeId rangeTypeId) =>
ToCanonicalTypeId((DatabaseInfo.GetPostgresType(rangeTypeId) as PostgresRangeType)?.Subtype
?? throw new NotSupportedException("Cannot resolve range subtype type id"));
public PgTypeId GetMultirangeTypeId(PgTypeId rangeTypeId) =>
ToCanonicalTypeId((DatabaseInfo.GetPostgresType(rangeTypeId) as PostgresRangeType)?.Multirange
?? throw new NotSupportedException("Cannot resolve multirange type id"));
public PgTypeId GetMultirangeElementTypeId(PgTypeId multirangeTypeId) =>
ToCanonicalTypeId((DatabaseInfo.GetPostgresType(multirangeTypeId) as PostgresMultirangeType)?.Subrange
?? throw new NotSupportedException("Cannot resolve multirange element type id"));
public bool TryGetDataTypeName(PgTypeId pgTypeId, out DataTypeName dataTypeName)
{
if (DatabaseInfo.FindPostgresType(pgTypeId) is { } pgType)
{
dataTypeName = pgType.DataTypeName;
return true;
}
dataTypeName = default;
return false;
}
public DataTypeName GetDataTypeName(PgTypeId pgTypeId)
=> !TryGetDataTypeName(pgTypeId, out var name)
? throw new ArgumentException("Unknown type id", nameof(pgTypeId))
: name;
}