diff --git a/src/NHibernate.Test/Associations/OneToOneFixture.cs b/src/NHibernate.Test/Associations/OneToOneFixture.cs index 8ea08d6a425..ee1096a585f 100644 --- a/src/NHibernate.Test/Associations/OneToOneFixture.cs +++ b/src/NHibernate.Test/Associations/OneToOneFixture.cs @@ -76,12 +76,12 @@ public void OneToOneCompositeQueryOverByNotNull() { using (var session = OpenSession()) { - var loadedEntity = session.QueryOver().Where(p => p.OneToOneComp != null).SingleOrDefault(); + var loadedEntity = session.QueryOver().Where(p => p.OneToOneComp != null).JoinQueryOver(x => x.OneToOneComp).SingleOrDefault(); Assert.That(loadedEntity, Is.Not.Null); } } - + [Test] public void OneToOneCompositeQueryCompareWithJoin() { @@ -92,7 +92,20 @@ public void OneToOneCompositeQueryCompareWithJoin() Assert.That(loadedEntity, Is.Not.Null); } } - + + [Explicit("Not supported.")] + [Test] + public void OneToOneCompositeQueryCompareWithJoinOrIsNull() + { + using(new SqlLogSpy()) + using (var session = OpenSession()) + { + var loadedEntities = session.CreateQuery("select p from Parent p, EntityWithCompositeId e where p.OneToOneComp = e or p.OneToOneComp is null").List(); + + Assert.That(loadedEntities.Count, Is.EqualTo(2)); + } + } + [Explicit("Expression in Restrictions.Where can't recognize direct alias comparison.")] [Test] public void OneToOneCompositeQueryOverCompareWithJoin() @@ -134,7 +147,7 @@ public void OneToOneCompositeQuerySelectProjection() { using (var session = OpenSession()) { - var loadedProjection = session.Query().Select(x => new {x.OneToOneComp, x.Key}).FirstOrDefault(); + var loadedProjection = session.Query().Where(x => x.OneToOneComp != null).Select(x => new {x.OneToOneComp, x.Key}).FirstOrDefault(); Assert.That(loadedProjection.OneToOneComp, Is.Not.Null); } @@ -147,6 +160,7 @@ public void OneToOneQueryOverSelectProjection() using (var session = OpenSession()) { var loadedEntity = session.QueryOver() + .JoinQueryOver(x => x.OneToOneComp) .Select(x => x.OneToOneComp) .SingleOrDefault(); @@ -225,6 +239,7 @@ protected override void OnSetUp() session.Save(oneToOneParent.OneToOneComp); session.Save(oneToOneParent); + session.Save(new Parent() {Key = new CompositeKey() {Id1 = 1, Id2 = 1}}); session.Flush(); transaction.Commit(); diff --git a/src/NHibernate.Test/Async/Associations/OneToOneFixture.cs b/src/NHibernate.Test/Async/Associations/OneToOneFixture.cs index a1bd0932d32..18f07b778e8 100644 --- a/src/NHibernate.Test/Async/Associations/OneToOneFixture.cs +++ b/src/NHibernate.Test/Async/Associations/OneToOneFixture.cs @@ -88,12 +88,12 @@ public async Task OneToOneCompositeQueryOverByNotNullAsync() { using (var session = OpenSession()) { - var loadedEntity = await (session.QueryOver().Where(p => p.OneToOneComp != null).SingleOrDefaultAsync()); + var loadedEntity = await (session.QueryOver().Where(p => p.OneToOneComp != null).JoinQueryOver(x => x.OneToOneComp).SingleOrDefaultAsync()); Assert.That(loadedEntity, Is.Not.Null); } } - + [Test] public async Task OneToOneCompositeQueryCompareWithJoinAsync() { @@ -104,7 +104,20 @@ public async Task OneToOneCompositeQueryCompareWithJoinAsync() Assert.That(loadedEntity, Is.Not.Null); } } - + + [Explicit("Not supported.")] + [Test] + public async Task OneToOneCompositeQueryCompareWithJoinOrIsNullAsync() + { + using(new SqlLogSpy()) + using (var session = OpenSession()) + { + var loadedEntities = await (session.CreateQuery("select p from Parent p, EntityWithCompositeId e where p.OneToOneComp = e or p.OneToOneComp is null").ListAsync()); + + Assert.That(loadedEntities.Count, Is.EqualTo(2)); + } + } + [Explicit("Expression in Restrictions.Where can't recognize direct alias comparison.")] [Test] public async Task OneToOneCompositeQueryOverCompareWithJoinAsync() @@ -146,7 +159,7 @@ public async Task OneToOneCompositeQuerySelectProjectionAsync() { using (var session = OpenSession()) { - var loadedProjection = await (session.Query().Select(x => new {x.OneToOneComp, x.Key}).FirstOrDefaultAsync()); + var loadedProjection = await (session.Query().Where(x => x.OneToOneComp != null).Select(x => new {x.OneToOneComp, x.Key}).FirstOrDefaultAsync()); Assert.That(loadedProjection.OneToOneComp, Is.Not.Null); } @@ -159,6 +172,7 @@ public async Task OneToOneQueryOverSelectProjectionAsync() using (var session = OpenSession()) { var loadedEntity = await (session.QueryOver() + .JoinQueryOver(x => x.OneToOneComp) .Select(x => x.OneToOneComp) .SingleOrDefaultAsync()); @@ -237,6 +251,7 @@ protected override void OnSetUp() session.Save(oneToOneParent.OneToOneComp); session.Save(oneToOneParent); + session.Save(new Parent() {Key = new CompositeKey() {Id1 = 1, Id2 = 1}}); session.Flush(); transaction.Commit(); diff --git a/src/NHibernate.Test/Async/Hql/EntityJoinHqlTest.cs b/src/NHibernate.Test/Async/Hql/EntityJoinHqlTest.cs index 0632a01771a..30755c9b1a4 100644 --- a/src/NHibernate.Test/Async/Hql/EntityJoinHqlTest.cs +++ b/src/NHibernate.Test/Async/Hql/EntityJoinHqlTest.cs @@ -8,8 +8,10 @@ //------------------------------------------------------------------------------ +using System.Linq; using System.Text.RegularExpressions; using NHibernate.Cfg.MappingSchema; +using NHibernate.Linq; using NHibernate.Mapping.ByCode; using NHibernate.Test.Hql.EntityJoinHqlTestEntities; using NUnit.Framework; @@ -164,12 +166,12 @@ public async Task EntityJoinWithNullableOneToOneEntityComparisonInWithClausShoul .CreateQuery( "select ex " + "from NullableOwner ex " - + "left join OneToOneEntity st with st = ex.OneToOne " - ).SetMaxResults(1) + + "inner join OneToOneEntity st with st = ex.OneToOne " + ) .UniqueResultAsync()); - Assert.That(Regex.Matches(sqlLog.GetWholeLog(), "OneToOneEntity").Count, Is.EqualTo(2)); - Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + Assert.That(entity, Is.Not.Null); + Assert.That(Regex.Matches(sqlLog.Appender.GetEvents()[0].RenderedMessage, "OneToOneEntity").Count, Is.EqualTo(2)); } } @@ -188,8 +190,28 @@ public async Task NullableOneToOneWhereEntityIsNotNullAsync() ).SetMaxResults(1) .UniqueResultAsync()); - Assert.That(Regex.Matches(sqlLog.GetWholeLog(), "OneToOneEntity").Count, Is.EqualTo(1)); - Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + Assert.That(entity, Is.Not.Null); + Assert.That(Regex.Matches(sqlLog.Appender.GetEvents()[0].RenderedMessage, "OneToOneEntity").Count, Is.EqualTo(1)); + } + } + + [Test] + public async Task NullableOneToOneWhereEntityIsNullAsync() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + var entity = + await (session + .CreateQuery( + "select ex " + + "from NullableOwner ex " + + "where ex.OneToOne is null " + ).SetMaxResults(1) + .UniqueResultAsync()); + + Assert.That(entity, Is.Not.Null); + Assert.That(Regex.Matches(sqlLog.Appender.GetEvents()[0].RenderedMessage, "OneToOneEntity").Count, Is.EqualTo(1)); } } @@ -208,8 +230,8 @@ public async Task NullableOneToOneWhereIdIsNotNullAsync() ).SetMaxResults(1) .UniqueResultAsync()); - Assert.That(Regex.Matches(sqlLog.GetWholeLog(), "OneToOneEntity").Count, Is.EqualTo(1)); - Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + Assert.That(entity, Is.Not.Null); + Assert.That(Regex.Matches(sqlLog.Appender.GetEvents()[0].RenderedMessage, "OneToOneEntity").Count, Is.EqualTo(1)); } } @@ -222,14 +244,32 @@ public async Task NullablePropRefWhereIdEntityNotNullShouldAddJoinAsync() var entity = await (session .CreateQuery( - "select ex " - + "from NullableOwner ex " + "select ex from NullableOwner ex " + "where ex.PropRef is not null " - ).SetMaxResults(1) + ) .UniqueResultAsync()); - Assert.That(Regex.Matches(sqlLog.GetWholeLog(), "PropRefEntity").Count, Is.EqualTo(1)); - Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + Assert.That(entity, Is.Not.Null); + Assert.That(Regex.Matches(sqlLog.Appender.GetEvents()[0].RenderedMessage, "PropRefEntity").Count, Is.EqualTo(1)); + } + } + + [Test] + public async Task NullablePropRefWhereIdEntityNullShouldAddJoinAsync() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + var entity = + await (session + .CreateQuery( + "select ex from NullableOwner ex " + + "where ex.PropRef is null " + ) + .UniqueResultAsync()); + + Assert.That(entity, Is.Not.Null); + Assert.That(Regex.Matches(sqlLog.Appender.GetEvents()[0].RenderedMessage, "PropRefEntity").Count, Is.EqualTo(1)); } } @@ -245,13 +285,34 @@ public async Task NullableOneToOneFetchQueryIsNotAffectedAsync() "select ex " + "from NullableOwner ex left join fetch ex.OneToOne o " + "where o is null " - ).SetMaxResults(1) + ) .UniqueResultAsync()); + Assert.That(entity, Is.Not.Null); Assert.That(Regex.Matches(sqlLog.GetWholeLog(), "OneToOneEntity").Count, Is.EqualTo(1)); } } - + + [Test(Description = "GH-2611")] + public async Task NullableOneIsNullLinqAsync() + { + using (var session = OpenSession()) + { + var entity = await (session.Query().Where(x => x.OneToOne == null).SingleOrDefaultAsync()); + Assert.That(entity, Is.Not.Null); + } + } + + [Test(Description = "GH-2611")] + public async Task NullableOneFetchIsNullLinqAsync() + { + using (var session = OpenSession()) + { + var entity = await (session.Query().Fetch(x => x.OneToOne).Where(x => x.OneToOne == null).SingleOrDefaultAsync()); + Assert.That(entity, Is.Not.Null); + } + } + [Test] public async Task NullableOneToOneFetchQueryIsNotAffected2Async() { @@ -264,9 +325,10 @@ public async Task NullableOneToOneFetchQueryIsNotAffected2Async() "select ex " + "from NullableOwner ex left join fetch ex.OneToOne o " + "where o.Id is null " - ).SetMaxResults(1) + ) .UniqueResultAsync()); + Assert.That(entity, Is.Not.Null); Assert.That(Regex.Matches(sqlLog.GetWholeLog(), "OneToOneEntity").Count, Is.EqualTo(1)); } } @@ -488,8 +550,9 @@ protected override HbmMapping GetMappings() mapper.Class( rc => { - rc.Id(e => e.Id, m => m.Generator(Generators.GuidComb)); + rc.Id(e => e.Id, m => m.Generator(Generators.Foreign(x => x.Parent))); rc.Property(e => e.Name); + rc.OneToOne(x => x.Parent, x => x.Constrained(true)); }); mapper.Class( @@ -505,7 +568,11 @@ protected override HbmMapping GetMappings() { rc.Id(e => e.Id, m => m.Generator(Generators.GuidComb)); rc.Property(e => e.Name); - rc.OneToOne(e => e.OneToOne, m => m.Constrained(false)); + rc.OneToOne(e => e.OneToOne, m => + { + m.Constrained(false); + m.Cascade(Mapping.ByCode.Cascade.All); + }); rc.ManyToOne( e => e.PropRef, m => @@ -514,6 +581,7 @@ protected override HbmMapping GetMappings() m.PropertyRef(nameof(PropRefEntity.PropertyRef)); m.ForeignKey("none"); m.NotFound(NotFoundMode.Ignore); + m.Cascade(Mapping.ByCode.Cascade.All); }); }); @@ -584,7 +652,10 @@ protected override void OnSetUp() }; session.Save(_noAssociation); - session.Flush(); + session.Save(new NullableOwner() {Name = "NoAssociation"}); + var nullableOwner = new NullableOwner() {Name = "Association", PropRef = new PropRefEntity() {Name = "x", PropertyRef = "xx"}, OneToOne = new OneToOneEntity() {Name = "x"},}; + nullableOwner.OneToOne.Parent = nullableOwner; + session.Save(nullableOwner); transaction.Commit(); } } diff --git a/src/NHibernate.Test/Hql/EntityJoinHqlTest.cs b/src/NHibernate.Test/Hql/EntityJoinHqlTest.cs index f01b6303d30..9dd6712b2df 100644 --- a/src/NHibernate.Test/Hql/EntityJoinHqlTest.cs +++ b/src/NHibernate.Test/Hql/EntityJoinHqlTest.cs @@ -1,5 +1,7 @@ -using System.Text.RegularExpressions; +using System.Linq; +using System.Text.RegularExpressions; using NHibernate.Cfg.MappingSchema; +using NHibernate.Linq; using NHibernate.Mapping.ByCode; using NHibernate.Test.Hql.EntityJoinHqlTestEntities; using NUnit.Framework; @@ -153,12 +155,12 @@ public void EntityJoinWithNullableOneToOneEntityComparisonInWithClausShouldAddJo .CreateQuery( "select ex " + "from NullableOwner ex " - + "left join OneToOneEntity st with st = ex.OneToOne " - ).SetMaxResults(1) + + "inner join OneToOneEntity st with st = ex.OneToOne " + ) .UniqueResult(); - Assert.That(Regex.Matches(sqlLog.GetWholeLog(), "OneToOneEntity").Count, Is.EqualTo(2)); - Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + Assert.That(entity, Is.Not.Null); + Assert.That(Regex.Matches(sqlLog.Appender.GetEvents()[0].RenderedMessage, "OneToOneEntity").Count, Is.EqualTo(2)); } } @@ -177,8 +179,28 @@ public void NullableOneToOneWhereEntityIsNotNull() ).SetMaxResults(1) .UniqueResult(); - Assert.That(Regex.Matches(sqlLog.GetWholeLog(), "OneToOneEntity").Count, Is.EqualTo(1)); - Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + Assert.That(entity, Is.Not.Null); + Assert.That(Regex.Matches(sqlLog.Appender.GetEvents()[0].RenderedMessage, "OneToOneEntity").Count, Is.EqualTo(1)); + } + } + + [Test] + public void NullableOneToOneWhereEntityIsNull() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + var entity = + session + .CreateQuery( + "select ex " + + "from NullableOwner ex " + + "where ex.OneToOne is null " + ).SetMaxResults(1) + .UniqueResult(); + + Assert.That(entity, Is.Not.Null); + Assert.That(Regex.Matches(sqlLog.Appender.GetEvents()[0].RenderedMessage, "OneToOneEntity").Count, Is.EqualTo(1)); } } @@ -197,8 +219,8 @@ public void NullableOneToOneWhereIdIsNotNull() ).SetMaxResults(1) .UniqueResult(); - Assert.That(Regex.Matches(sqlLog.GetWholeLog(), "OneToOneEntity").Count, Is.EqualTo(1)); - Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + Assert.That(entity, Is.Not.Null); + Assert.That(Regex.Matches(sqlLog.Appender.GetEvents()[0].RenderedMessage, "OneToOneEntity").Count, Is.EqualTo(1)); } } @@ -211,14 +233,32 @@ public void NullablePropRefWhereIdEntityNotNullShouldAddJoin() var entity = session .CreateQuery( - "select ex " - + "from NullableOwner ex " + "select ex from NullableOwner ex " + "where ex.PropRef is not null " - ).SetMaxResults(1) + ) .UniqueResult(); - Assert.That(Regex.Matches(sqlLog.GetWholeLog(), "PropRefEntity").Count, Is.EqualTo(1)); - Assert.That(sqlLog.Appender.GetEvents().Length, Is.EqualTo(1), "Only one SQL select is expected"); + Assert.That(entity, Is.Not.Null); + Assert.That(Regex.Matches(sqlLog.Appender.GetEvents()[0].RenderedMessage, "PropRefEntity").Count, Is.EqualTo(1)); + } + } + + [Test] + public void NullablePropRefWhereIdEntityNullShouldAddJoin() + { + using (var sqlLog = new SqlLogSpy()) + using (var session = OpenSession()) + { + var entity = + session + .CreateQuery( + "select ex from NullableOwner ex " + + "where ex.PropRef is null " + ) + .UniqueResult(); + + Assert.That(entity, Is.Not.Null); + Assert.That(Regex.Matches(sqlLog.Appender.GetEvents()[0].RenderedMessage, "PropRefEntity").Count, Is.EqualTo(1)); } } @@ -234,13 +274,34 @@ public void NullableOneToOneFetchQueryIsNotAffected() "select ex " + "from NullableOwner ex left join fetch ex.OneToOne o " + "where o is null " - ).SetMaxResults(1) + ) .UniqueResult(); + Assert.That(entity, Is.Not.Null); Assert.That(Regex.Matches(sqlLog.GetWholeLog(), "OneToOneEntity").Count, Is.EqualTo(1)); } } - + + [Test(Description = "GH-2611")] + public void NullableOneIsNullLinq() + { + using (var session = OpenSession()) + { + var entity = session.Query().Where(x => x.OneToOne == null).SingleOrDefault(); + Assert.That(entity, Is.Not.Null); + } + } + + [Test(Description = "GH-2611")] + public void NullableOneFetchIsNullLinq() + { + using (var session = OpenSession()) + { + var entity = session.Query().Fetch(x => x.OneToOne).Where(x => x.OneToOne == null).SingleOrDefault(); + Assert.That(entity, Is.Not.Null); + } + } + [Test] public void NullableOneToOneFetchQueryIsNotAffected2() { @@ -253,9 +314,10 @@ public void NullableOneToOneFetchQueryIsNotAffected2() "select ex " + "from NullableOwner ex left join fetch ex.OneToOne o " + "where o.Id is null " - ).SetMaxResults(1) + ) .UniqueResult(); + Assert.That(entity, Is.Not.Null); Assert.That(Regex.Matches(sqlLog.GetWholeLog(), "OneToOneEntity").Count, Is.EqualTo(1)); } } @@ -493,8 +555,9 @@ protected override HbmMapping GetMappings() mapper.Class( rc => { - rc.Id(e => e.Id, m => m.Generator(Generators.GuidComb)); + rc.Id(e => e.Id, m => m.Generator(Generators.Foreign(x => x.Parent))); rc.Property(e => e.Name); + rc.OneToOne(x => x.Parent, x => x.Constrained(true)); }); mapper.Class( @@ -510,7 +573,11 @@ protected override HbmMapping GetMappings() { rc.Id(e => e.Id, m => m.Generator(Generators.GuidComb)); rc.Property(e => e.Name); - rc.OneToOne(e => e.OneToOne, m => m.Constrained(false)); + rc.OneToOne(e => e.OneToOne, m => + { + m.Constrained(false); + m.Cascade(Mapping.ByCode.Cascade.All); + }); rc.ManyToOne( e => e.PropRef, m => @@ -519,6 +586,7 @@ protected override HbmMapping GetMappings() m.PropertyRef(nameof(PropRefEntity.PropertyRef)); m.ForeignKey("none"); m.NotFound(NotFoundMode.Ignore); + m.Cascade(Mapping.ByCode.Cascade.All); }); }); @@ -589,7 +657,10 @@ protected override void OnSetUp() }; session.Save(_noAssociation); - session.Flush(); + session.Save(new NullableOwner() {Name = "NoAssociation"}); + var nullableOwner = new NullableOwner() {Name = "Association", PropRef = new PropRefEntity() {Name = "x", PropertyRef = "xx"}, OneToOne = new OneToOneEntity() {Name = "x"},}; + nullableOwner.OneToOne.Parent = nullableOwner; + session.Save(nullableOwner); transaction.Commit(); } } diff --git a/src/NHibernate.Test/Hql/EntityJoinHqlTestEntities.cs b/src/NHibernate.Test/Hql/EntityJoinHqlTestEntities.cs index 277b0ea706b..5571b0decf4 100644 --- a/src/NHibernate.Test/Hql/EntityJoinHqlTestEntities.cs +++ b/src/NHibernate.Test/Hql/EntityJoinHqlTestEntities.cs @@ -16,8 +16,9 @@ public class OneToOneEntity { public virtual Guid Id { get; set; } public virtual string Name { get; set; } + public virtual NullableOwner Parent { get; set; } } - + public class PropRefEntity { public virtual Guid Id { get; set; } diff --git a/src/NHibernate/Hql/Ast/ANTLR/Tree/DotNode.cs b/src/NHibernate/Hql/Ast/ANTLR/Tree/DotNode.cs index bbe0a91b740..7daa43f84bb 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/Tree/DotNode.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/Tree/DotNode.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using Antlr.Runtime; using NHibernate.Engine; @@ -409,9 +410,19 @@ private void DereferenceEntity(EntityType entityType, bool implicitJoin, string } else { - comparisonWithNullableEntity = (Walker.IsComparativeExpressionClause && entityType.IsNullable); - joinIsNeeded = generateJoin || (Walker.IsInSelect && !Walker.IsInCase) || (Walker.IsInFrom && !Walker.IsComparativeExpressionClause) - || comparisonWithNullableEntity; + if (Walker.IsComparativeExpressionClause && entityType.IsNullable) + { + joinIsNeeded = comparisonWithNullableEntity = true; + + //TODO: Fix this hack. Ideally we should always left join here. Skip left join for nullable entity if query contains implicit joins. We currently don't support left joins for such queries (see OneToOneCompositeQueryCompareWithJoinOrIsNull) + var fromJoins = ASTUtil.CollectChildren(Walker.CurrentFromClause, x => x is FromElement fe && !fe.IsImplied && fe.Type == HqlSqlWalker.FROM_FRAGMENT); + if(fromJoins.Count == 1) + _joinType = JoinType.LeftOuterJoin; + } + else + { + joinIsNeeded = generateJoin || (Walker.IsInSelect && !Walker.IsInCase) || (Walker.IsInFrom && !Walker.IsComparativeExpressionClause); + } } if ( joinIsNeeded ) @@ -517,7 +528,7 @@ private void DereferenceEntityJoin(string classAlias, EntityType propertyType, b // If this is an implied join in a from element, then use the impled join type which is part of the // tree parser's state (set by the gramamar actions). JoinSequence joinSequence = SessionFactoryHelper - .CreateJoinSequence( impliedJoin, propertyType, tableAlias, _joinType, joinColumns ); + .CreateJoinSequence(_joinType != JoinType.LeftOuterJoin && impliedJoin, propertyType, tableAlias, _joinType, joinColumns); var factory = new FromElementFactory( currentFromClause,