diff --git a/src/NHibernate.Test/Async/NHSpecificTest/GH3334/Fixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/GH3334/Fixture.cs new file mode 100644 index 00000000000..8978ae20a39 --- /dev/null +++ b/src/NHibernate.Test/Async/NHSpecificTest/GH3334/Fixture.cs @@ -0,0 +1,185 @@ +//------------------------------------------------------------------------------ +// +// 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.Collections.Generic; +using System.Runtime.CompilerServices; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.GH3334 +{ + using System.Threading.Tasks; + [TestFixture] + public class FixtureAsync : BugTestCase + { + [OneTimeSetUp] + public void OneTimeSetUp() + { + using (var session = OpenSession()) + using (var t = session.BeginTransaction()) + { + var parent = new Entity + { + Name = "Parent1", + Children = { new ChildEntity { Name = "Child", Child = new GrandChildEntity { Name = "GrandChild" } } } + }; + session.Save(parent); + parent = new Entity + { + Name = "Parent2", + Children = { new ChildEntity { Name = "Child", Child = new GrandChildEntity { Name = "XGrandChild" } } } + }; + var other = new OtherEntity { Name = "ABC", Entities = {parent}}; + parent.OtherEntity = other; + session.Save(parent); + session.Save(other); + t.Commit(); + } + + Sfi.Statistics.IsStatisticsEnabled = true; + } + + [OneTimeTearDown] + public void OneTimeTearDown() + { + Sfi.Statistics.IsStatisticsEnabled = false; + + using var session = OpenSession(); + using var transaction = session.BeginTransaction(); + + session.CreateQuery("delete from ChildEntity").ExecuteUpdate(); + session.CreateQuery("delete from GrandChildEntity").ExecuteUpdate(); + session.CreateQuery("delete from Entity").ExecuteUpdate(); + session.CreateQuery("delete from OtherEntity").ExecuteUpdate(); + + transaction.Commit(); + } + + public class TestCaseItem + { + public string Name { get; } + public string Hql { get; } + public int LineNumber { get; } + + public TestCaseItem(string name, string hql, [CallerLineNumber] int lineNumber = 0) + { + Name = name; + Hql = hql; + LineNumber = lineNumber; + } + + public override string ToString() => $"{LineNumber:0000}: {Name}"; + } + + public static IEnumerable GetNoExceptionOnExecuteQueryTestCases() + { + /* does not work because of inner join or theta join created for many-to-one + @" + SELECT ROOT + FROM Entity AS ROOT + WHERE + EXISTS + (FROM ELEMENTS(ROOT.Children) AS child + WHERE + child.Child.Name like 'G%' + OR ROOT.OtherEntity.Name like 'A%' + )");*/ + + yield return new("Basic Elements case 1 FoundViaGrandChildG", @" + SELECT ROOT + FROM Entity AS ROOT + WHERE + EXISTS + (FROM ELEMENTS(ROOT.Children) AS child + LEFT JOIN child.Child AS grandChild + WHERE + grandChild.Name like 'G%' + )"); + yield return new("Basic Elements case 2 FoundViaOtherEntityA", @" + SELECT ROOT + FROM Entity AS ROOT + WHERE + EXISTS + (FROM ELEMENTS(ROOT.OtherEntity) AS otherEntity + WHERE + otherEntity.Name like 'A%' + )"); + yield return new("HQL Elements FoundViaGrandChildG", @" + SELECT ROOT + FROM Entity AS ROOT + WHERE + EXISTS + (FROM ELEMENTS(ROOT.Children) AS child + LEFT JOIN child.Child AS grandChild + LEFT JOIN ROOT.OtherEntity AS otherEntity + WHERE + grandChild.Name like 'G%' + OR otherEntity.Name like 'G%' + )"); + yield return new("HQL Elements FoundViaOtherEntityA", @" + SELECT ROOT + FROM Entity AS ROOT + WHERE + EXISTS + (FROM ELEMENTS(ROOT.Children) AS child + LEFT JOIN child.Child AS grandChild + LEFT JOIN ROOT.OtherEntity AS otherEntity + WHERE + grandChild.Name like 'A%' + OR otherEntity.Name like 'A%' + )"); + yield return new("HQL Entity FoundViaGrandChildG", @" + SELECT ROOT + FROM Entity AS ROOT + WHERE + EXISTS + (FROM ChildEntity AS child + LEFT JOIN child.Child AS grandChild + LEFT JOIN ROOT.OtherEntity AS otherEntity + WHERE + child.Parent = ROOT + AND ( + grandChild.Name like 'G%' + OR otherEntity.Name like 'G%' + ) + )"); + yield return new("HQL Entity FoundViaOtherEntityA", @" + SELECT ROOT + FROM Entity AS ROOT + WHERE + EXISTS + (FROM ChildEntity AS child + LEFT JOIN child.Child AS grandChild + LEFT JOIN ROOT.OtherEntity AS otherEntity + WHERE + child.Parent = ROOT + AND ( + grandChild.Name like 'A%' + OR otherEntity.Name like 'A%' + ) + )"); + } + + [Test, TestCaseSource(nameof(GetNoExceptionOnExecuteQueryTestCases))] + public async Task NoExceptionOnExecuteQueryAsync(TestCaseItem testCase) + { + using var session = OpenSession(); + using var _ = session.BeginTransaction(); + + var q = session.CreateQuery(testCase.Hql); + Assert.That(await (q.ListAsync()), Has.Count.EqualTo(1)); + } + + protected override bool CheckDatabaseWasCleaned() + { + // same set of objects for each test + return true; + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH3334/Entity.cs b/src/NHibernate.Test/NHSpecificTest/GH3334/Entity.cs new file mode 100644 index 00000000000..1203e6d8eb9 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH3334/Entity.cs @@ -0,0 +1,33 @@ +using System.Collections.Generic; + +namespace NHibernate.Test.NHSpecificTest.GH3334 +{ + public class Entity + { + public virtual int Id { get; set; } + public virtual string Name { get; set; } + public virtual ISet Children { get; set; } = new HashSet(); + public virtual OtherEntity OtherEntity { get; set; } + } + + public class ChildEntity + { + public virtual int Id { get; set; } + public virtual Entity Parent { get; set; } + public virtual string Name { get; set; } + public virtual GrandChildEntity Child { get; set; } + } + + public class GrandChildEntity + { + public virtual int Id { get; set; } + public virtual string Name { get; set; } + } + + public class OtherEntity + { + public virtual int Id { get; set; } + public virtual string Name { get; set; } + public virtual ISet Entities { get; set; } = new HashSet(); + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH3334/Fixture.cs b/src/NHibernate.Test/NHSpecificTest/GH3334/Fixture.cs new file mode 100644 index 00000000000..aed5ab062ef --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH3334/Fixture.cs @@ -0,0 +1,174 @@ +using System.Collections.Generic; +using System.Runtime.CompilerServices; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.GH3334 +{ + [TestFixture] + public class Fixture : BugTestCase + { + [OneTimeSetUp] + public void OneTimeSetUp() + { + using (var session = OpenSession()) + using (var t = session.BeginTransaction()) + { + var parent = new Entity + { + Name = "Parent1", + Children = { new ChildEntity { Name = "Child", Child = new GrandChildEntity { Name = "GrandChild" } } } + }; + session.Save(parent); + parent = new Entity + { + Name = "Parent2", + Children = { new ChildEntity { Name = "Child", Child = new GrandChildEntity { Name = "XGrandChild" } } } + }; + var other = new OtherEntity { Name = "ABC", Entities = {parent}}; + parent.OtherEntity = other; + session.Save(parent); + session.Save(other); + t.Commit(); + } + + Sfi.Statistics.IsStatisticsEnabled = true; + } + + [OneTimeTearDown] + public void OneTimeTearDown() + { + Sfi.Statistics.IsStatisticsEnabled = false; + + using var session = OpenSession(); + using var transaction = session.BeginTransaction(); + + session.CreateQuery("delete from ChildEntity").ExecuteUpdate(); + session.CreateQuery("delete from GrandChildEntity").ExecuteUpdate(); + session.CreateQuery("delete from Entity").ExecuteUpdate(); + session.CreateQuery("delete from OtherEntity").ExecuteUpdate(); + + transaction.Commit(); + } + + public class TestCaseItem + { + public string Name { get; } + public string Hql { get; } + public int LineNumber { get; } + + public TestCaseItem(string name, string hql, [CallerLineNumber] int lineNumber = 0) + { + Name = name; + Hql = hql; + LineNumber = lineNumber; + } + + public override string ToString() => $"{LineNumber:0000}: {Name}"; + } + + public static IEnumerable GetNoExceptionOnExecuteQueryTestCases() + { + /* does not work because of inner join or theta join created for many-to-one + @" + SELECT ROOT + FROM Entity AS ROOT + WHERE + EXISTS + (FROM ELEMENTS(ROOT.Children) AS child + WHERE + child.Child.Name like 'G%' + OR ROOT.OtherEntity.Name like 'A%' + )");*/ + + yield return new("Basic Elements case 1 FoundViaGrandChildG", @" + SELECT ROOT + FROM Entity AS ROOT + WHERE + EXISTS + (FROM ELEMENTS(ROOT.Children) AS child + LEFT JOIN child.Child AS grandChild + WHERE + grandChild.Name like 'G%' + )"); + yield return new("Basic Elements case 2 FoundViaOtherEntityA", @" + SELECT ROOT + FROM Entity AS ROOT + WHERE + EXISTS + (FROM ELEMENTS(ROOT.OtherEntity) AS otherEntity + WHERE + otherEntity.Name like 'A%' + )"); + yield return new("HQL Elements FoundViaGrandChildG", @" + SELECT ROOT + FROM Entity AS ROOT + WHERE + EXISTS + (FROM ELEMENTS(ROOT.Children) AS child + LEFT JOIN child.Child AS grandChild + LEFT JOIN ROOT.OtherEntity AS otherEntity + WHERE + grandChild.Name like 'G%' + OR otherEntity.Name like 'G%' + )"); + yield return new("HQL Elements FoundViaOtherEntityA", @" + SELECT ROOT + FROM Entity AS ROOT + WHERE + EXISTS + (FROM ELEMENTS(ROOT.Children) AS child + LEFT JOIN child.Child AS grandChild + LEFT JOIN ROOT.OtherEntity AS otherEntity + WHERE + grandChild.Name like 'A%' + OR otherEntity.Name like 'A%' + )"); + yield return new("HQL Entity FoundViaGrandChildG", @" + SELECT ROOT + FROM Entity AS ROOT + WHERE + EXISTS + (FROM ChildEntity AS child + LEFT JOIN child.Child AS grandChild + LEFT JOIN ROOT.OtherEntity AS otherEntity + WHERE + child.Parent = ROOT + AND ( + grandChild.Name like 'G%' + OR otherEntity.Name like 'G%' + ) + )"); + yield return new("HQL Entity FoundViaOtherEntityA", @" + SELECT ROOT + FROM Entity AS ROOT + WHERE + EXISTS + (FROM ChildEntity AS child + LEFT JOIN child.Child AS grandChild + LEFT JOIN ROOT.OtherEntity AS otherEntity + WHERE + child.Parent = ROOT + AND ( + grandChild.Name like 'A%' + OR otherEntity.Name like 'A%' + ) + )"); + } + + [Test, TestCaseSource(nameof(GetNoExceptionOnExecuteQueryTestCases))] + public void NoExceptionOnExecuteQuery(TestCaseItem testCase) + { + using var session = OpenSession(); + using var _ = session.BeginTransaction(); + + var q = session.CreateQuery(testCase.Hql); + Assert.That(q.List(), Has.Count.EqualTo(1)); + } + + protected override bool CheckDatabaseWasCleaned() + { + // same set of objects for each test + return true; + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH3334/Mappings.hbm.xml b/src/NHibernate.Test/NHSpecificTest/GH3334/Mappings.hbm.xml new file mode 100644 index 00000000000..1f5bcdfe8e6 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH3334/Mappings.hbm.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs b/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs index 7cc0bcae9c6..3ff11867273 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs @@ -835,10 +835,10 @@ void CreateFromJoinElement( if (fromElement.Parent == null) { - // Most likely means association join is used in invalid context - // I.e. in subquery: from EntityA a where exists (from EntityB join a.Assocation) - // Maybe we should throw exception instead - fromElement.FromClause.AddChild(fromElement); + // happens e.g. on ToMany association join with "EXISTS(FROM ELEMENTS(ROOT.Children))" + // or on an association join in subquery from a parent entity in parent from clause + // like in "from EntityA a where exists (from EntityB join a.Assocation)" + fromElement.FromClause.AppendFromElement(fromElement); if (fromElement.IsImplied) fromElement.JoinSequence.SetUseThetaStyle(true); } diff --git a/src/NHibernate/Hql/Ast/ANTLR/Tree/FromClause.cs b/src/NHibernate/Hql/Ast/ANTLR/Tree/FromClause.cs index 39179f1419a..1fbc18a3d32 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/Tree/FromClause.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/Tree/FromClause.cs @@ -288,6 +288,8 @@ public bool IsSubQuery internal bool IsJoinSubQuery { get; set; } + internal bool HasRegisteredFromElements => _fromElements.Any(); + public string GetDisplayText() { return "FromClause{" + diff --git a/src/NHibernate/Hql/Ast/ANTLR/Tree/FromElementFactory.cs b/src/NHibernate/Hql/Ast/ANTLR/Tree/FromElementFactory.cs index 4f80d774f43..3077d5e3af9 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/Tree/FromElementFactory.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/Tree/FromElementFactory.cs @@ -291,6 +291,7 @@ public FromElement CreateEntityJoin( bool inFrom, EntityType type) { + var isFirstFromElement = !_fromClause.HasRegisteredFromElements; FromElement elem = CreateJoin(entityClass, tableAlias, joinSequence, type, false); elem.Fetch = fetchFlag; @@ -321,7 +322,7 @@ public FromElement CreateEntityJoin( // 1) 'elem' is the "root from-element" in correlated subqueries // 2) The DotNode.useThetaStyleImplicitJoins has been set to true // and 'elem' represents an implicit join - if (elem.FromClause != elem.Origin.FromClause || DotNode.UseThetaStyleImplicitJoins) + if (isFirstFromElement && elem.FromClause != elem.Origin.FromClause || DotNode.UseThetaStyleImplicitJoins) { // the "root from-element" in correlated subqueries do need this piece elem.Type = HqlSqlWalker.FROM_FRAGMENT;