diff --git a/src/NHibernate.DomainModel/Northwind/Entities/Animal.cs b/src/NHibernate.DomainModel/Northwind/Entities/Animal.cs index 384e6e760d5..507a30a96a3 100644 --- a/src/NHibernate.DomainModel/Northwind/Entities/Animal.cs +++ b/src/NHibernate.DomainModel/Northwind/Entities/Animal.cs @@ -16,7 +16,12 @@ public class Animal public virtual Animal FatherOrMother => Father ?? Mother; } - public abstract class Reptile : Animal + public interface IReptile + { + EnumStoredAsString Enum1 { get; } + } + + public abstract class Reptile : Animal, IReptile { public virtual double BodyTemperature { get; set; } diff --git a/src/NHibernate.Test/Async/NHSpecificTest/GH2858/FixtureByCode.cs b/src/NHibernate.Test/Async/NHSpecificTest/GH2858/FixtureByCode.cs new file mode 100644 index 00000000000..ee92242df19 --- /dev/null +++ b/src/NHibernate.Test/Async/NHSpecificTest/GH2858/FixtureByCode.cs @@ -0,0 +1,187 @@ +//------------------------------------------------------------------------------ +// +// 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; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Cfg.MappingSchema; +using NHibernate.Mapping.ByCode; +using NUnit.Framework; +using NHibernate.Linq; + +namespace NHibernate.Test.NHSpecificTest.GH2858 +{ + using System.Threading.Tasks; + [TestFixture] + public class ByCodeFixtureAsync : TestCaseMappingByCode + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + + mapper.Class( + rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + }); + mapper.Class( + rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.ManyToOne(x => x.Department, m => m.Column("DepartmentId")); + }); + mapper.Class( + rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.IdBag( + x => x.Departments, + m => m.Table("IssuesToDepartments"), + r => r.ManyToMany()); + rc.ManyToOne(x => x.Project, m => m.Column("ProjectId")); + }); + mapper.Class( + rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.ManyToOne(x => x.Issue, m => m.Column("IssueId")); + rc.Property(x => x.Seconds); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + + protected override void OnSetUp() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + var deptA = new Department {Name = "A"}; + session.Save(deptA); + var deptB = new Department {Name = "B"}; + session.Save(deptB); + var deptC = new Department {Name = "C"}; + session.Save(deptC); + var deptD = new Department {Name = "D"}; + session.Save(deptD); + var deptE = new Department {Name = "E"}; + session.Save(deptE); + + var projectX = new Project {Name = "X", Department = deptA}; + session.Save(projectX); + var projectY = new Project {Name = "Y", Department = deptC}; + session.Save(projectY); + var projectZ = new Project {Name = "Z", Department = deptE}; + session.Save(projectZ); + + var issue1 = new Issue {Name = "TEST-1", Project = projectX,}; + session.Save(issue1); + var issue2 = new Issue {Name = "TEST-2", Project = projectX, Departments = {deptA},}; + session.Save(issue2); + var issue3 = new Issue {Name = "TEST-3", Project = projectX, Departments = {deptA, deptB},}; + session.Save(issue3); + var issue4 = new Issue {Name = "TEST-4", Project = projectY,}; + session.Save(issue4); + var issue5 = new Issue {Name = "TEST-5", Project = projectY, Departments = {deptD}}; + session.Save(issue5); + + session.Save(new TimeChunk {Issue = issue1}); + session.Save(new TimeChunk {Issue = issue1}); + session.Save(new TimeChunk {Issue = issue2}); + session.Save(new TimeChunk {Issue = issue2}); + session.Save(new TimeChunk {Issue = issue3}); + session.Save(new TimeChunk {Issue = issue3}); + session.Save(new TimeChunk {Issue = issue4}); + session.Save(new TimeChunk {Issue = issue4}); + session.Save(new TimeChunk {Issue = issue5}); + session.Save(new TimeChunk {Issue = issue5}); + + transaction.Commit(); + } + } + + protected override void OnTearDown() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + session.Delete("from System.Object"); + transaction.Commit(); + } + } + + [Test(Description = "GH-2857")] + public async Task GroupLevelQueryAsync() + { + using (var session = OpenSession()) + { + var query = session.Query() + .Select(x => new object[] {(object) x}) + .GroupBy(g => new object[] {(Guid?) (((ITimeChunk) g[0]).Issue.Project.Id)}, v => (ITimeChunk) v[0]) + .Select(r => new object[] {r.Key, r.Sum(t => (int?) t.Seconds)}); + + var results = await (query.ToListAsync()); + Assert.That(results, Has.Count.EqualTo(2)); + } + } + + [Test(Description = "GH-2857")] + public async Task GroupLevelQuery_SimplifiedAsync() + { + using (var session = OpenSession()) + { + var query = session.Query() + .Select(x => new object[] {x}) + .GroupBy(g => new object[] {((ITimeChunk) g[0]).Issue.Project.Id}, v => (ITimeChunk) v[0]) + .Select(r => new object[] {r.Key, r.Sum(t => (int?) t.Seconds)}); + + var results = await (query.ToListAsync()); + Assert.That(results, Has.Count.EqualTo(2)); + } + } + + [Test] + public async Task SelectManySubQueryWithCoalesceAsync() + { + using (var session = OpenSession()) + { + var usedDepartments = session.Query() + .SelectMany(x => ((IEnumerable) x.Issue.Departments).DefaultIfEmpty().Select(d => (object) ((Guid?) ((Guid?) (((IDepartment) d).Id) ?? x.Issue.Project.Department.Id)))) + .Where(id => id != null) + .Select(id => (Guid?) id); + + var result = session.Query() + .Where(d => usedDepartments.Contains(d.Id)) + .Select(d => new {d.Id, d.Name}); + + Assert.That(await (result.ToListAsync()), Has.Count.EqualTo(4)); + } + } + + [Test] + public async Task SelectManySubQueryWithCoalesce_SimplifiedAsync() + { + using (var session = OpenSession()) + { + var usedDepartments = session.Query() + .SelectMany(x => ((IEnumerable) x.Issue.Departments).DefaultIfEmpty().Select(d => (Guid?) ((IDepartment) d).Id ?? x.Issue.Project.Department.Id)); + + var result = session.Query() + .Where(d => usedDepartments.Contains(d.Id)) + .Select(d => new {d.Id, d.Name}); + + Assert.That(await (result.ToListAsync()), Has.Count.EqualTo(4)); + } + } + } +} diff --git a/src/NHibernate.Test/Linq/ParameterTypeLocatorTests.cs b/src/NHibernate.Test/Linq/ParameterTypeLocatorTests.cs index 50be6a9e470..771d5c9fd73 100644 --- a/src/NHibernate.Test/Linq/ParameterTypeLocatorTests.cs +++ b/src/NHibernate.Test/Linq/ParameterTypeLocatorTests.cs @@ -97,6 +97,16 @@ public void SubClassStringEnumTest() ); } + [Test] + public void SubClassAsUnamppedInterfaceStringEnumTest() + { + AssertResults( + new Dictionary> {{"0", o => o is EnumStoredAsStringType}}, + db.Animals.Where(o => o.Children.OfType().Any(r => r.Enum1 == EnumStoredAsString.Unspecified)), + db.Animals.Where(o => o.Children.OfType().Any(r => EnumStoredAsString.Unspecified == r.Enum1)) + ); + } + [Test] public void EqualsMethodStringTest() { diff --git a/src/NHibernate.Test/Linq/TryGetMappedTests.cs b/src/NHibernate.Test/Linq/TryGetMappedTests.cs index 880ce2e3e69..964072efdc2 100644 --- a/src/NHibernate.Test/Linq/TryGetMappedTests.cs +++ b/src/NHibernate.Test/Linq/TryGetMappedTests.cs @@ -124,7 +124,7 @@ public void SelfCastNotMappedTest() false, typeof(A).FullName, null, - o => o is SerializableType serializableType && serializableType.ReturnedClass == typeof(object)); + o => o is EntityType entityType && entityType.ReturnedClass == typeof(A)); } [Test] diff --git a/src/NHibernate.Test/NHSpecificTest/GH2858/Entity.cs b/src/NHibernate.Test/NHSpecificTest/GH2858/Entity.cs new file mode 100644 index 00000000000..108d0a38aae --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH2858/Entity.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; + +namespace NHibernate.Test.NHSpecificTest.GH2858 +{ + public interface IDepartment + { + Guid Id { get; set; } + string Name { get; set; } + } + + public class Department : IDepartment + { + public virtual Guid Id { get; set; } + public virtual string Name { get; set; } + } + + public interface IProject + { + Guid Id { get; set; } + string Name { get; set; } + Department Department { get; set; } + } + + public class Project : IProject + { + public virtual Guid Id { get; set; } + public virtual string Name { get; set; } + public virtual Department Department { get; set; } + } + + public interface IIssue + { + Guid Id { get; set; } + string Name { get; set; } + Project Project { get; set; } + IList Departments { get; set; } + } + + public class Issue : IIssue + { + public virtual Guid Id { get; set; } + public virtual string Name { get; set; } + public virtual IList Departments { get; set; } = new List(); + public virtual Project Project { get; set; } + } + + public interface ITimeChunk + { + Guid Id { get; set; } + Issue Issue { get; set; } + int Seconds { get; set; } + } + + public class TimeChunk : ITimeChunk + { + public virtual Guid Id { get; set; } + public virtual Issue Issue { get; set; } + public virtual int Seconds { get; set; } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH2858/FixtureByCode.cs b/src/NHibernate.Test/NHSpecificTest/GH2858/FixtureByCode.cs new file mode 100644 index 00000000000..9e4008433e1 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH2858/FixtureByCode.cs @@ -0,0 +1,175 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Cfg.MappingSchema; +using NHibernate.Mapping.ByCode; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.GH2858 +{ + [TestFixture] + public class ByCodeFixture : TestCaseMappingByCode + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + + mapper.Class( + rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + }); + mapper.Class( + rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.ManyToOne(x => x.Department, m => m.Column("DepartmentId")); + }); + mapper.Class( + rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name); + rc.IdBag( + x => x.Departments, + m => m.Table("IssuesToDepartments"), + r => r.ManyToMany()); + rc.ManyToOne(x => x.Project, m => m.Column("ProjectId")); + }); + mapper.Class( + rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.ManyToOne(x => x.Issue, m => m.Column("IssueId")); + rc.Property(x => x.Seconds); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + + protected override void OnSetUp() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + var deptA = new Department {Name = "A"}; + session.Save(deptA); + var deptB = new Department {Name = "B"}; + session.Save(deptB); + var deptC = new Department {Name = "C"}; + session.Save(deptC); + var deptD = new Department {Name = "D"}; + session.Save(deptD); + var deptE = new Department {Name = "E"}; + session.Save(deptE); + + var projectX = new Project {Name = "X", Department = deptA}; + session.Save(projectX); + var projectY = new Project {Name = "Y", Department = deptC}; + session.Save(projectY); + var projectZ = new Project {Name = "Z", Department = deptE}; + session.Save(projectZ); + + var issue1 = new Issue {Name = "TEST-1", Project = projectX,}; + session.Save(issue1); + var issue2 = new Issue {Name = "TEST-2", Project = projectX, Departments = {deptA},}; + session.Save(issue2); + var issue3 = new Issue {Name = "TEST-3", Project = projectX, Departments = {deptA, deptB},}; + session.Save(issue3); + var issue4 = new Issue {Name = "TEST-4", Project = projectY,}; + session.Save(issue4); + var issue5 = new Issue {Name = "TEST-5", Project = projectY, Departments = {deptD}}; + session.Save(issue5); + + session.Save(new TimeChunk {Issue = issue1}); + session.Save(new TimeChunk {Issue = issue1}); + session.Save(new TimeChunk {Issue = issue2}); + session.Save(new TimeChunk {Issue = issue2}); + session.Save(new TimeChunk {Issue = issue3}); + session.Save(new TimeChunk {Issue = issue3}); + session.Save(new TimeChunk {Issue = issue4}); + session.Save(new TimeChunk {Issue = issue4}); + session.Save(new TimeChunk {Issue = issue5}); + session.Save(new TimeChunk {Issue = issue5}); + + transaction.Commit(); + } + } + + protected override void OnTearDown() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + session.Delete("from System.Object"); + transaction.Commit(); + } + } + + [Test(Description = "GH-2857")] + public void GroupLevelQuery() + { + using (var session = OpenSession()) + { + var query = session.Query() + .Select(x => new object[] {(object) x}) + .GroupBy(g => new object[] {(Guid?) (((ITimeChunk) g[0]).Issue.Project.Id)}, v => (ITimeChunk) v[0]) + .Select(r => new object[] {r.Key, r.Sum(t => (int?) t.Seconds)}); + + var results = query.ToList(); + Assert.That(results, Has.Count.EqualTo(2)); + } + } + + [Test(Description = "GH-2857")] + public void GroupLevelQuery_Simplified() + { + using (var session = OpenSession()) + { + var query = session.Query() + .Select(x => new object[] {x}) + .GroupBy(g => new object[] {((ITimeChunk) g[0]).Issue.Project.Id}, v => (ITimeChunk) v[0]) + .Select(r => new object[] {r.Key, r.Sum(t => (int?) t.Seconds)}); + + var results = query.ToList(); + Assert.That(results, Has.Count.EqualTo(2)); + } + } + + [Test] + public void SelectManySubQueryWithCoalesce() + { + using (var session = OpenSession()) + { + var usedDepartments = session.Query() + .SelectMany(x => ((IEnumerable) x.Issue.Departments).DefaultIfEmpty().Select(d => (object) ((Guid?) ((Guid?) (((IDepartment) d).Id) ?? x.Issue.Project.Department.Id)))) + .Where(id => id != null) + .Select(id => (Guid?) id); + + var result = session.Query() + .Where(d => usedDepartments.Contains(d.Id)) + .Select(d => new {d.Id, d.Name}); + + Assert.That(result.ToList(), Has.Count.EqualTo(4)); + } + } + + [Test] + public void SelectManySubQueryWithCoalesce_Simplified() + { + using (var session = OpenSession()) + { + var usedDepartments = session.Query() + .SelectMany(x => ((IEnumerable) x.Issue.Departments).DefaultIfEmpty().Select(d => (Guid?) ((IDepartment) d).Id ?? x.Issue.Project.Department.Id)); + + var result = session.Query() + .Where(d => usedDepartments.Contains(d.Id)) + .Select(d => new {d.Id, d.Name}); + + Assert.That(result.ToList(), Has.Count.EqualTo(4)); + } + } + } +} diff --git a/src/NHibernate/Util/ExpressionsHelper.cs b/src/NHibernate/Util/ExpressionsHelper.cs index 221b8031266..efa12b4e661 100644 --- a/src/NHibernate/Util/ExpressionsHelper.cs +++ b/src/NHibernate/Util/ExpressionsHelper.cs @@ -173,7 +173,14 @@ internal static bool TryGetMappedNullability( return true; } - index = entityPersister.EntityMetamodel.GetPropertyIndex(memberPath); + var propIndex = entityPersister.EntityMetamodel.GetPropertyIndexOrNull(memberPath); + if (propIndex == null) + { + nullable = false; + return false; + } + + index = propIndex.Value; nullable = entityPersister.PropertyNullability[index]; return true; } @@ -338,7 +345,7 @@ private static bool TraverseMembers( // Traverse the members that were traversed by the TryGetAllMemberMetadata method in the reverse order and try to keep // tracking the entity persister until all members are traversed. var member = memberPaths.Pop(); - var currentType = currentEntityPersister.EntityMetamodel.GetPropertyType(member.Path); + var currentType = GetPropertyType(currentEntityPersister, member.Path); IAbstractComponentType currentComponentType = null; while (memberPaths.Count > 0 && currentType != null) { @@ -407,6 +414,12 @@ private static bool TraverseMembers( return false; } + private static IType GetPropertyType(IEntityPersister currentEntityPersister, string path) + { + ((IPropertyMapping) currentEntityPersister).TryToType(path, out var type); + return type; + } + private static IType TryGetComponentPropertyType(IAbstractComponentType componentType, string memberPath) { var index = Array.IndexOf(componentType.PropertyNames, memberPath); @@ -471,7 +484,7 @@ private static void ProcessAssociationType( memberComponent = null; memberType = memberPersister != null - ? memberPersister.EntityMetamodel.GetPropertyType(member.Path) + ? GetPropertyType(memberPersister, member.Path) : null; // q.AnyType.Member, ((NotMappedClass)q.ManyToOne) } @@ -523,10 +536,16 @@ private static bool TryGetEntityPersister( .Select(sessionFactory.GetEntityPersister) .FirstOrDefault(p => p.MappedClass == convertedType); - return persister != null; + if (persister != null) + return true; } + else if (TryGetEntityPersister(convertedType, sessionFactory, out persister)) + return true; - return TryGetEntityPersister(convertedType, sessionFactory, out persister); + // Assume type conversion doesn't change entity type + // TODO: Consider removing convertedType related logic above and always return currentEntityPersister + persister = currentEntityPersister; + return true; } private static bool TryGetEntityPersister(