Skip to content

Commit 91d86bf

Browse files
Fix filtering sub-queries on property-ref (#3685)
Fix #3609 Also fix a regression on filtering by comparing a property-ref association with an entity parameter. (NH-892) Co-authored-by: Alex Zaytsev <[email protected]>
1 parent ee854c3 commit 91d86bf

File tree

5 files changed

+295
-7
lines changed

5 files changed

+295
-7
lines changed
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
//------------------------------------------------------------------------------
2+
// <auto-generated>
3+
// This code was generated by AsyncGenerator.
4+
//
5+
// Changes to this file may cause incorrect behavior and will be lost if
6+
// the code is regenerated.
7+
// </auto-generated>
8+
//------------------------------------------------------------------------------
9+
10+
11+
using System;
12+
using System.Linq;
13+
using NUnit.Framework;
14+
using NHibernate.Linq;
15+
16+
namespace NHibernate.Test.NHSpecificTest.GH3609
17+
{
18+
using System.Threading.Tasks;
19+
[TestFixture]
20+
public class FixtureAsync : BugTestCase
21+
{
22+
protected override void OnSetUp()
23+
{
24+
using var session = OpenSession();
25+
using var transaction = session.BeginTransaction();
26+
27+
var order = new Order
28+
{
29+
UniqueId = "0ab92479-8a17-4dbc-9bef-ce4344940cec",
30+
CreatedDate = new DateTime(2024, 09, 24)
31+
};
32+
session.Save(order);
33+
session.Save(new LineItem { Order = order, ItemName = "Bananas", Amount = 5 });
34+
session.Save(new CleanLineItem { Order = order, ItemName = "Bananas", Amount = 5 });
35+
36+
order = new Order
37+
{
38+
UniqueId = "4ca17d84-97aa-489f-8701-302a3879a388",
39+
CreatedDate = new DateTime(2021, 09, 19)
40+
};
41+
session.Save(order);
42+
session.Save(new LineItem { Order = order, ItemName = "Apples", Amount = 10 });
43+
session.Save(new CleanLineItem { Order = order, ItemName = "Apples", Amount = 10 });
44+
45+
transaction.Commit();
46+
}
47+
48+
protected override void OnTearDown()
49+
{
50+
using var session = OpenSession();
51+
using var transaction = session.BeginTransaction();
52+
53+
session.CreateQuery("delete from CleanLineItem").ExecuteUpdate();
54+
session.CreateQuery("delete from System.Object").ExecuteUpdate();
55+
56+
transaction.Commit();
57+
}
58+
59+
[Test]
60+
public async Task QueryWithAnyAsync()
61+
{
62+
using var session = OpenSession();
63+
using var transaction = session.BeginTransaction();
64+
65+
// This form of query is how we first discovered the issue. This is a simplified reproduction of the
66+
// sort of Linq that we were using in our app. It seems to occur when we force an EXISTS( ... ) subquery.
67+
var validOrders = session.Query<Order>().Where(x => x.CreatedDate > new DateTime(2024, 9, 10));
68+
var orderCount = await (session.Query<LineItem>().CountAsync(x => validOrders.Any(y => y == x.Order)));
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+
85+
Assert.That(orderCount, Is.EqualTo(1));
86+
await (transaction.CommitAsync());
87+
}
88+
89+
[Test]
90+
public async Task QueryWithContainsAsync()
91+
{
92+
using var session = OpenSession();
93+
using var transaction = session.BeginTransaction();
94+
95+
var validOrders = session.Query<Order>().Where(x => x.CreatedDate > new DateTime(2024, 9, 10));
96+
var orderCount = await (session.Query<LineItem>().CountAsync(x => validOrders.Contains(x.Order)));
97+
98+
Assert.That(orderCount, Is.EqualTo(1));
99+
await (transaction.CommitAsync());
100+
}
101+
102+
[Test]
103+
public async Task SimpleQueryForDataWhichWasInsertedViaAdoShouldProvideExpectedResultsAsync()
104+
{
105+
using var session = OpenSession();
106+
using var transaction = session.BeginTransaction();
107+
108+
// This style of equivalent query does not exhibit the problem. This test passes no matter which NH version.
109+
var lineItem = await (session.Query<LineItem>().FirstOrDefaultAsync(x => x.Order.CreatedDate > new DateTime(2024, 9, 10)));
110+
Assert.That(lineItem, Is.Not.Null);
111+
await (transaction.CommitAsync());
112+
}
113+
}
114+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using System;
2+
3+
namespace NHibernate.Test.NHSpecificTest.GH3609
4+
{
5+
public class Order
6+
{
7+
public virtual long Id { get; set; }
8+
9+
public virtual string UniqueId { get; set; } = Guid.NewGuid().ToString();
10+
11+
public virtual DateTime CreatedDate { get; set; }
12+
}
13+
14+
public class LineItem
15+
{
16+
public virtual long Id { get; set; }
17+
18+
public virtual Order Order { get; set; }
19+
20+
public virtual string ItemName { get; set; }
21+
22+
public virtual decimal Amount { get; set; }
23+
}
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+
}
35+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
using System;
2+
using System.Linq;
3+
using NUnit.Framework;
4+
5+
namespace NHibernate.Test.NHSpecificTest.GH3609
6+
{
7+
[TestFixture]
8+
public class Fixture : BugTestCase
9+
{
10+
protected override void OnSetUp()
11+
{
12+
using var session = OpenSession();
13+
using var transaction = session.BeginTransaction();
14+
15+
var order = new Order
16+
{
17+
UniqueId = "0ab92479-8a17-4dbc-9bef-ce4344940cec",
18+
CreatedDate = new DateTime(2024, 09, 24)
19+
};
20+
session.Save(order);
21+
session.Save(new LineItem { Order = order, ItemName = "Bananas", Amount = 5 });
22+
session.Save(new CleanLineItem { Order = order, ItemName = "Bananas", Amount = 5 });
23+
24+
order = new Order
25+
{
26+
UniqueId = "4ca17d84-97aa-489f-8701-302a3879a388",
27+
CreatedDate = new DateTime(2021, 09, 19)
28+
};
29+
session.Save(order);
30+
session.Save(new LineItem { Order = order, ItemName = "Apples", Amount = 10 });
31+
session.Save(new CleanLineItem { Order = order, ItemName = "Apples", Amount = 10 });
32+
33+
transaction.Commit();
34+
}
35+
36+
protected override void OnTearDown()
37+
{
38+
using var session = OpenSession();
39+
using var transaction = session.BeginTransaction();
40+
41+
session.CreateQuery("delete from CleanLineItem").ExecuteUpdate();
42+
session.CreateQuery("delete from System.Object").ExecuteUpdate();
43+
44+
transaction.Commit();
45+
}
46+
47+
[Test]
48+
public void QueryWithAny()
49+
{
50+
using var session = OpenSession();
51+
using var transaction = session.BeginTransaction();
52+
53+
// This form of query is how we first discovered the issue. This is a simplified reproduction of the
54+
// sort of Linq that we were using in our app. It seems to occur when we force an EXISTS( ... ) subquery.
55+
var validOrders = session.Query<Order>().Where(x => x.CreatedDate > new DateTime(2024, 9, 10));
56+
var orderCount = session.Query<LineItem>().Count(x => validOrders.Any(y => y == x.Order));
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+
73+
Assert.That(orderCount, Is.EqualTo(1));
74+
transaction.Commit();
75+
}
76+
77+
[Test]
78+
public void QueryWithContains()
79+
{
80+
using var session = OpenSession();
81+
using var transaction = session.BeginTransaction();
82+
83+
var validOrders = session.Query<Order>().Where(x => x.CreatedDate > new DateTime(2024, 9, 10));
84+
var orderCount = session.Query<LineItem>().Count(x => validOrders.Contains(x.Order));
85+
86+
Assert.That(orderCount, Is.EqualTo(1));
87+
transaction.Commit();
88+
}
89+
90+
[Test]
91+
public void SimpleQueryForDataWhichWasInsertedViaAdoShouldProvideExpectedResults()
92+
{
93+
using var session = OpenSession();
94+
using var transaction = session.BeginTransaction();
95+
96+
// This style of equivalent query does not exhibit the problem. This test passes no matter which NH version.
97+
var lineItem = session.Query<LineItem>().FirstOrDefault(x => x.Order.CreatedDate > new DateTime(2024, 9, 10));
98+
Assert.That(lineItem, Is.Not.Null);
99+
transaction.Commit();
100+
}
101+
}
102+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" assembly="NHibernate.Test"
3+
namespace="NHibernate.Test.NHSpecificTest.GH3609">
4+
5+
<class name="Order" table="TheOrder">
6+
<id name="Id" generator="identity" />
7+
<property name="CreatedDate" />
8+
<property name="UniqueId" unique="true" />
9+
</class>
10+
11+
<class name="LineItem">
12+
<id name="Id" generator="identity" />
13+
<property name="ItemName" />
14+
<property name="Amount" />
15+
<many-to-one name="Order"
16+
property-ref="UniqueId"
17+
not-found="ignore"
18+
column="OrderId" />
19+
</class>
20+
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+
30+
</hibernate-mapping>

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

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ public override void ResolveFirstChild()
161161
string propName = property.Text;
162162
_propertyName = propName;
163163

164-
// If the uresolved property path isn't set yet, just use the property name.
164+
// If the unresolved property path isn't set yet, just use the property name.
165165
if (_propertyPath == null)
166166
{
167167
_propertyPath = propName;
@@ -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)