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); }