Skip to content

Commit 62f4331

Browse files
committed
Convert EntityJoin to FROM_FRAGMENT only for first element Adjust comment in HqlSqlWalker before adding an element without Parent to the tree and use AppendFromElement instead of direct AddChild
1 parent 0398d47 commit 62f4331

File tree

8 files changed

+279
-29
lines changed

8 files changed

+279
-29
lines changed
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
using System.Collections.Generic;
2+
3+
namespace NHibernate.Test.NHSpecificTest.GH3334
4+
{
5+
public class Entity
6+
{
7+
public virtual int Id { get; set; }
8+
public virtual string Name { get; set; }
9+
public virtual ISet<ChildEntity> Children { get; set; } = new HashSet<ChildEntity>();
10+
public virtual OtherEntity OtherEntity { get; set; }
11+
}
12+
13+
public class ChildEntity
14+
{
15+
public virtual int Id { get; set; }
16+
public virtual Entity Parent { get; set; }
17+
public virtual string Name { get; set; }
18+
public virtual GrandChildEntity Child { get; set; }
19+
}
20+
21+
public class GrandChildEntity
22+
{
23+
public virtual int Id { get; set; }
24+
public virtual string Name { get; set; }
25+
}
26+
27+
public class OtherEntity
28+
{
29+
public virtual int Id { get; set; }
30+
public virtual string Name { get; set; }
31+
public virtual ISet<Entity> Entities { get; set; } = new HashSet<Entity>();
32+
}
33+
}
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
using System.Collections.Generic;
2+
using System.Runtime.CompilerServices;
3+
using NUnit.Framework;
4+
5+
namespace NHibernate.Test.NHSpecificTest.GH3334
6+
{
7+
[TestFixture]
8+
public class Fixture : BugTestCase
9+
{
10+
[OneTimeSetUp]
11+
public void OneTimeSetUp()
12+
{
13+
using (var session = OpenSession())
14+
using (var t = session.BeginTransaction())
15+
{
16+
var parent = new Entity
17+
{
18+
Name = "Parent1",
19+
Children = { new ChildEntity { Name = "Child", Child = new GrandChildEntity { Name = "GrandChild" } } }
20+
};
21+
session.Save(parent);
22+
parent = new Entity
23+
{
24+
Name = "Parent2",
25+
Children = { new ChildEntity { Name = "Child", Child = new GrandChildEntity { Name = "XGrandChild" } } }
26+
};
27+
var other = new OtherEntity { Name = "ABC", Entities = {parent}};
28+
parent.OtherEntity = other;
29+
session.Save(parent);
30+
session.Save(other);
31+
t.Commit();
32+
}
33+
34+
Sfi.Statistics.IsStatisticsEnabled = true;
35+
}
36+
37+
[OneTimeTearDown]
38+
public void OneTimeTearDown()
39+
{
40+
Sfi.Statistics.IsStatisticsEnabled = false;
41+
42+
using var session = OpenSession();
43+
using var transaction = session.BeginTransaction();
44+
45+
session.CreateQuery("delete from ChildEntity").ExecuteUpdate();
46+
session.CreateQuery("delete from GrandChildEntity").ExecuteUpdate();
47+
session.CreateQuery("delete from Entity").ExecuteUpdate();
48+
session.CreateQuery("delete from OtherEntity").ExecuteUpdate();
49+
50+
transaction.Commit();
51+
}
52+
53+
public class TestCase
54+
{
55+
public string Name { get; }
56+
public string Hql { get; }
57+
public int LineNumber { get; }
58+
59+
internal TestCase(string name, string hql, [CallerLineNumber] int lineNumber = 0)
60+
{
61+
Name = name;
62+
Hql = hql;
63+
LineNumber = lineNumber;
64+
}
65+
66+
public override string ToString() => $"{LineNumber:0000}: {Name}";
67+
}
68+
69+
public static IEnumerable<TestCase> GetNoExceptionOnExecuteQueryTestCases()
70+
{
71+
/* does not work because of inner join or theta join created for many-to-one
72+
@"
73+
SELECT ROOT
74+
FROM Entity AS ROOT
75+
WHERE
76+
EXISTS
77+
(FROM ELEMENTS(ROOT.Children) AS child
78+
WHERE
79+
child.Child.Name like 'G%'
80+
OR ROOT.OtherEntity.Name like 'A%'
81+
)");*/
82+
83+
yield return new("Basic Elements case 1 FoundViaGrandChildG", @"
84+
SELECT ROOT
85+
FROM Entity AS ROOT
86+
WHERE
87+
EXISTS
88+
(FROM ELEMENTS(ROOT.Children) AS child
89+
LEFT JOIN child.Child AS grandChild
90+
WHERE
91+
grandChild.Name like 'G%'
92+
)");
93+
yield return new("Basic Elements case 2 FoundViaOtherEntityA", @"
94+
SELECT ROOT
95+
FROM Entity AS ROOT
96+
WHERE
97+
EXISTS
98+
(FROM ELEMENTS(ROOT.OtherEntity) AS otherEntity
99+
WHERE
100+
otherEntity.Name like 'A%'
101+
)");
102+
yield return new("HQL Elements FoundViaGrandChildG", @"
103+
SELECT ROOT
104+
FROM Entity AS ROOT
105+
WHERE
106+
EXISTS
107+
(FROM ELEMENTS(ROOT.Children) AS child
108+
LEFT JOIN child.Child AS grandChild
109+
LEFT JOIN ROOT.OtherEntity AS otherEntity
110+
WHERE
111+
grandChild.Name like 'G%'
112+
OR otherEntity.Name like 'G%'
113+
)");
114+
yield return new("HQL Elements FoundViaOtherEntityA", @"
115+
SELECT ROOT
116+
FROM Entity AS ROOT
117+
WHERE
118+
EXISTS
119+
(FROM ELEMENTS(ROOT.Children) AS child
120+
LEFT JOIN child.Child AS grandChild
121+
LEFT JOIN ROOT.OtherEntity AS otherEntity
122+
WHERE
123+
grandChild.Name like 'A%'
124+
OR otherEntity.Name like 'A%'
125+
)");
126+
yield return new("HQL Entity FoundViaGrandChildG", @"
127+
SELECT ROOT
128+
FROM Entity AS ROOT
129+
WHERE
130+
EXISTS
131+
(FROM ChildEntity AS child
132+
LEFT JOIN child.Child AS grandChild
133+
LEFT JOIN ROOT.OtherEntity AS otherEntity
134+
WHERE
135+
child.Parent = ROOT
136+
AND (
137+
grandChild.Name like 'G%'
138+
OR otherEntity.Name like 'G%'
139+
)
140+
)");
141+
yield return new("HQL Entity FoundViaOtherEntityA", @"
142+
SELECT ROOT
143+
FROM Entity AS ROOT
144+
WHERE
145+
EXISTS
146+
(FROM ChildEntity AS child
147+
LEFT JOIN child.Child AS grandChild
148+
LEFT JOIN ROOT.OtherEntity AS otherEntity
149+
WHERE
150+
child.Parent = ROOT
151+
AND (
152+
grandChild.Name like 'A%'
153+
OR otherEntity.Name like 'A%'
154+
)
155+
)");
156+
}
157+
158+
[Test, TestCaseSource(nameof(GetNoExceptionOnExecuteQueryTestCases))]
159+
public void NoExceptionOnExecuteQuery(TestCase testCase)
160+
{
161+
using var session = OpenSession();
162+
using var _ = session.BeginTransaction();
163+
164+
var q = session.CreateQuery(testCase.Hql);
165+
Assert.AreEqual(1, q.List().Count);
166+
}
167+
168+
protected override bool CheckDatabaseWasCleaned()
169+
{
170+
// same set of objects for each test
171+
return true;
172+
}
173+
}
174+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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.GH3334">
4+
5+
<class name="Entity">
6+
<id name="Id" generator="identity" />
7+
<property name="Name"/>
8+
<set name="Children" cascade="persist,delete,save-update,evict,lock,replicate,delete-orphan">
9+
<key column="Parent" />
10+
<one-to-many class="ChildEntity"/>
11+
</set>
12+
<many-to-one name="OtherEntity" class="OtherEntity"/>
13+
</class>
14+
15+
<class name="ChildEntity">
16+
<id name="Id" generator="identity" />
17+
<many-to-one name="Parent" class="Entity" column="Parent" />
18+
<property name="Name"/>
19+
<many-to-one name="Child" class="GrandChildEntity" cascade="persist,delete,save-update,evict,lock,replicate,delete-orphan"/>
20+
</class>
21+
22+
<class name="GrandChildEntity">
23+
<id name="Id" generator="identity" />
24+
<property name="Name"/>
25+
</class>
26+
27+
<class name="OtherEntity">
28+
<id name="Id" generator="identity" />
29+
<property name="Name"/>
30+
<set name="Entities" inverse="true" lazy="true">
31+
<key column="OtherEntity" />
32+
<one-to-many class="Entity" />
33+
</set>
34+
</class>
35+
36+
</hibernate-mapping>

src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -835,10 +835,10 @@ void CreateFromJoinElement(
835835

836836
if (fromElement.Parent == null)
837837
{
838-
// Most likely means association join is used in invalid context
839-
// I.e. in subquery: from EntityA a where exists (from EntityB join a.Assocation)
840-
// Maybe we should throw exception instead
841-
fromElement.FromClause.AddChild(fromElement);
838+
// happens e.g. on ToMany association join with "EXISTS(FROM ELEMENTS(ROOT.Children))"
839+
// or on an association join in subquery from a parent entity in parent from clause
840+
// like in "from EntityA a where exists (from EntityB join a.Assocation)"
841+
fromElement.FromClause.AppendFromElement(fromElement);
842842
if (fromElement.IsImplied)
843843
fromElement.JoinSequence.SetUseThetaStyle(true);
844844
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public EntityJoinFromElement(FromClause fromClause, IQueryable entityPersister,
1515
string tableAlias = fromClause.AliasGenerator.CreateName(entityPersister.EntityName);
1616

1717
EntityType entityType = (EntityType) entityPersister.Type;
18-
InitializeEntity(fromClause, entityPersister.EntityName, entityPersister, entityType, alias, tableAlias);
18+
InitializeEntity(fromClause, entityPersister.EntityName, entityPersister, entityType, alias, tableAlias, out _);
1919

2020
//NH Specific: hibernate uses special class EntityJoinJoinSequenceImpl
2121
JoinSequence = new JoinSequence(SessionFactoryHelper.Factory) {ForceFilter = true}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -373,8 +373,9 @@ private FromElement FindIntendedAliasedFromElementBasedOnCrazyJPARequirements(st
373373
return null;
374374
}
375375

376-
public void RegisterFromElement(FromElement element)
376+
public void RegisterFromElement(FromElement element, out bool isFirst)
377377
{
378+
isFirst = !_fromElements.Any();
378379
_fromElements.Add(element);
379380
string classAlias = element.ClassAlias;
380381
if (classAlias != null)

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

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
using NHibernate.Persister.Entity;
1111
using NHibernate.SqlCommand;
1212
using NHibernate.Type;
13-
using NHibernate.Util;
1413
using IQueryable = NHibernate.Persister.Entity.IQueryable;
1514

1615
namespace NHibernate.Hql.Ast.ANTLR.Tree
@@ -67,7 +66,7 @@ protected FromElement(FromClause fromClause,FromElement origin,string alias):thi
6766
protected void InitializeComponentJoin(FromElementType elementType)
6867
{
6968
_elementType = elementType;
70-
_fromClause.RegisterFromElement(this);
69+
_fromClause.RegisterFromElement(this, out _);
7170
_initialized = true;
7271
}
7372

@@ -739,7 +738,7 @@ public virtual string GetDisplayText()
739738

740739
public void InitializeCollection(FromClause fromClause, string classAlias, string tableAlias)
741740
{
742-
DoInitialize(fromClause, tableAlias, null, classAlias, null, null, null);
741+
DoInitialize(fromClause, tableAlias, null, classAlias, null, null, null, out _);
743742
_initialized = true;
744743
}
745744

@@ -748,9 +747,10 @@ public void InitializeEntity(FromClause fromClause,
748747
IEntityPersister persister,
749748
EntityType type,
750749
string classAlias,
751-
string tableAlias)
750+
string tableAlias,
751+
out bool isFirstElement)
752752
{
753-
DoInitialize(fromClause, tableAlias, className, classAlias, persister, type, null);
753+
DoInitialize(fromClause, tableAlias, className, classAlias, persister, type, null, out isFirstElement);
754754
_initialized = true;
755755
}
756756

@@ -762,7 +762,7 @@ internal void Initialize(
762762
string tableAlias,
763763
IEntityPersister persister)
764764
{
765-
DoInitialize(fromClause, tableAlias, null, classAlias, persister, type, propertyMapping);
765+
DoInitialize(fromClause, tableAlias, null, classAlias, persister, type, propertyMapping, out _);
766766
_initialized = true;
767767
}
768768

@@ -810,7 +810,7 @@ private void AddDestination(FromElement fromElement)
810810
}
811811

812812
private void DoInitialize(FromClause fromClause, string tableAlias, string className, string classAlias,
813-
IEntityPersister persister, IType type, IPropertyMapping propertyMapping)
813+
IEntityPersister persister, IType type, IPropertyMapping propertyMapping, out bool isFirstElement)
814814
{
815815
if (_initialized)
816816
{
@@ -827,7 +827,7 @@ private void DoInitialize(FromClause fromClause, string tableAlias, string class
827827
}
828828

829829
// Register the FromElement with the FROM clause, now that we have the names and aliases.
830-
fromClause.RegisterFromElement(this);
830+
fromClause.RegisterFromElement(this, out isFirstElement);
831831

832832
if (Log.IsDebugEnabled())
833833
{

0 commit comments

Comments
 (0)