Skip to content

CSHARP-5519: In a filter Any with array constant and predicate comparing item to field should translate to $in. #1632

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,14 @@ private static TranslatedExpression TranslateEnumerableContains(TranslationConte
NestedAsQueryableHelper.EnsureQueryableMethodHasNestedAsQueryableSource(expression, sourceTranslation);

var valueTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, valueExpression);
var ast = AstExpression.In(valueTranslation.Ast, sourceTranslation.Ast);

var itemSerializer = ArraySerializerHelper.GetItemSerializer(sourceTranslation.Serializer);
if (!itemSerializer.Equals(valueTranslation.Serializer))
{
throw new ExpressionNotSupportedException(expression, because: "the array items and the value are serialized differently");
}

var ast = AstExpression.In(valueTranslation.Ast, sourceTranslation.Ast);
return new TranslatedExpression(expression, ast, BooleanSerializer.Instance);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ public static AstFilter Translate(TranslationContext context, MethodCallExpressi
return AnyWithContainsInPredicateMethodToFilterTranslator.Translate(context, arrayFieldExpression, arrayConstantExpression);
}

if (AnyWithArrayConstantAndItemEqualsFieldPredicateMethodToFilterTranslator.CanTranslate(expression, out arrayConstantExpression, out var fieldExpression))
{
return AnyWithArrayConstantAndItemEqualsFieldPredicateMethodToFilterTranslator.Translate(context, arrayConstantExpression, fieldExpression);
}

var method = expression.Method;
var arguments = expression.Arguments;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/* Copyright 2010-present MongoDB Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using System.Collections;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Filters;
using MongoDB.Driver.Linq.Linq3Implementation.Misc;
using MongoDB.Driver.Linq.Linq3Implementation.Reflection;
using MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToFilterTranslators.ToFilterFieldTranslators;

namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToFilterTranslators.MethodTranslators
{
internal static class AnyWithArrayConstantAndItemEqualsFieldPredicateMethodToFilterTranslator
{
private static readonly MethodInfo[] __anyWithPredicateMethods =
[
EnumerableMethod.AnyWithPredicate,
QueryableMethod.AnyWithPredicate,
];

public static bool CanTranslate(MethodCallExpression expression, out ConstantExpression arrayConstantExpression, out Expression fieldExpression)
{
var method = expression.Method;
var arguments = expression.Arguments;

// arrayConstant.Any(item => item == <field>)
// arrayConstant.Any(item => <field> == item)
if (method.IsOneOf(__anyWithPredicateMethods))
{
var sourceExpression = arguments[0];
var predicateExpression = ExpressionHelper.UnquoteLambdaIfQueryableMethod(method, arguments[1]);

if (sourceExpression.Type.IsArray &&
sourceExpression is ConstantExpression constantExpression)
{
arrayConstantExpression = constantExpression;

var parameter = predicateExpression.Parameters.Single();
var body = predicateExpression.Body;

if (IsItemEqualsFieldComparison(body, parameter, out fieldExpression))
{
return true;
}
}
}

arrayConstantExpression = null;
fieldExpression = null;
return false;
}

public static AstFilter Translate(TranslationContext context, ConstantExpression arrayConstantExpression, Expression fieldExpression)
{
var fieldTranslation = ExpressionToFilterFieldTranslator.Translate(context, fieldExpression);
var itemSerializer = fieldTranslation.Serializer;
var values = (IEnumerable)arrayConstantExpression.Value;
var serializedArrayValues = SerializationHelper.SerializeValues(itemSerializer, values);
return AstFilter.In(fieldTranslation.Ast, serializedArrayValues);
}

private static bool IsItemEqualsFieldComparison(
Expression expression,
ParameterExpression parameter,
out Expression fieldExpression)
{
if (expression is BinaryExpression binaryExpression &&
binaryExpression.NodeType == ExpressionType.Equal)
{
var left = binaryExpression.Left;
var right = binaryExpression.Right;

if (left == parameter)
{
fieldExpression = right; // defer to Translate to throw if fieldExpression can't be translated to a field
return true;
}

if (right == parameter)
{
fieldExpression = left; // defer to Translate to throw if fieldExpression can't be translated to a field
return true;
}
}

fieldExpression = null;
return false;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
/* Copyright 2010-present MongoDB Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

using System.Collections.Generic;
using System.Linq;
using MongoDB.Driver.TestHelpers;
using FluentAssertions;
using MongoDB.Bson;
using MongoDB.Bson.Serialization.Attributes;
using MongoDB.TestHelpers.XunitExtensions;
using Xunit;

namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Jira;

public class CSharp5519Tests : LinqIntegrationTest<CSharp5519Tests.ClassFixture>
{
public CSharp5519Tests(ClassFixture fixture)
: base(fixture)
{
}

[Theory]
[ParameterAttributeData]
public void Filter_Array_Any_item_equals_field_should_work(
[Values(false, true)] bool withNestedAsQueryable)
{
var collection = Fixture.Collection;
var array = new string[] { "0102030405060708090a0b0c" };
var find = withNestedAsQueryable ?
collection.Find(t => array.AsQueryable().Any(item => item == t.Id)) :
collection.Find(t => array.Any(item => item == t.Id));

var filter = TranslateFindFilter(collection, find);

filter.Should().Be("{ _id : { $in : [{ $oid : '0102030405060708090a0b0c' }] } }");
}

[Theory]
[ParameterAttributeData]
public void Filter_Array_Any_field_equals_item_should_work(
[Values(false, true)] bool withNestedAsQueryable)
{
var collection = Fixture.Collection;
var array = new string[] { "0102030405060708090a0b0c" };
var find = withNestedAsQueryable ?
collection.Find(t => array.AsQueryable().Any(item => t.Id == item)) :
collection.Find(t => array.Any(item => t.Id == item));

var filter = TranslateFindFilter(collection, find);

filter.Should().Be("{ _id : { $in : [{ $oid : '0102030405060708090a0b0c' }] } }");
}

[Theory]
[ParameterAttributeData]
public void Filter_Array_Contains_field_should_work(
[Values(false, true)] bool withNestedAsQueryable)
{
var collection = Fixture.Collection;
var array = new string[] { "0102030405060708090a0b0c" };
var find = withNestedAsQueryable ?
collection.Find(t => array.AsQueryable().Contains(t.Id)) :
collection.Find(t => array.Contains(t.Id));

var filter = TranslateFindFilter(collection, find);

filter.Should().Be("{ _id : { $in : [{ $oid : '0102030405060708090a0b0c' }] } }");
}

[Theory]
[ParameterAttributeData]
public void Where_Array_Any_item_equals_field_should_work(
[Values(false, true)] bool withNestedAsQueryable)
{
var collection = Fixture.Collection;
var array = new string[] { "0102030405060708090a0b0c" };

var queryable = withNestedAsQueryable ?
collection.AsQueryable().Where(t => array.AsQueryable().Any(item => item == t.Id)) :
collection.AsQueryable().Where(t => array.Any(item => item == t.Id));

var stages = Translate(collection, queryable);
AssertStages(stages, "{ $match : { _id : { $in : [{ $oid : '0102030405060708090a0b0c' }] } } }");
}

[Theory]
[ParameterAttributeData]
public void Where_Array_Any_field_equals_item_should_work(
[Values(false, true)] bool withNestedAsQueryable)
{
var collection = Fixture.Collection;
var array = new string[] { "0102030405060708090a0b0c" };

var queryable = withNestedAsQueryable ?
collection.AsQueryable().Where(t => array.AsQueryable().Any(item => t.Id == item)) :
collection.AsQueryable().Where(t => array.Any(item => t.Id == item));

var stages = Translate(collection, queryable);
AssertStages(stages, "{ $match : { _id : { $in : [{ $oid : '0102030405060708090a0b0c' }] } } }");
}

[Theory]
[ParameterAttributeData]
public void Where_Array_Contains_field_should_work(
[Values(false, true)] bool withNestedAsQueryable)
{
var collection = Fixture.Collection;
var array = new string[] { "0102030405060708090a0b0c" };

var queryable = withNestedAsQueryable ?
collection.AsQueryable().Where(t => array.AsQueryable().Contains(t.Id)) :
collection.AsQueryable().Where(t => array.Contains(t.Id));

var stages = Translate(collection, queryable);
AssertStages(stages, "{ $match : { _id : { $in : [{ $oid : '0102030405060708090a0b0c' }] } } }");
}

[Theory]
[ParameterAttributeData]
public void Select_Array_Any_item_equals_field_should_work(
[Values(false, true)] bool withNestedAsQueryable)
{
var collection = Fixture.Collection;
var array = new string[] { "0102030405060708090a0b0c" };

var queryable = withNestedAsQueryable ?
collection.AsQueryable().Select(t => array.AsQueryable().Any(item => item == t.Id)) :
collection.AsQueryable().Select(t => array.Any(item => item == t.Id));

var exception = Record.Exception(() => Translate(collection, queryable)); // TODO: support?
exception.Message.Should().Contain("Expression not supported: (id == t.Id) because the two arguments are serialized differently");
}

[Theory]
[ParameterAttributeData]
public void Select_Array_Any_field_equals_item_should_work(
[Values(false, true)] bool withNestedAsQueryable)
{
var collection = Fixture.Collection;
var array = new string[] { "0102030405060708090a0b0c" };

var queryable = withNestedAsQueryable ?
collection.AsQueryable().Select(t => array.AsQueryable().Any(item => t.Id == item)) :
collection.AsQueryable().Select(t => array.Any(item => t.Id == item));

var exception = Record.Exception(() => Translate(collection, queryable)); // TODO: support?
exception.Message.Should().Contain("Expression not supported: (id == t.Id) because the two arguments are serialized differently");
}

[Theory]
[ParameterAttributeData]
public void Select_Array_Contains_field_should_work(
[Values(false, true)] bool withNestedAsQueryable)
{
var collection = Fixture.Collection;
var array = new string[] { "0102030405060708090a0b0c" };

var queryable = withNestedAsQueryable ?
collection.AsQueryable().Select(t => array.AsQueryable().Contains(t.Id)) :
collection.AsQueryable().Select(t => array.Contains(t.Id));

var exception = Record.Exception(() => Translate(collection, queryable)); // TODO: support?
exception.Message.Should().Contain("Expression not supported: value(System.String[]).Contains(t.Id) because the array items and the value are serialized differently");
}

public class Test
{
[BsonId]
[BsonRepresentation(BsonType.ObjectId)]
public string Id { get; set; }
}

public sealed class ClassFixture : MongoCollectionFixture<Test>
{
protected override IEnumerable<Test> InitialData => null;
}
}