Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,27 @@ public NpgsqlByteArrayMethodTranslator(ISqlExpressionFactory sqlExpressionFactor
Check.NotNull(method, nameof(method));
Check.NotNull(arguments, nameof(arguments));

// bytea || bytea. Note that we return a byte[] (rather than the method's IEnumerable<byte>) so that further bytea
// operations (ToArray, Length...) can be composed on top.
if (method is { IsGenericMethod: true, Name: nameof(Enumerable.Concat) }
&& method.DeclaringType == typeof(Enumerable)
&& arguments is [var first, var second]
&& (first.TypeMapping ?? second.TypeMapping) is NpgsqlByteArrayTypeMapping concatTypeMapping)
{
return _sqlExpressionFactory.Add(
_sqlExpressionFactory.ApplyTypeMapping(first, concatTypeMapping),
_sqlExpressionFactory.ApplyTypeMapping(second, concatTypeMapping),
concatTypeMapping);
}

if (method.IsGenericMethod && arguments[0].TypeMapping is NpgsqlByteArrayTypeMapping typeMapping)
{
// ToArray over bytea (e.g. after Concat) is a no-op
if (method is { Name: nameof(Enumerable.ToArray) } && method.DeclaringType == typeof(Enumerable))
{
return arguments[0];
}

// Note: we only translate if the array argument is a column mapped to bytea. There are various other
// cases (e.g. Where(b => new byte[] { 1, 2, 3 }.Contains(b.SomeByte))) where we prefer to translate via
// regular PostgreSQL array logic.
Expand Down
3 changes: 2 additions & 1 deletion src/EFCore.PG/Query/Internal/NpgsqlQuerySqlGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ protected override string GetOperator(SqlBinaryExpression e)
{
// PostgreSQL has a special string concatenation operator: ||
// We switch to it if the expression itself has type string, or if one of the sides has a string type mapping.
// Same for full-text search's TsVector, arrays.
// Same for full-text search's TsVector, arrays and bytea.
ExpressionType.Add when
e.Type == typeof(string)
|| e.Left.TypeMapping?.ClrType == typeof(string)
Expand All @@ -127,6 +127,7 @@ ExpressionType.Add when
|| e.Left.TypeMapping?.ClrType == typeof(NpgsqlTsVector)
|| e.Right.TypeMapping?.ClrType == typeof(NpgsqlTsVector)
|| e.Left.TypeMapping is NpgsqlArrayTypeMapping && e.Right.TypeMapping is NpgsqlArrayTypeMapping
|| e.Left.TypeMapping is NpgsqlByteArrayTypeMapping && e.Right.TypeMapping is NpgsqlByteArrayTypeMapping
=> " || ",

ExpressionType.And when e.Type == typeof(bool) => " AND ",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,74 @@ WHERE length(b."ByteArray") > 0
""");
}

[ConditionalFact]
public virtual async Task Concat_with_parameter()
{
var suffix = new byte[] { 1, 2 };

await AssertQuery(ss => ss.Set<BasicTypesEntity>().Where(e => e.ByteArray.Concat(suffix).ToArray().Length == 6));

AssertSql(
"""
@suffix='0x0102'

SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan"
FROM "BasicTypesEntities" AS b
WHERE length(b."ByteArray" || @suffix) = 6
""");
}

[ConditionalFact]
public virtual async Task Concat_with_column()
{
await AssertQuery(ss => ss.Set<BasicTypesEntity>().Where(e => e.ByteArray.Concat(e.ByteArray).ToArray().Length == 8));

AssertSql(
"""
SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan"
FROM "BasicTypesEntities" AS b
WHERE length(b."ByteArray" || b."ByteArray") = 8
""");
}

[ConditionalFact]
public virtual async Task Concat_in_projection()
{
var suffix = new byte[] { 1, 2 };

await AssertQuery(
ss => ss.Set<BasicTypesEntity>().OrderBy(e => e.Id).Select(e => e.ByteArray.Concat(suffix).ToArray()),
assertOrder: true);

AssertSql(
"""
@suffix='0x0102'

SELECT b."ByteArray" || @suffix
FROM "BasicTypesEntities" AS b
ORDER BY b."Id" NULLS FIRST
""");
}

[ConditionalFact]
public virtual async Task Concat_SequenceEqual()
{
var suffix = new byte[] { 1, 2 };
var expected = new byte[] { 0xDE, 0xAD, 0xBE, 0xEF, 1, 2 };

await AssertQuery(ss => ss.Set<BasicTypesEntity>().Where(e => e.ByteArray.Concat(suffix).SequenceEqual(expected)));

AssertSql(
"""
@suffix='0x0102'
@expected='0xDEADBEEF0102'

SELECT b."Id", b."Bool", b."Byte", b."ByteArray", b."DateOnly", b."DateTime", b."DateTimeOffset", b."Decimal", b."Double", b."Enum", b."FlagsEnum", b."Float", b."Guid", b."Int", b."Long", b."Short", b."String", b."TimeOnly", b."TimeSpan"
FROM "BasicTypesEntities" AS b
WHERE b."ByteArray" || @suffix = @expected
""");
}

private void AssertSql(params string[] expected)
=> Fixture.TestSqlLoggerFactory.AssertBaseline(expected);
}
Loading