diff --git a/releasenotes.txt b/releasenotes.txt index 420c05ea2ff..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. @@ -209,6 +234,23 @@ Release notes - NHibernate - Version 5.4.0 * #2242 Test case for NH-3972 - SQL error when selecting a column of a subclass when sibling classes have a column of the same name +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 ============================= diff --git a/src/NHibernate.Test/Async/Hql/EntityJoinHqlTest.cs b/src/NHibernate.Test/Async/Hql/EntityJoinHqlTest.cs index d185faef1e0..e1c6011fcf2 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/Async/Linq/WhereTests.cs b/src/NHibernate.Test/Async/Linq/WhereTests.cs index 3be794730b5..71b9a76cc99 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; @@ -670,6 +671,38 @@ where sheet.Users.Contains(user) Assert.That(query.Count, Is.EqualTo(2)); } + [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) + select sheet).ToListAsync()); + + 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/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/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/Hql/EntityJoinHqlTest.cs b/src/NHibernate.Test/Hql/EntityJoinHqlTest.cs index 4833f1de14f..9c55fbb54d7 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.Test/Linq/WhereTests.cs b/src/NHibernate.Test/Linq/WhereTests.cs index ab6d6ec8763..99edd450d62 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; @@ -671,6 +672,38 @@ where sheet.Users.Contains(user) Assert.That(query.Count, Is.EqualTo(2)); } + [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) + select sheet).ToList(); + + 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.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.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.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; } diff --git a/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs b/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs index 39867333255..7cc0bcae9c6 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.cs @@ -43,7 +43,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; @@ -1301,8 +1300,6 @@ public IASTFactory ASTFactory } } - internal bool IsNullComparison => _isNullComparison; - public void AddQuerySpaces(IEntityPersister persister) { AddPersister(persister); diff --git a/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g b/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g index 60d16331c53..47b42f286ea 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g +++ b/src/NHibernate/Hql/Ast/ANTLR/HqlSqlWalker.g @@ -391,7 +391,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 2bbfeb03e7d..f0ac747bf4d 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/Tree/DotNode.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/Tree/DotNode.cs @@ -426,8 +426,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(); @@ -558,6 +557,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); } diff --git a/src/NHibernate/Impl/SessionFactoryImpl.cs b/src/NHibernate/Impl/SessionFactoryImpl.cs index b7731943118..8ff1e9d1599 100644 --- a/src/NHibernate/Impl/SessionFactoryImpl.cs +++ b/src/NHibernate/Impl/SessionFactoryImpl.cs @@ -277,6 +277,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; @@ -302,6 +303,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; @@ -463,10 +465,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 || strategy == CacheFactory.Never || !settings.IsSecondLevelCacheEnabled) @@ -479,7 +481,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; } 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) 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); 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 +}