Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions src/Npgsql/Internal/TypeHandlers/RecordHandler.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
using System;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Npgsql.BackendMessages;
using Npgsql.Internal.TypeHandling;
using Npgsql.Internal.TypeMapping;
using Npgsql.PostgresTypes;
using Npgsql.TypeMapping;

namespace Npgsql.Internal.TypeHandlers;

Expand Down
48 changes: 48 additions & 0 deletions src/Npgsql/Internal/TypeHandlers/UnsupportedHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Npgsql.BackendMessages;
using Npgsql.Internal.TypeHandling;
using Npgsql.PostgresTypes;

namespace Npgsql.Internal.TypeHandlers;

sealed class UnsupportedHandler : NpgsqlTypeHandler
{
readonly string _exceptionMessage;

public UnsupportedHandler(PostgresType postgresType, string exceptionMessage) : base(postgresType)
=> _exceptionMessage = exceptionMessage;

public override ValueTask<object> ReadAsObject(NpgsqlReadBuffer buf, int len, bool async, FieldDescription? fieldDescription = null)
=> throw new NotSupportedException(_exceptionMessage);

public override int ValidateObjectAndGetLength(object value, ref NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter)
=> throw new NotSupportedException(_exceptionMessage);

public override Task WriteObjectWithLength(object? value, NpgsqlWriteBuffer buf, NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter, bool async,
CancellationToken cancellationToken = default)
=> throw new NotSupportedException(_exceptionMessage);

protected internal override ValueTask<TAny> ReadCustom<TAny>(NpgsqlReadBuffer buf, int len, bool async, FieldDescription? fieldDescription)
=> throw new NotSupportedException(_exceptionMessage);

protected override Task WriteWithLengthCustom<TAny>(TAny value, NpgsqlWriteBuffer buf, NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter, bool async,
CancellationToken cancellationToken)
=> throw new NotSupportedException(_exceptionMessage);

protected internal override int ValidateAndGetLengthCustom<TAny>(TAny value, ref NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter)
=> throw new NotSupportedException(_exceptionMessage);

public override Type GetFieldType(FieldDescription? fieldDescription = null)
=> throw new NotSupportedException(_exceptionMessage);

public override NpgsqlTypeHandler CreateArrayHandler(PostgresArrayType pgArrayType, ArrayNullabilityMode arrayNullabilityMode)
=> throw new NotSupportedException(_exceptionMessage);

public override NpgsqlTypeHandler CreateRangeHandler(PostgresType pgRangeType)
=> throw new NotSupportedException(_exceptionMessage);

public override NpgsqlTypeHandler CreateMultirangeHandler(PostgresMultirangeType pgMultirangeType)
=> throw new NotSupportedException(_exceptionMessage);
}
1 change: 1 addition & 0 deletions src/Npgsql/NpgsqlDataSourceBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -315,5 +315,6 @@ void AddDefaultFeatures()
_internalBuilder.EnableEncryption();
_internalBuilder.AddDefaultTypeResolverFactory(new JsonTypeHandlerResolverFactory());
_internalBuilder.AddDefaultTypeResolverFactory(new RangeTypeHandlerResolverFactory());
_internalBuilder.AddDefaultTypeResolverFactory(new RecordTypeHandlerResolverFactory());
}
}
9 changes: 9 additions & 0 deletions src/Npgsql/NpgsqlSlimDataSourceBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,15 @@ public NpgsqlSlimDataSourceBuilder UseSystemTextJson(
return this;
}

/// <summary>
/// Sets up mappings for the PostgreSQL <c>record</c> type.
/// </summary>
public NpgsqlSlimDataSourceBuilder EnableRecords()
{
AddTypeResolverFactory(new RecordTypeHandlerResolverFactory());
return this;
}

/// <summary>
/// Enables the possibility to use TLS/SSl encryption for connections to PostgreSQL. This does not guarantee that encryption will
/// actually be used; see <see href="https://www.npgsql.org/doc/security.html"/> for more details.
Expand Down
1 change: 1 addition & 0 deletions src/Npgsql/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
#nullable enable
Npgsql.NpgsqlSlimDataSourceBuilder.EnableRecords() -> Npgsql.NpgsqlSlimDataSourceBuilder!
override Npgsql.NpgsqlBatch.Dispose() -> void
Npgsql.NpgsqlBinaryImporter.WriteRow(params object?[]! values) -> void
Npgsql.NpgsqlBinaryImporter.WriteRowAsync(System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken), params object?[]! values) -> System.Threading.Tasks.Task!
Expand Down
4 changes: 1 addition & 3 deletions src/Npgsql/TypeMapping/BuiltInTypeHandlerResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,6 @@ sealed class BuiltInTypeHandlerResolver : TypeHandlerResolver
UuidHandler? _uuidHandler;
BitStringHandler? _bitVaryingHandler;
BitStringHandler? _bitHandler;
RecordHandler? _recordHandler;
VoidHandler? _voidHandler;
HstoreHandler? _hstoreHandler;

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

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

NpgsqlTypeHandler UnknownHandler() => _unknownHandler ??= new UnknownTypeHandler(_connector.TextEncoding);
Expand Down
31 changes: 31 additions & 0 deletions src/Npgsql/TypeMapping/RecordTypeHandlerResolver.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;
using Npgsql.Internal;
using Npgsql.Internal.TypeHandlers;
using Npgsql.Internal.TypeHandling;
using Npgsql.Internal.TypeMapping;
using Npgsql.PostgresTypes;

namespace Npgsql.TypeMapping;

sealed class RecordTypeHandlerResolver : TypeHandlerResolver
{
readonly TypeMapper _typeMapper;
readonly NpgsqlDatabaseInfo _databaseInfo;

RecordHandler? _recordHandler;

public RecordTypeHandlerResolver(TypeMapper typeMapper, NpgsqlConnector connector)
{
_typeMapper = typeMapper;
_databaseInfo = connector.DatabaseInfo;
}

public override NpgsqlTypeHandler? ResolveByDataTypeName(string typeName)
=> typeName == "record" ? GetHandler() : null;

public override NpgsqlTypeHandler? ResolveByClrType(Type type) => null;

public override TypeMappingInfo? GetMappingByPostgresType(PostgresType type) => null;

NpgsqlTypeHandler GetHandler() => _recordHandler ??= new RecordHandler(_databaseInfo.GetPostgresTypeByName("record"), _typeMapper);
}
22 changes: 22 additions & 0 deletions src/Npgsql/TypeMapping/RecordTypeHandlerResolverFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using System;
using Npgsql.Internal;
using Npgsql.Internal.TypeHandling;
using Npgsql.Internal.TypeMapping;

namespace Npgsql.TypeMapping;

sealed class RecordTypeHandlerResolverFactory : TypeHandlerResolverFactory
{
public override TypeHandlerResolver Create(TypeMapper typeMapper, NpgsqlConnector connector)
=> new RecordTypeHandlerResolver(typeMapper, connector);

// Records aren't mapped to anything
public override string? GetDataTypeNameByClrType(Type clrType)
=> null;

public override string? GetDataTypeNameByValueDependentValue(object value)
=> null;

public override TypeMappingInfo? GetMappingByDataTypeName(string dataTypeName)
=> null;
}
28 changes: 28 additions & 0 deletions test/Npgsql.Tests/Types/MiscTypeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Threading.Tasks;
using NpgsqlTypes;
using NUnit.Framework;
using NUnit.Framework.Constraints;

namespace Npgsql.Tests.Types;

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

[Test]
public async Task Records_supported_only_with_EnableRecords([Values] bool withMappings)
{
const string unsupportedMessage =
"Records aren't supported; please call EnableRecords on NpgsqlSlimDataSourceBuilder to enable records.";
Func<IResolveConstraint> assertExpr = () => withMappings
? Throws.Nothing
: Throws.Exception
.TypeOf<NotSupportedException>()
.With.Property("Message").EqualTo(unsupportedMessage);

var dataSourceBuilder = new NpgsqlSlimDataSourceBuilder(ConnectionString);
if (withMappings)
dataSourceBuilder.EnableRecords();
var dataSource = dataSourceBuilder.Build();
await using var conn = await dataSource.OpenConnectionAsync();
await using var cmd = conn.CreateCommand();

// RecordHandler doesn't support writing, so we only check for reading
cmd.CommandText = "SELECT ('one'::text, 2)";
await using var reader = await cmd.ExecuteReaderAsync();
await reader.ReadAsync();

Assert.That(() => reader.GetValue(0), assertExpr());
Assert.That(() => reader.GetFieldValue<object[]>(0), assertExpr());
}

#endregion Record

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