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;