diff --git a/src/NHibernate.Test/Async/Hql/EntityJoinHqlTest.cs b/src/NHibernate.Test/Async/Hql/EntityJoinHqlTest.cs new file mode 100644 index 00000000000..c1bb4926d0d --- /dev/null +++ b/src/NHibernate.Test/Async/Hql/EntityJoinHqlTest.cs @@ -0,0 +1,294 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using NHibernate.Cfg.MappingSchema; +using NHibernate.Mapping.ByCode; +using NHibernate.Test.Hql.EntityJoinHqlTestEntities; +using NUnit.Framework; + +namespace NHibernate.Test.Hql +{ + using System.Threading.Tasks; + /// + /// Tests for explicit entity joins (not associated entities) + /// + [TestFixture] + public class EntityJoinHqlTestAsync : TestCaseMappingByCode + { + private const string _customEntityName = "CustomEntityName"; + private EntityWithCompositeId _entityWithCompositeId; + private EntityWithNoAssociation _noAssociation; + private EntityCustomEntityName _entityWithCustomEntityName; + + [Test] + public async Task CanJoinNotAssociatedEntityAsync() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityComplex entityComplex = + await (session + .CreateQuery("select ex " + + "from EntityWithNoAssociation root " + + "left join EntityComplex ex with root.Complex1Id = ex.Id") + .SetMaxResults(1) + .UniqueResultAsync()); + + Assert.That(entityComplex, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(entityComplex), Is.True); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + [Test] + public async Task EntityJoinForCompositeKeyAsync() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + await (session.CreateQuery( + "select root, ejComposite from EntityWithNoAssociation root " + + "inner join EntityWithCompositeId ejComposite " + + " with (root.Composite1Key1 = ejComposite.Key.Id1 and root.Composite1Key2 = ejComposite.Key.Id2)") + .SetMaxResults(1).ListAsync()); + + var composite = await (session.LoadAsync(_entityWithCompositeId.Key)); + + Assert.That(composite, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(composite), Is.True, "Object must be initialized"); + Assert.That(composite, Is.EqualTo(_entityWithCompositeId).Using((EntityWithCompositeId x, EntityWithCompositeId y) => (Equals(x.Key, y.Key) && Equals(x.Name, y.Name)) ? 0 : 1)); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + [Test] + public async Task NullLeftEntityJoinAsync() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + var objs =await (session.CreateQuery( + "select ejLeftNull, root " + + "from EntityWithNoAssociation root " + + "left join EntityComplex ejLeftNull with ejLeftNull.Id is null") + .SetMaxResults(1).UniqueResultAsync()); + EntityComplex ejLeftNull = (EntityComplex)objs[0]; + EntityWithNoAssociation root = (EntityWithNoAssociation) objs[1]; + + Assert.That(root, Is.Not.Null, "root should not be null (looks like left join didn't work)"); + Assert.That(NHibernateUtil.IsInitialized(root), Is.True); + Assert.That(ejLeftNull, Is.Null); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + [Test] + public async Task EntityJoinForCustomEntityNameAsync() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityCustomEntityName ejCustomEntity = await (session.CreateQuery( + $"select ejCustomEntity from EntityWithNoAssociation root inner join {_customEntityName} ejCustomEntity with ejCustomEntity.Id = root.CustomEntityNameId") + .SetMaxResults(1).UniqueResultAsync()); + + Assert.That(ejCustomEntity, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(ejCustomEntity), Is.True); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + [Test] + public async Task EntityJoinFoSubqueryAsync() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityComplex ej = null; + EntityWithNoAssociation root = null; + + var subquery = "from EntityWithNoAssociation rootSub " + + "inner join EntityComplex ejSub with rootSub.Complex1Id = ejSub.Id " + + "where ejSub.Name = ej.Name"; + var objs = await (session.CreateQuery( + "select root, ej from EntityWithNoAssociation root " + + "inner join EntityComplex ej with root.Complex1Id = ej.Id " + + $"where exists ({subquery})") + .UniqueResultAsync()); + root = (EntityWithNoAssociation) objs[0]; + ej = (EntityComplex)objs[1]; + + Assert.That(NHibernateUtil.IsInitialized(root), Is.True); + Assert.That(ej, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(ej), Is.True); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + [Test, Ignore("Failing for unrelated reasons")] + public async Task CrossJoinAndWithClauseAsync() + { + //This is about complex theta style join fix that was implemented in hibernate along with entity join functionality + //https://hibernate.atlassian.net/browse/HHH-7321 + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + await (session.CreateQuery( + "SELECT s " + + "FROM EntityComplex s, EntityComplex q " + + "LEFT JOIN s.SameTypeChild AS sa WITH sa.SameTypeChild.Id = q.SameTypeChild.Id" + ).ListAsync()); + } + } + + #region Test Setup + + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class( + rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + + rc.Version(ep => ep.Version, vm => { }); + + rc.Property(x => x.Name); + + rc.Property(ep => ep.LazyProp, m => m.Lazy(true)); + + rc.ManyToOne(ep => ep.SameTypeChild, m => m.Column("SameTypeChildId")); + + + }); + + + mapper.Class( + rc => + { + rc.ComponentAsId( + e => e.Key, + ekm => + { + ekm.Property(ek => ek.Id1); + ekm.Property(ek => ek.Id2); + }); + + rc.Property(e => e.Name); + }); + + mapper.Class( + rc => + { + rc.ComponentAsId( + e => e.Key, + ekm => + { + ekm.Property(ek => ek.Id1); + ekm.Property(ek => ek.Id2); + }); + + rc.Property(e => e.Name); + }); + + mapper.Class( + rc => + { + rc.Id(e => e.Id, m => m.Generator(Generators.GuidComb)); + + rc.Property(e => e.Complex1Id); + rc.Property(e => e.Complex2Id); + rc.Property(e => e.Simple1Id); + rc.Property(e => e.Simple2Id); + rc.Property(e => e.Composite1Key1); + rc.Property(e => e.Composite1Key2); + rc.Property(e => e.CustomEntityNameId); + + }); + + mapper.Class( + rc => + { + rc.EntityName(_customEntityName); + + rc.Id(e => e.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(e => e.Name); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + + protected override void OnTearDown() + { + using (ISession session = OpenSession()) + using (ITransaction transaction = session.BeginTransaction()) + { + session.Delete("from System.Object"); + + session.Flush(); + transaction.Commit(); + } + } + + protected override void OnSetUp() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + var parent = new EntityComplex + { + Name = "ComplexEnityParent", + LazyProp = "SomeBigValue", + SameTypeChild = new EntityComplex() + { + Name = "ComplexEntityChild" + } + }; + + _entityWithCompositeId = new EntityWithCompositeId + { + Key = new CompositeKey + { + Id1 = 1, + Id2 = 2 + }, + Name = "Composite" + }; + + session.Save(parent.SameTypeChild); + session.Save(parent); + session.Save(_entityWithCompositeId); + + _entityWithCustomEntityName = new EntityCustomEntityName() + { + Name = "EntityCustomEntityName" + }; + + session.Save(_customEntityName, _entityWithCustomEntityName); + + _noAssociation = new EntityWithNoAssociation() + { + Complex1Id = parent.Id, + Complex2Id = parent.SameTypeChild.Id, + Composite1Key1 = _entityWithCompositeId.Key.Id1, + Composite1Key2 = _entityWithCompositeId.Key.Id2, + CustomEntityNameId = _entityWithCustomEntityName.Id + }; + + session.Save(_noAssociation); + + session.Flush(); + transaction.Commit(); + } + } + + #endregion Test Setup + } +} diff --git a/src/NHibernate.Test/Hql/EntityJoinHqlTest.cs b/src/NHibernate.Test/Hql/EntityJoinHqlTest.cs new file mode 100644 index 00000000000..a80cc13f6fd --- /dev/null +++ b/src/NHibernate.Test/Hql/EntityJoinHqlTest.cs @@ -0,0 +1,283 @@ +using NHibernate.Cfg.MappingSchema; +using NHibernate.Mapping.ByCode; +using NHibernate.Test.Hql.EntityJoinHqlTestEntities; +using NUnit.Framework; + +namespace NHibernate.Test.Hql +{ + /// + /// Tests for explicit entity joins (not associated entities) + /// + [TestFixture] + public class EntityJoinHqlTest : TestCaseMappingByCode + { + private const string _customEntityName = "CustomEntityName"; + private EntityWithCompositeId _entityWithCompositeId; + private EntityWithNoAssociation _noAssociation; + private EntityCustomEntityName _entityWithCustomEntityName; + + [Test] + public void CanJoinNotAssociatedEntity() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityComplex entityComplex = + session + .CreateQuery("select ex " + + "from EntityWithNoAssociation root " + + "left join EntityComplex ex with root.Complex1Id = ex.Id") + .SetMaxResults(1) + .UniqueResult(); + + Assert.That(entityComplex, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(entityComplex), Is.True); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + [Test] + public void EntityJoinForCompositeKey() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + session.CreateQuery( + "select root, ejComposite from EntityWithNoAssociation root " + + "inner join EntityWithCompositeId ejComposite " + + " with (root.Composite1Key1 = ejComposite.Key.Id1 and root.Composite1Key2 = ejComposite.Key.Id2)") + .SetMaxResults(1).List(); + + var composite = session.Load(_entityWithCompositeId.Key); + + Assert.That(composite, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(composite), Is.True, "Object must be initialized"); + Assert.That(composite, Is.EqualTo(_entityWithCompositeId).Using((EntityWithCompositeId x, EntityWithCompositeId y) => (Equals(x.Key, y.Key) && Equals(x.Name, y.Name)) ? 0 : 1)); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + [Test] + public void NullLeftEntityJoin() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + var objs =session.CreateQuery( + "select ejLeftNull, root " + + "from EntityWithNoAssociation root " + + "left join EntityComplex ejLeftNull with ejLeftNull.Id is null") + .SetMaxResults(1).UniqueResult(); + EntityComplex ejLeftNull = (EntityComplex)objs[0]; + EntityWithNoAssociation root = (EntityWithNoAssociation) objs[1]; + + Assert.That(root, Is.Not.Null, "root should not be null (looks like left join didn't work)"); + Assert.That(NHibernateUtil.IsInitialized(root), Is.True); + Assert.That(ejLeftNull, Is.Null); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + [Test] + public void EntityJoinForCustomEntityName() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityCustomEntityName ejCustomEntity = session.CreateQuery( + $"select ejCustomEntity from EntityWithNoAssociation root inner join {_customEntityName} ejCustomEntity with ejCustomEntity.Id = root.CustomEntityNameId") + .SetMaxResults(1).UniqueResult(); + + Assert.That(ejCustomEntity, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(ejCustomEntity), Is.True); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + [Test] + public void EntityJoinFoSubquery() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + EntityComplex ej = null; + EntityWithNoAssociation root = null; + + var subquery = "from EntityWithNoAssociation rootSub " + + "inner join EntityComplex ejSub with rootSub.Complex1Id = ejSub.Id " + + "where ejSub.Name = ej.Name"; + var objs = session.CreateQuery( + "select root, ej from EntityWithNoAssociation root " + + "inner join EntityComplex ej with root.Complex1Id = ej.Id " + + $"where exists ({subquery})") + .UniqueResult(); + root = (EntityWithNoAssociation) objs[0]; + ej = (EntityComplex)objs[1]; + + Assert.That(NHibernateUtil.IsInitialized(root), Is.True); + Assert.That(ej, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(ej), Is.True); + Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + } + } + + [Test, Ignore("Failing for unrelated reasons")] + public void CrossJoinAndWithClause() + { + //This is about complex theta style join fix that was implemented in hibernate along with entity join functionality + //https://hibernate.atlassian.net/browse/HHH-7321 + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + session.CreateQuery( + "SELECT s " + + "FROM EntityComplex s, EntityComplex q " + + "LEFT JOIN s.SameTypeChild AS sa WITH sa.SameTypeChild.Id = q.SameTypeChild.Id" + ).List(); + } + } + + #region Test Setup + + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class( + rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + + rc.Version(ep => ep.Version, vm => { }); + + rc.Property(x => x.Name); + + rc.Property(ep => ep.LazyProp, m => m.Lazy(true)); + + rc.ManyToOne(ep => ep.SameTypeChild, m => m.Column("SameTypeChildId")); + + + }); + + + mapper.Class( + rc => + { + rc.ComponentAsId( + e => e.Key, + ekm => + { + ekm.Property(ek => ek.Id1); + ekm.Property(ek => ek.Id2); + }); + + rc.Property(e => e.Name); + }); + + mapper.Class( + rc => + { + rc.ComponentAsId( + e => e.Key, + ekm => + { + ekm.Property(ek => ek.Id1); + ekm.Property(ek => ek.Id2); + }); + + rc.Property(e => e.Name); + }); + + mapper.Class( + rc => + { + rc.Id(e => e.Id, m => m.Generator(Generators.GuidComb)); + + rc.Property(e => e.Complex1Id); + rc.Property(e => e.Complex2Id); + rc.Property(e => e.Simple1Id); + rc.Property(e => e.Simple2Id); + rc.Property(e => e.Composite1Key1); + rc.Property(e => e.Composite1Key2); + rc.Property(e => e.CustomEntityNameId); + + }); + + mapper.Class( + rc => + { + rc.EntityName(_customEntityName); + + rc.Id(e => e.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(e => e.Name); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + + protected override void OnTearDown() + { + using (ISession session = OpenSession()) + using (ITransaction transaction = session.BeginTransaction()) + { + session.Delete("from System.Object"); + + session.Flush(); + transaction.Commit(); + } + } + + protected override void OnSetUp() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + var parent = new EntityComplex + { + Name = "ComplexEnityParent", + LazyProp = "SomeBigValue", + SameTypeChild = new EntityComplex() + { + Name = "ComplexEntityChild" + } + }; + + _entityWithCompositeId = new EntityWithCompositeId + { + Key = new CompositeKey + { + Id1 = 1, + Id2 = 2 + }, + Name = "Composite" + }; + + session.Save(parent.SameTypeChild); + session.Save(parent); + session.Save(_entityWithCompositeId); + + _entityWithCustomEntityName = new EntityCustomEntityName() + { + Name = "EntityCustomEntityName" + }; + + session.Save(_customEntityName, _entityWithCustomEntityName); + + _noAssociation = new EntityWithNoAssociation() + { + Complex1Id = parent.Id, + Complex2Id = parent.SameTypeChild.Id, + Composite1Key1 = _entityWithCompositeId.Key.Id1, + Composite1Key2 = _entityWithCompositeId.Key.Id2, + CustomEntityNameId = _entityWithCustomEntityName.Id + }; + + session.Save(_noAssociation); + + session.Flush(); + transaction.Commit(); + } + } + + #endregion Test Setup + } +} diff --git a/src/NHibernate.Test/Hql/EntityJoinHqlTestEntities.cs b/src/NHibernate.Test/Hql/EntityJoinHqlTestEntities.cs new file mode 100644 index 00000000000..f6c0670799c --- /dev/null +++ b/src/NHibernate.Test/Hql/EntityJoinHqlTestEntities.cs @@ -0,0 +1,64 @@ +using System; + +namespace NHibernate.Test.Hql.EntityJoinHqlTestEntities +{ + public class EntityComplex + { + public virtual Guid Id { get; set; } + + public virtual int Version { get; set; } + + public virtual string Name { get; set; } + + public virtual string LazyProp { get; set; } + + public virtual EntityComplex SameTypeChild { get; set; } + + } + + public class EntityWithCompositeId + { + public virtual CompositeKey Key { get; set; } + public virtual string Name { get; set; } + } + public class CompositeKey + { + public int Id1 { get; set; } + public int Id2 { get; set; } + + public override bool Equals(object obj) + { + var key = obj as CompositeKey; + return key != null + && Id1 == key.Id1 + && Id2 == key.Id2; + } + + public override int GetHashCode() + { + var hashCode = -1596524975; + hashCode = hashCode * -1521134295 + Id1.GetHashCode(); + hashCode = hashCode * -1521134295 + Id2.GetHashCode(); + return hashCode; + } + } + + public class EntityCustomEntityName + { + public virtual Guid Id { get; set; } + public virtual string Name { get; set; } + } + + public class EntityWithNoAssociation + { + public virtual Guid Id { get; set; } + public virtual Guid Complex1Id { get; set; } + public virtual Guid Complex2Id { get; set; } + public virtual Guid Simple1Id { get; set; } + public virtual Guid Simple2Id { get; set; } + public virtual int Composite1Key1 { get; set; } + public virtual int Composite1Key2 { get; set; } + public virtual Guid CustomEntityNameId { get; set; } + public virtual string Name { get; set; } + } +} diff --git a/src/NHibernate/Engine/JoinSequence.cs b/src/NHibernate/Engine/JoinSequence.cs index 11f6ac4383d..8d5a20b15ab 100644 --- a/src/NHibernate/Engine/JoinSequence.cs +++ b/src/NHibernate/Engine/JoinSequence.cs @@ -145,7 +145,7 @@ public JoinFragment ToJoinFragment( return ToJoinFragment(enabledFilters, includeExtraJoins, withClauseFragment, withClauseJoinAlias, rootAlias); } - internal JoinFragment ToJoinFragment( + internal virtual JoinFragment ToJoinFragment( IDictionary enabledFilters, bool includeExtraJoins, SqlString withClauseFragment, @@ -325,5 +325,7 @@ public interface ISelector } internal string RootAlias => rootAlias; + + public ISessionFactoryImplementor Factory => factory; } } diff --git a/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs b/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs index c34be9f1ebd..e814c690721 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs @@ -424,6 +424,11 @@ void HandleClauseEnd(int clauseType) _currentClauseType=clauseStack.Pop(); } + void FinishFromClause() + { + _currentFromClause.FinishInit(); + } + IASTNode CreateIntoClause(string path, IASTNode propertySpec) { var persister = (IQueryable) SessionFactoryHelper.RequireClassPersister(path); @@ -676,6 +681,20 @@ void CreateFromJoinElement( { throw new QueryException( "fetch not allowed in subquery from-elements" ); } + + // the incoming "path" can be either: + // 1) an implicit join path (join p.address.city) + // 2) an entity-join (join com.acme.User) + // + // so make the proper interpretation here... + var entityJoinReferencedPersister = ResolveEntityJoinReferencedPersister(path); + if (entityJoinReferencedPersister != null) + { + var entityJoin = CreateEntityJoin(entityJoinReferencedPersister, alias, joinType, with); + ((FromReferenceNode) path).FromElement = entityJoin; + SetPropertyFetch(entityJoin, propertyFetch, alias); + return; + } // The path AST should be a DotNode, and it should have been evaluated already. if ( path.Type != DOT ) { @@ -725,6 +744,48 @@ void CreateFromJoinElement( } } + private EntityJoinFromElement CreateEntityJoin( + IQueryable entityPersister, + IASTNode aliasNode, + int joinType, + IASTNode with) + { + if (log.IsDebugEnabled()) + { + log.Debug($"Creating entity-join FromElement [{aliasNode?.Text} -> {entityPersister.Name}]"); + } + + EntityJoinFromElement join = new EntityJoinFromElement( + CurrentFromClause, + entityPersister, + JoinProcessor.ToHibernateJoinType(joinType), + aliasNode?.Text + ); + + if (with != null) + { + HandleWithFragment(join, with); + } + + return join; + } + + private IQueryable ResolveEntityJoinReferencedPersister(IASTNode path) + { + if (path.Type == IDENT) + { + var pathIdentNode = (IdentNode) path; + string name = path.Text ?? pathIdentNode.OriginalText; + return SessionFactoryHelper.FindQueryableUsingImports(name); + } + else if (path.Type == DOT) + { + var pathText = ASTUtil.GetPathText(path); + return SessionFactoryHelper.FindQueryableUsingImports(pathText); + } + return null; + } + private static string GetPropertyPath(DotNode dotNode, IASTNode alias) { var lhs = dotNode.GetLhs(); @@ -1143,8 +1204,9 @@ private void HandleWithFragment(FromElement fromElement, IASTNode hqlWithNode) FromElement referencedFromElement = visitor.GetReferencedFromElement(); if (referencedFromElement != fromElement) { - throw new InvalidWithClauseException( - "with-clause expressions did not reference from-clause element to which the with-clause was associated"); + if (!referencedFromElement.IsEntityJoin() && !fromElement.IsEntityJoin()) + throw new InvalidWithClauseException( + "with-clause expressions did not reference from-clause element to which the with-clause was associated"); } SqlGenerator sql = new SqlGenerator(_sessionFactoryHelper.Factory, new CommonTreeNodeStream(adaptor, hqlSqlWithNode.GetChild(0))); @@ -1195,14 +1257,7 @@ public void Visit(IASTNode node) { DotNode dotNode = ( DotNode ) node; FromElement fromElement = dotNode.FromElement; - if ( _referencedFromElement != null ) - { - if ( fromElement != _referencedFromElement ) - { - throw new HibernateException( "with-clause referenced two different from-clause elements" ); - } - } - else + if ( _referencedFromElement == null ) { _referencedFromElement = fromElement; _joinAlias = ExtractAppliedAlias( dotNode ); diff --git a/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g b/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g index d195d02348e..78825998019 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g +++ b/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g @@ -13,6 +13,7 @@ tokens FROM_FRAGMENT; // A fragment of SQL that represents a table reference in a FROM clause. IMPLIED_FROM; // An implied FROM element. JOIN_FRAGMENT; // A JOIN fragment. + ENTITY_JOIN; // An "ad-hoc" join to an entity SELECT_CLAUSE; LEFT_OUTER; RIGHT_OUTER; @@ -249,7 +250,7 @@ propertyFetch fromClause : ^(f=FROM { PushFromClause($f.tree); HandleClauseStart( FROM ); } fromElementList ) ; - finally {HandleClauseEnd( FROM );} + finally { HandleClauseEnd( FROM ); FinishFromClause(); } fromElementList @init{ bool oldInFrom = _inFrom; diff --git a/src/NHibernate/Hql/Ast/ANTLR/SqlGenerator.cs b/src/NHibernate/Hql/Ast/ANTLR/SqlGenerator.cs index a820a08cb01..6ab875c8c8c 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/SqlGenerator.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/SqlGenerator.cs @@ -206,8 +206,11 @@ protected virtual void FromFragmentSeparator(IASTNode a) { return; } - - if (right.RealOrigin == left || (right.RealOrigin != null && right.RealOrigin == left.RealOrigin)) + if (right.Type == ENTITY_JOIN) + { + Out(" "); + } + else if (right.RealOrigin == left || (right.RealOrigin != null && right.RealOrigin == left.RealOrigin)) { // right represents a joins originating from left; or // both right and left reprersent joins originating from the same FromElement diff --git a/src/NHibernate/Hql/Ast/ANTLR/SqlGenerator.g b/src/NHibernate/Hql/Ast/ANTLR/SqlGenerator.g index 618bc3b16cb..25eabe3954d 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/SqlGenerator.g +++ b/src/NHibernate/Hql/Ast/ANTLR/SqlGenerator.g @@ -188,6 +188,7 @@ fromTable // Write the table node (from fragment) and all the join fragments associated with it. : ^( a=FROM_FRAGMENT { Out(a); } (tableJoin [ a ])* ) | ^( a=JOIN_FRAGMENT { Out(a); } (tableJoin [ a ])* ) + | ^( a=ENTITY_JOIN { Out(a); } ) ; tableJoin [ IASTNode parent ] diff --git a/src/NHibernate/Hql/Ast/ANTLR/Tree/EntityJoinFromElement.cs b/src/NHibernate/Hql/Ast/ANTLR/Tree/EntityJoinFromElement.cs new file mode 100644 index 00000000000..e2f4ce33365 --- /dev/null +++ b/src/NHibernate/Hql/Ast/ANTLR/Tree/EntityJoinFromElement.cs @@ -0,0 +1,28 @@ +using Antlr.Runtime; +using NHibernate.Persister.Entity; +using NHibernate.SqlCommand; +using NHibernate.Type; + +namespace NHibernate.Hql.Ast.ANTLR.Tree +{ + internal class EntityJoinFromElement : FromElement + { + public EntityJoinFromElement(FromClause fromClause, IQueryable entityPersister, JoinType joinType, string alias) + :base(new CommonToken(HqlSqlWalker.ENTITY_JOIN, entityPersister.TableName)) + { + string tableAlias = fromClause.AliasGenerator.CreateName(entityPersister.EntityName); + + EntityType entityType = (EntityType) entityPersister.Type; + InitializeEntity(fromClause, entityPersister.EntityName, entityPersister, entityType, alias, tableAlias); + + JoinSequence = new EntityJoinJoinSequenceImpl( + SessionFactoryHelper.Factory, + entityType, + entityPersister.TableName, + tableAlias, + joinType); + + fromClause.Walker.AddQuerySpaces(entityPersister.QuerySpaces); + } + } +} diff --git a/src/NHibernate/Hql/Ast/ANTLR/Tree/EntityJoinJoinSequenceImpl.cs b/src/NHibernate/Hql/Ast/ANTLR/Tree/EntityJoinJoinSequenceImpl.cs new file mode 100644 index 00000000000..21040c961bc --- /dev/null +++ b/src/NHibernate/Hql/Ast/ANTLR/Tree/EntityJoinJoinSequenceImpl.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using NHibernate.Engine; +using NHibernate.SqlCommand; +using NHibernate.Type; + +namespace NHibernate.Hql.Ast.ANTLR.Tree +{ + class EntityJoinJoinSequenceImpl : JoinSequence + { + private readonly EntityType _entityType; + private readonly string _tableName; + private readonly string _tableAlias; + private readonly JoinType _joinType; + + public EntityJoinJoinSequenceImpl(ISessionFactoryImplementor factory, EntityType entityType, string tableName, string tableAlias, JoinType joinType):base(factory) + { + _entityType = entityType; + _tableName = tableName; + _tableAlias = tableAlias; + _joinType = joinType; + } + + internal override JoinFragment ToJoinFragment( + IDictionary enabledFilters, + bool includeExtraJoins, + SqlString withClauseFragment, + string withClauseJoinAlias, + string withRootAlias) + { + var joinFragment = new ANSIJoinFragment(); + + var on = withClauseFragment ?? new SqlString(); + var filters = _entityType.GetOnCondition(_tableAlias, Factory, enabledFilters); + if (!string.IsNullOrEmpty(filters)) + { + on.Append(" and ").Append(filters); + } + joinFragment.AddJoin(_tableName, _tableAlias, Array.Empty(), Array.Empty(), _joinType, on); + return joinFragment; + } + } +} diff --git a/src/NHibernate/Hql/Ast/ANTLR/Tree/FromClause.cs b/src/NHibernate/Hql/Ast/ANTLR/Tree/FromClause.cs index e7e3f25bef6..8680b7be748 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/Tree/FromClause.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/Tree/FromClause.cs @@ -36,6 +36,7 @@ public class FromClause : HqlSqlWalkerNode, IDisplayableNode private readonly Dictionary _fromElementByTableAlias = new Dictionary(); private readonly NullableDictionary _fromElementsByPath = new NullableDictionary(); private readonly List _fromElements = new List(); + private readonly List _entityJoinFromElements = new List(); /// /// All of the implicit FROM xxx JOIN yyy elements that are the destination of a collection. These are created from @@ -353,6 +354,11 @@ public void RegisterFromElement(FromElement element) { _fromElementByTableAlias[tableAlias] = element; } + + if (element.IsEntityJoin()) + { + _entityJoinFromElements.Add((EntityJoinFromElement) element); + } } private FromElement FindJoinByPathLocal(string path) @@ -386,5 +392,14 @@ public FromElement GetFromElementByClassName(string className) { return _fromElementByClassAlias.Values.FirstOrDefault(variable => variable.ClassName == className); } + + internal void FinishInit() + { + foreach (var item in _entityJoinFromElements) + { + AddChild(item); + } + _entityJoinFromElements.Clear(); + } } } diff --git a/src/NHibernate/Hql/Ast/ANTLR/Tree/FromElement.cs b/src/NHibernate/Hql/Ast/ANTLR/Tree/FromElement.cs index 6c3a8215670..5f1ef0ac4d4 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/Tree/FromElement.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/Tree/FromElement.cs @@ -101,7 +101,7 @@ public bool IsEntity public bool IsFromOrJoinFragment { - get { return Type == HqlSqlWalker.FROM_FRAGMENT || Type == HqlSqlWalker.JOIN_FRAGMENT; } + get { return Type == HqlSqlWalker.FROM_FRAGMENT || Type == HqlSqlWalker.JOIN_FRAGMENT || Type == HqlSqlWalker.ENTITY_JOIN; } } public bool IsAllPropertyFetch @@ -697,6 +697,10 @@ private void DoInitialize(FromClause fromClause, string tableAlias, string class _className = className; _classAlias = classAlias; _elementType = new FromElementType(this, persister, type); + if (Walker == null) + { + Walker = _fromClause.Walker; + } // Register the FromElement with the FROM clause, now that we have the names and aliases. fromClause.RegisterFromElement(this); @@ -723,5 +727,9 @@ public void AddEmbeddedParameter(IParameterSpecification specification) _embeddedParameters.Add(specification); } + internal bool IsEntityJoin() + { + return Type == HqlSqlWalker.ENTITY_JOIN; + } } } diff --git a/src/NHibernate/Hql/Ast/ANTLR/Tree/HqlSqlWalkerNode.cs b/src/NHibernate/Hql/Ast/ANTLR/Tree/HqlSqlWalkerNode.cs index 52ebb1d1a5b..c801aa588ba 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/Tree/HqlSqlWalkerNode.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/Tree/HqlSqlWalkerNode.cs @@ -29,6 +29,7 @@ public virtual void Initialize(object param) public HqlSqlWalker Walker { get { return _walker; } + protected set { _walker = value; } } internal SessionFactoryHelperExtensions SessionFactoryHelper @@ -47,4 +48,4 @@ public AliasGenerator AliasGenerator get { return _walker.AliasGenerator; } } } -} \ No newline at end of file +} diff --git a/src/NHibernate/Hql/QuerySplitter.cs b/src/NHibernate/Hql/QuerySplitter.cs index d7e72f7b041..07dec6e9d2a 100644 --- a/src/NHibernate/Hql/QuerySplitter.cs +++ b/src/NHibernate/Hql/QuerySplitter.cs @@ -21,6 +21,7 @@ static QuerySplitter() beforeClassTokens.Add("update"); //beforeClassTokens.Add("new"); DEFINITELY DON'T HAVE THIS!! (form H3.2) beforeClassTokens.Add(","); + beforeClassTokens.Add("join"); notAfterClassTokens.Add("in"); //notAfterClassTokens.Add(","); notAfterClassTokens.Add("from");