diff --git a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslator.cs b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslator.cs index 4e76f4854c7..70964e72ecf 100644 --- a/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslator.cs +++ b/src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslator.cs @@ -75,10 +75,11 @@ public static ExecutableQuery Translate(MongoQuer pipeline.OutputSerializer); } - pipeline = pipeline.AddStage( + pipeline = pipeline.AddStages( AstStage.Group( id: BsonNull.Value, fields: AstExpression.AccumulatorField("_last", AstUnaryAccumulatorOperator.Last, AstExpression.RootVar)), + AstStage.ReplaceRoot(AstExpression.GetField(AstExpression.RootVar, "_last")), pipeline.OutputSerializer); var finalizer = method.Name == "LastOrDefault" ? __singleOrDefaultFinalizer : __singleFinalizer; diff --git a/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslatorTests.cs b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslatorTests.cs new file mode 100644 index 00000000000..28ee60042a4 --- /dev/null +++ b/tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Translators/ExpressionToExecutableQueryTranslators/LastMethodToExecutableQueryTranslatorTests.cs @@ -0,0 +1,199 @@ +/* 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.Collections.Generic; +using System.Linq; +using FluentAssertions; +using MongoDB.Driver.Linq; +using MongoDB.Driver.TestHelpers; +using Xunit; + +namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Translators.ExpressionToExecutableQueryTranslators +{ + public class LastMethodToExecutableQueryTranslator : LinqIntegrationTest + { + public LastMethodToExecutableQueryTranslator(ClassFixture fixture) + : base(fixture) + { + } + + [Fact] + public void Last_should_work() + { + var collection = Fixture.Collection; + var queryable = collection.AsQueryable().OrderBy(t => t.Id); + + var result = queryable.Last(); + var stages = queryable.GetMongoQueryProvider().LoggedStages; + + AssertStages( + stages, + "{ $sort : { _id : 1 } }", + "{ $group : { _id : null, _last : { $last : '$$ROOT' } } }", + "{ $replaceRoot : { newRoot : '$_last' } }"); + + result.Id.Should().Be(3); + } + + [Fact] + public void Last_with_no_matching_documents_should_throw() + { + var collection = Fixture.Collection; + var queryable = collection.AsQueryable().Where(t => t.Id > 5); + + var exception = Record.Exception(() => queryable.Last()); + var stages = queryable.GetMongoQueryProvider().LoggedStages; + + AssertStages( + stages, + "{ $match : { _id : { $gt : 5 } }}", + "{ $group : { _id : null, _last : { $last : '$$ROOT' } } }", + "{ $replaceRoot : { newRoot : '$_last' } }"); + + exception.Should().BeOfType(); + exception.Message.Should().Contain("Sequence contains no elements"); + } + + [Fact] + public void LastOrDefault_should_work() + { + var collection = Fixture.Collection; + var queryable = collection.AsQueryable().OrderBy(t => t.Id); + + var result = queryable.LastOrDefault(); + var stages = queryable.GetMongoQueryProvider().LoggedStages; + + AssertStages( + stages, + "{ $sort : { _id : 1 } }", + "{ $group : { _id : null, _last : { $last : '$$ROOT' } } }", + "{ $replaceRoot : { newRoot : '$_last' } }"); + + result.Should().NotBeNull(); + result.Id.Should().Be(3); + } + + [Fact] + public void LastOrDefault_with_no_matching_documents_should_work() + { + var collection = Fixture.Collection; + var queryable = collection.AsQueryable().Where(t => t.Id > 5); + + var result = queryable.LastOrDefault(); + var stages = queryable.GetMongoQueryProvider().LoggedStages; + + AssertStages( + stages, + "{ $match : { _id : { $gt : 5 } }}", + "{ $group : { _id : null, _last : { $last : '$$ROOT' } } }", + "{ $replaceRoot : { newRoot : '$_last' } }"); + + result.Should().BeNull(); + } + + [Fact] + public void LastOrDefaultWithPredicate_should_work() + { + var collection = Fixture.Collection; + var queryable = collection.AsQueryable().OrderBy(t => t.Id); + + var result = queryable.LastOrDefault(t => t.Id > 1); + var stages = queryable.GetMongoQueryProvider().LoggedStages; + + AssertStages( + stages, + "{ $sort : { _id : 1 } }", + "{ $match : { _id : { $gt : 1 } }}", + "{ $group : { _id : null, _last : { $last : '$$ROOT' } } }", + "{ $replaceRoot : { newRoot : '$_last' } }"); + + result.Should().NotBeNull(); + result.Id.Should().Be(3); + } + + [Fact] + public void LastOrDefaultWithPredicate_with_no_matching_documents_should_work() + { + var collection = Fixture.Collection; + var queryable = collection.AsQueryable(); + + var result = queryable.LastOrDefault(t => t.Id > 4); + var stages = queryable.GetMongoQueryProvider().LoggedStages; + + AssertStages( + stages, + "{ $match : { _id : { $gt : 4 } }}", + "{ $group : { _id : null, _last : { $last : '$$ROOT' } } }", + "{ $replaceRoot : { newRoot : '$_last' } }"); + + result.Should().BeNull(); + } + + [Fact] + public void LastWithPredicate_should_work() + { + var collection = Fixture.Collection; + var queryable = collection.AsQueryable().OrderBy(t => t.Id); + + var result = queryable.Last(t => t.Id > 1); + var stages = queryable.GetMongoQueryProvider().LoggedStages; + + AssertStages( + stages, + "{ $sort : { _id : 1 } }", + "{ $match : { _id : { $gt : 1 } }}", + "{ $group : { _id : null, _last : { $last : '$$ROOT' } } }", + "{ $replaceRoot : { newRoot : '$_last' } }"); + + result.Id.Should().Be(3); + } + + [Fact] + public void LastWithPredicate_with_no_matching_documents_should_throw() + { + var collection = Fixture.Collection; + var queryable = collection.AsQueryable(); + + var exception = Record.Exception(() => queryable.Last(t => t.Id > 4)); + var stages = queryable.GetMongoQueryProvider().LoggedStages; + + AssertStages( + stages, + "{ $match : { _id : { $gt : 4 } }}", + "{ $group : { _id : null, _last : { $last : '$$ROOT' } } }", + "{ $replaceRoot : { newRoot : '$_last' } }"); + + exception.Should().BeOfType(); + exception.Message.Should().Contain("Sequence contains no elements"); + } + + public class TestClass + { + public int Id { get; set; } + public string StringProperty { get; set; } + } + + public sealed class ClassFixture : MongoCollectionFixture + { + protected override IEnumerable InitialData { get; } = + [ + new TestClass { Id = 1, StringProperty = "A" }, + new TestClass { Id = 2, StringProperty = "B" }, + new TestClass { Id = 3, StringProperty = "C" } + ]; + } + } +}