Skip to content

Commit f0fc46c

Browse files
authored
Add a separate resolver for records (#4971)
Part of #4965
1 parent 5461fa8 commit f0fc46c

File tree

9 files changed

+141
-5
lines changed

9 files changed

+141
-5
lines changed

src/Npgsql/Internal/TypeHandlers/RecordHandler.cs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
using System;
22
using System.Linq;
3-
using System.Runtime.CompilerServices;
43
using System.Threading;
54
using System.Threading.Tasks;
65
using Npgsql.BackendMessages;
76
using Npgsql.Internal.TypeHandling;
87
using Npgsql.Internal.TypeMapping;
98
using Npgsql.PostgresTypes;
10-
using Npgsql.TypeMapping;
119

1210
namespace Npgsql.Internal.TypeHandlers;
1311

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System;
2+
using System.Threading;
3+
using System.Threading.Tasks;
4+
using Npgsql.BackendMessages;
5+
using Npgsql.Internal.TypeHandling;
6+
using Npgsql.PostgresTypes;
7+
8+
namespace Npgsql.Internal.TypeHandlers;
9+
10+
sealed class UnsupportedHandler : NpgsqlTypeHandler
11+
{
12+
readonly string _exceptionMessage;
13+
14+
public UnsupportedHandler(PostgresType postgresType, string exceptionMessage) : base(postgresType)
15+
=> _exceptionMessage = exceptionMessage;
16+
17+
public override ValueTask<object> ReadAsObject(NpgsqlReadBuffer buf, int len, bool async, FieldDescription? fieldDescription = null)
18+
=> throw new NotSupportedException(_exceptionMessage);
19+
20+
public override int ValidateObjectAndGetLength(object value, ref NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter)
21+
=> throw new NotSupportedException(_exceptionMessage);
22+
23+
public override Task WriteObjectWithLength(object? value, NpgsqlWriteBuffer buf, NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter, bool async,
24+
CancellationToken cancellationToken = default)
25+
=> throw new NotSupportedException(_exceptionMessage);
26+
27+
protected internal override ValueTask<TAny> ReadCustom<TAny>(NpgsqlReadBuffer buf, int len, bool async, FieldDescription? fieldDescription)
28+
=> throw new NotSupportedException(_exceptionMessage);
29+
30+
protected override Task WriteWithLengthCustom<TAny>(TAny value, NpgsqlWriteBuffer buf, NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter, bool async,
31+
CancellationToken cancellationToken)
32+
=> throw new NotSupportedException(_exceptionMessage);
33+
34+
protected internal override int ValidateAndGetLengthCustom<TAny>(TAny value, ref NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter)
35+
=> throw new NotSupportedException(_exceptionMessage);
36+
37+
public override Type GetFieldType(FieldDescription? fieldDescription = null)
38+
=> throw new NotSupportedException(_exceptionMessage);
39+
40+
public override NpgsqlTypeHandler CreateArrayHandler(PostgresArrayType pgArrayType, ArrayNullabilityMode arrayNullabilityMode)
41+
=> throw new NotSupportedException(_exceptionMessage);
42+
43+
public override NpgsqlTypeHandler CreateRangeHandler(PostgresType pgRangeType)
44+
=> throw new NotSupportedException(_exceptionMessage);
45+
46+
public override NpgsqlTypeHandler CreateMultirangeHandler(PostgresMultirangeType pgMultirangeType)
47+
=> throw new NotSupportedException(_exceptionMessage);
48+
}

src/Npgsql/NpgsqlDataSourceBuilder.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,5 +315,6 @@ void AddDefaultFeatures()
315315
_internalBuilder.EnableEncryption();
316316
_internalBuilder.AddDefaultTypeResolverFactory(new JsonTypeHandlerResolverFactory());
317317
_internalBuilder.AddDefaultTypeResolverFactory(new RangeTypeHandlerResolverFactory());
318+
_internalBuilder.AddDefaultTypeResolverFactory(new RecordTypeHandlerResolverFactory());
318319
}
319320
}

src/Npgsql/NpgsqlSlimDataSourceBuilder.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,15 @@ public NpgsqlSlimDataSourceBuilder UseSystemTextJson(
408408
return this;
409409
}
410410

411+
/// <summary>
412+
/// Sets up mappings for the PostgreSQL <c>record</c> type.
413+
/// </summary>
414+
public NpgsqlSlimDataSourceBuilder EnableRecords()
415+
{
416+
AddTypeResolverFactory(new RecordTypeHandlerResolverFactory());
417+
return this;
418+
}
419+
411420
/// <summary>
412421
/// Enables the possibility to use TLS/SSl encryption for connections to PostgreSQL. This does not guarantee that encryption will
413422
/// actually be used; see <see href="https://www.npgsql.org/doc/security.html"/> for more details.

src/Npgsql/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#nullable enable
2+
Npgsql.NpgsqlSlimDataSourceBuilder.EnableRecords() -> Npgsql.NpgsqlSlimDataSourceBuilder!
23
override Npgsql.NpgsqlBatch.Dispose() -> void
34
Npgsql.NpgsqlBinaryImporter.WriteRow(params object?[]! values) -> void
45
Npgsql.NpgsqlBinaryImporter.WriteRowAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken), params object?[]! values) -> System.Threading.Tasks.Task!

src/Npgsql/TypeMapping/BuiltInTypeHandlerResolver.cs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,6 @@ sealed class BuiltInTypeHandlerResolver : TypeHandlerResolver
228228
UuidHandler? _uuidHandler;
229229
BitStringHandler? _bitVaryingHandler;
230230
BitStringHandler? _bitHandler;
231-
RecordHandler? _recordHandler;
232231
VoidHandler? _voidHandler;
233232
HstoreHandler? _hstoreHandler;
234233

@@ -344,7 +343,7 @@ internal BuiltInTypeHandlerResolver(NpgsqlConnector connector)
344343
"pg_lsn" => PgLsnHandler(),
345344
"tid" => TidHandler(),
346345
"char" => InternalCharHandler(),
347-
"record" => RecordHandler(),
346+
"record" => new UnsupportedHandler(PgType("record"), $"Records aren't supported; please call {nameof(NpgsqlSlimDataSourceBuilder.EnableRecords)} on {nameof(NpgsqlSlimDataSourceBuilder)} to enable records."),
348347
"void" => VoidHandler(),
349348

350349
"unknown" => UnknownHandler(),
@@ -715,7 +714,6 @@ static DateTimeKind GetMultirangeKind(IList<NpgsqlRange<DateTime>> multirange)
715714
NpgsqlTypeHandler PgLsnHandler() => _pgLsnHandler ??= new PgLsnHandler(PgType("pg_lsn"));
716715
NpgsqlTypeHandler TidHandler() => _tidHandler ??= new TidHandler(PgType("tid"));
717716
NpgsqlTypeHandler InternalCharHandler() => _internalCharHandler ??= new InternalCharHandler(PgType("char"));
718-
NpgsqlTypeHandler RecordHandler() => _recordHandler ??= new RecordHandler(PgType("record"), _connector.TypeMapper);
719717
NpgsqlTypeHandler VoidHandler() => _voidHandler ??= new VoidHandler(PgType("void"));
720718

721719
NpgsqlTypeHandler UnknownHandler() => _unknownHandler ??= new UnknownTypeHandler(_connector.TextEncoding);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
using System;
2+
using Npgsql.Internal;
3+
using Npgsql.Internal.TypeHandlers;
4+
using Npgsql.Internal.TypeHandling;
5+
using Npgsql.Internal.TypeMapping;
6+
using Npgsql.PostgresTypes;
7+
8+
namespace Npgsql.TypeMapping;
9+
10+
sealed class RecordTypeHandlerResolver : TypeHandlerResolver
11+
{
12+
readonly TypeMapper _typeMapper;
13+
readonly NpgsqlDatabaseInfo _databaseInfo;
14+
15+
RecordHandler? _recordHandler;
16+
17+
public RecordTypeHandlerResolver(TypeMapper typeMapper, NpgsqlConnector connector)
18+
{
19+
_typeMapper = typeMapper;
20+
_databaseInfo = connector.DatabaseInfo;
21+
}
22+
23+
public override NpgsqlTypeHandler? ResolveByDataTypeName(string typeName)
24+
=> typeName == "record" ? GetHandler() : null;
25+
26+
public override NpgsqlTypeHandler? ResolveByClrType(Type type) => null;
27+
28+
public override TypeMappingInfo? GetMappingByPostgresType(PostgresType type) => null;
29+
30+
NpgsqlTypeHandler GetHandler() => _recordHandler ??= new RecordHandler(_databaseInfo.GetPostgresTypeByName("record"), _typeMapper);
31+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using System;
2+
using Npgsql.Internal;
3+
using Npgsql.Internal.TypeHandling;
4+
using Npgsql.Internal.TypeMapping;
5+
6+
namespace Npgsql.TypeMapping;
7+
8+
sealed class RecordTypeHandlerResolverFactory : TypeHandlerResolverFactory
9+
{
10+
public override TypeHandlerResolver Create(TypeMapper typeMapper, NpgsqlConnector connector)
11+
=> new RecordTypeHandlerResolver(typeMapper, connector);
12+
13+
// Records aren't mapped to anything
14+
public override string? GetDataTypeNameByClrType(Type clrType)
15+
=> null;
16+
17+
public override string? GetDataTypeNameByValueDependentValue(object value)
18+
=> null;
19+
20+
public override TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName)
21+
=> null;
22+
}

test/Npgsql.Tests/Types/MiscTypeTests.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Threading.Tasks;
44
using NpgsqlTypes;
55
using NUnit.Framework;
6+
using NUnit.Framework.Constraints;
67

78
namespace Npgsql.Tests.Types;
89

@@ -131,6 +132,33 @@ public async Task Read_Record_as_Tuple()
131132
public Task Write_Record_is_not_supported()
132133
=> AssertTypeUnsupportedWrite<object[], NotSupportedException>(new object[] { 1, "foo" }, "record");
133134

135+
[Test]
136+
public async Task Records_supported_only_with_EnableRecords([Values] bool withMappings)
137+
{
138+
const string unsupportedMessage =
139+
"Records aren't supported; please call EnableRecords on NpgsqlSlimDataSourceBuilder to enable records.";
140+
Func<IResolveConstraint> assertExpr = () => withMappings
141+
? Throws.Nothing
142+
: Throws.Exception
143+
.TypeOf<NotSupportedException>()
144+
.With.Property("Message").EqualTo(unsupportedMessage);
145+
146+
var dataSourceBuilder = new NpgsqlSlimDataSourceBuilder(ConnectionString);
147+
if (withMappings)
148+
dataSourceBuilder.EnableRecords();
149+
var dataSource = dataSourceBuilder.Build();
150+
await using var conn = await dataSource.OpenConnectionAsync();
151+
await using var cmd = conn.CreateCommand();
152+
153+
// RecordHandler doesn't support writing, so we only check for reading
154+
cmd.CommandText = "SELECT ('one'::text, 2)";
155+
await using var reader = await cmd.ExecuteReaderAsync();
156+
await reader.ReadAsync();
157+
158+
Assert.That(() => reader.GetValue(0), assertExpr());
159+
Assert.That(() => reader.GetFieldValue<object[]>(0), assertExpr());
160+
}
161+
134162
#endregion Record
135163

136164
[Test, Description("Makes sure that setting DbType.Object makes Npgsql infer the type")]

0 commit comments

Comments
 (0)