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 @@ -3252,7 +3252,7 @@ private void AddJoin(

// If this is a to-one join, then we know that it doesn't increase cardinality — the outer identifiers are already sufficient.
// Skipping the inner's identifiers avoids unnecessary ORDER BY columns and projections (#29182).
if (_identifier.Count == 0 || innerSelect._identifier.Count == 0)
if (_identifier.Count == 0 || (innerSelect._identifier.Count == 0 && !isToOneJoin))
{
// Either the outer and inner sides aren't uniquely identifiable; mark everything as non-uniquely-identifiable.
_identifier.Clear();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -622,4 +622,145 @@ public sealed class Tag
}

#endregion

#region 38660

[Theory]
[InlineData(false, false)]
[InlineData(false, true)]
[InlineData(true, false)]
[InlineData(true, true)]
public virtual async Task To_one_join_with_keyless_inner_preserves_outer_collection_identifiers(bool async, bool splitQuery)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Override it in AdHocQuerySplittingQuerySqlServerTest and assert the SQL

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the SQL Server override and baselines for both single- and split-query paths in beed29a. The SQL Server test project builds cleanly, Check_all_tests_overridden passes, and the full 21-test SQLite class still passes.

{
var contextFactory = await InitializeNonSharedTest<Context38660>(seed: c => c.SeedAsync());

using var context = contextFactory.CreateDbContext();
var pivot = context.TaxesA
.Select(t => new Context38660.TaxRow
{
OrderId = t.OrderId,
LineNo = t.LineNo,
Kind = t.Kind,
})
.Concat(
context.TaxesB.Select(t => new Context38660.TaxRow
{
OrderId = t.OrderId,
LineNo = t.LineNo,
Kind = t.Kind,
}));

var query = context.Orders.Select(o => new Context38660.OrderDto
{
Lines = o.Lines.Select(l => new Context38660.LineDto
{
LineNo = l.LineNo,
Tax = pivot
.Where(r => r.OrderId == l.OrderId && r.LineNo == l.LineNo)
.Select(r => new Context38660.TaxDto { Kind = r.Kind })
.FirstOrDefault(),
}).ToList(),
});

query = splitQuery ? query.AsSplitQuery() : query.AsSingleQuery();
var result = async ? await query.SingleAsync() : query.Single();

Assert.Collection(
result.Lines.OrderBy(l => l.LineNo),
line =>
{
Assert.Equal(1, line.LineNo);
Assert.Equal(10, line.Tax.Kind);
},
line =>
{
Assert.Equal(2, line.LineNo);
Assert.Equal(20, line.Tax.Kind);
});
}

protected class Context38660(DbContextOptions options) : DbContext(options)
{
public DbSet<Order> Orders { get; set; }
public DbSet<TaxA> TaxesA { get; set; }
public DbSet<TaxB> TaxesB { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>().Property(o => o.Id).ValueGeneratedNever();
modelBuilder.Entity<OrderLine>().HasKey(l => new { l.OrderId, l.LineNo });
modelBuilder.Entity<Order>().HasMany(o => o.Lines).WithOne().HasForeignKey(l => l.OrderId);
Comment thread
AndriySvyryd marked this conversation as resolved.
}

public Task SeedAsync()
{
Add(
new Order
{
Id = 1,
Lines =
[
new OrderLine { OrderId = 1, LineNo = 1 },
new OrderLine { OrderId = 1, LineNo = 2 },
],
});
Add(new TaxA { OrderId = 1, LineNo = 1, Kind = 10 });
Add(new TaxB { OrderId = 1, LineNo = 2, Kind = 20 });

return SaveChangesAsync();
}

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

public class OrderLine
{
public int OrderId { get; set; }
public int LineNo { get; set; }
}

public class TaxA
{
public int Id { get; set; }
public int OrderId { get; set; }
public int LineNo { get; set; }
public int Kind { get; set; }
}

public class TaxB
{
public int Id { get; set; }
public int OrderId { get; set; }
public int LineNo { get; set; }
public int Kind { get; set; }
}

public class TaxRow
{
public int OrderId { get; init; }
public int LineNo { get; init; }
public int Kind { get; init; }
}

public class OrderDto
{
public List<LineDto> Lines { get; set; } = [];
}

public class LineDto
{
public int LineNo { get; set; }
public TaxDto Tax { get; set; }
}

public class TaxDto
{
public int Kind { get; set; }
}
}

#endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,69 @@ protected override TestStore CreateTestStore25225()
return testStore;
}

public override async Task To_one_join_with_keyless_inner_preserves_outer_collection_identifiers(bool async, bool splitQuery)
{
await base.To_one_join_with_keyless_inner_preserves_outer_collection_identifiers(async, splitQuery);

if (splitQuery)
{
AssertSql(
"""
SELECT [o].[Id]
FROM [Orders] AS [o]
ORDER BY [o].[Id]
""",
//
"""
SELECT [o2].[LineNo], [u9].[Kind], [u9].[c], [o].[Id]
FROM [Orders] AS [o]
INNER JOIN [OrderLine] AS [o2] ON [o].[Id] = [o2].[OrderId]
LEFT JOIN (
SELECT [u8].[Kind], [u8].[c], [u8].[OrderId], [u8].[LineNo]
FROM (
SELECT [u7].[Kind], 1 AS [c], [u7].[OrderId], [u7].[LineNo], ROW_NUMBER() OVER(PARTITION BY [u7].[OrderId], [u7].[LineNo] ORDER BY (SELECT 1)) AS [row]
FROM (
SELECT [t15].[OrderId], [t15].[LineNo], [t15].[Kind]
FROM [TaxesA] AS [t15]
UNION ALL
SELECT [t16].[OrderId], [t16].[LineNo], [t16].[Kind]
FROM [TaxesB] AS [t16]
) AS [u7]
) AS [u8]
WHERE [u8].[row] <= 1
) AS [u9] ON [o2].[OrderId] = [u9].[OrderId] AND [o2].[LineNo] = [u9].[LineNo]
ORDER BY [o].[Id]
""");
}
else
{
AssertSql(
"""
SELECT [o].[Id], [s].[LineNo], [s].[Kind], [s].[c], [s].[OrderId]
FROM [Orders] AS [o]
LEFT JOIN (
SELECT [o0].[LineNo], [u1].[Kind], [u1].[c], [o0].[OrderId]
FROM [OrderLine] AS [o0]
LEFT JOIN (
SELECT [u0].[Kind], [u0].[c], [u0].[OrderId], [u0].[LineNo]
FROM (
SELECT [u].[Kind], 1 AS [c], [u].[OrderId], [u].[LineNo], ROW_NUMBER() OVER(PARTITION BY [u].[OrderId], [u].[LineNo] ORDER BY (SELECT 1)) AS [row]
FROM (
SELECT [t].[OrderId], [t].[LineNo], [t].[Kind]
FROM [TaxesA] AS [t]
UNION ALL
SELECT [t0].[OrderId], [t0].[LineNo], [t0].[Kind]
FROM [TaxesB] AS [t0]
) AS [u]
) AS [u0]
WHERE [u0].[row] <= 1
) AS [u1] ON [o0].[OrderId] = [u1].[OrderId] AND [o0].[LineNo] = [u1].[LineNo]
) AS [s] ON [o].[Id] = [s].[OrderId]
ORDER BY [o].[Id], [s].[OrderId]
""");
}
}

public override async Task Can_configure_SingleQuery_at_context_level()
{
await base.Can_configure_SingleQuery_at_context_level();
Expand Down
Loading