Skip to content

Commit d678b7b

Browse files
Fix #3609
1 parent 6bb0a10 commit d678b7b

File tree

5 files changed

+87
-18
lines changed

5 files changed

+87
-18
lines changed

src/NHibernate.Test/Async/NHSpecificTest/GH3609/Fixture.cs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ protected override void OnSetUp()
3131
};
3232
session.Save(order);
3333
session.Save(new LineItem { Order = order, ItemName = "Bananas", Amount = 5 });
34+
session.Save(new CleanLineItem { Order = order, ItemName = "Bananas", Amount = 5 });
3435

3536
order = new Order
3637
{
@@ -39,6 +40,7 @@ protected override void OnSetUp()
3940
};
4041
session.Save(order);
4142
session.Save(new LineItem { Order = order, ItemName = "Apples", Amount = 10 });
43+
session.Save(new CleanLineItem { Order = order, ItemName = "Apples", Amount = 10 });
4244

4345
transaction.Commit();
4446
}
@@ -48,6 +50,7 @@ protected override void OnTearDown()
4850
using var session = OpenSession();
4951
using var transaction = session.BeginTransaction();
5052

53+
session.CreateQuery("delete from CleanLineItem").ExecuteUpdate();
5154
session.CreateQuery("delete from System.Object").ExecuteUpdate();
5255

5356
transaction.Commit();
@@ -63,10 +66,26 @@ public async Task QueryWithAnyAsync()
6366
// sort of Linq that we were using in our app. It seems to occur when we force an EXISTS( ... ) subquery.
6467
var validOrders = session.Query<Order>().Where(x => x.CreatedDate > new DateTime(2024, 9, 10));
6568
var orderCount = await (session.Query<LineItem>().CountAsync(x => validOrders.Any(y => y == x.Order)));
66-
69+
70+
Assert.That(orderCount, Is.EqualTo(1));
71+
await (transaction.CommitAsync());
72+
}
73+
74+
[Test]
75+
public async Task QueryWithAnyOnCleanLinesAsync()
76+
{
77+
using var session = OpenSession();
78+
using var transaction = session.BeginTransaction();
79+
80+
// This form of query is how we first discovered the issue. This is a simplified reproduction of the
81+
// sort of Linq that we were using in our app. It seems to occur when we force an EXISTS( ... ) subquery.
82+
var validOrders = session.Query<Order>().Where(x => x.CreatedDate > new DateTime(2024, 9, 10));
83+
var orderCount = await (session.Query<CleanLineItem>().CountAsync(x => validOrders.Any(y => y == x.Order)));
84+
6785
Assert.That(orderCount, Is.EqualTo(1));
86+
await (transaction.CommitAsync());
6887
}
69-
88+
7089
[Test]
7190
public async Task QueryWithContainsAsync()
7291
{
@@ -75,10 +94,11 @@ public async Task QueryWithContainsAsync()
7594

7695
var validOrders = session.Query<Order>().Where(x => x.CreatedDate > new DateTime(2024, 9, 10));
7796
var orderCount = await (session.Query<LineItem>().CountAsync(x => validOrders.Contains(x.Order)));
78-
97+
7998
Assert.That(orderCount, Is.EqualTo(1));
99+
await (transaction.CommitAsync());
80100
}
81-
101+
82102
[Test]
83103
public async Task SimpleQueryForDataWhichWasInsertedViaAdoShouldProvideExpectedResultsAsync()
84104
{
@@ -88,6 +108,7 @@ public async Task SimpleQueryForDataWhichWasInsertedViaAdoShouldProvideExpectedR
88108
// This style of equivalent query does not exhibit the problem. This test passes no matter which NH version.
89109
var lineItem = await (session.Query<LineItem>().FirstOrDefaultAsync(x => x.Order.CreatedDate > new DateTime(2024, 9, 10)));
90110
Assert.That(lineItem, Is.Not.Null);
111+
await (transaction.CommitAsync());
91112
}
92113
}
93114
}

src/NHibernate.Test/NHSpecificTest/GH3609/Entities.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System;
1+
using System;
22

33
namespace NHibernate.Test.NHSpecificTest.GH3609
44
{
@@ -21,4 +21,15 @@ public class LineItem
2121

2222
public virtual decimal Amount { get; set; }
2323
}
24+
25+
public class CleanLineItem
26+
{
27+
public virtual long Id { get; set; }
28+
29+
public virtual Order Order { get; set; }
30+
31+
public virtual string ItemName { get; set; }
32+
33+
public virtual decimal Amount { get; set; }
34+
}
2435
}

src/NHibernate.Test/NHSpecificTest/GH3609/Fixture.cs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ protected override void OnSetUp()
1919
};
2020
session.Save(order);
2121
session.Save(new LineItem { Order = order, ItemName = "Bananas", Amount = 5 });
22+
session.Save(new CleanLineItem { Order = order, ItemName = "Bananas", Amount = 5 });
2223

2324
order = new Order
2425
{
@@ -27,6 +28,7 @@ protected override void OnSetUp()
2728
};
2829
session.Save(order);
2930
session.Save(new LineItem { Order = order, ItemName = "Apples", Amount = 10 });
31+
session.Save(new CleanLineItem { Order = order, ItemName = "Apples", Amount = 10 });
3032

3133
transaction.Commit();
3234
}
@@ -36,6 +38,7 @@ protected override void OnTearDown()
3638
using var session = OpenSession();
3739
using var transaction = session.BeginTransaction();
3840

41+
session.CreateQuery("delete from CleanLineItem").ExecuteUpdate();
3942
session.CreateQuery("delete from System.Object").ExecuteUpdate();
4043

4144
transaction.Commit();
@@ -51,10 +54,26 @@ public void QueryWithAny()
5154
// sort of Linq that we were using in our app. It seems to occur when we force an EXISTS( ... ) subquery.
5255
var validOrders = session.Query<Order>().Where(x => x.CreatedDate > new DateTime(2024, 9, 10));
5356
var orderCount = session.Query<LineItem>().Count(x => validOrders.Any(y => y == x.Order));
54-
57+
58+
Assert.That(orderCount, Is.EqualTo(1));
59+
transaction.Commit();
60+
}
61+
62+
[Test]
63+
public void QueryWithAnyOnCleanLines()
64+
{
65+
using var session = OpenSession();
66+
using var transaction = session.BeginTransaction();
67+
68+
// This form of query is how we first discovered the issue. This is a simplified reproduction of the
69+
// sort of Linq that we were using in our app. It seems to occur when we force an EXISTS( ... ) subquery.
70+
var validOrders = session.Query<Order>().Where(x => x.CreatedDate > new DateTime(2024, 9, 10));
71+
var orderCount = session.Query<CleanLineItem>().Count(x => validOrders.Any(y => y == x.Order));
72+
5573
Assert.That(orderCount, Is.EqualTo(1));
74+
transaction.Commit();
5675
}
57-
76+
5877
[Test]
5978
public void QueryWithContains()
6079
{
@@ -63,10 +82,11 @@ public void QueryWithContains()
6382

6483
var validOrders = session.Query<Order>().Where(x => x.CreatedDate > new DateTime(2024, 9, 10));
6584
var orderCount = session.Query<LineItem>().Count(x => validOrders.Contains(x.Order));
66-
85+
6786
Assert.That(orderCount, Is.EqualTo(1));
87+
transaction.Commit();
6888
}
69-
89+
7090
[Test]
7191
public void SimpleQueryForDataWhichWasInsertedViaAdoShouldProvideExpectedResults()
7292
{
@@ -76,6 +96,7 @@ public void SimpleQueryForDataWhichWasInsertedViaAdoShouldProvideExpectedResults
7696
// This style of equivalent query does not exhibit the problem. This test passes no matter which NH version.
7797
var lineItem = session.Query<LineItem>().FirstOrDefault(x => x.Order.CreatedDate > new DateTime(2024, 9, 10));
7898
Assert.That(lineItem, Is.Not.Null);
99+
transaction.Commit();
79100
}
80101
}
81102
}
Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
<?xml version="1.0" encoding="utf-8" ?>
22
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="NHibernate.Test"
33
namespace="NHibernate.Test.NHSpecificTest.GH3609">
4-
4+
55
<class name="Order" table="TheOrder">
66
<id name="Id" generator="identity" />
77
<property name="CreatedDate" />
8-
<property name="UniqueId" />
8+
<property name="UniqueId" unique="true" />
99
</class>
10-
10+
1111
<class name="LineItem">
1212
<id name="Id" generator="identity" />
1313
<property name="ItemName" />
@@ -18,4 +18,13 @@
1818
column="OrderId" />
1919
</class>
2020

21+
<class name="CleanLineItem">
22+
<id name="Id" generator="identity" />
23+
<property name="ItemName" />
24+
<property name="Amount" />
25+
<many-to-one name="Order"
26+
property-ref="UniqueId"
27+
column="OrderId" />
28+
</class>
29+
2130
</hibernate-mapping>

src/NHibernate/Hql/Ast/ANTLR/Tree/DotNode.cs

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -397,10 +397,14 @@ private void DereferenceEntity(EntityType entityType, bool implicitJoin, string
397397
string property = _propertyName;
398398
bool joinIsNeeded;
399399

400-
//For nullable entity comparisons we always need to add join (like not constrained one-to-one or not-found ignore associations)
401-
bool comparisonWithNullableEntity = entityType.IsNullable && Walker.IsComparativeExpressionClause && !IsCorrelatedSubselect;
400+
// For nullable entity comparisons we always need to add join (like not constrained one-to-one or not-found ignore associations).
401+
var comparisonWithNullableEntity = Walker.IsComparativeExpressionClause && entityType.IsNullable;
402+
// For property-ref association comparison, we also need to join unless finding a way in the node for the other hand of the comparison
403+
// to detect it should yield the property-ref columns instead of the primary key columns. And if the other hand is an association too,
404+
// it may be a reference to the primary key, so we would need to join anyway.
405+
var comparisonThroughPropertyRef = Walker.IsComparativeExpressionClause && !entityType.IsReferenceToPrimaryKey;
402406

403-
if ( IsDotNode( parent ) )
407+
if (IsDotNode(parent))
404408
{
405409
// our parent is another dot node, meaning we are being further dereferenced.
406410
// thus we need to generate a join unless the parent refers to the associated
@@ -421,15 +425,18 @@ private void DereferenceEntity(EntityType entityType, bool implicitJoin, string
421425
else
422426
{
423427
joinIsNeeded = generateJoin || (Walker.IsInSelect && !Walker.IsInCase) || (Walker.IsInFrom && !Walker.IsComparativeExpressionClause)
424-
|| comparisonWithNullableEntity;
428+
|| comparisonWithNullableEntity || comparisonThroughPropertyRef;
425429
}
426430

427431
if ( joinIsNeeded )
428432
{
429-
DereferenceEntityJoin(classAlias, entityType, implicitJoin, parent, comparisonWithNullableEntity);
430-
if (comparisonWithNullableEntity)
433+
// Subselect queries use theta style joins, which cannot be forced to left outer joins.
434+
var forceLeftJoin = comparisonWithNullableEntity && !IsCorrelatedSubselect;
435+
DereferenceEntityJoin(classAlias, entityType, implicitJoin, parent, forceLeftJoin);
436+
if (comparisonWithNullableEntity || comparisonThroughPropertyRef)
431437
{
432438
_columns = FromElement.GetIdentityColumns();
439+
DataType = FromElement.EntityPersister.EntityMetamodel.EntityType;
433440
}
434441
}
435442
else

0 commit comments

Comments
 (0)