diff --git a/Orm/Xtensive.Orm.Tests/Issues/IssueJira0743_IncludeDoesNotWorkWithSubqueries.cs b/Orm/Xtensive.Orm.Tests/Issues/IssueJira0743_IncludeDoesNotWorkWithSubqueries.cs new file mode 100644 index 0000000000..aa53998fcb --- /dev/null +++ b/Orm/Xtensive.Orm.Tests/Issues/IssueJira0743_IncludeDoesNotWorkWithSubqueries.cs @@ -0,0 +1,596 @@ +// Copyright (C) 2018-2020 Xtensive LLC. +// This code is distributed under MIT license terms. +// See the License.txt file in the project root for more information. +// Created by: Denis Kudelin +// Created: 2018.10.18 + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using NUnit.Framework; +using Xtensive.Orm.Configuration; +using Xtensive.Orm.Providers; +using Xtensive.Orm.Tests.Issues.IssueJira0743_IncludeDoesNotWorkWithSubqueriesModel; + +namespace Xtensive.Orm.Tests.Issues +{ + public class IssueJira0743_IncludeDoesNotWorkWithSubqueries : AutoBuildTest + { + protected override void CheckRequirements() + { + Require.AllFeaturesSupported(ProviderFeatures.ScalarSubqueries); + } + + protected override DomainConfiguration BuildConfiguration() + { + var configuration = base.BuildConfiguration(); + + configuration.Types.Register(typeof(TestEntity).Assembly, typeof(TestEntity).Namespace); + configuration.UpgradeMode = DomainUpgradeMode.Recreate; + + Expression> exp = e => e.List.FirstOrDefault().Link; + Expression> exp2 = e => e.LinkOnList.Link; + + configuration.LinqExtensions.Register(typeof(TestEntity).GetProperty("VirtualList"), exp); + configuration.LinqExtensions.Register(typeof(TestEntity).GetProperty("VirtualLink"), exp2); + return configuration; + } + + protected override void PopulateData() + { + using (var session = Domain.OpenSession()) + using (var transaction = session.OpenTransaction()) { + var item = new TestEntity(session) { String = "test1" }; + var item2 = new TestEntity2(session) { String = "test2", Value3 = MyEnum.Foo }; + item.Link = item2; + var list = new ListEntity(session) { String = "test3", Owner = item, Link = item2 }; + item.LinkOnList = list; + var item3 = new TestEntity(session) { String = "test4" }; + + transaction.Complete(); + } + } + + [Test] + public void Case01Test() + { + using (var session = Domain.OpenSession()) + using (var transaction = session.OpenTransaction()) { + var ids = new long[] { 1 }; + var result = session.Query.All() + .Where(e => ids.Contains(e.List.FirstOrDefault().Link.Id) && e.List.FirstOrDefault().Link.Id != 0) + .ToArray(); + Assert.That(result.Single().Link.Id, Is.EqualTo(1)); + } + } + + [Test] + public void Case02Test() + { + using (var session = Domain.OpenSession()) + using (var transaction = session.OpenTransaction()) { + var ids = new long[] { 1 }; + var result = session.Query.All() + .Where(e => e.List.FirstOrDefault().Link.Id != 0 && ids.Contains(e.List.FirstOrDefault().Link.Id)) + .ToArray(); + Assert.That(result.Single().Link.Id, Is.EqualTo(1)); + } + } + + [Test] + public void Case03Test() + { + using (var session = Domain.OpenSession()) + using (var transaction = session.OpenTransaction()) { + var ids = new long[] { 1 }; + var result = session.Query.All() + .Where(e => ids.Contains(e.List.FirstOrDefault().Link.Id)) + .ToArray(); + Assert.That(result.Single().Link.Id, Is.EqualTo(1)); + } + } + + [Test] + public void Case04Test() + { + using (var session = Domain.OpenSession()) + using (var transaction = session.OpenTransaction()) { + var result = session.Query.All() + .Where(e => e.Link != null && e.Link.Id != 632) + .OrderBy(e => e.Link) + .ToArray(); + + Assert.That(result.Single().Link.Id, Is.EqualTo(1)); + } + } + + [Test] + public void Case05Test() + { + using (var session = Domain.OpenSession()) + using (var transaction = session.OpenTransaction()) { + var result = session.Query.All() + .Where(e => e.VirtualLink != null && e.VirtualLink.Id != 6456) + .OrderBy(e => e.VirtualLink) + .ToArray(); + + Assert.That(result.Single().Link.Id, Is.EqualTo(1)); + } + } + + [Test] + public void Case06Test() + { + using (var session = Domain.OpenSession()) + using (var transaction = session.OpenTransaction()) { + var result = session.Query.All() + .Where(e => e.VirtualList != null && e.VirtualList.Id != 2457567L) + .OrderBy(e => e.VirtualList) + .ToArray(); + Assert.That(result.Single().Link.Id, Is.EqualTo(1)); + } + } + + [Test] + public void Case07Test() + { + using (var session = Domain.OpenSession()) + using (var transaction = session.OpenTransaction()) { + var ids = new long[] { 325453 }; + var result = session.Query.All() + .Where(e => e.Link != null && !ids.Contains(e.Link.Id)) + .OrderBy(e => e.Link) + .ToArray(); + + Assert.That(result.Single().Link.Id, Is.EqualTo(1)); + } + } + + [Test] + public void Case08Test() + { + using (var session = Domain.OpenSession()) + using (var transaction = session.OpenTransaction()) { + var ids = new long[] { 325453 }; + var result = session.Query.All() + .Where(e => e.VirtualLink != null && !ids.Contains(e.VirtualLink.Id)) + .OrderBy(e => e.VirtualLink) + .ToArray(); + + Assert.That(result.Single().Link.Id, Is.EqualTo(1)); + } + } + + [Test] + public void Case09Test() + { + using (var session = Domain.OpenSession()) + using (var transaction = session.OpenTransaction()) { + var ids = new long[] { 325453 }; + var result = session.Query.All() + .Where(e => e.VirtualList != null && !ids.Contains(e.VirtualList.Id)) + .OrderBy(e => e.VirtualList) + .ToArray(); + Assert.That(result.Single().Link.Id, Is.EqualTo(1)); + } + } + + [Test] + public void Case10Test() + { + using (var session = Domain.OpenSession()) + using (var transaction = session.OpenTransaction()) { + var result = session.Query.All() + .Where(e => e.Link.Id != 4573567L) + .OrderBy(e => e.Link) + .ToArray(); + + Assert.That(result.Single().Link.Id, Is.EqualTo(1)); + } + } + + [Test] + public void Case11Test() + { + using (var session = Domain.OpenSession()) + using (var transaction = session.OpenTransaction()) { + var result = session.Query.All() + .Where(e => e.VirtualLink.Id != 45275466L) + .OrderBy(e => e.VirtualLink) + .ToArray(); + + Assert.That(result.Single().Link.Id, Is.EqualTo(1)); + } + } + + [Test] + public void Case12Test() + { + using (var session = Domain.OpenSession()) + using (var transaction = session.OpenTransaction()) { + var result = session.Query.All() + .Where(e => e.VirtualList.Id != 247456L) + .OrderBy(e => e.VirtualList) + .ToArray(); + + Assert.That(result.Single().Link.Id, Is.EqualTo(1)); + } + } + + [Test] + public void Case13Test() + { + using (var session = Domain.OpenSession()) + using (var transaction = session.OpenTransaction()) { + var ids = new long[] { 325453 }; + var result = session.Query.All() + .Where(e => !ids.Contains(e.Link.Id)) + .OrderBy(e => e.Link) + .ToArray(); + + Assert.That(result.Single().Link.Id, Is.EqualTo(1)); + } + } + + [Test] + public void Case14Test() + { + using (var session = Domain.OpenSession()) + using (var transaction = session.OpenTransaction()) { + var ids = new long[] { 325453 }; + var result = session.Query.All() + .Where(e => !ids.Contains(e.VirtualLink.Id)) + .OrderBy(e => e.VirtualLink) + .ToArray(); + + Assert.That(result.Single().Link.Id, Is.EqualTo(1)); + } + } + + [Test] + public void Case15Test() + { + using (var session = Domain.OpenSession()) + using (var transaction = session.OpenTransaction()) { + var ids = new long[] { 325453 }; + var result = session.Query.All() + .Where(e => !ids.Contains(e.VirtualList.Id)) + .OrderBy(e => e.VirtualList) + .ToArray(); + + Assert.That(result.Single().Link.Id, Is.EqualTo(1)); + } + } + + [Test] + public void Case16Test() + { + using (var session = Domain.OpenSession()) + using (var transaction = session.OpenTransaction()) { + var ids = new long[] { 325453 }; + var result = session.Query.All() + .Where(e => !ids.Contains(e.VirtualLink.Id)) + .OrderBy(e => e.VirtualList) + .ToArray(); + + Assert.That(result.Single().Link.Id, Is.EqualTo(1)); + } + } + + [Test] + public void Case17Test() + { + using (var session = Domain.OpenSession()) + using (var transaction = session.OpenTransaction()) { + var ids = new[] { "test1", "test2" }; + var result = session.Query.All() + .Where(e => ids.Contains(e.List.Where(y => y.Id != Guid.NewGuid()).First().Owner.List.FirstOrDefault().Link.String)) + .OrderBy(e => e.VirtualList) + .ToArray(); + + Assert.That(result.Single().Link.Id, Is.EqualTo(1)); + } + } + + [Test] + public void Case18Test() + { + using (var session = Domain.OpenSession()) + using (var transaction = session.OpenTransaction()) { + var ids = new[] { "test1", "test2" }; + var result = session.Query.All() + .Where(e => e.List.Where(y => y.Id != Guid.NewGuid()).First().Owner.List.FirstOrDefault().Link.String.In(ids)) + .OrderBy(e => e.VirtualList) + .ToArray(); + + Assert.That(result.Single().Link.Id, Is.EqualTo(1)); + } + } + + [Test] + public void Case19Test() + { + using (var session = Domain.OpenSession()) + using (var transaction = session.OpenTransaction()) { + var ids = new long[] { 1 }; + var result = session.Query.All() + .Where(e => e.List.FirstOrDefault().Link.Id.In(ids)) + .ToArray(); + + Assert.That(result.Single().Link.Id, Is.EqualTo(1)); + } + } + + [Test] + public void Case20Test() + { + using (var session = Domain.OpenSession()) + using (var transaction = session.OpenTransaction()) { + var result = session.Query.All() + .Where(e => e.List.FirstOrDefault().Link.Id.In(1L, 34654565637L, 45756723L)) + .ToArray(); + + Assert.That(result.Single().Link.Id, Is.EqualTo(1)); + } + } + + [Test] + public void Case21Test() + { + using (var session = Domain.OpenSession()) + using (var tx = session.OpenTransaction()) { + var values = new[] { MyEnum.Bar, MyEnum.Foo }; + var result = session.Query.All() + .Select(e => values.Contains(e.List.FirstOrDefault().Link.Value3.Value)).ToArray(); + + Assert.That(result.Single(), Is.True); + } + } + + [Test] + public void Case22Test() + { + using (var session = Domain.OpenSession()) + using (var tx = session.OpenTransaction()) { + var values = new[] { MyEnum.Bar, MyEnum.Foo }; + var result = session.Query.All() + .Select(e => e.List.FirstOrDefault().Link.Value3.Value.In(values)).ToArray(); + + Assert.That(result.Single(), Is.True); + } + } + + [Test] + public void Case23Test() + { + using (var session = Domain.OpenSession()) + using (var tx = session.OpenTransaction()) { + var values = new long[] { 56, 89, 0 }; + var result = session.Query.All() + .Select(e => values.Contains(e.Link.Value2.GetValueOrDefault())).ToArray(); + + Assert.That(result, Is.All.True); + } + } + + [Test] + public void Case24Test() + { + using (var session = Domain.OpenSession()) + using (var tx = session.OpenTransaction()) { + var values = new long[] { 56, 89, 0 }; + var result = session.Query.All() + .Select(e => e.List.FirstOrDefault().Link.Value2.GetValueOrDefault().In(values)).ToArray(); + + Assert.That(result, Is.All.True); + } + } + + [Test] + public void Case25Test() + { + using (var session = Domain.OpenSession()) + using (var tx = session.OpenTransaction()) { + var values = new[] { "st1", "st2" }; + var result = session.Query.All() + .Select(e => values.Contains(e.List.FirstOrDefault().Link.String.Substring(2, 3))).ToArray(); + + Assert.That(result, Is.All.True); + } + } + + [Test] + public void Case26Test() + { + using (var session = Domain.OpenSession()) + using (var tx = session.OpenTransaction()) { + var values = new long[] { 56, 89, 1 }; + var result = session.Query.All() + .Select(e => new { Contains = values.Contains(e.Link.Value2.GetValueOrDefault() + e.Link.Id), String = e.String }).ToArray(); + + Assert.That(result.Length, Is.EqualTo(2)); + Assert.That(result.First(i => i.String == "test1").Contains, Is.True); + Assert.That(result.First(i => i.String == "test4").Contains, Is.False); + } + } + + [Test] + public void Case27Test() + { + using (var session = Domain.OpenSession()) + using (var tx = session.OpenTransaction()) { + var values = new long[] { 56, 89, 1 }; + var result = session.Query.All() + .Select(e => values.Contains(e.List.Select(l => l.Link.Value2 + e.Link.Value2).First().Value)).ToArray(); + + Assert.That(result, Is.All.False); + } + } + + [Test] + public void Case28Test() + { + using (var session = Domain.OpenSession()) + using (var tx = session.OpenTransaction()) { + var result = session.Query.All() + .Select( + e => new { + IsIn = e.List.Select(x => x.String).First().In("test1", "test2", "test3", "test4"), + String = e.String + }) + .ToArray(); + + Assert.That(result.Length, Is.EqualTo(2)); + Assert.That(result.First(i => i.String == "test1").IsIn, Is.True); + Assert.That(result.First(i => i.String == "test4").IsIn, Is.False); + } + } + + [Test] + public void Case29Test() + { + using (var session = Domain.OpenSession()) + using (var tx = session.OpenTransaction()) { + var result = session.Query.All() + .Where( + x => (x.Active + ? x.Archived + ? ListCategory.Archived + : x.Status.In(CampaignStatus.Draft, CampaignStatus.Stopped) + ? ListCategory.InactiveList + : ListCategory.ActiveList + : ListCategory.Deleted) + .In(ListCategory.ActiveList, + ListCategory.InactiveList, + ListCategory.Archived, + ListCategory.Deleted)) + .ToList(); + } + } + } +} + +namespace Xtensive.Orm.Tests.Issues.IssueJira0743_IncludeDoesNotWorkWithSubqueriesModel +{ + [HierarchyRoot] + public class PowerDialerCampaign : Entity + { + [Field, Key] + public int Id { get; private set; } + + [Field] + public bool Active { get; set; } + + [Field] + public bool Archived { get; set; } + + [Field] + public CampaignStatus Status { get; set; } + + [Field] + public ListCategory ListCategory { get; set; } + } + + public enum CampaignStatus + { + Draft, + Stopped + } + + public enum ListCategory + { + ActiveList, + InactiveList, + Archived, + Deleted + } + + [HierarchyRoot] + public class TestEntity : Entity + { + [Field, Key] + public Guid Id { get; private set; } + + [Field] + public string String { get; set; } + + [Field] + public ListEntity LinkOnList { get; set; } + + [Field] + [Association(PairTo = "Owner")] + public EntitySet List { get; set; } + + [Field] + public TestEntity2 Link { get; set; } + + public TestEntity2 VirtualList { get; set; } + + public TestEntity2 VirtualLink { get; set; } + + public TestEntity(Session session) + : base(session) + { + } + + public TestEntity(Session session, string value) + : this(session) + { + String = value; + } + } + + [HierarchyRoot] + public class ListEntity : Entity + { + [Field, Key] + public Guid Id { get; private set; } + + [Field] + public string String { get; set; } + + [Field(Nullable = false)] + public TestEntity Owner { get; set; } + + [Field(Nullable = false)] + public TestEntity2 Link { get; set; } + + public ListEntity(Session session) + : base(session) + { + } + } + + [HierarchyRoot] + public class TestEntity2 : Entity + { + [Field, Key] + public long Id { get; private set; } + + [Field(Nullable = false)] + public string String { get; set; } + + [Field] + public MyEnum? Value { get; set; } + + [Field] + public long? Value2 { get; set; } + + [Field] + public MyEnum? Value3 { get; set; } + + public TestEntity2(Session session) + : base(session) + { + } + } + + public enum MyEnum + { + Foo = 0, + Bar = 1, + Qux = 2, + } +} diff --git a/Orm/Xtensive.Orm/Orm/Linq/Expressions/Visitors/IncludeFilterMappingGatherer.cs b/Orm/Xtensive.Orm/Orm/Linq/Expressions/Visitors/IncludeFilterMappingGatherer.cs index 991cabd68f..ed81dc2e3d 100644 --- a/Orm/Xtensive.Orm/Orm/Linq/Expressions/Visitors/IncludeFilterMappingGatherer.cs +++ b/Orm/Xtensive.Orm/Orm/Linq/Expressions/Visitors/IncludeFilterMappingGatherer.cs @@ -1,6 +1,6 @@ -// Copyright (C) 2003-2010 Xtensive LLC. -// All rights reserved. -// For conditions of distribution and use, see license. +// Copyright (C) 2009-2020 Xtensive LLC. +// This code is distributed under MIT license terms. +// See the License.txt file in the project root for more information. // Created by: Alexey Gamzov // Created: 2009.11.16 @@ -39,16 +39,14 @@ public MappingEntry(LambdaExpression calculatedColumn) private readonly ApplyParameter filteredTuple; private readonly MappingEntry[] resultMapping; - private int tupleIndex = -1; - private int providerIndex = -1; - public static MappingEntry[] Gather(Expression filterExpression, Expression filterDataTuple, ApplyParameter filteredTuple, int columnCount) { var mapping = Enumerable.Repeat((MappingEntry) null, columnCount).ToArray(); var visitor = new IncludeFilterMappingGatherer(filterDataTuple, filteredTuple, mapping); - visitor.Visit(filterExpression); - if (mapping.Contains(null)) + _ = visitor.Visit(filterExpression); + if (mapping.Contains(null)) { throw Exceptions.InternalError("Failed to gather mappings for IncludeProvider", OrmLog.Instance); + } return mapping; } @@ -59,42 +57,51 @@ protected override Expression VisitBinary(BinaryExpression b) var filterDataAccessor = expressions.FirstOrDefault(e => { var tupleAccess = e.StripCasts().AsTupleAccess(); - return tupleAccess!=null && tupleAccess.Object==filterDataTuple; + return tupleAccess != null && tupleAccess.Object == filterDataTuple; }); - if (filterDataAccessor==null) + if (filterDataAccessor == null) { return result; + } var filteredExpression = expressions.FirstOrDefault(e => e!=filterDataAccessor); - if (filteredExpression==null) + if (filteredExpression == null) { return result; + } var filterDataIndex = filterDataAccessor.StripCasts().GetTupleAccessArgument(); + if (resultMapping.Length <= filterDataIndex) { + return result; + } resultMapping[filterDataIndex] = CreateMappingEntry(filteredExpression); - return result; } protected override Expression VisitMemberAccess(MemberExpression m) { var target = m.Expression; - if (target==null) + if (target == null) { return base.VisitMemberAccess(m); - if (target.NodeType==ExpressionType.Constant && ((ConstantExpression) target).Value==filteredTuple) + } + + if (target.NodeType == ExpressionType.Constant && ((ConstantExpression) target).Value == filteredTuple) { return calculatedColumnParameter; + } return base.VisitMemberAccess(m); } private MappingEntry CreateMappingEntry(Expression expression) { var tupleAccess = expression.StripCasts().AsTupleAccess(); - if (tupleAccess!=null) + if (tupleAccess != null) { return new MappingEntry(tupleAccess.GetTupleAccessArgument()); + } + expression = ExpressionReplacer.Replace(expression, filterDataTuple, calculatedColumnParameter); return new MappingEntry(FastExpression.Lambda(expression, calculatedColumnParameter)); } private IncludeFilterMappingGatherer(Expression filterDataTuple, ApplyParameter filteredTuple, MappingEntry[] resultMapping) { - calculatedColumnParameter = Expression.Parameter(typeof (Tuple), "filteredRow"); + calculatedColumnParameter = Expression.Parameter(typeof(Tuple), "filteredRow"); this.filterDataTuple = filterDataTuple; this.filteredTuple = filteredTuple; diff --git a/Orm/Xtensive.Orm/Orm/Linq/LocalCollectionKeyTypeExtractor.cs b/Orm/Xtensive.Orm/Orm/Linq/LocalCollectionKeyTypeExtractor.cs index 60f3692c6d..d728618ed5 100644 --- a/Orm/Xtensive.Orm/Orm/Linq/LocalCollectionKeyTypeExtractor.cs +++ b/Orm/Xtensive.Orm/Orm/Linq/LocalCollectionKeyTypeExtractor.cs @@ -1,6 +1,6 @@ -// Copyright (C) 2013 Xtensive LLC. -// All rights reserved. -// For conditions of distribution and use, see license. +// Copyright (C) 2013-2020 Xtensive LLC. +// This code is distributed under MIT license terms. +// See the License.txt file in the project root for more information. // Created by: Alexey Kulakov // Created: 2013.12.30 @@ -10,6 +10,7 @@ using System.Linq.Expressions; using System.Text; using Xtensive.Core; +using Xtensive.Orm.Linq.Expressions; namespace Xtensive.Orm.Linq { @@ -18,28 +19,34 @@ internal static class LocalCollectionKeyTypeExtractor public static Type Extract(BinaryExpression expression) { ArgumentValidator.EnsureArgumentNotNull(expression, "expression"); + if (expression.Right.StripMarkers() is KeyExpression key) { + return key.EntityType.UnderlyingType; + } + var expr = VisitBinaryExpession(expression); - if (expr.Type.IsSubclassOf(typeof (Entity))) + if (expr.Type.IsSubclassOf(typeof(Entity))) { return expr.Type; + } + throw new NotSupportedException(string.Format(Strings.ExCurrentTypeXIsNotSupported, expr.Type)); } private static Expression VisitBinaryExpession(BinaryExpression binaryExpression) { - var memberExpression = binaryExpression.Right as MemberExpression; - if (memberExpression==null) - throw new InvalidOperationException(string.Format(Strings.ExCantConvertXToY, binaryExpression.Type, typeof (MemberExpression))); + if (!(binaryExpression.Right is MemberExpression memberExpression)) { + throw new InvalidOperationException(string.Format(Strings.ExCantConvertXToY, binaryExpression.Type, typeof(MemberExpression))); + } return VisitMemberExpression(memberExpression); } private static Expression VisitMemberExpression(MemberExpression memberExpression) { - var parameter = memberExpression.Expression as ParameterExpression; - if (parameter!=null) + if (memberExpression.Expression is ParameterExpression parameter) { return parameter; - var member = memberExpression.Expression as MemberExpression; - if (member!=null) + } + if (memberExpression.Expression is MemberExpression member) { return member; + } throw new NotSupportedException(string.Format(Strings.ExCurrentTypeOfExpressionXIsNotSupported, memberExpression.Expression.Type)); } } diff --git a/Orm/Xtensive.Orm/Orm/Linq/Translator.Queryable.cs b/Orm/Xtensive.Orm/Orm/Linq/Translator.Queryable.cs index 68a6661b9e..797e2f36ba 100644 --- a/Orm/Xtensive.Orm/Orm/Linq/Translator.Queryable.cs +++ b/Orm/Xtensive.Orm/Orm/Linq/Translator.Queryable.cs @@ -305,6 +305,10 @@ private ProjectionExpression VisitCast(Expression source, Type targetType, Type private Expression VisitContains(Expression source, Expression match, bool isRoot) { + if (source.IsLocalCollection(context)) { + match = Visit(match); + } + var matchedElementType = match.Type; var sequenceElementType = QueryHelper.GetSequenceElementType(source.Type); if (sequenceElementType != matchedElementType) { @@ -322,12 +326,13 @@ private Expression VisitContains(Expression source, Expression match, bool isRoo var p = Expression.Parameter(match.Type, "p"); var le = FastExpression.Lambda(Expression.Equal(p, match), p); - if (isRoot) + if (isRoot) { return VisitRootExists(source, le, false); + } - if (source.IsQuery() || source.IsLocalCollection(context)) + if (source.IsQuery() || source.IsLocalCollection(context)) { return VisitExists(source, le, false); - + } throw new NotSupportedException(Strings.ExContainsMethodIsOnlySupportedForRootExpressionsOrSubqueries); }