forked from npgsql/npgsql
-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathTypeMapperTests.cs
More file actions
281 lines (232 loc) · 10.9 KB
/
TypeMapperTests.cs
File metadata and controls
281 lines (232 loc) · 10.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
using Npgsql.Internal;
using NUnit.Framework;
using System;
using System.Data;
using System.Threading.Tasks;
using Npgsql.Internal.Converters;
using Npgsql.Internal.Postgres;
using Npgsql.TypeMapping;
using NpgsqlTypes;
using static Npgsql.Tests.TestUtil;
namespace Npgsql.Tests;
public class TypeMapperTests : TestBase
{
[Test]
public async Task ReloadTypes_across_connections_in_data_source()
{
await using var adminConnection = await OpenConnectionAsync();
var type = await GetTempTypeName(adminConnection);
// Note that we don't actually create the type in the database at this point; we want to exercise the type being created later,
// via the data source.
var dataSourceBuilder = CreateDataSourceBuilder();
dataSourceBuilder.MapEnum<Mood>(type);
await using var dataSource = dataSourceBuilder.Build();
await using var connection1 = await dataSource.OpenConnectionAsync();
await using var connection2 = await dataSource.OpenConnectionAsync();
await connection1.ExecuteNonQueryAsync($"CREATE TYPE {type} AS ENUM ('sad', 'ok', 'happy')");
await connection1.ReloadTypesAsync();
// The data source type mapper has been replaced and connection1 should have the new mapper, but connection2 should retain the older
// type mapper - where there's no mapping - as long as it's still open
Assert.ThrowsAsync<InvalidCastException>(async () => await connection2.ExecuteScalarAsync($"SELECT 'happy'::{type}"));
Assert.DoesNotThrowAsync(async () => await connection1.ExecuteScalarAsync($"SELECT 'happy'::{type}"));
// Close connection2 and reopen to make sure it picks up the new type and mapping from the data source
var connId = connection2.ProcessID;
await connection2.CloseAsync();
await connection2.OpenAsync();
Assert.That(connection2.ProcessID, Is.EqualTo(connId), "Didn't get the same connector back");
Assert.DoesNotThrowAsync(async () => await connection2.ExecuteScalarAsync($"SELECT 'happy'::{type}"));
}
[Test]
public async Task String_to_citext()
{
await using var adminConnection = await OpenConnectionAsync();
await EnsureExtensionAsync(adminConnection, "citext");
var dataSourceBuilder = CreateDataSourceBuilder();
dataSourceBuilder.AddTypeInfoResolverFactory(new CitextToStringTypeHandlerResolverFactory());
await using var dataSource = dataSourceBuilder.Build();
await using var connection = await dataSource.OpenConnectionAsync();
await using var command = new NpgsqlCommand("SELECT @p = 'hello'::citext", connection);
command.Parameters.AddWithValue("p", "HeLLo");
Assert.That(command.ExecuteScalar(), Is.True);
}
[Test]
public async Task String_to_citext_with_db_type_string()
{
await using var adminConnection = await OpenConnectionAsync();
await EnsureExtensionAsync(adminConnection, "citext");
var dataSourceBuilder = CreateDataSourceBuilder();
((INpgsqlTypeMapper)dataSourceBuilder).AddDbTypeResolverFactory(new ForceStringToCitextResolverFactory());
await using var dataSource = dataSourceBuilder.Build();
await using var connection = await dataSource.OpenConnectionAsync();
await using var command = new NpgsqlCommand("SELECT @p = 'hello'::citext", connection);
var parameter = new NpgsqlParameter("p", DbType.String)
{
Value = "HeLLo"
};
command.Parameters.Add(parameter);
Assert.That(command.ExecuteScalar(), Is.True);
Assert.That(parameter.DbType, Is.EqualTo(DbType.String));
Assert.That(parameter.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Citext));
Assert.That(parameter.DataTypeName, Is.EqualTo("citext"));
}
[Test]
public async Task Guid_to_custom_type()
{
await using var adminConnection = await OpenConnectionAsync();
var type = await GetTempTypeName(adminConnection);
var dataSourceBuilder = CreateDataSourceBuilder();
dataSourceBuilder.AddTypeInfoResolverFactory(new GuidTextConverterFactory(type));
((INpgsqlTypeMapper)dataSourceBuilder).AddDbTypeResolverFactory(new GuidTextDbTypeResolverFactory(type));
await using var dataSource = dataSourceBuilder.Build();
await using var connection = await dataSource.OpenConnectionAsync();
await connection.ExecuteNonQueryAsync($"CREATE TYPE {type}");
await connection.ExecuteNonQueryAsync($"""
-- Input: cstring -> Custom type
CREATE FUNCTION {type}_in(cstring)
RETURNS {type}
AS 'textin'
LANGUAGE internal IMMUTABLE STRICT;
-- Output: Custom type -> cstring
CREATE FUNCTION {type}_out({type})
RETURNS cstring
AS 'textout'
LANGUAGE internal IMMUTABLE STRICT;
-- 3️⃣ Create wrappers for binary I/O
CREATE FUNCTION {type}_recv(internal)
RETURNS {type}
AS 'textrecv'
LANGUAGE internal IMMUTABLE STRICT;
CREATE FUNCTION {type}_send({type})
RETURNS bytea
AS 'textsend'
LANGUAGE internal IMMUTABLE STRICT;
""");
await connection.ExecuteNonQueryAsync($"""
CREATE TYPE {type} (
internallength = variable,
input = {type}_in,
output = {type}_out,
receive = {type}_recv,
send = {type}_send,
alignment = int4
);
CREATE CAST ({type} AS text) WITH INOUT AS IMPLICIT;
""");
await connection.ReloadTypesAsync();
var guid = Guid.NewGuid();
await using var command = new NpgsqlCommand($"SELECT @p::text = '{guid}'", connection);
var parameter = new NpgsqlParameter("p", DbType.Guid)
{
Value = guid
};
command.Parameters.Add(parameter);
Assert.That(command.ExecuteScalar(), Is.True);
Assert.That(parameter.DbType, Is.EqualTo(DbType.Guid));
Assert.That(parameter.NpgsqlDbType, Is.EqualTo(NpgsqlDbType.Unknown));
Assert.That(parameter.DataTypeName, Is.EqualTo(type));
}
[Test, IssueLink("https://github.com/npgsql/npgsql/issues/4582")]
[NonParallelizable] // Drops global citext extension.
public async Task Type_in_non_default_schema()
{
await using var conn = await OpenConnectionAsync();
var schemaName = await CreateTempSchema(conn);
await conn.ExecuteNonQueryAsync(@$"
DROP EXTENSION IF EXISTS citext;
CREATE EXTENSION citext SCHEMA ""{schemaName}""");
try
{
await conn.ReloadTypesAsync();
var tableName = await CreateTempTable(conn, $"created_by {schemaName}.citext NOT NULL");
const string expected = "SomeValue";
await conn.ExecuteNonQueryAsync($"INSERT INTO \"{tableName}\" VALUES('{expected}')");
var value = (string?)await conn.ExecuteScalarAsync($"SELECT created_by FROM \"{tableName}\" LIMIT 1");
Assert.That(value, Is.EqualTo(expected));
}
finally
{
await conn.ExecuteNonQueryAsync(@"DROP EXTENSION citext CASCADE");
}
}
#region Support
class CitextToStringTypeHandlerResolverFactory : PgTypeInfoResolverFactory
{
public override IPgTypeInfoResolver CreateResolver() => new Resolver();
public override IPgTypeInfoResolver? CreateArrayResolver() => null;
sealed class Resolver : IPgTypeInfoResolver
{
public PgTypeInfo? GetTypeInfo(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options)
{
if (type == typeof(string) || dataTypeName?.UnqualifiedName == "citext")
if (options.DatabaseInfo.TryGetPostgresTypeByName("citext", out var pgType))
return new(options, new StringTextConverter(options.TextEncoding), options.ToCanonicalTypeId(pgType));
return null;
}
}
}
class ForceStringToCitextResolverFactory : DbTypeResolverFactory
{
public override IDbTypeResolver CreateDbTypeResolver(NpgsqlDatabaseInfo databaseInfo) => new DbTypeResolver();
sealed class DbTypeResolver : IDbTypeResolver
{
public string? GetDataTypeName(DbType dbType, Type? type)
{
if (dbType == DbType.String)
return "citext";
return null;
}
public DbType? GetDbType(DataTypeName dataTypeName)
{
if (dataTypeName.UnqualifiedName == "citext")
return DbType.String;
return null;
}
}
}
class GuidTextConverterFactory(string typeName) : PgTypeInfoResolverFactory
{
public override IPgTypeInfoResolver? CreateArrayResolver() => null;
public override IPgTypeInfoResolver CreateResolver() => new GuidTextTypeInfoResolver(typeName);
sealed class GuidTextTypeInfoResolver(string typeName) : IPgTypeInfoResolver
{
public PgTypeInfo? GetTypeInfo(Type? type, DataTypeName? dataTypeName, PgSerializerOptions options)
{
if (type == typeof(Guid) || dataTypeName?.UnqualifiedName == typeName)
if (options.DatabaseInfo.TryGetPostgresTypeByName(typeName, out var pgType))
return new(options, new GuidTextConverter(options.TextEncoding), options.ToCanonicalTypeId(pgType));
return null;
}
}
sealed class GuidTextConverter(System.Text.Encoding encoding) : StringBasedTextConverter<Guid>(encoding)
{
public override bool CanConvert(DataFormat format, out BufferRequirements bufferRequirements)
{
bufferRequirements = BufferRequirements.None;
return format is DataFormat.Text;
}
protected override Guid ConvertFrom(string value) => Guid.Parse(value);
protected override ReadOnlyMemory<char> ConvertTo(Guid value) => value.ToString().AsMemory();
}
}
class GuidTextDbTypeResolverFactory(string typeName) : DbTypeResolverFactory
{
public override IDbTypeResolver CreateDbTypeResolver(NpgsqlDatabaseInfo databaseInfo) => new DbTypeResolver(typeName);
sealed class DbTypeResolver(string typeName) : IDbTypeResolver
{
public string? GetDataTypeName(DbType dbType, Type? type)
{
if (dbType == DbType.Guid)
return typeName;
return null;
}
public DbType? GetDbType(DataTypeName dataTypeName)
{
if (dataTypeName == typeName)
return DbType.Guid;
return null;
}
}
}
enum Mood { Sad, Ok, Happy }
#endregion Support
}