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

Large diffs are not rendered by default.

55 changes: 48 additions & 7 deletions src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1913,6 +1913,52 @@ public RelationalGroupByShaperExpression ApplyGrouping(
new ShapedQueryExpression(clonedSelectExpression, rebuiltShaperExpression));
}

// Replaces the grouping with the first row of each group, keeping the rows numbered 1 by
// ROW_NUMBER() OVER(PARTITION BY <grouping terms> ORDER BY <orderings>); this is the filter AddJoin builds for a group's
// element, applied to the source rows directly. `projection` is an expression over the grouping terms (such as the key
// selector), returned remapped to the new columns.
// Internal infrastructure: only called from RelationalQueryableMethodTranslatingExpressionVisitor.
internal Expression ApplyFirstRowPerGroup(
Expression projection,
IReadOnlyList<OrderingExpression> orderings,
ISqlExpressionFactory sqlExpressionFactory)
{
Check.DebugAssert(_groupBy.Count > 0, "The selectExpression doesn't have grouping terms.");
Check.DebugAssert(
Limit == null && Offset == null && !IsDistinct && Having == null,
"Grouped SelectExpression has state over the groups.");

var partitions = _groupBy.ToList();
_groupBy.Clear();

// The rows are identified by the identifier from before the grouping again, as in ApplyProjection for a final GroupBy.
if (_preGroupByIdentifier is not null)
{
_identifier.Clear();
_identifier.AddRange(_preGroupByIdentifier);
_preGroupByIdentifier = null;
}

var one = sqlExpressionFactory.ApplyDefaultTypeMapping(sqlExpressionFactory.Constant(1));
var rowNumberExpression = new RowNumberExpression(partitions, GetRowNumberOrderings(orderings, _identifier), one.TypeMapping);
var sqlRemappingVisitor = PushdownIntoSubqueryInternal();
var subquery = (SelectExpression)_tables[0];
var rowNumberColumn = subquery.GenerateOuterColumn(subquery.Alias!, rowNumberExpression, "row");
ApplyPredicate(sqlExpressionFactory.LessThanOrEqual(rowNumberColumn, one));

return sqlRemappingVisitor.Visit(projection);
}

// ROW_NUMBER() needs an ordering; without one, the rows are numbered by the identifier, or in no particular order.
private static List<OrderingExpression> GetRowNumberOrderings(
IReadOnlyList<OrderingExpression> orderings,
IReadOnlyList<(ColumnExpression Column, ValueComparer Comparer)> identifier)
=> orderings.Count > 0
? orderings.ToList()
: identifier.Count > 0
? identifier.Select(e => new OrderingExpression(e.Column, true)).ToList()
: [new OrderingExpression(new SqlFragmentExpression("(SELECT 1)", typeof(int)), true)];

private static void PopulateGroupByTerms(
Expression keySelector,
List<SqlExpression> groupByTerms,
Expand Down Expand Up @@ -3225,14 +3271,9 @@ private void AddJoin(
{
var partitions = new List<SqlExpression>();
GetPartitions(innerSelect, joinPredicate, partitions);
var orderings = innerSelect.Orderings.Count > 0
? innerSelect.Orderings
: innerSelect._identifier.Count > 0
? innerSelect._identifier.Select(e => new OrderingExpression(e.Column, true))
: [new OrderingExpression(new SqlFragmentExpression("(SELECT 1)", typeof(int)), true)];

var rowNumberExpression = new RowNumberExpression(
partitions, orderings.ToList(), (limit ?? offset)!.TypeMapping);
partitions, GetRowNumberOrderings(innerSelect.Orderings, innerSelect._identifier),
(limit ?? offset)!.TypeMapping);
innerSelect.ClearOrdering();

joinPredicate = innerSelect.PushdownIntoSubqueryInternal().Remap(joinPredicate);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,4 +164,11 @@ public override Task GroupBy_Select_Entire_Entity_Select_referenced_twice(bool a
[Theory(Skip = "Issue#31209")]
public override Task GroupBy_Select_Entire_Entity_Join(bool async)
=> base.GroupBy_Select_Entire_Entity_Join(async);

[Theory(Skip = "Issue#31209")]
public override Task GroupBy_Select_First_ordered_composed(bool async)
=> base.GroupBy_Select_First_ordered_composed(async);

public override Task SelectMany_correlated_GroupBy_Select_First_ordered(bool async)
=> Assert.ThrowsAsync<NotImplementedException>(() => base.SelectMany_correlated_GroupBy_Select_First_ordered(async));
}
Original file line number Diff line number Diff line change
Expand Up @@ -2090,6 +2090,140 @@ public class Company
}

#endregion

#region 29240

[Theory, MemberData(nameof(IsAsyncData))]
public virtual async Task GroupBy_nullable_key_Select_First_ordered_by_date(bool async)
{
var contextFactory = await InitializeNonSharedTest<Context29240>(seed: async c =>
{
c.AddRange(
new Context29240.Category { GroupId = 1, CreationDateTime = new DateTime(2022, 1, 1) },
new Context29240.Category { GroupId = 1, CreationDateTime = new DateTime(2022, 1, 2) },
new Context29240.Category { CreationDateTime = new DateTime(2022, 1, 3) },
new Context29240.Category { CreationDateTime = new DateTime(2022, 1, 4) });
await c.SaveChangesAsync();
});
using var context = contextFactory.CreateDbContext();

var query = context.Categories
.GroupBy(c => c.GroupId)
.Select(g => g.OrderByDescending(c => c.CreationDateTime).First());

var result = async ? await query.ToListAsync() : query.ToList();

Assert.All(result, Assert.NotNull);
Assert.Equal([new DateTime(2022, 1, 2), new DateTime(2022, 1, 4)], result.Select(c => c.CreationDateTime).Order());
}

[Theory, MemberData(nameof(IsAsyncData))]
public virtual async Task GroupBy_nullable_key_Select_First_ordered_with_owned_types(bool async)
{
var contextFactory = await InitializeNonSharedTest<Context29240>(seed: async c =>
{
c.AddRange(
new Context29240.Product
{
GroupId = 1, Rank = 1, Details = new() { Description = "a" }, Tags = [new() { Name = "x" }]
},
new Context29240.Product
{
GroupId = 1, Rank = 2, Details = new() { Description = "b" }, Tags = [new() { Name = "y" }, new() { Name = "z" }]
},
new Context29240.Product { Rank = 3, Details = new() { Description = "c" }, Tags = [] },
new Context29240.Product { Rank = 4, Details = new() { Description = "d" }, Tags = [new() { Name = "w" }] });
await c.SaveChangesAsync();
});
using var context = contextFactory.CreateDbContext();

var query = context.Products
.GroupBy(p => p.GroupId)
.Select(g => g.OrderByDescending(p => p.Rank).First());

var result = async ? await query.ToListAsync() : query.ToList();

Assert.Collection(
result.OrderBy(p => p.Rank),
p =>
{
Assert.Equal(2, p.Rank);
Assert.Equal("b", p.Details.Description);
Assert.Equal(["y", "z"], p.Tags.Select(t => t.Name));
},
p =>
{
Assert.Equal(4, p.Rank);
Assert.Equal("d", p.Details.Description);
Assert.Equal(["w"], p.Tags.Select(t => t.Name));
});
}

[Theory, MemberData(nameof(IsAsyncData))]
public virtual async Task GroupBy_Select_First_ordered_with_owned_types_then_DefaultIfEmpty(bool async)
{
var contextFactory = await InitializeNonSharedTest<Context29240>(seed: async c =>
{
c.Add(new Context29240.Product { GroupId = 1, Rank = 1, Details = new() { Description = "a" }, Tags = [] });
await c.SaveChangesAsync();
});
using var context = contextFactory.CreateDbContext();

var query = context.Products
.Where(p => p.Rank > 1)
.GroupBy(p => p.GroupId)
.Select(g => g.OrderByDescending(p => p.Rank).First())
.DefaultIfEmpty();

var result = async ? await query.ToListAsync() : query.ToList();

Assert.Null(Assert.Single(result));
}

// Protected so that it can be used by inheriting tests, and so that things like unused setters are not removed.
protected class Context29240(DbContextOptions options) : DbContext(options)
{
public DbSet<Category> Categories
=> Set<Category>();

public DbSet<Product> Products
=> Set<Product>();

protected override void OnModelCreating(ModelBuilder modelBuilder)
=> modelBuilder.Entity<Product>(b =>
{
b.OwnsOne(p => p.Details);
b.ComplexCollection(p => p.Tags, t => t.ToJson());
});

public class Category
{
public int Id { get; set; }
public int? GroupId { get; set; }
public DateTime CreationDateTime { get; set; }
}

public class Product
{
public int Id { get; set; }
public int? GroupId { get; set; }
public int Rank { get; set; }
public ProductDetails Details { get; set; } = null!;
public List<Tag> Tags { get; set; } = null!;
}

public class ProductDetails
{
public string? Description { get; set; }
}

public class Tag
{
public string? Name { get; set; }
}
}

#endregion
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,10 @@ public virtual Task Final_GroupBy()
public virtual Task Final_GroupBy_projecting_grouping_elements()
=> Test("""var blogs = await context.Blogs.GroupBy(b => b.Name).Select(g => g.Select(b => b.Id).ToList()).ToListAsync();""");

[Fact]
public virtual Task GroupBy_Select_first_grouping_element()
=> Test("""var blogs = await context.Blogs.GroupBy(b => b.Name).Select(g => g.OrderBy(b => b.Id).First()).ToListAsync();""");

#endregion Regular operators

#region Terminating operators
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1578,7 +1578,7 @@ public virtual Task Group_by_Skip_Take_on_json_scalar(bool async)
ss => ss.Set<JsonEntityBasic>()
.GroupBy(x => x.OwnedReferenceRoot.Name).Select(g => g.OrderBy(x => x.Id).Skip(1).Take(5)));

[Theory(Skip = "issue #29287"), MemberData(nameof(IsAsyncData))]
[Theory, MemberData(nameof(IsAsyncData))]
public virtual Task Group_by_json_scalar_Orderby_json_scalar_FirstOrDefault(bool async)
=> AssertQuery(
async,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3279,6 +3279,150 @@ public virtual Task GroupBy_select_grouping_composed_list_2(bool async)
AssertCollection(e.List, a.List);
});

[Theory, MemberData(nameof(IsAsyncData))]
public virtual Task GroupBy_nullable_key_Select_First_ordered(bool async)
=> AssertQuery(
async,
ss => ss.Set<Customer>()
.GroupBy(c => c.Region)
.Select(g => g.OrderBy(c => c.CustomerID).First()));

[Theory, MemberData(nameof(IsAsyncData))]
public virtual Task GroupBy_nullable_value_type_key_Select_FirstOrDefault_ordered(bool async)
=> AssertQuery(
async,
ss => ss.Set<Employee>()
.GroupBy(e => e.ReportsTo)
.Select(g => g.OrderByDescending(e => e.EmployeeID).FirstOrDefault()));

[Theory, MemberData(nameof(IsAsyncData))]
public virtual Task GroupBy_nullable_key_Select_Key_and_First_ordered(bool async)
=> AssertQuery(
async,
ss => ss.Set<Customer>()
.GroupBy(c => c.Region)
.Select(g => new { g.Key, Customer = g.OrderBy(c => c.City).ThenBy(c => c.CustomerID).First() }),
elementSorter: e => e.Key,
elementAsserter: (e, a) =>
{
AssertEqual(e.Key, a.Key);
AssertEqual(e.Customer, a.Customer);
});

[Theory, MemberData(nameof(IsAsyncData))]
public virtual Task GroupBy_nullable_key_Select_LastOrDefault_ordered(bool async)
=> AssertQuery(
async,
ss => ss.Set<Customer>()
.GroupBy(c => c.Region)
.Select(g => g.OrderBy(c => c.CustomerID).LastOrDefault()));

[Theory, MemberData(nameof(IsAsyncData))]
public virtual Task GroupBy_nullable_key_Select_MaxBy(bool async)
=> AssertQuery(
async,
ss => ss.Set<Employee>()
.GroupBy(e => e.ReportsTo)
.Select(g => g.MaxBy(e => e.EmployeeID)));

[Theory, MemberData(nameof(IsAsyncData))]
public virtual Task GroupBy_nullable_key_Select_projected_First_ordered(bool async)
=> AssertQuery(
async,
ss => ss.Set<Customer>()
.GroupBy(c => c.Region)
.Select(g => g.OrderBy(c => c.CustomerID).Select(c => new { c.CustomerID, c.City }).First()),
elementSorter: e => e.CustomerID);

[Theory, MemberData(nameof(IsAsyncData))]
public virtual Task GroupBy_nullable_key_with_element_selector_Select_First_ordered(bool async)
=> AssertQuery(
async,
ss => ss.Set<Employee>()
.GroupBy(e => e.ReportsTo, e => new { e.EmployeeID, e.City })
.Select(g => g.OrderBy(e => e.EmployeeID).First()),
elementSorter: e => e.EmployeeID);

[Theory, MemberData(nameof(IsAsyncData))]
public virtual Task GroupBy_nullable_key_with_result_selector_First_ordered(bool async)
=> AssertQuery(
async,
ss => ss.Set<Employee>()
.GroupBy(e => e.ReportsTo, (key, employees) => new { key, Employee = employees.OrderBy(e => e.EmployeeID).First() }),
elementSorter: e => e.key,
elementAsserter: (e, a) =>
{
AssertEqual(e.key, a.key);
AssertEqual(e.Employee, a.Employee);
});

[Theory, MemberData(nameof(IsAsyncData))]
public virtual Task GroupBy_nullable_navigation_key_Select_First_ordered(bool async)
=> AssertQuery(
async,
ss => ss.Set<Order>()
.GroupBy(o => o.Customer!.Region)
.Select(g => g.OrderBy(o => o.OrderID).First()));

[Theory, MemberData(nameof(IsAsyncData))]
public virtual Task GroupBy_nullable_composite_key_Select_First_ordered(bool async)
=> AssertQuery(
async,
ss => ss.Set<Customer>()
.GroupBy(c => new { c.Country, c.Region })
.Select(g => new { g.Key.Country, g.Key.Region, Customer = g.OrderBy(c => c.CustomerID).First() }),
elementSorter: e => (e.Country, e.Region),
elementAsserter: (e, a) =>
{
AssertEqual(e.Country, a.Country);
AssertEqual(e.Region, a.Region);
AssertEqual(e.Customer, a.Customer);
});

[Theory, MemberData(nameof(IsAsyncData))]
public virtual Task GroupBy_Select_First_ordered_with_aggregate_in_inline_collection(bool async)
=> AssertQuery(
async,
ss => ss.Set<Order>()
.GroupBy(o => o.CustomerID)
.Select(g => new { g.Key, Order = g.OrderBy(o => o.OrderID).First(), HasTwo = new[] { g.Count(), 0 }.Contains(2) }),
elementSorter: e => e.Key,
elementAsserter: (e, a) =>
{
AssertEqual(e.Key, a.Key);
AssertEqual(e.Order, a.Order);
AssertEqual(e.HasTwo, a.HasTwo);
});

[Theory, MemberData(nameof(IsAsyncData))]
public virtual Task GroupBy_composite_key_member_in_projected_First_ordered(bool async)
=> AssertQuery(
async,
ss => ss.Set<Order>()
.GroupBy(o => new { o.CustomerID, o.EmployeeID })
.Select(g => g.OrderBy(o => o.OrderID).Select(o => new { o.OrderID, g.Key.EmployeeID }).First()),
elementSorter: e => e.OrderID);

[Theory, MemberData(nameof(IsAsyncData))]
public virtual Task SelectMany_correlated_GroupBy_Select_First_ordered(bool async)
=> AssertQuery(
async,
ss => ss.Set<Customer>()
.Where(c => c.CustomerID.StartsWith("A"))
.SelectMany(c => c.Orders.GroupBy(o => o.EmployeeID).Select(g => g.OrderBy(o => o.OrderID).First())));

[Theory, MemberData(nameof(IsAsyncData))]
public virtual Task GroupBy_Select_First_ordered_composed(bool async)
=> AssertQuery(
async,
ss => ss.Set<Order>()
.GroupBy(o => o.CustomerID)
.Select(g => g.OrderByDescending(o => o.OrderDate).ThenBy(o => o.OrderID).First())
.Where(o => o.EmployeeID == 4)
.OrderBy(o => o.OrderID)
.Take(5),
assertOrder: true);

#endregion

#region GroupByEntityType
Expand Down
Loading
Loading