From 116d34a2b20b97336aad6a9a233a87f55ba26d92 Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Thu, 16 Mar 2023 09:11:00 +0200 Subject: [PATCH 01/11] Fix fetching many-to-many with subclasses (#3252) Fixes #3239 --- .../Async/NHSpecificTest/NH2174/Fixture.cs | 28 ++++++++++++++++++- .../NHSpecificTest/NH2174/Entity.cs | 1 + .../NHSpecificTest/NH2174/Fixture.cs | 28 ++++++++++++++++++- .../NHSpecificTest/NH2174/Mappings.hbm.xml | 13 +++++++++ src/NHibernate/Engine/TableGroupJoinHelper.cs | 17 +++++++---- 5 files changed, 80 insertions(+), 7 deletions(-) diff --git a/src/NHibernate.Test/Async/NHSpecificTest/NH2174/Fixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/NH2174/Fixture.cs index c6fa377a043..77c3127e73f 100644 --- a/src/NHibernate.Test/Async/NHSpecificTest/NH2174/Fixture.cs +++ b/src/NHibernate.Test/Async/NHSpecificTest/NH2174/Fixture.cs @@ -25,8 +25,10 @@ protected override void OnSetUp() { var doc = new Document {Id_Base = 1, Id_Doc = 2}; session.Save(doc); - session.Save(new DocumentDetailDocument {Id_Base = 1, Id_Doc = 2, Id_Item = 1, ReferencedDocument = doc}); + var detail = new DocumentDetailDocument {Id_Base = 1, Id_Doc = 2, Id_Item = 1, ReferencedDocument = doc}; + session.Save(detail); + doc.RefferedDetailsManyToMany.Add(detail); transaction.Commit(); } } @@ -53,6 +55,14 @@ public async Task LinqFetchAsync() } } + [Test(Description = "GH-3239")] + public async Task LinqFetchManyToManyAsync() + { + using var session = OpenSession(); + var result = await (session.Query().Fetch(x => x.RefferedDetailsManyToMany).FirstAsync()); + Assert.That(result.RefferedDetailsManyToMany, Has.Count.EqualTo(1)); + } + [Test] public async Task QueryOverFetchAsync() { @@ -63,6 +73,14 @@ public async Task QueryOverFetchAsync() } } + [Test(Description = "GH-3239")] + public async Task QueryOverFetchManyToManyAsync() + { + using var session = OpenSession(); + var result = await (session.QueryOver().Fetch(SelectMode.Fetch, x => x.RefferedDetailsManyToMany).SingleOrDefaultAsync()); + Assert.That(result.RefferedDetailsManyToMany, Has.Count.EqualTo(1)); + } + [Test] public async Task LazyLoadAsync() { @@ -73,5 +91,13 @@ public async Task LazyLoadAsync() Assert.That(result.RefferedDetails.Count, Is.EqualTo(1)); } } + + [Test] + public async Task LazyLoadManyToManyAsync() + { + using var session = OpenSession(); + var result = await (session.Query().FirstAsync()); + Assert.That(result.RefferedDetailsManyToMany.Count, Is.EqualTo(1)); + } } } diff --git a/src/NHibernate.Test/NHSpecificTest/NH2174/Entity.cs b/src/NHibernate.Test/NHSpecificTest/NH2174/Entity.cs index d3c4702ac69..2b9ea9bfd4f 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH2174/Entity.cs +++ b/src/NHibernate.Test/NHSpecificTest/NH2174/Entity.cs @@ -86,6 +86,7 @@ public override int GetHashCode() private int _id_Doc; private int _id_base; public virtual IList RefferedDetails { get; set; } = new List(); + public virtual IList RefferedDetailsManyToMany { get; set; } = new List(); public int Id_Doc { diff --git a/src/NHibernate.Test/NHSpecificTest/NH2174/Fixture.cs b/src/NHibernate.Test/NHSpecificTest/NH2174/Fixture.cs index 13d9922c9d2..a216a4d2357 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH2174/Fixture.cs +++ b/src/NHibernate.Test/NHSpecificTest/NH2174/Fixture.cs @@ -14,8 +14,10 @@ protected override void OnSetUp() { var doc = new Document {Id_Base = 1, Id_Doc = 2}; session.Save(doc); - session.Save(new DocumentDetailDocument {Id_Base = 1, Id_Doc = 2, Id_Item = 1, ReferencedDocument = doc}); + var detail = new DocumentDetailDocument {Id_Base = 1, Id_Doc = 2, Id_Item = 1, ReferencedDocument = doc}; + session.Save(detail); + doc.RefferedDetailsManyToMany.Add(detail); transaction.Commit(); } } @@ -42,6 +44,14 @@ public void LinqFetch() } } + [Test(Description = "GH-3239")] + public void LinqFetchManyToMany() + { + using var session = OpenSession(); + var result = session.Query().Fetch(x => x.RefferedDetailsManyToMany).First(); + Assert.That(result.RefferedDetailsManyToMany, Has.Count.EqualTo(1)); + } + [Test] public void QueryOverFetch() { @@ -52,6 +62,14 @@ public void QueryOverFetch() } } + [Test(Description = "GH-3239")] + public void QueryOverFetchManyToMany() + { + using var session = OpenSession(); + var result = session.QueryOver().Fetch(SelectMode.Fetch, x => x.RefferedDetailsManyToMany).SingleOrDefault(); + Assert.That(result.RefferedDetailsManyToMany, Has.Count.EqualTo(1)); + } + [Test] public void LazyLoad() { @@ -62,5 +80,13 @@ public void LazyLoad() Assert.That(result.RefferedDetails.Count, Is.EqualTo(1)); } } + + [Test] + public void LazyLoadManyToMany() + { + using var session = OpenSession(); + var result = session.Query().First(); + Assert.That(result.RefferedDetailsManyToMany.Count, Is.EqualTo(1)); + } } } diff --git a/src/NHibernate.Test/NHSpecificTest/NH2174/Mappings.hbm.xml b/src/NHibernate.Test/NHSpecificTest/NH2174/Mappings.hbm.xml index b6b056c2f80..d04459f0265 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH2174/Mappings.hbm.xml +++ b/src/NHibernate.Test/NHSpecificTest/NH2174/Mappings.hbm.xml @@ -42,5 +42,18 @@ + + + + + + + + + + + + + diff --git a/src/NHibernate/Engine/TableGroupJoinHelper.cs b/src/NHibernate/Engine/TableGroupJoinHelper.cs index 18dddb12dfe..12ad3be28a2 100644 --- a/src/NHibernate/Engine/TableGroupJoinHelper.cs +++ b/src/NHibernate/Engine/TableGroupJoinHelper.cs @@ -68,15 +68,18 @@ private static bool NeedsTableGroupJoin(IReadOnlyList joins, SqlString[] foreach (var join in joins) { - var entityPersister = GetEntityPersister(join.Joinable); + var entityPersister = GetEntityPersister(join.Joinable, out var isManyToMany); if (entityPersister?.HasSubclassJoins(includeSubclasses && isSubclassIncluded(join.Alias)) != true) continue; if (hasWithClause) return true; - if (entityPersister.ColumnsDependOnSubclassJoins(join.RHSColumns)) + if (!isManyToMany // many-to-many keys are stored in separate table + && entityPersister.ColumnsDependOnSubclassJoins(join.RHSColumns)) + { return true; + } } return false; @@ -91,14 +94,16 @@ private static SqlString GetTableGroupJoinWithClause(SqlString[] withClauseFragm var isAssociationJoin = lhsColumns.Length > 0; if (isAssociationJoin) { - var entityPersister = GetEntityPersister(first.Joinable); + var entityPersister = GetEntityPersister(first.Joinable, out var isManyToMany); string rhsAlias = first.Alias; string[] rhsColumns = first.RHSColumns; for (int j = 0; j < lhsColumns.Length; j++) { fromFragment.Add(lhsColumns[j]) .Add("=") - .Add(entityPersister?.GenerateTableAliasForColumn(rhsAlias, rhsColumns[j]) ?? rhsAlias) + .Add((entityPersister == null || isManyToMany) // many-to-many keys are stored in separate table + ? rhsAlias + : entityPersister.GenerateTableAliasForColumn(rhsAlias, rhsColumns[j])) .Add(".") .Add(rhsColumns[j]); if (j != lhsColumns.Length - 1) @@ -111,12 +116,14 @@ private static SqlString GetTableGroupJoinWithClause(SqlString[] withClauseFragm return fromFragment.ToSqlString(); } - private static AbstractEntityPersister GetEntityPersister(IJoinable joinable) + private static AbstractEntityPersister GetEntityPersister(IJoinable joinable, out bool isManyToMany) { + isManyToMany = false; if (!joinable.IsCollection) return joinable as AbstractEntityPersister; var collection = (IQueryableCollection) joinable; + isManyToMany = collection.IsManyToMany; return collection.ElementType.IsEntityType ? collection.ElementPersister as AbstractEntityPersister : null; } From 2a7a74b04164fa5423014e2a63c8e415e9799389 Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Thu, 16 Mar 2023 14:15:16 +0200 Subject: [PATCH 02/11] MappingByCode: Support backfield property access (#3254) Fixes #3251 --- .../AllPropertiesRegistrationTests.cs | 14 ++++++++++++++ .../Mapping/ByCode/IAccessorPropertyMapper.cs | 5 +++-- .../Mapping/ByCode/Impl/AccessorPropertyMapper.cs | 5 ++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/NHibernate.Test/MappingByCode/ExplicitMappingTests/AllPropertiesRegistrationTests.cs b/src/NHibernate.Test/MappingByCode/ExplicitMappingTests/AllPropertiesRegistrationTests.cs index 11f66934fa3..8b1c2d2a269 100644 --- a/src/NHibernate.Test/MappingByCode/ExplicitMappingTests/AllPropertiesRegistrationTests.cs +++ b/src/NHibernate.Test/MappingByCode/ExplicitMappingTests/AllPropertiesRegistrationTests.cs @@ -188,6 +188,20 @@ public void WhenMapPropertiesInTheBaseJumpedClassUsingMemberNameThenMapInInherit Assert.That(hbmClass.Properties.Select(p => p.Access).All(a => a.StartsWith("field.")), Is.True); } + [Test] + public void BackfieldAccessPropertyMapping() + { + var mapper = new ModelMapper(); + mapper.Class(mc => + { + mc.Id(x => x.Id, m => m.Access(Accessor.Backfield)); + }); + + HbmMapping mappings = mapper.CompileMappingForAllExplicitlyAddedEntities(); + HbmClass hbmClass = mappings.RootClasses[0]; + Assert.That(hbmClass.Id.access, Is.EqualTo("backfield")); + } + [Test] public void WhenMapBagWithWrongElementTypeThenThrows() { diff --git a/src/NHibernate/Mapping/ByCode/IAccessorPropertyMapper.cs b/src/NHibernate/Mapping/ByCode/IAccessorPropertyMapper.cs index 278043f853a..9c51762cd60 100644 --- a/src/NHibernate/Mapping/ByCode/IAccessorPropertyMapper.cs +++ b/src/NHibernate/Mapping/ByCode/IAccessorPropertyMapper.cs @@ -6,7 +6,8 @@ public enum Accessor Field, NoSetter, ReadOnly, - None + None, + Backfield, } public interface IAccessorPropertyMapper @@ -14,4 +15,4 @@ public interface IAccessorPropertyMapper void Access(Accessor accessor); void Access(System.Type accessorType); } -} \ No newline at end of file +} diff --git a/src/NHibernate/Mapping/ByCode/Impl/AccessorPropertyMapper.cs b/src/NHibernate/Mapping/ByCode/Impl/AccessorPropertyMapper.cs index f264dcbf55e..6bf059af9c7 100644 --- a/src/NHibernate/Mapping/ByCode/Impl/AccessorPropertyMapper.cs +++ b/src/NHibernate/Mapping/ByCode/Impl/AccessorPropertyMapper.cs @@ -92,6 +92,9 @@ public void Access(Accessor accessor) case Accessor.None: setAccessor("none"); break; + case Accessor.Backfield: + setAccessor("backfield"); + break; default: throw new ArgumentOutOfRangeException("accessor"); } @@ -136,4 +139,4 @@ private static MemberInfo GetField(System.Type type, string fieldName) return member; } } -} \ No newline at end of file +} From a90a45cf706e54472432e414ca9f9df7e9313c0f Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Thu, 16 Mar 2023 14:30:05 +0200 Subject: [PATCH 03/11] Set version 5.4.2-dev --- build-common/NHibernate.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build-common/NHibernate.props b/build-common/NHibernate.props index ce7a53ef53a..a6cc2ceb822 100644 --- a/build-common/NHibernate.props +++ b/build-common/NHibernate.props @@ -3,9 +3,9 @@ 5.4 - 1 + 2 - + dev 9.0 $(NhVersion).$(VersionPatch) From 3088367d851538d7d5801c7dade11acf90432888 Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Fri, 24 Mar 2023 11:03:49 +0200 Subject: [PATCH 04/11] Fix missing join for reused fetch join not used in Select (#3267) Fixes #3263 --- .../GH3263/ReuseFetchJoinFixture.cs | 99 +++++++++++++++++++ .../NHSpecificTest/GH3263/Entity.cs | 16 +++ .../NHSpecificTest/GH3263/Mappings.hbm.xml | 25 +++++ .../GH3263/ReuseFetchJoinFixture.cs | 88 +++++++++++++++++ src/NHibernate/Hql/Ast/ANTLR/Tree/DotNode.cs | 2 + .../Hql/Ast/ANTLR/Tree/FromElement.cs | 1 + .../Hql/Ast/ANTLR/Tree/SelectClause.cs | 2 +- 7 files changed, 232 insertions(+), 1 deletion(-) create mode 100644 src/NHibernate.Test/Async/NHSpecificTest/GH3263/ReuseFetchJoinFixture.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/GH3263/Entity.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/GH3263/Mappings.hbm.xml create mode 100644 src/NHibernate.Test/NHSpecificTest/GH3263/ReuseFetchJoinFixture.cs 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); } From 7deea30d5e56842903ab2e93ce3d8368235fd08e Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Sun, 26 Mar 2023 11:15:07 +0300 Subject: [PATCH 05/11] Force left join for comparisons with nullable entities (#3270) Fixes #3269 --- src/NHibernate.Test/Async/Hql/EntityJoinHqlTest.cs | 9 +++++++++ src/NHibernate.Test/Hql/EntityJoinHqlTest.cs | 9 +++++++++ src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs | 3 --- src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g | 2 +- src/NHibernate/Hql/Ast/ANTLR/Tree/DotNode.cs | 3 +-- 5 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/NHibernate.Test/Async/Hql/EntityJoinHqlTest.cs b/src/NHibernate.Test/Async/Hql/EntityJoinHqlTest.cs index 65d0459ef3d..54b07fa8b96 100644 --- a/src/NHibernate.Test/Async/Hql/EntityJoinHqlTest.cs +++ b/src/NHibernate.Test/Async/Hql/EntityJoinHqlTest.cs @@ -358,6 +358,15 @@ from x2 in session.Query() //GH-2988 var withNullOrValidList = await (session.Query().Where(x => x.ManyToOne.Id == validManyToOne.Id || x.ManyToOne == null).ToListAsync()); var withNullOrValidList2 = await (session.Query().Where(x => x.ManyToOne == null || x.ManyToOne.Id == validManyToOne.Id).ToListAsync()); + //GH-3269 + var invalidId = Guid.NewGuid(); + var withInvalidOrValid = await (session.Query().Where(x => x.OneToOne.Id == invalidId || x.ManyToOne.Id == validManyToOne.Id).ToListAsync()); + var withInvalidOrNull = await (session.Query().Where(x => x.ManyToOne.Id == invalidId || x.OneToOne == null).ToListAsync()); + var withInvalidOrNotNull = await (session.Query().Where(x => x.ManyToOne.Id == invalidId || x.OneToOne != null).ToListAsync()); + + Assert.That(withInvalidOrValid.Count, Is.EqualTo(1)); + Assert.That(withInvalidOrNull.Count, Is.EqualTo(2)); + Assert.That(withInvalidOrNotNull.Count, Is.EqualTo(0)); //GH-3185 var mixImplicitAndLeftJoinList = await (session.Query().Where(x => x.ManyToOne.Id == validManyToOne.Id && x.OneToOne == null).ToListAsync()); diff --git a/src/NHibernate.Test/Hql/EntityJoinHqlTest.cs b/src/NHibernate.Test/Hql/EntityJoinHqlTest.cs index 607e1e80cb8..a07ec58379a 100644 --- a/src/NHibernate.Test/Hql/EntityJoinHqlTest.cs +++ b/src/NHibernate.Test/Hql/EntityJoinHqlTest.cs @@ -346,6 +346,15 @@ from x2 in session.Query() //GH-2988 var withNullOrValidList = session.Query().Where(x => x.ManyToOne.Id == validManyToOne.Id || x.ManyToOne == null).ToList(); var withNullOrValidList2 = session.Query().Where(x => x.ManyToOne == null || x.ManyToOne.Id == validManyToOne.Id).ToList(); + //GH-3269 + var invalidId = Guid.NewGuid(); + var withInvalidOrValid = session.Query().Where(x => x.OneToOne.Id == invalidId || x.ManyToOne.Id == validManyToOne.Id).ToList(); + var withInvalidOrNull = session.Query().Where(x => x.ManyToOne.Id == invalidId || x.OneToOne == null).ToList(); + var withInvalidOrNotNull = session.Query().Where(x => x.ManyToOne.Id == invalidId || x.OneToOne != null).ToList(); + + Assert.That(withInvalidOrValid.Count, Is.EqualTo(1)); + Assert.That(withInvalidOrNull.Count, Is.EqualTo(2)); + Assert.That(withInvalidOrNotNull.Count, Is.EqualTo(0)); //GH-3185 var mixImplicitAndLeftJoinList = session.Query().Where(x => x.ManyToOne.Id == validManyToOne.Id && x.OneToOne == null).ToList(); diff --git a/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs b/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs index c93384c1344..76325152051 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs @@ -42,7 +42,6 @@ public partial class HqlSqlWalker private SelectClause _selectClause; private readonly AliasGenerator _aliasGenerator = new AliasGenerator(); private readonly ASTPrinter _printer = new ASTPrinter(); - private bool _isNullComparison; // //Maps each top-level result variable to its SelectExpression; @@ -1209,8 +1208,6 @@ public IASTFactory ASTFactory } } - internal bool IsNullComparison => _isNullComparison; - public void AddQuerySpaces(string[] spaces) { for (int i = 0; i < spaces.Length; i++) diff --git a/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g b/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g index d99340b9726..fba1010337c 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g +++ b/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g @@ -380,7 +380,7 @@ comparisonExpr | ^(NOT_BETWEEN exprOrSubquery exprOrSubquery exprOrSubquery) | ^(IN exprOrSubquery inRhs ) | ^(NOT_IN exprOrSubquery inRhs ) - | ^(IS_NULL { _isNullComparison = true; } exprOrSubquery { _isNullComparison = false; }) + | ^(IS_NULL exprOrSubquery) | ^(IS_NOT_NULL exprOrSubquery) // | ^(IS_TRUE expr) // | ^(IS_FALSE expr) diff --git a/src/NHibernate/Hql/Ast/ANTLR/Tree/DotNode.cs b/src/NHibernate/Hql/Ast/ANTLR/Tree/DotNode.cs index 5c41ca001c4..f7743426826 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/Tree/DotNode.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/Tree/DotNode.cs @@ -417,8 +417,7 @@ private void DereferenceEntity(EntityType entityType, bool implicitJoin, string if ( joinIsNeeded ) { - var forceLeftJoin = comparisonWithNullableEntity && Walker.IsNullComparison; - DereferenceEntityJoin(classAlias, entityType, implicitJoin, parent, forceLeftJoin); + DereferenceEntityJoin(classAlias, entityType, implicitJoin, parent, comparisonWithNullableEntity); if (comparisonWithNullableEntity) { _columns = FromElement.GetIdentityColumns(); From a925c58c2cc6d3e5e17f8b066fe19975dcc0d59c Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Sun, 26 Mar 2023 13:24:48 +0300 Subject: [PATCH 06/11] Add entity name to mutable read-only cache warning log (#3268) Fixes #3210 --- src/NHibernate/Impl/SessionFactoryImpl.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/NHibernate/Impl/SessionFactoryImpl.cs b/src/NHibernate/Impl/SessionFactoryImpl.cs index b25b45410e6..458d5ecce9f 100644 --- a/src/NHibernate/Impl/SessionFactoryImpl.cs +++ b/src/NHibernate/Impl/SessionFactoryImpl.cs @@ -270,6 +270,7 @@ public SessionFactoryImpl(Configuration cfg, IMapping mapping, Settings settings model.RootClazz.CacheRegionName, model.CacheConcurrencyStrategy, model.IsMutable, + model.EntityName, caches); var cp = PersisterFactory.CreateClassPersister(model, cache, this, mapping); entityPersisters[model.EntityName] = cp; @@ -290,6 +291,7 @@ public SessionFactoryImpl(Configuration cfg, IMapping mapping, Settings settings model.CacheRegionName, model.CacheConcurrencyStrategy, model.Owner.IsMutable, + model.OwnerEntityName, caches); var persister = PersisterFactory.CreateCollectionPersister(model, cache, this); collectionPersisters[model.Role] = persister; @@ -446,10 +448,10 @@ private IQueryCache BuildQueryCache(string queryCacheName) properties); } - private ICacheConcurrencyStrategy GetCacheConcurrencyStrategy( - string cacheRegion, + private ICacheConcurrencyStrategy GetCacheConcurrencyStrategy(string cacheRegion, string strategy, bool isMutable, + string entityName, Dictionary, ICacheConcurrencyStrategy> caches) { if (strategy == null || !settings.IsSecondLevelCacheEnabled) @@ -462,7 +464,7 @@ private ICacheConcurrencyStrategy GetCacheConcurrencyStrategy( cache = CacheFactory.CreateCache(strategy, GetCache(cacheRegion), settings); caches.Add(cacheKey, cache); if (isMutable && strategy == CacheFactory.ReadOnly) - log.Warn("read-only cache configured for mutable: {0}", name); + log.Warn("read-only cache configured for mutable: {0}", entityName); return cache; } From 83db5074e5d72587098a86766c1e204d33a85435 Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Fri, 31 Mar 2023 12:43:12 +0300 Subject: [PATCH 07/11] LINQ subqueries wrongly altered by SelectClauseVisitor (#3271) --- src/NHibernate.Test/Async/Linq/WhereTests.cs | 11 ++++++++++ src/NHibernate.Test/Linq/WhereTests.cs | 11 ++++++++++ .../Linq/Visitors/QueryModelVisitor.cs | 21 +++++++++++++++---- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/NHibernate.Test/Async/Linq/WhereTests.cs b/src/NHibernate.Test/Async/Linq/WhereTests.cs index 56d183e49c9..5d8a936b342 100644 --- a/src/NHibernate.Test/Async/Linq/WhereTests.cs +++ b/src/NHibernate.Test/Async/Linq/WhereTests.cs @@ -644,6 +644,17 @@ where sheet.Users.Contains(user) Assert.That(query.Count, Is.EqualTo(2)); } + [Test] + public async Task TimesheetsWithEnumerableContainsOnSelectAsync() + { + var value = (EnumStoredAsInt32) 1000; + var query = await ((from sheet in db.Timesheets + where sheet.Users.Select(x => x.NullableEnum2 ?? value).Contains(value) + select sheet).ToListAsync()); + + Assert.That(query.Count, Is.EqualTo(1)); + } + [Test] public async Task SearchOnObjectTypeWithExtensionMethodAsync() { diff --git a/src/NHibernate.Test/Linq/WhereTests.cs b/src/NHibernate.Test/Linq/WhereTests.cs index 02dc58b34b7..0b4b5da6575 100644 --- a/src/NHibernate.Test/Linq/WhereTests.cs +++ b/src/NHibernate.Test/Linq/WhereTests.cs @@ -645,6 +645,17 @@ where sheet.Users.Contains(user) Assert.That(query.Count, Is.EqualTo(2)); } + [Test] + public void TimesheetsWithEnumerableContainsOnSelect() + { + var value = (EnumStoredAsInt32) 1000; + var query = (from sheet in db.Timesheets + where sheet.Users.Select(x => x.NullableEnum2 ?? value).Contains(value) + select sheet).ToList(); + + Assert.That(query.Count, Is.EqualTo(1)); + } + [Test] public void SearchOnObjectTypeWithExtensionMethod() { diff --git a/src/NHibernate/Linq/Visitors/QueryModelVisitor.cs b/src/NHibernate/Linq/Visitors/QueryModelVisitor.cs index 3c95d90c9ab..c9f2a054bb1 100644 --- a/src/NHibernate/Linq/Visitors/QueryModelVisitor.cs +++ b/src/NHibernate/Linq/Visitors/QueryModelVisitor.cs @@ -114,6 +114,7 @@ public static ExpressionToHqlTranslationResults GenerateHqlQuery(QueryModel quer private readonly NhLinqExpressionReturnType? _rootReturnType; private static readonly ResultOperatorMap ResultOperatorMap; private bool _serverSide = true; + private readonly bool _root; public VisitorParameters VisitorParameters { get; } @@ -161,6 +162,7 @@ private QueryModelVisitor(VisitorParameters visitorParameters, bool root, QueryM _queryMode = root ? visitorParameters.RootQueryMode : QueryMode.Select; VisitorParameters = visitorParameters; Model = queryModel; + _root = root; _rootReturnType = root ? rootReturnType : null; _hqlTree = new IntermediateHqlTree(root, _queryMode); } @@ -467,19 +469,27 @@ public override void VisitSelectClause(SelectClause selectClause, QueryModel que } //This is a standard select query + _hqlTree.AddSelectClause(GetSelectClause(selectClause.Selector)); + + base.VisitSelectClause(selectClause, queryModel); + } + + private HqlSelect GetSelectClause(Expression selectClause) + { + if (!_root) + return _hqlTree.TreeBuilder.Select( + HqlGeneratorExpressionVisitor.Visit(selectClause, VisitorParameters).AsExpression()); var visitor = new SelectClauseVisitor(typeof(object[]), VisitorParameters); - visitor.VisitSelector(selectClause.Selector); + visitor.VisitSelector(selectClause); if (visitor.ProjectionExpression != null) { _hqlTree.AddItemTransformer(visitor.ProjectionExpression); } - _hqlTree.AddSelectClause(_hqlTree.TreeBuilder.Select(visitor.GetHqlNodes())); - - base.VisitSelectClause(selectClause, queryModel); + return _hqlTree.TreeBuilder.Select(visitor.GetHqlNodes()); } private void VisitInsertClause(Expression expression) @@ -527,6 +537,9 @@ private void VisitUpdateClause(Expression expression) private void VisitDeleteClause(Expression expression) { + if (!_root) + return; + // We only need to check there is no unexpected select, for avoiding silently ignoring them. var visitor = new SelectClauseVisitor(typeof(object[]), VisitorParameters); visitor.VisitSelector(expression); From de2d941d03466e35b652e632069c94a43c6e21e9 Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Sun, 2 Apr 2023 13:55:15 +0300 Subject: [PATCH 08/11] Improve LINQ Contains subquery parameter detection (#3274) We should always try to detect parameters. And parameter detection shouldn't skip query transformation (Fixes failure case from #3212) Replaces #3212 --- src/NHibernate.Test/Async/Linq/WhereTests.cs | 22 ++++++++++++++ src/NHibernate.Test/Linq/WhereTests.cs | 22 ++++++++++++++ .../Linq/Visitors/ParameterTypeLocator.cs | 29 +++++++------------ 3 files changed, 55 insertions(+), 18 deletions(-) diff --git a/src/NHibernate.Test/Async/Linq/WhereTests.cs b/src/NHibernate.Test/Async/Linq/WhereTests.cs index 5d8a936b342..aceda381352 100644 --- a/src/NHibernate.Test/Async/Linq/WhereTests.cs +++ b/src/NHibernate.Test/Async/Linq/WhereTests.cs @@ -15,6 +15,7 @@ using System.Linq; using System.Linq.Expressions; using log4net.Core; +using NHibernate.Dialect; using NHibernate.Engine.Query; using NHibernate.Linq; using NHibernate.DomainModel.Northwind.Entities; @@ -647,6 +648,9 @@ where sheet.Users.Contains(user) [Test] public async Task TimesheetsWithEnumerableContainsOnSelectAsync() { + if (Dialect is MsSqlCeDialect) + Assert.Ignore("Dialect is not supported"); + var value = (EnumStoredAsInt32) 1000; var query = await ((from sheet in db.Timesheets where sheet.Users.Select(x => x.NullableEnum2 ?? value).Contains(value) @@ -655,6 +659,24 @@ where sheet.Users.Select(x => x.NullableEnum2 ?? value).Contains(value) Assert.That(query.Count, Is.EqualTo(1)); } + [Test] + public async Task ContainsSubqueryWithCoalesceStringEnumSelectAsync() + { + if (Dialect is MsSqlCeDialect || Dialect is SQLiteDialect) + Assert.Ignore("Dialect is not supported"); + + var results = + await (db.Timesheets.Where( + o => + o.Users + .Where(u => u.Id != 0.MappedAs(NHibernateUtil.Int32)) + .Select(u => u.Name == u.Name ? u.Enum1 : u.NullableEnum1.Value) + .Contains(EnumStoredAsString.Small)) + .ToListAsync()); + + Assert.That(results.Count, Is.EqualTo(1)); + } + [Test] public async Task SearchOnObjectTypeWithExtensionMethodAsync() { diff --git a/src/NHibernate.Test/Linq/WhereTests.cs b/src/NHibernate.Test/Linq/WhereTests.cs index 0b4b5da6575..5fffc56c052 100644 --- a/src/NHibernate.Test/Linq/WhereTests.cs +++ b/src/NHibernate.Test/Linq/WhereTests.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Linq.Expressions; using log4net.Core; +using NHibernate.Dialect; using NHibernate.Engine.Query; using NHibernate.Linq; using NHibernate.DomainModel.Northwind.Entities; @@ -648,6 +649,9 @@ where sheet.Users.Contains(user) [Test] public void TimesheetsWithEnumerableContainsOnSelect() { + if (Dialect is MsSqlCeDialect) + Assert.Ignore("Dialect is not supported"); + var value = (EnumStoredAsInt32) 1000; var query = (from sheet in db.Timesheets where sheet.Users.Select(x => x.NullableEnum2 ?? value).Contains(value) @@ -656,6 +660,24 @@ where sheet.Users.Select(x => x.NullableEnum2 ?? value).Contains(value) Assert.That(query.Count, Is.EqualTo(1)); } + [Test] + public void ContainsSubqueryWithCoalesceStringEnumSelect() + { + if (Dialect is MsSqlCeDialect || Dialect is SQLiteDialect) + Assert.Ignore("Dialect is not supported"); + + var results = + db.Timesheets.Where( + o => + o.Users + .Where(u => u.Id != 0.MappedAs(NHibernateUtil.Int32)) + .Select(u => u.Name == u.Name ? u.Enum1 : u.NullableEnum1.Value) + .Contains(EnumStoredAsString.Small)) + .ToList(); + + Assert.That(results.Count, Is.EqualTo(1)); + } + [Test] public void SearchOnObjectTypeWithExtensionMethod() { diff --git a/src/NHibernate/Linq/Visitors/ParameterTypeLocator.cs b/src/NHibernate/Linq/Visitors/ParameterTypeLocator.cs index eeb458568d0..3f5b37eab03 100644 --- a/src/NHibernate/Linq/Visitors/ParameterTypeLocator.cs +++ b/src/NHibernate/Linq/Visitors/ParameterTypeLocator.cs @@ -288,42 +288,35 @@ protected override Expression VisitConstant(ConstantExpression node) protected override Expression VisitSubQuery(SubQueryExpression node) { - if (!TryLinkContainsMethod(node.QueryModel)) - { - node.QueryModel.TransformExpressions(Visit); - } + TryLinkContainsMethod(node.QueryModel); + node.QueryModel.TransformExpressions(Visit); return node; } - private bool TryLinkContainsMethod(QueryModel queryModel) + private void TryLinkContainsMethod(QueryModel queryModel) { // ReLinq wraps all ResultOperatorExpressionNodeBase into a SubQueryExpression. In case of // ContainsResultOperator where the constant expression is dislocated from the related expression, // we have to manually link the related expressions. if (queryModel.ResultOperators.Count != 1 || - !(queryModel.ResultOperators[0] is ContainsResultOperator containsOperator) || - !(queryModel.SelectClause.Selector is QuerySourceReferenceExpression querySourceReference) || - !(querySourceReference.ReferencedQuerySource is MainFromClause mainFromClause)) + !(queryModel.ResultOperators[0] is ContainsResultOperator containsOperator)) { - return false; + return; } - var left = UnwrapUnary(Visit(mainFromClause.FromExpression)); + Expression selector = + queryModel.SelectClause.Selector is QuerySourceReferenceExpression { ReferencedQuerySource: MainFromClause mainFromClause } + ? mainFromClause.FromExpression + : queryModel.SelectClause.Selector; + + var left = UnwrapUnary(Visit(selector)); var right = UnwrapUnary(Visit(containsOperator.Item)); - // The constant is on the left side (e.g. db.Users.Where(o => users.Contains(o))) - // The constant is on the right side (e.g. db.Customers.Where(o => o.Orders.Contains(item))) - if (left.NodeType != ExpressionType.Constant && right.NodeType != ExpressionType.Constant) - { - return false; - } // Copy all found MemberExpressions to the constant expression // (e.g. values.Contains(o.Name != o.Name2 ? o.Enum1 : o.Enum2) -> copy o.Enum1 and o.Enum2) AddRelatedExpression(null, left, right); AddRelatedExpression(null, right, left); - - return true; } private void VisitAssign(Expression leftNode, Expression rightNode) From 484897af769b0c5aa87e1bf76b1ec09732e51459 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delaporte?= <12201973+fredericDelaporte@users.noreply.github.com> Date: Sun, 2 Apr 2023 14:08:29 +0200 Subject: [PATCH 09/11] Enable dev build for 5.3.16 --- build-common/NHibernate.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build-common/NHibernate.props b/build-common/NHibernate.props index cad830b3805..9fa99426a61 100644 --- a/build-common/NHibernate.props +++ b/build-common/NHibernate.props @@ -3,9 +3,9 @@ 5.3 - 15 + 16 - + dev $(NhVersion).$(VersionPatch) $(VersionSuffix).$(BuildNumber) From 342c01f18bf48bf2b20c6515ede7c8c63038aaf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delaporte?= <12201973+fredericDelaporte@users.noreply.github.com> Date: Sun, 2 Apr 2023 19:06:05 +0200 Subject: [PATCH 10/11] Release 5.3.16 (#3276) --- build-common/NHibernate.props | 2 +- releasenotes.txt | 19 ++++++++++++++++++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/build-common/NHibernate.props b/build-common/NHibernate.props index 9fa99426a61..9f4d60b50c2 100644 --- a/build-common/NHibernate.props +++ b/build-common/NHibernate.props @@ -5,7 +5,7 @@ 5.3 16 - dev + $(NhVersion).$(VersionPatch) $(VersionSuffix).$(BuildNumber) diff --git a/releasenotes.txt b/releasenotes.txt index 0dd270c943e..feca1375199 100644 --- a/releasenotes.txt +++ b/releasenotes.txt @@ -1,4 +1,21 @@ -Build 5.3.15 +Build 5.3.16 +============================= + +Release notes - NHibernate - Version 5.3.16 + +3 issues were resolved in this release. + +** Bug + + * #3269 "Or" clause in a "where" condition returns a wrong result with not-found-ignore + * #3210 Wrong name value for L2 read-only cache warning on mutable + +** Task + + * #3276 Release 5.3.16 + + +Build 5.3.15 ============================= Release notes - NHibernate - Version 5.3.15 From cddbac13adbd29883038f72d1b82e4e50b73cd27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delaporte?= <12201973+fredericDelaporte@users.noreply.github.com> Date: Wed, 5 Apr 2023 18:58:59 +0200 Subject: [PATCH 11/11] Release 5.4.2 (#3277) --- build-common/NHibernate.props | 2 +- releasenotes.txt | 29 +++++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/build-common/NHibernate.props b/build-common/NHibernate.props index a6cc2ceb822..b7cc6a6a984 100644 --- a/build-common/NHibernate.props +++ b/build-common/NHibernate.props @@ -5,7 +5,7 @@ 5.4 2 - dev + 9.0 $(NhVersion).$(VersionPatch) diff --git a/releasenotes.txt b/releasenotes.txt index b16d3d4e60d..5fd64fc3975 100644 --- a/releasenotes.txt +++ b/releasenotes.txt @@ -1,9 +1,33 @@ -Build 5.4.1 +Build 5.4.2 +============================= + +Release notes - NHibernate - Version 5.4.2 + +6 issues were resolved in this release. + +** Bug + + * #3274 Improve LINQ Contains subquery parameter detection + * #3271 LINQ subqueries wrongly altered by SelectClauseVisitor + * #3263 Wrong alias in Where clause if using Fetch and scalar Select + * #3239 Incorrect SQL generated fetching many-to-many with subclasses + +** New Feature + + * #3251 MappingByCode: Support backfield property access + +** Task + + * #3281 Merge 5.3.16 in 5.4.x + * #3277 Release 5.4.2 + + +Build 5.4.1 ============================= Release notes - NHibernate - Version 5.4.1 -4 issues were resolved in this release. +5 issues were resolved in this release. ** Bug @@ -14,6 +38,7 @@ Release notes - NHibernate - Version 5.4.1 ** Task * #3232 Release 5.4.1 + * #3227 Merge 5.3.15 in 5.4.x As part of releasing 5.4.1, a missing 5.4.0 possible breaking change has been added, about one-to-one associations and optimistic locking. See 5.4.0 possible breaking changes.