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
179 changes: 85 additions & 94 deletions src/Npgsql/Internal/TypeHandlers/ArrayHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,89 @@ protected async ValueTask<List<TRequestedElement>> ReadList<TRequestedElement>(N

#endregion Read

#region Write

// Take care of multi-dimensional arrays and non-generic IList, we have no choice but to box/unbox
protected int ValidateAndGetLengthNonGeneric(ICollection value, ref NpgsqlLengthCache lengthCache)
{
var asMultidimensional = value as Array;
var dimensions = asMultidimensional?.Rank ?? 1;

// Leave empty slot for the entire array length, and go ahead an populate the element slots
var pos = lengthCache.Position;
var len =
4 + // dimensions
4 + // has_nulls (unused)
4 + // type OID
dimensions * 8 + // number of dimensions * (length + lower bound)
4 * value.Count; // sum of element lengths

lengthCache.Set(0);
NpgsqlLengthCache? elemLengthCache = lengthCache;

foreach (var element in value)
{
if (element is null)
continue;

try
{
len += ElementHandler.ValidateObjectAndGetLength(element, ref elemLengthCache, null);
}
catch (Exception e)
{
throw MixedTypesOrJaggedArrayException(e);
}
}

lengthCache.Lengths[pos] = len;
return len;
}

protected async Task WriteNonGeneric(ICollection value, NpgsqlWriteBuffer buf, NpgsqlLengthCache? lengthCache, bool async, CancellationToken cancellationToken = default)
{
var asArray = value as Array;
var dimensions = asArray?.Rank ?? 1;

var len =
4 + // ndim
4 + // has_nulls
4 + // element_oid
dimensions * 8; // dim (4) + lBound (4)

if (buf.WriteSpaceLeft < len)
{
await buf.Flush(async, cancellationToken);
Debug.Assert(buf.WriteSpaceLeft >= len, "Buffer too small for header");
}

buf.WriteInt32(dimensions);
buf.WriteInt32(1); // HasNulls=1. Not actually used by the backend.
buf.WriteUInt32(ElementHandler.PostgresType.OID);
if (asArray != null)
{
for (var i = 0; i < dimensions; i++)
{
buf.WriteInt32(asArray.GetLength(i));
buf.WriteInt32(LowerBound); // We don't map .NET lower bounds to PG
}
}
else
{
buf.WriteInt32(value.Count);
buf.WriteInt32(LowerBound); // We don't map .NET lower bounds to PG
}

foreach (var element in value)
await ElementHandler.WriteObjectWithLength(element, buf, lengthCache, null, async, cancellationToken);
}

protected static Exception MixedTypesOrJaggedArrayException(Exception innerException)
=> new("While trying to write an array, one of its elements failed validation. " +
"You may be trying to mix types in a non-generic IList, or to write a jagged array.", innerException);

#endregion Write

#region Static generic caching helpers

internal static class ElementTypeInfo<TElement>
Expand Down Expand Up @@ -297,12 +380,8 @@ public override async ValueTask<object> ReadAsObject(NpgsqlReadBuffer buf, int l

#region Write

static Exception MixedTypesOrJaggedArrayException(Exception innerException)
=> new("While trying to write an array, one of its elements failed validation. " +
"You may be trying to mix types in a non-generic IList, or to write a jagged array.", innerException);

static Exception CantWriteTypeException(Type type)
=> new InvalidCastException($"Can't write type {type} as an array of {typeof(TElement)}");
static InvalidCastException CantWriteTypeException(Type type)
=> new($"Can't write type '{type}' as an array of {typeof(TElement)}");

// Since TAny isn't constrained to class? or struct (C# doesn't have a non-nullable constraint that doesn't limit us to either struct or class),
// we must use the bang operator here to tell the compiler that a null value will never be returned.
Expand Down Expand Up @@ -363,43 +442,6 @@ int ValidateAndGetLengthGeneric(ICollection<TElement> value, ref NpgsqlLengthCac
return len;
}

// Take care of multi-dimensional arrays and non-generic IList, we have no choice but to box/unbox
int ValidateAndGetLengthNonGeneric(ICollection value, ref NpgsqlLengthCache lengthCache)
{
var asMultidimensional = value as Array;
var dimensions = asMultidimensional?.Rank ?? 1;

// Leave empty slot for the entire array length, and go ahead an populate the element slots
var pos = lengthCache.Position;
var len =
4 + // dimensions
4 + // has_nulls (unused)
4 + // type OID
dimensions * 8 + // number of dimensions * (length + lower bound)
4 * value.Count; // sum of element lengths

lengthCache.Set(0);
NpgsqlLengthCache? elemLengthCache = lengthCache;

foreach (var element in value)
{
if (element is null)
continue;

try
{
len += ElementHandler.ValidateObjectAndGetLength(element, ref elemLengthCache, null);
}
catch (Exception e)
{
throw MixedTypesOrJaggedArrayException(e);
}
}

lengthCache.Lengths[pos] = len;
return len;
}

protected override Task WriteWithLengthCustom<TAny>([DisallowNull] TAny value, NpgsqlWriteBuffer buf, NpgsqlLengthCache? lengthCache, NpgsqlParameter? parameter, bool async, CancellationToken cancellationToken)
{
buf.WriteInt32(ValidateAndGetLength(value, ref lengthCache, parameter));
Expand Down Expand Up @@ -444,44 +486,6 @@ async Task WriteGeneric(ICollection<TElement> value, NpgsqlWriteBuffer buf, Npgs
await ElementHandler.WriteWithLength(element, buf, lengthCache, null, async, cancellationToken);
}

async Task WriteNonGeneric(ICollection value, NpgsqlWriteBuffer buf, NpgsqlLengthCache? lengthCache, bool async, CancellationToken cancellationToken = default)
{
var asArray = value as Array;
var dimensions = asArray?.Rank ?? 1;

var len =
4 + // ndim
4 + // has_nulls
4 + // element_oid
dimensions * 8; // dim (4) + lBound (4)

if (buf.WriteSpaceLeft < len)
{
await buf.Flush(async, cancellationToken);
Debug.Assert(buf.WriteSpaceLeft >= len, "Buffer too small for header");
}

buf.WriteInt32(dimensions);
buf.WriteInt32(1); // HasNulls=1. Not actually used by the backend.
buf.WriteUInt32(ElementHandler.PostgresType.OID);
if (asArray != null)
{
for (var i = 0; i < dimensions; i++)
{
buf.WriteInt32(asArray.GetLength(i));
buf.WriteInt32(LowerBound); // We don't map .NET lower bounds to PG
}
}
else
{
buf.WriteInt32(value.Count);
buf.WriteInt32(LowerBound); // We don't map .NET lower bounds to PG
}

foreach (var element in value)
await ElementHandler.WriteObjectWithLength(element, buf, lengthCache, null, async, cancellationToken);
}

#endregion
}

Expand All @@ -495,19 +499,6 @@ sealed class ArrayHandlerWithPsv<TElement, TElementPsv> : ArrayHandler<TElement>
public ArrayHandlerWithPsv(PostgresType arrayPostgresType, NpgsqlTypeHandler elementHandler, ArrayNullabilityMode arrayNullabilityMode)
: base(arrayPostgresType, elementHandler, arrayNullabilityMode) { }

protected internal override async ValueTask<TRequestedArray> ReadCustom<TRequestedArray>(NpgsqlReadBuffer buf, int len, bool async, FieldDescription? fieldDescription = null)
{
if (ArrayTypeInfo<TRequestedArray>.ElementType == typeof(TElementPsv))
{
if (ArrayTypeInfo<TRequestedArray>.IsArray)
return (TRequestedArray)(object)await ReadArray<TElementPsv>(buf, async, typeof(TRequestedArray).GetArrayRank());

if (ArrayTypeInfo<TRequestedArray>.IsList)
return (TRequestedArray)(object)await ReadList<TElementPsv>(buf, async);
}
return await base.ReadCustom<TRequestedArray>(buf, len, async, fieldDescription);
}

internal override object ReadPsvAsObject(NpgsqlReadBuffer buf, int len, FieldDescription? fieldDescription = null)
=> ReadPsvAsObject(buf, len, false, fieldDescription).GetAwaiter().GetResult();

Expand Down
24 changes: 0 additions & 24 deletions src/Npgsql/Internal/TypeHandlers/BitStringHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -266,30 +266,6 @@ public class BitStringArrayHandler : ArrayHandler<BitArray>
public BitStringArrayHandler(PostgresType postgresType, BitStringHandler elementHandler, ArrayNullabilityMode arrayNullabilityMode)
: base(postgresType, elementHandler, arrayNullabilityMode) {}

/// <inheritdoc />
protected internal override async ValueTask<TRequestedArray> ReadCustom<TRequestedArray>(NpgsqlReadBuffer buf, int len, bool async, FieldDescription? fieldDescription = null)
{
if (ArrayTypeInfo<TRequestedArray>.ElementType == typeof(BitArray))
{
if (ArrayTypeInfo<TRequestedArray>.IsArray)
return (TRequestedArray)(object)await ReadArray<BitArray>(buf, async);

if (ArrayTypeInfo<TRequestedArray>.IsList)
return (TRequestedArray)(object)await ReadList<BitArray>(buf, async);
}

if (ArrayTypeInfo<TRequestedArray>.ElementType == typeof(bool))
{
if (ArrayTypeInfo<TRequestedArray>.IsArray)
return (TRequestedArray)(object)await ReadArray<bool>(buf, async);

if (ArrayTypeInfo<TRequestedArray>.IsList)
return (TRequestedArray)(object)await ReadList<bool>(buf, async);
}

return await base.ReadCustom<TRequestedArray>(buf, len, async, fieldDescription);
}

public override async ValueTask<object> ReadAsObject(NpgsqlReadBuffer buf, int len, bool async, FieldDescription? fieldDescription = null)
=> fieldDescription?.TypeModifier == 1
? await ReadArray<bool>(buf, async)
Expand Down
11 changes: 10 additions & 1 deletion test/Npgsql.Tests/Types/NetworkTypeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ public Task Inet_v6_as_tuple()
NpgsqlDbType.Inet,
isDefaultForReading: false);

[Test]
public Task Inet_v6_array_as_tuple()
=> AssertType(
new[] { (IPAddress.Parse("2001:1db8:85a3:1142:1000:8a2e:1370:7334"), 24) },
"{2001:1db8:85a3:1142:1000:8a2e:1370:7334/24}",
"inet[]",
NpgsqlDbType.Inet | NpgsqlDbType.Array,
isDefaultForReading: false);

[Test, IssueLink("https://github.com/dotnet/corefx/issues/33373")]
public Task IPAddress_Any()
=> AssertTypeWrite(IPAddress.Any, "0.0.0.0/32", "inet", NpgsqlDbType.Inet);
Expand Down Expand Up @@ -127,4 +136,4 @@ public async Task Macaddr_write_validation()
}

public NetworkTypeTests(MultiplexingMode multiplexingMode) : base(multiplexingMode) {}
}
}