Skip to content

Commit d25d07b

Browse files
authored
Add left join support for Linq query provider (#2328)
* Add test for #1128 Fixes #864
1 parent 9006998 commit d25d07b

15 files changed

+769
-38
lines changed

src/NHibernate.Test/Async/Linq/ByMethod/JoinTests.cs

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@
1313
using System.Reflection;
1414
using NHibernate.Cfg;
1515
using NHibernate.Engine.Query;
16+
using NHibernate.Linq;
1617
using NHibernate.Util;
1718
using NSubstitute;
1819
using NUnit.Framework;
19-
using NHibernate.Linq;
2020

2121
namespace NHibernate.Test.Linq.ByMethod
2222
{
@@ -27,15 +27,91 @@ public class JoinTestsAsync : LinqTestCase
2727
[Test]
2828
public async Task MultipleLinqJoinsWithSameProjectionNamesAsync()
2929
{
30-
var orders = await (db.Orders
30+
using (var sqlSpy = new SqlLogSpy())
31+
{
32+
var orders = await (db.Orders
3133
.Join(db.Orders, x => x.OrderId, x => x.OrderId - 1, (order, order1) => new { order, order1 })
3234
.Select(x => new { First = x.order, Second = x.order1 })
3335
.Join(db.Orders, x => x.First.OrderId, x => x.OrderId - 2, (order, order1) => new { order, order1 })
3436
.Select(x => new { FirstId = x.order.First.OrderId, SecondId = x.order.Second.OrderId, ThirdId = x.order1.OrderId })
3537
.ToListAsync());
3638

37-
Assert.That(orders.Count, Is.EqualTo(828));
38-
Assert.IsTrue(orders.All(x => x.FirstId == x.SecondId - 1 && x.SecondId == x.ThirdId - 1));
39+
var sql = sqlSpy.GetWholeLog();
40+
Assert.That(orders.Count, Is.EqualTo(828));
41+
Assert.IsTrue(orders.All(x => x.FirstId == x.SecondId - 1 && x.SecondId == x.ThirdId - 1));
42+
Assert.That(GetTotalOccurrences(sql, "inner join"), Is.EqualTo(2));
43+
}
44+
}
45+
46+
[Test]
47+
public async Task MultipleLinqJoinsWithSameProjectionNamesWithLeftJoinAsync()
48+
{
49+
using (var sqlSpy = new SqlLogSpy())
50+
{
51+
var orders = await (db.Orders
52+
.GroupJoin(db.Orders, x => x.OrderId, x => x.OrderId - 1, (order, order1) => new { order, order1 })
53+
.SelectMany(x => x.order1.DefaultIfEmpty(), (x, order1) => new { First = x.order, Second = order1 })
54+
.GroupJoin(db.Orders, x => x.First.OrderId, x => x.OrderId - 2, (order, order1) => new { order, order1 })
55+
.SelectMany(x => x.order1.DefaultIfEmpty(), (x, order1) => new
56+
{
57+
FirstId = x.order.First.OrderId,
58+
SecondId = (int?) x.order.Second.OrderId,
59+
ThirdId = (int?) order1.OrderId
60+
})
61+
.ToListAsync());
62+
63+
var sql = sqlSpy.GetWholeLog();
64+
Assert.That(orders.Count, Is.EqualTo(830));
65+
Assert.IsTrue(orders.Where(x => x.SecondId.HasValue && x.ThirdId.HasValue)
66+
.All(x => x.FirstId == x.SecondId - 1 && x.SecondId == x.ThirdId - 1));
67+
Assert.That(GetTotalOccurrences(sql, "left outer join"), Is.EqualTo(2));
68+
}
69+
}
70+
71+
[Test]
72+
public async Task MultipleLinqJoinsWithSameProjectionNamesWithLeftJoinExtensionMethodAsync()
73+
{
74+
using (var sqlSpy = new SqlLogSpy())
75+
{
76+
var orders = await (db.Orders
77+
.LeftJoin(db.Orders, x => x.OrderId, x => x.OrderId - 1, (order, order1) => new { order, order1 })
78+
.Select(x => new { First = x.order, Second = x.order1 })
79+
.LeftJoin(db.Orders, x => x.First.OrderId, x => x.OrderId - 2, (order, order1) => new { order, order1 })
80+
.Select(x => new
81+
{
82+
FirstId = x.order.First.OrderId,
83+
SecondId = (int?) x.order.Second.OrderId,
84+
ThirdId = (int?) x.order1.OrderId
85+
})
86+
.ToListAsync());
87+
88+
var sql = sqlSpy.GetWholeLog();
89+
Assert.That(orders.Count, Is.EqualTo(830));
90+
Assert.IsTrue(orders.Where(x => x.SecondId.HasValue && x.ThirdId.HasValue)
91+
.All(x => x.FirstId == x.SecondId - 1 && x.SecondId == x.ThirdId - 1));
92+
Assert.That(GetTotalOccurrences(sql, "left outer join"), Is.EqualTo(2));
93+
}
94+
}
95+
96+
[Test]
97+
public async Task LeftJoinExtensionMethodWithMultipleKeyPropertiesAsync()
98+
{
99+
using (var sqlSpy = new SqlLogSpy())
100+
{
101+
var orders = await (db.Orders
102+
.LeftJoin(
103+
db.Orders,
104+
x => new {x.OrderId, x.Customer.CustomerId},
105+
x => new {x.OrderId, x.Customer.CustomerId},
106+
(order, order1) => new {order, order1})
107+
.Select(x => new {FirstId = x.order.OrderId, SecondId = x.order1.OrderId})
108+
.ToListAsync());
109+
110+
var sql = sqlSpy.GetWholeLog();
111+
Assert.That(orders.Count, Is.EqualTo(830));
112+
Assert.IsTrue(orders.All(x => x.FirstId == x.SecondId));
113+
Assert.That(GetTotalOccurrences(sql, "left outer join"), Is.EqualTo(1));
114+
}
39115
}
40116

41117
[TestCase(false)]

src/NHibernate.Test/Async/Linq/LinqQuerySamples.cs

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,6 +768,26 @@ from o in c.Orders
768768
}
769769
}
770770

771+
[Category("JOIN")]
772+
[Test(Description = "This sample uses foreign key navigation in the " +
773+
"from clause to select all orders for customers in London.")]
774+
public async Task DLinqJoin1LeftJoinAsync()
775+
{
776+
IQueryable<Order> q =
777+
from c in db.Customers
778+
from o in c.Orders.DefaultIfEmpty()
779+
where c.Address.City == "London"
780+
select o;
781+
782+
using (var sqlSpy = new SqlLogSpy())
783+
{
784+
await (ObjectDumper.WriteAsync(q));
785+
786+
var sql = sqlSpy.GetWholeLog();
787+
Assert.That(GetTotalOccurrences(sql, "left outer join"), Is.EqualTo(1));
788+
}
789+
}
790+
771791
[Category("JOIN")]
772792
[Test(Description = "This sample shows how to construct a join where one side is nullable and the other isn't.")]
773793
public async Task DLinqJoin10Async()
@@ -974,6 +994,26 @@ join o in db.Orders on c.CustomerId equals o.Customer.CustomerId
974994
}
975995
}
976996

997+
[Category("JOIN")]
998+
[Test(Description = "This sample explictly joins two tables and projects results from both tables.")]
999+
public async Task DLinqJoin5aLeftJoinAsync()
1000+
{
1001+
var q =
1002+
from c in db.Customers
1003+
join o in db.Orders on c.CustomerId equals o.Customer.CustomerId into orders
1004+
from o in orders.DefaultIfEmpty()
1005+
where o != null
1006+
select new { c.ContactName, o.OrderId };
1007+
1008+
using (var sqlSpy = new SqlLogSpy())
1009+
{
1010+
await (ObjectDumper.WriteAsync(q));
1011+
1012+
var sql = sqlSpy.GetWholeLog();
1013+
Assert.That(GetTotalOccurrences(sql, "left outer join"), Is.EqualTo(1));
1014+
}
1015+
}
1016+
9771017
[Category("JOIN")]
9781018
[Test(Description = "This sample explictly joins two tables and projects results from both tables using a group join.")]
9791019
public async Task DLinqJoin5bAsync()
@@ -1032,6 +1072,21 @@ join o in db.Orders on
10321072
}
10331073
}
10341074

1075+
[Category("JOIN")]
1076+
[Test(Description = "This sample explictly joins two tables with a composite key and projects results from both tables.")]
1077+
public void DLinqJoin5dLeftJoinAsync()
1078+
{
1079+
var q =
1080+
from c in db.Customers
1081+
join o in db.Orders on
1082+
new { c.CustomerId, HasContractTitle = c.ContactTitle != null } equals
1083+
new { o.Customer.CustomerId, HasContractTitle = o.Customer.ContactTitle != null } into orders
1084+
from o in orders.DefaultIfEmpty()
1085+
select new { c.ContactName, o.OrderId };
1086+
1087+
Assert.ThrowsAsync<NotSupportedException>(() => ObjectDumper.WriteAsync(q));
1088+
}
1089+
10351090
[Category("JOIN")]
10361091
[Test(Description = "This sample joins two tables and projects results from the first table.")]
10371092
public async Task DLinqJoin5eAsync()
@@ -1051,6 +1106,26 @@ join o in db.Orders on c.CustomerId equals o.Customer.CustomerId
10511106
}
10521107
}
10531108

1109+
[Category("JOIN")]
1110+
[Test(Description = "This sample joins two tables and projects results from the first table.")]
1111+
public async Task DLinqJoin5eLeftJoinAsync()
1112+
{
1113+
var q =
1114+
from c in db.Customers
1115+
join o in db.Orders on c.CustomerId equals o.Customer.CustomerId into orders
1116+
from o in orders.DefaultIfEmpty()
1117+
where c.ContactName != null
1118+
select o;
1119+
1120+
using (var sqlSpy = new SqlLogSpy())
1121+
{
1122+
await (ObjectDumper.WriteAsync(q));
1123+
1124+
var sql = sqlSpy.GetWholeLog();
1125+
Assert.That(GetTotalOccurrences(sql, "left outer join"), Is.EqualTo(1));
1126+
}
1127+
}
1128+
10541129
[Category("JOIN")]
10551130
[TestCase(Description = "This sample explictly joins two tables with a composite key and projects results from both tables.")]
10561131
public async Task DLinqJoin5fAsync()
@@ -1072,6 +1147,28 @@ join c in db.Customers on
10721147
}
10731148
}
10741149

1150+
[Category("JOIN")]
1151+
[TestCase(Description = "This sample explictly joins two tables with a composite key and projects results from both tables.")]
1152+
public async Task DLinqJoin5fLeftJoinAsync()
1153+
{
1154+
var q =
1155+
from o in db.Orders
1156+
join c in db.Customers on
1157+
new { o.Customer.CustomerId, HasContractTitle = o.Customer.ContactTitle != null } equals
1158+
new { c.CustomerId, HasContractTitle = c.ContactTitle != null } into customers
1159+
from c in customers.DefaultIfEmpty()
1160+
select new { c.ContactName, o.OrderId };
1161+
1162+
using (var sqlSpy = new SqlLogSpy())
1163+
{
1164+
await (ObjectDumper.WriteAsync(q));
1165+
1166+
var sql = sqlSpy.GetWholeLog();
1167+
Assert.That(GetTotalOccurrences(sql, "left outer join"), Is.EqualTo(2));
1168+
Assert.That(GetTotalOccurrences(sql, "inner join"), Is.EqualTo(0));
1169+
}
1170+
}
1171+
10751172
[Category("JOIN")]
10761173
[Test(Description = "This sample explictly joins three tables and projects results from each of them.")]
10771174
public async Task DLinqJoin6Async()
@@ -1094,6 +1191,28 @@ join e in db.Employees on c.Address.City equals e.Address.City into emps
10941191
}
10951192
}
10961193

1194+
[Category("JOIN")]
1195+
[Test(
1196+
Description =
1197+
"This sample shows how to get LEFT OUTER JOIN by using DefaultIfEmpty(). The DefaultIfEmpty() method returns null when there is no Order for the Employee."
1198+
)]
1199+
public async Task DLinqJoin7Async()
1200+
{
1201+
var q =
1202+
from e in db.Employees
1203+
join o in db.Orders on e equals o.Employee into ords
1204+
from o in ords.DefaultIfEmpty()
1205+
select new {e.FirstName, e.LastName, Order = o};
1206+
1207+
using (var sqlSpy = new SqlLogSpy())
1208+
{
1209+
await (ObjectDumper.WriteAsync(q));
1210+
1211+
var sql = sqlSpy.GetWholeLog();
1212+
Assert.That(GetTotalOccurrences(sql, "left outer join"), Is.EqualTo(1));
1213+
}
1214+
}
1215+
10971216
[Category("JOIN")]
10981217
[Test(Description = "This sample projects a 'let' expression resulting from a join.")]
10991218
public async Task DLinqJoin8Async()
@@ -1156,6 +1275,50 @@ from d in details
11561275
}
11571276
}
11581277

1278+
[Category("JOIN")]
1279+
[TestCase(true, Description = "This sample shows a group left join with a composite key.")]
1280+
[TestCase(false, Description = "This sample shows a group left join with a composite key.")]
1281+
public async Task DLinqJoin9LeftJoinAsync(bool useCrossJoin)
1282+
{
1283+
if (useCrossJoin && !Dialect.SupportsCrossJoin)
1284+
{
1285+
Assert.Ignore("Dialect does not support cross join.");
1286+
}
1287+
1288+
// The expected collection can be obtained from the below Linq to Objects query.
1289+
//var expected =
1290+
// (from o in db.Orders.ToList()
1291+
// from p in db.Products.ToList()
1292+
// join d in db.OrderLines.ToList()
1293+
// on new { o.OrderId, p.ProductId } equals new { d.Order.OrderId, d.Product.ProductId }
1294+
// into details
1295+
// from d in details.DefaultIfEmpty()
1296+
// where d != null && d.UnitPrice > 50
1297+
// select new { o.OrderId, p.ProductId, d.UnitPrice }).ToList();
1298+
1299+
using (var substitute = SubstituteDialect())
1300+
using (var sqlSpy = new SqlLogSpy())
1301+
{
1302+
ClearQueryPlanCache();
1303+
substitute.Value.SupportsCrossJoin.Returns(useCrossJoin);
1304+
1305+
var actual =
1306+
await ((from o in db.Orders
1307+
from p in db.Products
1308+
join d in db.OrderLines
1309+
on new {o.OrderId, p.ProductId} equals new {d.Order.OrderId, d.Product.ProductId}
1310+
into details
1311+
from d in details.DefaultIfEmpty()
1312+
where d != null && d.UnitPrice > 50
1313+
select new {o.OrderId, p.ProductId, d.UnitPrice}).ToListAsync());
1314+
1315+
var sql = sqlSpy.GetWholeLog();
1316+
Assert.That(actual.Count, Is.EqualTo(163));
1317+
Assert.That(sql, Does.Contain(useCrossJoin ? "cross join" : "inner join"));
1318+
Assert.That(GetTotalOccurrences(sql, "left outer join"), Is.EqualTo(1));
1319+
}
1320+
}
1321+
11591322
[Category("JOIN")]
11601323
[Test(Description = "This sample shows a join which is then grouped")]
11611324
public async Task DLinqJoin9bAsync()
@@ -1186,5 +1349,26 @@ join s2 in db.Employees on s.Superior.EmployeeId equals s2.EmployeeId
11861349
Assert.That(GetTotalOccurrences(sql, "inner join"), Is.EqualTo(2));
11871350
}
11881351
}
1352+
1353+
[Category("JOIN")]
1354+
[Test(Description = "This sample shows how to join multiple tables using a left join.")]
1355+
public async Task DLinqJoin10aLeftJoinAsync()
1356+
{
1357+
var q =
1358+
from e in db.Employees
1359+
join s in db.Employees on e.Superior.EmployeeId equals s.EmployeeId into sup
1360+
from s in sup.DefaultIfEmpty()
1361+
join s2 in db.Employees on s.Superior.EmployeeId equals s2.EmployeeId into sup2
1362+
from s2 in sup2.DefaultIfEmpty()
1363+
select new { e.FirstName, SuperiorName = s.FirstName, Superior2Name = s2.FirstName };
1364+
1365+
using (var sqlSpy = new SqlLogSpy())
1366+
{
1367+
await (ObjectDumper.WriteAsync(q));
1368+
1369+
var sql = sqlSpy.GetWholeLog();
1370+
Assert.That(GetTotalOccurrences(sql, "left outer join"), Is.EqualTo(2));
1371+
}
1372+
}
11891373
}
11901374
}

0 commit comments

Comments
 (0)