diff --git a/src/NHibernate.Test/Async/NHSpecificTest/GH3263/ReuseFetchJoinFixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/GH3263/ReuseFetchJoinFixture.cs
new file mode 100644
index 00000000000..9c4a297af9c
--- /dev/null
+++ b/src/NHibernate.Test/Async/NHSpecificTest/GH3263/ReuseFetchJoinFixture.cs
@@ -0,0 +1,99 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by AsyncGenerator.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+
+using System.Linq;
+using NHibernate.Linq;
+using NUnit.Framework;
+
+namespace NHibernate.Test.NHSpecificTest.GH3263
+{
+ using System.Threading.Tasks;
+ [TestFixture]
+ public class ReuseFetchJoinFixtureAsync : BugTestCase
+ {
+ protected override void OnSetUp()
+ {
+ using var s = OpenSession();
+ using var t = s.BeginTransaction();
+ var em = new Employee() { Name = "x", OptionalInfo = new OptionalInfo() };
+ em.OptionalInfo.Employee = em;
+ s.Save(em);
+ t.Commit();
+ }
+ protected override void OnTearDown()
+ {
+ using var session = OpenSession();
+ using var transaction = session.BeginTransaction();
+ session.CreateQuery("delete from System.Object").ExecuteUpdate();
+
+ transaction.Commit();
+ }
+
+ [Test]
+ public async Task ReuseJoinScalarSelectAsync()
+ {
+ using var session = OpenSession();
+ await (session.Query()
+ .Fetch(x => x.OptionalInfo)
+ .Where(x => x.OptionalInfo != null)
+ .Select(x => new { x.OptionalInfo.Age })
+ .ToListAsync());
+ }
+
+ [Test]
+ public async Task ReuseJoinScalarSelectHqlAsync()
+ {
+ using var session = OpenSession();
+ await (session.CreateQuery(
+ "select x.OptionalInfo.Age " +
+ "from Employee x " +
+ "fetch x.OptionalInfo " +
+ "where x.OptionalInfo != null ").ListAsync());
+
+ }
+
+ [Test]
+ public async Task ReuseJoinScalarSelectHql2Async()
+ {
+ using var session = OpenSession();
+ await (session.CreateQuery(
+ "select x.OptionalInfo.Age " +
+ "from Employee x " +
+ "join fetch x.OptionalInfo o " +
+ "where o != null ").ListAsync());
+ }
+
+ [Test]
+ public async Task ReuseJoinScalarSelectHql3Async()
+ {
+ using var session = OpenSession();
+ await (session.CreateQuery(
+ "select x.OptionalInfo.Age from Employee x " +
+ "join fetch x.OptionalInfo " +
+ "where x.OptionalInfo != null ").ListAsync());
+ }
+
+ [Test]
+ public async Task ReuseJoinEntityAndScalarSelectAsync()
+ {
+ using var session = OpenSession();
+ using var sqlLog = new SqlLogSpy();
+
+ var x = await (session.Query()
+ .Fetch(x => x.OptionalInfo)
+ .Where(x => x.OptionalInfo != null)
+ .Select(x => new { x, x.OptionalInfo.Age })
+ .FirstAsync());
+
+ Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1));
+ Assert.That(NHibernateUtil.IsInitialized(x.x.OptionalInfo), Is.True);
+ }
+ }
+}
diff --git a/src/NHibernate.Test/NHSpecificTest/GH3263/Entity.cs b/src/NHibernate.Test/NHSpecificTest/GH3263/Entity.cs
new file mode 100644
index 00000000000..5ab30425878
--- /dev/null
+++ b/src/NHibernate.Test/NHSpecificTest/GH3263/Entity.cs
@@ -0,0 +1,16 @@
+namespace NHibernate.Test.NHSpecificTest.GH3263
+{
+ public class Employee
+ {
+ public virtual int EmployeeId { get; set; }
+ public virtual string Name { get; set; }
+ public virtual OptionalInfo OptionalInfo { get; set; }
+ }
+
+ public class OptionalInfo
+ {
+ public virtual int EmployeeId { get; set; }
+ public virtual int Age { get; set; }
+ public virtual Employee Employee { get; set; }
+ }
+}
diff --git a/src/NHibernate.Test/NHSpecificTest/GH3263/Mappings.hbm.xml b/src/NHibernate.Test/NHSpecificTest/GH3263/Mappings.hbm.xml
new file mode 100644
index 00000000000..371f348a5e9
--- /dev/null
+++ b/src/NHibernate.Test/NHSpecificTest/GH3263/Mappings.hbm.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Employee
+
+
+
+
+
+
+
+
diff --git a/src/NHibernate.Test/NHSpecificTest/GH3263/ReuseFetchJoinFixture.cs b/src/NHibernate.Test/NHSpecificTest/GH3263/ReuseFetchJoinFixture.cs
new file mode 100644
index 00000000000..88ee2b54bc6
--- /dev/null
+++ b/src/NHibernate.Test/NHSpecificTest/GH3263/ReuseFetchJoinFixture.cs
@@ -0,0 +1,88 @@
+using System.Linq;
+using NHibernate.Linq;
+using NUnit.Framework;
+
+namespace NHibernate.Test.NHSpecificTest.GH3263
+{
+ [TestFixture]
+ public class ReuseFetchJoinFixture : BugTestCase
+ {
+ protected override void OnSetUp()
+ {
+ using var s = OpenSession();
+ using var t = s.BeginTransaction();
+ var em = new Employee() { Name = "x", OptionalInfo = new OptionalInfo() };
+ em.OptionalInfo.Employee = em;
+ s.Save(em);
+ t.Commit();
+ }
+ protected override void OnTearDown()
+ {
+ using var session = OpenSession();
+ using var transaction = session.BeginTransaction();
+ session.CreateQuery("delete from System.Object").ExecuteUpdate();
+
+ transaction.Commit();
+ }
+
+ [Test]
+ public void ReuseJoinScalarSelect()
+ {
+ using var session = OpenSession();
+ session.Query()
+ .Fetch(x => x.OptionalInfo)
+ .Where(x => x.OptionalInfo != null)
+ .Select(x => new { x.OptionalInfo.Age })
+ .ToList();
+ }
+
+ [Test]
+ public void ReuseJoinScalarSelectHql()
+ {
+ using var session = OpenSession();
+ session.CreateQuery(
+ "select x.OptionalInfo.Age " +
+ "from Employee x " +
+ "fetch x.OptionalInfo " +
+ "where x.OptionalInfo != null ").List();
+
+ }
+
+ [Test]
+ public void ReuseJoinScalarSelectHql2()
+ {
+ using var session = OpenSession();
+ session.CreateQuery(
+ "select x.OptionalInfo.Age " +
+ "from Employee x " +
+ "join fetch x.OptionalInfo o " +
+ "where o != null ").List();
+ }
+
+ [Test]
+ public void ReuseJoinScalarSelectHql3()
+ {
+ using var session = OpenSession();
+ session.CreateQuery(
+ "select x.OptionalInfo.Age from Employee x " +
+ "join fetch x.OptionalInfo " +
+ "where x.OptionalInfo != null ").List();
+ }
+
+ [Test]
+ public void ReuseJoinEntityAndScalarSelect()
+ {
+ using var session = OpenSession();
+ using var sqlLog = new SqlLogSpy();
+
+ var x = session.Query()
+ .Fetch(x => x.OptionalInfo)
+ .Where(x => x.OptionalInfo != null)
+ .Select(x => new { x, x.OptionalInfo.Age })
+ .First();
+
+ Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1));
+ Assert.That(NHibernateUtil.IsInitialized(x.x.OptionalInfo), Is.True);
+ }
+ }
+}
diff --git a/src/NHibernate/Hql/Ast/ANTLR/Tree/DotNode.cs b/src/NHibernate/Hql/Ast/ANTLR/Tree/DotNode.cs
index 2bbfeb03e7d..7e486d27401 100644
--- a/src/NHibernate/Hql/Ast/ANTLR/Tree/DotNode.cs
+++ b/src/NHibernate/Hql/Ast/ANTLR/Tree/DotNode.cs
@@ -558,6 +558,8 @@ private void DereferenceEntityJoin(string classAlias, EntityType propertyType, b
{
elem.JoinSequence.SetJoinType(_joinType);
}
+
+ elem.ReusedJoin = true;
currentFromClause.AddDuplicateAlias(classAlias, elem);
}
diff --git a/src/NHibernate/Hql/Ast/ANTLR/Tree/FromElement.cs b/src/NHibernate/Hql/Ast/ANTLR/Tree/FromElement.cs
index f724e40e9e0..79d42962748 100644
--- a/src/NHibernate/Hql/Ast/ANTLR/Tree/FromElement.cs
+++ b/src/NHibernate/Hql/Ast/ANTLR/Tree/FromElement.cs
@@ -631,6 +631,7 @@ internal virtual string[] GetIdentityColumns(string alias)
}
internal bool UseTableAliases => Walker.StatementType == HqlSqlWalker.SELECT || Walker.IsSubQuery;
+ internal bool ReusedJoin { get; set; }
public void HandlePropertyBeingDereferenced(IType propertySource, string propertyName)
{
diff --git a/src/NHibernate/Hql/Ast/ANTLR/Tree/SelectClause.cs b/src/NHibernate/Hql/Ast/ANTLR/Tree/SelectClause.cs
index 616fdb37ba3..6119f5642e7 100644
--- a/src/NHibernate/Hql/Ast/ANTLR/Tree/SelectClause.cs
+++ b/src/NHibernate/Hql/Ast/ANTLR/Tree/SelectClause.cs
@@ -245,7 +245,7 @@ private List GetFetchedFromElements(FromClause fromClause)
// throw new QueryException(string.Format(JoinFetchWithoutOwnerExceptionMsg, fromElement.GetDisplayText()));
//throw away the fromElement. It's clearly redundant.
- if (fromElement.FromClause == fromClause)
+ if (fromElement.FromClause == fromClause && !fromElement.ReusedJoin)
{
fromElement.Parent.RemoveChild(fromElement);
}