Skip to content

Commit d5314a8

Browse files
committed
Add API to NpgsqlDataSource to resolve type mappings
1 parent 7bbf43a commit d5314a8

3 files changed

Lines changed: 197 additions & 0 deletions

File tree

src/Npgsql/NpgsqlDataSource.cs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
using System.Transactions;
1010
using Microsoft.Extensions.Logging;
1111
using Npgsql.Internal;
12+
using Npgsql.Internal.Postgres;
1213
using Npgsql.Internal.ResolverFactories;
1314
using Npgsql.Properties;
1415
using Npgsql.Util;
@@ -247,6 +248,68 @@ public async Task ReloadTypesAsync(CancellationToken cancellationToken = default
247248
}
248249
}
249250

251+
/// <summary>
252+
/// Attempts to return a mapping for a specific type.
253+
/// </summary>
254+
public PgTypeInfo? TryGetMapping<T>(string? dataTypeName = null) => TryGetMapping(typeof(T), dataTypeName);
255+
256+
/// <summary>
257+
/// Attempts to return a mapping for a specific type.
258+
/// </summary>
259+
public PgTypeInfo? TryGetMapping(Type? type = null, string? dataTypeName = null)
260+
{
261+
if (type is null && string.IsNullOrEmpty(dataTypeName))
262+
throw new ArgumentException($"Either {nameof(type)} or {nameof(dataTypeName)} must be specified.");
263+
264+
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
265+
if (SerializerOptions is null)
266+
using (OpenConnection()) { }
267+
268+
return TryGetMappingCore(type, dataTypeName);
269+
}
270+
271+
/// <summary>
272+
/// Attempts to return a mapping for a specific type.
273+
/// </summary>
274+
public ValueTask<PgTypeInfo?> TryGetMappingAsync<T>(string? dataTypeName = null, CancellationToken cancellationToken = default)
275+
=> TryGetMappingAsync(typeof(T), dataTypeName, cancellationToken);
276+
277+
/// <summary>
278+
/// Attempts to return a mapping for a specific type.
279+
/// </summary>
280+
public async ValueTask<PgTypeInfo?> TryGetMappingAsync(Type? type = null, string? dataTypeName = null, CancellationToken cancellationToken = default)
281+
{
282+
if (type is null && string.IsNullOrEmpty(dataTypeName))
283+
throw new ArgumentException($"Either {nameof(type)} or {nameof(dataTypeName)} must be specified.");
284+
285+
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
286+
if (SerializerOptions is null)
287+
{
288+
var connection = await OpenConnectionAsync(cancellationToken).ConfigureAwait(false);
289+
await connection.DisposeAsync().ConfigureAwait(false);
290+
}
291+
292+
return TryGetMappingCore(type, dataTypeName);
293+
}
294+
295+
PgTypeInfo? TryGetMappingCore(Type? type, string? dataTypeName)
296+
{
297+
Debug.Assert(IsBootstrapped);
298+
Debug.Assert(SerializerOptions is not null);
299+
Debug.Assert(DatabaseInfo is not null);
300+
301+
PgTypeId? pgTypeID = null;
302+
303+
if (!string.IsNullOrEmpty(dataTypeName))
304+
{
305+
if (!DatabaseInfo.TryGetPostgresTypeByName(DataTypeName.NormalizeName(dataTypeName), out var pgType))
306+
throw new NotSupportedException($"The data type name '{dataTypeName}' isn't present in your database. You may need to install an extension or upgrade to a newer version.");
307+
pgTypeID = SerializerOptions.ToCanonicalTypeId(pgType.GetRepresentationalType());
308+
}
309+
310+
return SerializerOptions.GetTypeInfoInternal(type, pgTypeID);
311+
}
312+
250313
internal async Task Bootstrap(
251314
NpgsqlConnector connector,
252315
NpgsqlTimeout timeout,

src/Npgsql/PublicAPI.Unshipped.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ Npgsql.NpgsqlConnectionStringBuilder.RequireAuth.get -> string?
1313
Npgsql.NpgsqlConnectionStringBuilder.RequireAuth.set -> void
1414
Npgsql.NpgsqlConnectionStringBuilder.SslNegotiation.get -> Npgsql.SslNegotiation
1515
Npgsql.NpgsqlConnectionStringBuilder.SslNegotiation.set -> void
16+
Npgsql.NpgsqlDataSource.TryGetMapping(System.Type? type = null, string? dataTypeName = null) -> Npgsql.Internal.PgTypeInfo?
17+
Npgsql.NpgsqlDataSource.TryGetMapping<T>(string? dataTypeName = null) -> Npgsql.Internal.PgTypeInfo?
18+
Npgsql.NpgsqlDataSource.TryGetMappingAsync(System.Type? type = null, string? dataTypeName = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Npgsql.Internal.PgTypeInfo?>
19+
Npgsql.NpgsqlDataSource.TryGetMappingAsync<T>(string? dataTypeName = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Npgsql.Internal.PgTypeInfo?>
1620
Npgsql.NpgsqlDataSourceBuilder.ConfigureTypeLoading(System.Action<Npgsql.NpgsqlTypeLoadingOptionsBuilder!>! configureAction) -> Npgsql.NpgsqlDataSourceBuilder!
1721
Npgsql.NpgsqlDataSourceBuilder.MapComposite(System.Type! clrType, string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> Npgsql.NpgsqlDataSourceBuilder!
1822
Npgsql.NpgsqlDataSourceBuilder.MapComposite<T>(string? pgName = null, Npgsql.INpgsqlNameTranslator? nameTranslator = null) -> Npgsql.NpgsqlDataSourceBuilder!

test/Npgsql.Tests/DataSourceTests.cs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -448,5 +448,135 @@ public async Task ReloadTypes_across_data_sources([Values] bool async)
448448
Assert.DoesNotThrowAsync(async () => await connection2.ExecuteScalarAsync($"SELECT 'happy'::{type}"));
449449
}
450450

451+
[Test]
452+
public async Task Resolve_type_mapping([Values] bool async)
453+
{
454+
var dataSource = DataSource;
455+
456+
var intClrTypeMapping = async
457+
? await dataSource.TryGetMappingAsync<int>()
458+
: dataSource.TryGetMapping<int>();
459+
460+
Assert.That(intClrTypeMapping, Is.Not.Null);
461+
Assert.That(intClrTypeMapping!.Type, Is.EqualTo(typeof(int)));
462+
463+
var int4DataTypeNameMapping = async
464+
? await dataSource.TryGetMappingAsync(dataTypeName: "int4")
465+
: dataSource.TryGetMapping(dataTypeName: "int4");
466+
467+
Assert.That(int4DataTypeNameMapping, Is.Not.Null);
468+
Assert.That(int4DataTypeNameMapping!.Type, Is.EqualTo(typeof(int)));
469+
}
470+
471+
[Test]
472+
public async Task Resolve_unknown_enum_type_mapping([Values] bool async)
473+
{
474+
await using var adminConnection = await OpenConnectionAsync();
475+
var type = await GetTempTypeName(adminConnection);
476+
await adminConnection.ExecuteNonQueryAsync($"CREATE TYPE {type} AS ENUM ('sad', 'ok', 'happy')");
477+
478+
var dataSource = DataSource;
479+
// Reload types to load the new enum from the database
480+
await dataSource.ReloadTypesAsync();
481+
482+
var enumClrTypeMapping = async
483+
? await dataSource.TryGetMappingAsync<Mood>()
484+
: dataSource.TryGetMapping<Mood>();
485+
486+
Assert.That(enumClrTypeMapping, Is.Null);
487+
488+
var enumDataTypeNameMapping = async
489+
? await dataSource.TryGetMappingAsync(dataTypeName: type)
490+
: dataSource.TryGetMapping(dataTypeName: type);
491+
492+
// We support mapping unknown enums to text
493+
Assert.That(enumDataTypeNameMapping, Is.Not.Null);
494+
Assert.That(enumDataTypeNameMapping!.Type, Is.EqualTo(typeof(string)));
495+
}
496+
497+
[Test]
498+
public async Task Resolve_registered_enum_type_mapping([Values] bool async)
499+
{
500+
await using var adminConnection = await OpenConnectionAsync();
501+
var type = await GetTempTypeName(adminConnection);
502+
await adminConnection.ExecuteNonQueryAsync($"CREATE TYPE {type} AS ENUM ('sad', 'ok', 'happy')");
503+
504+
await using var dataSource = CreateDataSource(dataSourceBuilder =>
505+
{
506+
dataSourceBuilder.MapEnum<Mood>(type);
507+
});
508+
509+
var enumClrTypeMapping = async
510+
? await dataSource.TryGetMappingAsync<Mood>()
511+
: dataSource.TryGetMapping<Mood>();
512+
513+
Assert.That(enumClrTypeMapping, Is.Not.Null);
514+
Assert.That(enumClrTypeMapping!.Type, Is.EqualTo(typeof(Mood)));
515+
516+
var enumDataTypeNameMapping = async
517+
? await dataSource.TryGetMappingAsync(dataTypeName: type)
518+
: dataSource.TryGetMapping(dataTypeName: type);
519+
520+
Assert.That(enumDataTypeNameMapping, Is.Not.Null);
521+
Assert.That(enumDataTypeNameMapping!.Type, Is.EqualTo(typeof(Mood)));
522+
}
523+
524+
[Test]
525+
public async Task Resolve_unknown_composite_type_mapping([Values] bool async)
526+
{
527+
await using var adminConnection = await OpenConnectionAsync();
528+
var type = await GetTempTypeName(adminConnection);
529+
await adminConnection.ExecuteNonQueryAsync($"CREATE TYPE {type} AS (x int, some_text text)");
530+
531+
var dataSource = DataSource;
532+
// Reload types to load the new enum from the database
533+
await dataSource.ReloadTypesAsync();
534+
535+
var compositeClrTypeMapping = async
536+
? await dataSource.TryGetMappingAsync<SomeComposite>()
537+
: dataSource.TryGetMapping<SomeComposite>();
538+
539+
Assert.That(compositeClrTypeMapping, Is.Null);
540+
541+
var compositeDataTypeNameMapping = async
542+
? await dataSource.TryGetMappingAsync(dataTypeName: type)
543+
: dataSource.TryGetMapping(dataTypeName: type);
544+
545+
Assert.That(compositeDataTypeNameMapping, Is.Null);
546+
}
547+
548+
[Test]
549+
public async Task Resolve_registered_composite_type_mapping([Values] bool async)
550+
{
551+
await using var adminConnection = await OpenConnectionAsync();
552+
var type = await GetTempTypeName(adminConnection);
553+
await adminConnection.ExecuteNonQueryAsync($"CREATE TYPE {type} AS (x int, some_text text)");
554+
555+
await using var dataSource = CreateDataSource(dataSourceBuilder =>
556+
{
557+
dataSourceBuilder.MapComposite<SomeComposite>(type);
558+
});
559+
560+
var compositeClrTypeMapping = async
561+
? await dataSource.TryGetMappingAsync<SomeComposite>()
562+
: dataSource.TryGetMapping<SomeComposite>();
563+
564+
Assert.That(compositeClrTypeMapping, Is.Not.Null);
565+
Assert.That(compositeClrTypeMapping!.Type, Is.EqualTo(typeof(SomeComposite)));
566+
567+
var compositeDataTypeNameMapping = async
568+
? await dataSource.TryGetMappingAsync(dataTypeName: type)
569+
: dataSource.TryGetMapping(dataTypeName: type);
570+
571+
Assert.That(compositeDataTypeNameMapping, Is.Not.Null);
572+
Assert.That(compositeDataTypeNameMapping!.Type, Is.EqualTo(typeof(SomeComposite)));
573+
}
574+
451575
enum Mood { Sad, Ok, Happy }
576+
577+
record SomeComposite
578+
{
579+
public int X { get; set; }
580+
public string SomeText { get; set; } = null!;
581+
}
452582
}

0 commit comments

Comments
 (0)