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
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
}
}

return newRowValues is null ? this : new PgRowValueExpression(newRowValues, Type);
return newRowValues is null ? this : new PgRowValueExpression(newRowValues, Type, TypeMapping);
}

/// <summary>
Expand All @@ -67,7 +67,7 @@ protected override Expression VisitChildren(ExpressionVisitor visitor)
public virtual PgRowValueExpression Update(IReadOnlyList<SqlExpression> values)
=> values.Count == Values.Count && values.Zip(Values, (x, y) => (x, y)).All(tup => tup.x == tup.y)
? this
: new PgRowValueExpression(values, Type);
: new PgRowValueExpression(values, Type, TypeMapping);

/// <inheritdoc />
public override Expression Quote()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,106 @@ public class AdHocQueryFiltersQueryNpgsqlTest : AdHocQueryFiltersQueryRelational
{
protected override ITestStoreFactory TestStoreFactory
=> NpgsqlTestStoreFactory.Instance;

[ConditionalTheory, MemberData(nameof(IsAsyncData))]
public virtual async Task Row_value_projection_over_filtered_navigation(bool async)
{
var contextFactory = await InitializeAsync<Context3933>(
onConfiguring: options => options.UseNpgsql(
NpgsqlTestStore.CreateConnectionString(StoreName),
o => o.ConfigureDataSource(b => b.EnableRecordsAsTuples())),
seed: async context =>
{
context.Orders.Add(new Context3933.Order
{
Id = 1,
VendorLinks =
[
new Context3933.VendorLink
{
Vendor = new Context3933.Vendor
{
OrgId = 1,
IsActive = true,
Representatives = [new Context3933.Representative { IsActive = true }]
}
}
]
});
await context.SaveChangesAsync();
});

await using var context = contextFactory.CreateContext();

var query = context.Orders
.Where(x => x.Id == 1)
.Select(x => ValueTuple.Create(
x.Id,
x.VendorLinks.Any(vl => vl.Vendor!.IsActive && vl.Vendor.Representatives.Any(r => r.IsActive))));

var result = async ? await query.FirstOrDefaultAsync() : query.FirstOrDefault();

Assert.Equal((1, true), result);

AssertSql(
"""
@__ef_filter__OrgId_0='1'

SELECT (o."Id", EXISTS (
SELECT 1
FROM "VendorLink" AS v
LEFT JOIN (
SELECT v0."Id", v0."IsActive"
FROM "Vendor" AS v0
WHERE v0."OrgId" = @__ef_filter__OrgId_0
) AS v1 ON v."VendorId" = v1."Id"
WHERE o."Id" = v."OrderId" AND v1."IsActive" AND EXISTS (
SELECT 1
FROM "Representative" AS r
WHERE v1."Id" IS NOT NULL AND v1."Id" = r."VendorId" AND r."IsActive")))
FROM "Orders" AS o
WHERE o."Id" = 1
LIMIT 1
""");
}

protected class Context3933(DbContextOptions options) : DbContext(options)
{
public short OrgId { get; set; } = 1;
public DbSet<Order> Orders => Set<Order>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
=> modelBuilder.Entity<Vendor>().HasQueryFilter(x => x.OrgId == OrgId);

public class Order
{
public int Id { get; set; }
public List<VendorLink> VendorLinks { get; set; } = [];
}

public class VendorLink
{
public int Id { get; set; }
public int OrderId { get; set; }
public int? VendorId { get; set; }
public Vendor Vendor { get; set; }
}

public class Vendor
{
public int Id { get; set; }
public short OrgId { get; set; }
// Removing this unused property prevents the regression from reproducing.
public Guid VendorIdentifier { get; set; }
public bool IsActive { get; set; }
public List<Representative> Representatives { get; set; } = [];
}

public class Representative
{
public int Id { get; set; }
public int VendorId { get; set; }
public bool IsActive { get; set; }
}
}
}
32 changes: 32 additions & 0 deletions test/EFCore.PG.Tests/Query/PgRowValueExpressionTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using Npgsql.EntityFrameworkCore.PostgreSQL.Query.Expressions.Internal;
using Npgsql.EntityFrameworkCore.PostgreSQL.Storage.Internal.Mapping;

namespace Npgsql.EntityFrameworkCore.PostgreSQL.Query;

public class PgRowValueExpressionTest
{
[Theory]
[InlineData(false, false)]
[InlineData(false, true)]
[InlineData(true, false)]
[InlineData(true, true)]
public void Rewriting_preserves_type_mapping(bool useVisitor, bool changeValue)
{
var value = new SqlConstantExpression(1, IntTypeMapping.Default);
var replacement = changeValue ? new SqlConstantExpression(2, IntTypeMapping.Default) : value;
var typeMapping = new NpgsqlRowValueTypeMapping(typeof(ValueTuple<int>));
var row = new PgRowValueExpression([value], typeof(ValueTuple<int>), typeMapping);

var rewritten = useVisitor
? (PgRowValueExpression)ReplacingExpressionVisitor.Replace(value, replacement, row)
: row.Update([replacement]);

Assert.Same(typeMapping, rewritten.TypeMapping);
Assert.Same(replacement, Assert.Single(rewritten.Values));
Assert.Equal(row.Type, rewritten.Type);
if (!changeValue)
{
Assert.Same(row, rewritten);
}
}
}
Loading