Skip to content

Commit bb5726e

Browse files
Attempt a fix
1 parent 3603f01 commit bb5726e

File tree

5 files changed

+76
-18
lines changed

5 files changed

+76
-18
lines changed

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

Lines changed: 21 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,24 @@ 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+
}
72+
73+
[Test]
74+
public async Task QueryWithAnyOnCleanLinesAsync()
75+
{
76+
using var session = OpenSession();
77+
using var transaction = session.BeginTransaction();
78+
79+
// This form of query is how we first discovered the issue. This is a simplified reproduction of the
80+
// sort of Linq that we were using in our app. It seems to occur when we force an EXISTS( ... ) subquery.
81+
var validOrders = session.Query<Order>().Where(x => x.CreatedDate > new DateTime(2024, 9, 10));
82+
var orderCount = await (session.Query<CleanLineItem>().CountAsync(x => validOrders.Any(y => y == x.Order)));
83+
6784
Assert.That(orderCount, Is.EqualTo(1));
6885
}
69-
86+
7087
[Test]
7188
public async Task QueryWithContainsAsync()
7289
{
@@ -75,10 +92,10 @@ public async Task QueryWithContainsAsync()
7592

7693
var validOrders = session.Query<Order>().Where(x => x.CreatedDate > new DateTime(2024, 9, 10));
7794
var orderCount = await (session.Query<LineItem>().CountAsync(x => validOrders.Contains(x.Order)));
78-
95+
7996
Assert.That(orderCount, Is.EqualTo(1));
8097
}
81-
98+
8299
[Test]
83100
public async Task SimpleQueryForDataWhichWasInsertedViaAdoShouldProvideExpectedResultsAsync()
84101
{

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: 21 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,24 @@ 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+
}
60+
61+
[Test]
62+
public void QueryWithAnyOnCleanLines()
63+
{
64+
using var session = OpenSession();
65+
using var transaction = session.BeginTransaction();
66+
67+
// This form of query is how we first discovered the issue. This is a simplified reproduction of the
68+
// sort of Linq that we were using in our app. It seems to occur when we force an EXISTS( ... ) subquery.
69+
var validOrders = session.Query<Order>().Where(x => x.CreatedDate > new DateTime(2024, 9, 10));
70+
var orderCount = session.Query<CleanLineItem>().Count(x => validOrders.Any(y => y == x.Order));
71+
5572
Assert.That(orderCount, Is.EqualTo(1));
5673
}
57-
74+
5875
[Test]
5976
public void QueryWithContains()
6077
{
@@ -63,10 +80,10 @@ public void QueryWithContains()
6380

6481
var validOrders = session.Query<Order>().Where(x => x.CreatedDate > new DateTime(2024, 9, 10));
6582
var orderCount = session.Query<LineItem>().Count(x => validOrders.Contains(x.Order));
66-
83+
6784
Assert.That(orderCount, Is.EqualTo(1));
6885
}
69-
86+
7087
[Test]
7188
public void SimpleQueryForDataWhichWasInsertedViaAdoShouldProvideExpectedResults()
7289
{
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: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -402,8 +402,11 @@ private void DereferenceEntity(EntityType entityType, bool implicitJoin, string
402402
string property = _propertyName;
403403
bool joinIsNeeded;
404404

405-
//For nullable entity comparisons we always need to add join (like not constrained one-to-one or not-found ignore associations)
406-
bool comparisonWithNullableEntity = entityType.IsNullable && Walker.IsComparativeExpressionClause && !IsCorrelatedSubselect;
405+
// For nullable entity comparisons we always need to add join (like not constrained one-to-one or not-found ignore associations).
406+
// For property-ref associations, we also need this unless finding a way in the IdentNode for the other hand of the comparison
407+
// to detect it should yield the property-ref columns instead of the primary key columns.
408+
bool comparisonWithNullableEntityOrThroughPropertyRef = Walker.IsComparativeExpressionClause
409+
&& (entityType.IsNullable || entityType.IsUniqueKeyReference);
407410

408411
if ( IsDotNode( parent ) )
409412
{
@@ -412,7 +415,7 @@ private void DereferenceEntity(EntityType entityType, bool implicitJoin, string
412415
// entity's PK (because 'our' table would know the FK).
413416
parentAsDotNode = ( DotNode ) parent;
414417
property = parentAsDotNode._propertyName;
415-
joinIsNeeded = generateJoin && ((Walker.IsSelectStatement && comparisonWithNullableEntity) || !IsReferenceToPrimaryKey( parentAsDotNode._propertyName, entityType ));
418+
joinIsNeeded = generateJoin && ((Walker.IsSelectStatement && comparisonWithNullableEntityOrThroughPropertyRef) || !IsReferenceToPrimaryKey( parentAsDotNode._propertyName, entityType ));
416419
}
417420
else if ( ! Walker.IsSelectStatement )
418421
{
@@ -426,13 +429,14 @@ private void DereferenceEntity(EntityType entityType, bool implicitJoin, string
426429
else
427430
{
428431
joinIsNeeded = generateJoin || (Walker.IsInSelect && !Walker.IsInCase) || (Walker.IsInFrom && !Walker.IsComparativeExpressionClause)
429-
|| comparisonWithNullableEntity;
432+
|| comparisonWithNullableEntityOrThroughPropertyRef;
430433
}
431434

432435
if ( joinIsNeeded )
433436
{
434-
DereferenceEntityJoin(classAlias, entityType, implicitJoin, parent, comparisonWithNullableEntity);
435-
if (comparisonWithNullableEntity)
437+
var forceLeftJoin = comparisonWithNullableEntityOrThroughPropertyRef && !IsCorrelatedSubselect;
438+
DereferenceEntityJoin(classAlias, entityType, implicitJoin, parent, forceLeftJoin);
439+
if (comparisonWithNullableEntityOrThroughPropertyRef)
436440
{
437441
_columns = FromElement.GetIdentityColumns();
438442
}

0 commit comments

Comments
 (0)