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 @@ -770,8 +770,9 @@ protected override ShapedQueryExpression TranslateExcept(ShapedQueryExpression s
}

var selectExpression = (SelectExpression)source.QueryExpression;
if (selectExpression.Predicate == null
&& selectExpression.Orderings.Count == 0)
if (selectExpression.Orderings.Count == 0
&& (selectExpression.Predicate == null
|| (_subquery && IsFilteredOnlyByForeignKeyCorrelation(selectExpression))))
{
_queryCompilationContext.Logger.FirstWithoutOrderByAndFilterWarning();
}
Expand Down Expand Up @@ -2011,6 +2012,97 @@ private ShapedQueryExpression TranslateSetOperationOperand(ShapedQueryExpression
LambdaExpression lambdaExpression)
=> TranslateExpression(RemapLambdaBody(shapedQueryExpression, lambdaExpression));

// Expanding a collection navigation produces a subquery filtered only by the correlation to the outer query, i.e. an equality
// between the foreign key columns and the principal key columns, optionally with a null check on the principal key. That doesn't
// determine which of the related rows is returned.
private static bool IsFilteredOnlyByForeignKeyCorrelation(SelectExpression selectExpression)
{
var tableAliases = new HashSet<string>();
foreach (var table in selectExpression.Tables)
{
if (table.UnwrapJoin().Alias is { } alias)
{
tableAliases.Add(alias);
}
}

var keyComparisons = new List<(ColumnExpression Outer, ColumnExpression Inner)>();
if (!IsCorrelation(selectExpression.Predicate!)
|| keyComparisons.Count == 0
|| keyComparisons[0].Inner.Column is not IColumn { Table: var dependentTable })
Comment thread
SimonCropp marked this conversation as resolved.
Resolved
{
return false;
}

// A unique foreign key matches at most one row, so there is nothing arbitrary about the row returned.
return dependentTable.ForeignKeyConstraints.Any(IsCompared)
&& !dependentTable.UniqueConstraints.Any(c => c.Columns.All(IsComparedColumn))
&& !dependentTable.Indexes.Any(i => i.IsUnique && i.Columns.All(IsComparedColumn));

bool IsCorrelation(SqlExpression predicate)
{
switch (predicate)
{
case SqlBinaryExpression { OperatorType: ExpressionType.AndAlso } andAlso:
return IsCorrelation(andAlso.Left) && IsCorrelation(andAlso.Right);

case SqlBinaryExpression
{
OperatorType: ExpressionType.NotEqual, Left: ColumnExpression column, Right: SqlConstantExpression { Value: null }
}:
return IsOuter(column);
Comment thread
AndriySvyryd marked this conversation as resolved.
Resolved

case SqlBinaryExpression
{
OperatorType: ExpressionType.Equal, Left: ColumnExpression left, Right: ColumnExpression right
}:
if (IsOuter(left) == IsOuter(right))
{
return false;
}

if (IsOuter(left))
{
keyComparisons.Add((left, right));
return true;
}

keyComparisons.Add((right, left));
return true;

default:
return false;
}
}

bool IsOuter(ColumnExpression column)
=> !tableAliases.Contains(column.TableAlias);

bool IsCompared(IForeignKeyConstraint foreignKey)
{
if (foreignKey.Columns.Count != keyComparisons.Count)
{
return false;
}

for (var i = 0; i < foreignKey.Columns.Count; i++)
{
var column = foreignKey.Columns[i];
var principalColumn = foreignKey.PrincipalColumns[i];

if (!keyComparisons.Any(c => c.Inner.Column == column && c.Outer.Column == principalColumn))
{
return false;
}
}

return true;
}

bool IsComparedColumn(IColumn column)
=> keyComparisons.Any(c => c.Inner.Column == column);
}

/// <summary>
/// Determines whether the given <see cref="SelectExpression" /> is ordered, typically because orderings have been added to it.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,93 @@ public virtual void FirstOrDefault_without_orderby_and_filter_issues_warning_sub
using var context = CreateContext();
var query = context.Customers.Where(c => c.CustomerID == "ALFKI" && c.Orders.FirstOrDefault()!.OrderID > 1000).ToList();
Assert.Single(query);

Assert.Contains(Fixture.ListLoggerFactory.Log, l => l.Id == CoreEventId.FirstWithoutOrderByAndFilterWarning);
}

[Fact]
public virtual void FirstOrDefault_without_orderby_and_filter_issues_warning_subquery_in_projection()
{
using var context = CreateContext();
var query = context.Customers
.OrderBy(c => c.CustomerID)
.Select(c => c.Orders.FirstOrDefault()!.OrderDate)
.ToList();
Assert.Equal(91, query.Count);

Assert.Contains(Fixture.ListLoggerFactory.Log, l => l.Id == CoreEventId.FirstWithoutOrderByAndFilterWarning);
}

[Fact]
public virtual void FirstOrDefault_without_orderby_but_with_filter_doesnt_issue_warning_subquery()
{
using var context = CreateContext();
var query = context.Customers
.OrderBy(c => c.CustomerID)
.Select(c => c.Orders.FirstOrDefault(o => o.OrderID > 10300)!.OrderDate)
.ToList();
Assert.Equal(91, query.Count);

Assert.DoesNotContain(Fixture.ListLoggerFactory.Log, l => l.Id == CoreEventId.FirstWithoutOrderByAndFilterWarning);
}

[Fact]
public virtual void FirstOrDefault_with_orderby_doesnt_issue_warning_subquery()
{
using var context = CreateContext();
var query = context.Customers
.OrderBy(c => c.CustomerID)
.Select(c => c.Orders.OrderBy(o => o.OrderID).FirstOrDefault()!.OrderDate)
.ToList();
Assert.Equal(91, query.Count);

Assert.DoesNotContain(Fixture.ListLoggerFactory.Log, l => l.Id == CoreEventId.FirstWithoutOrderByAndFilterWarning);
}

[Fact]
public virtual void FirstOrDefault_without_orderby_doesnt_issue_warning_subquery_correlated_by_non_key()
{
using var context = CreateContext();
var query = context.Customers
.OrderBy(c => c.CustomerID)
.Select(c => context.Employees
.Where(e => c.City != null && c.City == e.City)
.Select(e => e.FirstName)
.FirstOrDefault())
.ToList();
Assert.Equal(91, query.Count);

Assert.DoesNotContain(Fixture.ListLoggerFactory.Log, l => l.Id == CoreEventId.FirstWithoutOrderByAndFilterWarning);
}

[Fact]
public virtual void FirstOrDefault_without_orderby_doesnt_issue_warning_subquery_with_filter_correlated_by_non_key()
{
using var context = CreateContext();
var query = context.Customers
.OrderBy(c => c.CustomerID)
.Select(c => c.Orders.FirstOrDefault(o => c.City != null && c.City == o.CustomerID)!.OrderDate)
.ToList();
Assert.Equal(91, query.Count);

Assert.DoesNotContain(Fixture.ListLoggerFactory.Log, l => l.Id == CoreEventId.FirstWithoutOrderByAndFilterWarning);
}

[Fact]
public virtual void FirstOrDefault_without_orderby_doesnt_issue_warning_subquery_correlated_by_computed_column()
{
using var context = CreateContext();
var query = context.Customers
.Select(c => new { Key = c.City ?? c.CustomerID })
.Distinct()
.Select(x => context.Orders
.Where(o => o.CustomerID == x.Key)
.Select(o => o.OrderDate)
.FirstOrDefault())
.ToList();
Assert.NotEmpty(query);

Assert.DoesNotContain(Fixture.ListLoggerFactory.Log, l => l.Id == CoreEventId.FirstWithoutOrderByAndFilterWarning);
}

[Fact]
Expand Down
10 changes: 6 additions & 4 deletions test/EFCore.Specification.Tests/BuiltInDataTypesTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -659,10 +659,12 @@ public virtual string ReallyLargeString
=> string.Join("", Enumerable.Repeat(Environment.NewLine, 1001));

public override DbContextOptionsBuilder AddOptions(DbContextOptionsBuilder builder)
=> base.AddOptions(builder).ConfigureWarnings(w => w.Ignore(
CoreEventId.MappedEntityTypeIgnoredWarning,
CoreEventId.MappedPropertyIgnoredWarning,
CoreEventId.MappedNavigationIgnoredWarning));
=> base.AddOptions(builder).ConfigureWarnings(w => w
.Ignore(
CoreEventId.MappedEntityTypeIgnoredWarning,
CoreEventId.MappedPropertyIgnoredWarning,
CoreEventId.MappedNavigationIgnoredWarning)
.Log(CoreEventId.FirstWithoutOrderByAndFilterWarning));

protected override void OnModelCreating(ModelBuilder modelBuilder, DbContext context)
{
Expand Down
Loading