From 1b1934d8e6437907ad7b6866d13c1b27cb5dc09b Mon Sep 17 00:00:00 2001 From: rstam Date: Tue, 23 Jan 2024 15:17:51 -0800 Subject: [PATCH 1/2] CSHARP-4858: Add IsMissing and IsNullOrMissing MongoDBFunctions. --- .../Reflection/MqlMethod.cs | 40 +++++++++ ...essionToAggregationExpressionTranslator.cs | 4 + ...MethodToAggregationExpressionExpression.cs | 5 ++ ...MethodToAggregationExpressionTranslator.cs | 66 ++++++++++++++ src/MongoDB.Driver/Mql.cs | 58 ++++++++++++ ...dToAggregationExpressionTranslatorTests.cs | 88 +++++++++++++++++++ 6 files changed, 261 insertions(+) create mode 100644 src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/MqlMethod.cs create mode 100644 src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/FieldExistsOrIsMissingMethodToAggregationExpressionTranslator.cs create mode 100644 src/MongoDB.Driver/Mql.cs create mode 100644 tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/FieldExistsOrIsMissingMethodToAggregationExpressionTranslatorTests.cs diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/MqlMethod.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/MqlMethod.cs new file mode 100644 index 00000000000..d8419c107aa --- /dev/null +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/MqlMethod.cs @@ -0,0 +1,40 @@ +/* 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.Reflection; + +namespace MongoDB.Driver.Linq.Linq3Implementation.Reflection +{ + internal static class MqlMethod + { + // private static fields + private static readonly MethodInfo __exists; + private static readonly MethodInfo __isMissing; + private static readonly MethodInfo __isNullOrMissing; + + // static constructor + static MqlMethod() + { + __exists = ReflectionInfo.Method((object field) => Mql.Exists(field)); + __isMissing = ReflectionInfo.Method((object field) => Mql.IsMissing(field)); + __isNullOrMissing = ReflectionInfo.Method((object field) => Mql.IsNullOrMissing(field)); + } + + // public properties + public static MethodInfo Exists => __exists; + public static MethodInfo IsMissing => __isMissing; + public static MethodInfo IsNullOrMissing => __isNullOrMissing; + } +} diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodCallExpressionToAggregationExpressionTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodCallExpressionToAggregationExpressionTranslator.cs index d5876619663..b873aaa1084 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodCallExpressionToAggregationExpressionTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodCallExpressionToAggregationExpressionTranslator.cs @@ -138,6 +138,10 @@ public static AggregationExpression Translate(TranslationContext context, Method case "IndexOfAny": return IndexOfAnyMethodToAggregationExpressionTranslator.Translate(context, expression); + case "IsMissing": + case "IsNullOrMissing": + return FieldExistsOrIsMissingMethodToAggregationExpressionTranslator.Translate(context, expression); + case "Log": case "Log10": return LogMethodToAggregationExpressionTranslator.Translate(context, expression); diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/ExistsMethodToAggregationExpressionExpression.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/ExistsMethodToAggregationExpressionExpression.cs index 8a4e954e164..f0d94be0358 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/ExistsMethodToAggregationExpressionExpression.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/ExistsMethodToAggregationExpressionExpression.cs @@ -25,6 +25,11 @@ public static AggregationExpression Translate(TranslationContext context, Method { var method = expression.Method; + if (method.Is(MqlMethod.Exists)) + { + return FieldExistsOrIsMissingMethodToAggregationExpressionTranslator.Translate(context, expression); + } + if (method.Is(ArrayMethod.Exists) || ListMethod.IsExistsMethod(expression.Method)) { return AnyMethodToAggregationExpressionTranslator.Translate(context, expression); diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/FieldExistsOrIsMissingMethodToAggregationExpressionTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/FieldExistsOrIsMissingMethodToAggregationExpressionTranslator.cs new file mode 100644 index 00000000000..439cd2d4c67 --- /dev/null +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/FieldExistsOrIsMissingMethodToAggregationExpressionTranslator.cs @@ -0,0 +1,66 @@ +/* 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; +using System.Linq.Expressions; +using System.Reflection; +using MongoDB.Bson; +using MongoDB.Bson.Serialization.Serializers; +using MongoDB.Driver.Linq.Linq3Implementation.Ast; +using MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions; +using MongoDB.Driver.Linq.Linq3Implementation.Misc; +using MongoDB.Driver.Linq.Linq3Implementation.Reflection; + +namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.MethodTranslators +{ + internal static class FieldExistsOrIsMissingMethodToAggregationExpressionTranslator + { + private static readonly MethodInfo[] __isMissingMethods = + { + MqlMethod.Exists, + MqlMethod.IsMissing, + MqlMethod.IsNullOrMissing, + }; + + public static AggregationExpression Translate(TranslationContext context, MethodCallExpression expression) + { + var method = expression.Method; + var arguments = expression.Arguments; + + if (method.IsOneOf(__isMissingMethods)) + { + var fieldExpression = arguments[0]; + var fieldTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, fieldExpression); + var fieldAst = fieldTranslation.Ast; + if (fieldAst.NodeType != AstNodeType.GetFieldExpression) + { + throw new ExpressionNotSupportedException(expression, because: $"argument to {method.Name} must be a reference to a field"); + } + + var ast = method.Name switch + { + nameof(Mql.Exists) => AstExpression.Ne(AstExpression.Type(fieldAst), "missing"), + nameof(Mql.IsMissing) => AstExpression.Eq(AstExpression.Type(fieldAst), "missing"), + nameof(Mql.IsNullOrMissing) => AstExpression.In(AstExpression.Type(fieldAst), new BsonArray { "null", "missing" }), + _ => throw new InvalidOperationException("Unexpected method."), + }; + + return new AggregationExpression(expression, ast, BooleanSerializer.Instance); + } + + throw new ExpressionNotSupportedException(expression); + } + } +} diff --git a/src/MongoDB.Driver/Mql.cs b/src/MongoDB.Driver/Mql.cs new file mode 100644 index 00000000000..abfc8e654bc --- /dev/null +++ b/src/MongoDB.Driver/Mql.cs @@ -0,0 +1,58 @@ +/* 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; + +namespace MongoDB.Driver +{ + /// + /// Contains methods that can be used to access MongoDB specific functionality in LINQ queries. + /// + public static class Mql + { + /// + /// Tests whether a field exists. + /// + /// The type of the field. + /// The field. + /// true if the field exists. + public static bool Exists(TField field) + { + throw new NotSupportedException("This method is not functional. It is only usable in MongoDB LINQ queries."); + } + + /// + /// Tests whether a field is missing. + /// + /// The type of the field. + /// The field. + /// true if the field is missing. + public static bool IsMissing(TField field) + { + throw new NotSupportedException("This method is not functional. It is only usable in MongoDB LINQ queries."); + } + + /// + /// Tests whether a field is null or missing. + /// + /// The type of the field. + /// The field. + /// true if the field is null or missing. + public static bool IsNullOrMissing(TField field) + { + throw new NotSupportedException("This method is not functional. It is only usable in MongoDB LINQ queries."); + } + } +} diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/FieldExistsOrIsMissingMethodToAggregationExpressionTranslatorTests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/FieldExistsOrIsMissingMethodToAggregationExpressionTranslatorTests.cs new file mode 100644 index 00000000000..dc01a008b85 --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/FieldExistsOrIsMissingMethodToAggregationExpressionTranslatorTests.cs @@ -0,0 +1,88 @@ +/* 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.Linq; +using FluentAssertions; +using MongoDB.Bson; +using MongoDB.Driver.Linq; +using Xunit; + +namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.MethodTranslators +{ + public class FieldExistsOrIsMissingMethodToAggregationExpressionTranslatorTests : Linq3IntegrationTest + { + [Fact] + public void Select_Exists_should_work() + { + var collection = GetCollection(); + + var queryable = collection.AsQueryable() + .Select(x => Mql.Exists(x.S)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $ne : [{ $type : '$S' }, 'missing'] }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(false, true, true); + } + + [Fact] + public void Select_IsMissing_should_work() + { + var collection = GetCollection(); + + var queryable = collection.AsQueryable() + .Select(x => Mql.IsMissing(x.S)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $eq : [{ $type : '$S' }, 'missing'] }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(true, false, false); + } + + [Fact] + public void Select_IsNullOrMissing_should_work() + { + var collection = GetCollection(); + + var queryable = collection.AsQueryable() + .Select(x => Mql.IsNullOrMissing(x.S)); + + var stages = Translate(collection, queryable); + AssertStages(stages, "{ $project : { _v : { $in : [{ $type : '$S' }, ['null', 'missing']] }, _id : 0 } }"); + + var results = queryable.ToList(); + results.Should().Equal(true, true, false); + } + + private IMongoCollection GetCollection() + { + var collection = GetCollection("test"); + CreateCollection( + GetCollection("test"), + BsonDocument.Parse("{ _id : 1 }"), + BsonDocument.Parse("{ _id : 2, S : null }"), + BsonDocument.Parse("{ _id : 3, S : 'abc' }")); + return collection; + } + + private class C + { + public int Id { get; set; } + public string S { get; set; } + } + } +} From 370e8b047e1416222270572f5ee02a894d1e5b15 Mon Sep 17 00:00:00 2001 From: rstam Date: Tue, 23 Jan 2024 15:36:04 -0800 Subject: [PATCH 2/2] CSHARP-4858: Shorten long filename to work around Windows limitation when building on Evergreen. --- .../MethodCallExpressionToAggregationExpressionTranslator.cs | 2 +- .../ExistsMethodToAggregationExpressionExpression.cs | 2 +- ...r.cs => IsMissingMethodToAggregationExpressionTranslator.cs} | 2 +- ...=> IsMissingMethodToAggregationExpressionTranslatorTests.cs} | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) rename src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/{FieldExistsOrIsMissingMethodToAggregationExpressionTranslator.cs => IsMissingMethodToAggregationExpressionTranslator.cs} (96%) rename tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/{FieldExistsOrIsMissingMethodToAggregationExpressionTranslatorTests.cs => IsMissingMethodToAggregationExpressionTranslatorTests.cs} (96%) diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodCallExpressionToAggregationExpressionTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodCallExpressionToAggregationExpressionTranslator.cs index b873aaa1084..55ecb8f3379 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodCallExpressionToAggregationExpressionTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodCallExpressionToAggregationExpressionTranslator.cs @@ -140,7 +140,7 @@ public static AggregationExpression Translate(TranslationContext context, Method case "IsMissing": case "IsNullOrMissing": - return FieldExistsOrIsMissingMethodToAggregationExpressionTranslator.Translate(context, expression); + return IsMissingMethodToAggregationExpressionTranslator.Translate(context, expression); case "Log": case "Log10": diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/ExistsMethodToAggregationExpressionExpression.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/ExistsMethodToAggregationExpressionExpression.cs index f0d94be0358..ce3370d1a4b 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/ExistsMethodToAggregationExpressionExpression.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/ExistsMethodToAggregationExpressionExpression.cs @@ -27,7 +27,7 @@ public static AggregationExpression Translate(TranslationContext context, Method if (method.Is(MqlMethod.Exists)) { - return FieldExistsOrIsMissingMethodToAggregationExpressionTranslator.Translate(context, expression); + return IsMissingMethodToAggregationExpressionTranslator.Translate(context, expression); } if (method.Is(ArrayMethod.Exists) || ListMethod.IsExistsMethod(expression.Method)) diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/FieldExistsOrIsMissingMethodToAggregationExpressionTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/IsMissingMethodToAggregationExpressionTranslator.cs similarity index 96% rename from src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/FieldExistsOrIsMissingMethodToAggregationExpressionTranslator.cs rename to src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/IsMissingMethodToAggregationExpressionTranslator.cs index 439cd2d4c67..fc5dd265745 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/FieldExistsOrIsMissingMethodToAggregationExpressionTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/IsMissingMethodToAggregationExpressionTranslator.cs @@ -25,7 +25,7 @@ namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.MethodTranslators { - internal static class FieldExistsOrIsMissingMethodToAggregationExpressionTranslator + internal static class IsMissingMethodToAggregationExpressionTranslator { private static readonly MethodInfo[] __isMissingMethods = { diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/FieldExistsOrIsMissingMethodToAggregationExpressionTranslatorTests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/IsMissingMethodToAggregationExpressionTranslatorTests.cs similarity index 96% rename from tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/FieldExistsOrIsMissingMethodToAggregationExpressionTranslatorTests.cs rename to tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/IsMissingMethodToAggregationExpressionTranslatorTests.cs index dc01a008b85..017f64f66e4 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/FieldExistsOrIsMissingMethodToAggregationExpressionTranslatorTests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/IsMissingMethodToAggregationExpressionTranslatorTests.cs @@ -21,7 +21,7 @@ namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.MethodTranslators { - public class FieldExistsOrIsMissingMethodToAggregationExpressionTranslatorTests : Linq3IntegrationTest + public class IsMissingMethodToAggregationExpressionTranslatorTests : Linq3IntegrationTest { [Fact] public void Select_Exists_should_work()