Skip to content

Commit 10b0a25

Browse files
committed
CSHARP-4734: Throw an exception when an aggregation style projection is used with Find on servers prior to 4.4.
1 parent ff22548 commit 10b0a25

File tree

6 files changed

+306
-9
lines changed

6 files changed

+306
-9
lines changed

src/MongoDB.Driver.Core/Core/Operations/FindOneAndDeleteOperation.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,12 +127,14 @@ public BsonDocument Sort
127127
// methods
128128
internal override BsonDocument CreateCommand(ICoreSessionHandle session, ConnectionDescription connectionDescription, long? transactionNumber)
129129
{
130-
var maxWireVersion = connectionDescription.MaxWireVersion;
131-
if (Feature.HintForFindAndModifyFeature.DriverMustThrowIfNotSupported(maxWireVersion) || (WriteConcern != null && !WriteConcern.IsAcknowledged))
130+
var wireVersion = connectionDescription.MaxWireVersion;
131+
FindProjectionChecker.ThrowIfAggregationExpressionIsUsedWhenNotSupported(_projection, wireVersion);
132+
133+
if (Feature.HintForFindAndModifyFeature.DriverMustThrowIfNotSupported(wireVersion) || (WriteConcern != null && !WriteConcern.IsAcknowledged))
132134
{
133135
if (_hint != null)
134136
{
135-
throw new NotSupportedException($"Server version {WireVersion.GetServerVersionForErrorMessage(maxWireVersion)} does not support hints.");
137+
throw new NotSupportedException($"Server version {WireVersion.GetServerVersionForErrorMessage(wireVersion)} does not support hints.");
136138
}
137139
}
138140

src/MongoDB.Driver.Core/Core/Operations/FindOneAndReplaceOperation.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,12 +181,14 @@ public BsonDocument Sort
181181
// methods
182182
internal override BsonDocument CreateCommand(ICoreSessionHandle session, ConnectionDescription connectionDescription, long? transactionNumber)
183183
{
184-
var maxWireVersion = connectionDescription.MaxWireVersion;
185-
if (Feature.HintForFindAndModifyFeature.DriverMustThrowIfNotSupported(maxWireVersion) || (WriteConcern != null && !WriteConcern.IsAcknowledged))
184+
var wireVersion = connectionDescription.MaxWireVersion;
185+
FindProjectionChecker.ThrowIfAggregationExpressionIsUsedWhenNotSupported(_projection, wireVersion);
186+
187+
if (Feature.HintForFindAndModifyFeature.DriverMustThrowIfNotSupported(wireVersion) || (WriteConcern != null && !WriteConcern.IsAcknowledged))
186188
{
187189
if (_hint != null)
188190
{
189-
throw new NotSupportedException($"Server version {WireVersion.GetServerVersionForErrorMessage(maxWireVersion)} does not support hints.");
191+
throw new NotSupportedException($"Server version {WireVersion.GetServerVersionForErrorMessage(wireVersion)} does not support hints.");
190192
}
191193
}
192194

src/MongoDB.Driver.Core/Core/Operations/FindOneAndUpdateOperation.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,12 +196,14 @@ public BsonValue Update
196196
// methods
197197
internal override BsonDocument CreateCommand(ICoreSessionHandle session, ConnectionDescription connectionDescription, long? transactionNumber)
198198
{
199-
var maxWireVersion = connectionDescription.MaxWireVersion;
200-
if (Feature.HintForFindAndModifyFeature.DriverMustThrowIfNotSupported(maxWireVersion) || (WriteConcern != null && !WriteConcern.IsAcknowledged))
199+
var wireVersion = connectionDescription.MaxWireVersion;
200+
FindProjectionChecker.ThrowIfAggregationExpressionIsUsedWhenNotSupported(_projection, wireVersion);
201+
202+
if (Feature.HintForFindAndModifyFeature.DriverMustThrowIfNotSupported(wireVersion) || (WriteConcern != null && !WriteConcern.IsAcknowledged))
201203
{
202204
if (_hint != null)
203205
{
204-
throw new NotSupportedException($"Server version {WireVersion.GetServerVersionForErrorMessage(maxWireVersion)} does not support hints.");
206+
throw new NotSupportedException($"Server version {WireVersion.GetServerVersionForErrorMessage(wireVersion)} does not support hints.");
205207
}
206208
}
207209

src/MongoDB.Driver.Core/Core/Operations/FindOperation.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,9 @@ public BsonDocument Sort
469469
/// <inheritdoc/>
470470
public BsonDocument CreateCommand(ConnectionDescription connectionDescription, ICoreSession session)
471471
{
472+
var wireVersion = connectionDescription.MaxWireVersion;
473+
FindProjectionChecker.ThrowIfAggregationExpressionIsUsedWhenNotSupported(_projection, wireVersion);
474+
472475
var firstBatchSize = _firstBatchSize ?? (_batchSize > 0 ? _batchSize : null);
473476
var isShardRouter = connectionDescription.HelloResult.ServerType == ServerType.ShardRouter;
474477

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/* Copyright 2010-present MongoDB Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using MongoDB.Bson;
18+
using MongoDB.Driver.Core.Misc;
19+
20+
namespace MongoDB.Driver.Core.Operations
21+
{
22+
internal static class FindProjectionChecker
23+
{
24+
internal static void ThrowIfAggregationExpressionIsUsedWhenNotSupported(BsonDocument projection, int wireVersion)
25+
{
26+
if (projection == null || Feature.FindProjectionExpressions.IsSupported(wireVersion))
27+
{
28+
return;
29+
}
30+
31+
foreach (var specification in projection)
32+
{
33+
ThrowIfAggregationExpressionIsUsed(specification);
34+
}
35+
36+
static void ThrowIfAggregationExpressionIsUsed(BsonElement specification)
37+
{
38+
if (IsAggregationExpression(specification.Value))
39+
{
40+
var specificationAsDocument = new BsonDocument(specification);
41+
throw new NotSupportedException($"The projection specification {specificationAsDocument} uses an aggregation expression and is not supported with find on servers prior to version 4.4.");
42+
}
43+
}
44+
45+
static bool IsAggregationExpression(BsonValue value)
46+
{
47+
return value.BsonType switch
48+
{
49+
BsonType.Boolean => false,
50+
_ when value.IsNumeric => false,
51+
_ when value is BsonDocument documentValue =>
52+
documentValue.ElementCount == 1 && documentValue.GetElement(0).Name switch
53+
{
54+
"$elemMatch" => false,
55+
"$meta" => false,
56+
"$slice" => false,
57+
_ => true,
58+
},
59+
_ => true
60+
};
61+
}
62+
}
63+
}
64+
}
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
/* Copyright 2010-present MongoDB Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using System.Linq;
18+
using FluentAssertions;
19+
using MongoDB.Bson;
20+
using MongoDB.Driver.Core.Misc;
21+
using Xunit;
22+
23+
namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Jira
24+
{
25+
public class CSharp4734Tests : Linq3IntegrationTest
26+
{
27+
[Theory]
28+
[InlineData("{ X : false }", "{ _id : 1 }")]
29+
[InlineData("{ X : true }", "{ _id : 1, X : 2 }")]
30+
[InlineData("{ X : 0 }", "{ _id : 1 }")]
31+
[InlineData("{ X : 1 }", "{ _id : 1, X : 2 }")]
32+
[InlineData("{ X : -1 }", "{ _id : 1, X : 2 }")]
33+
[InlineData("{ X : { $numberLong : 0 } }", "{ _id : 1 }")]
34+
[InlineData("{ X : { $numberLong : 1 } }", "{ _id : 1, X : 2 }")]
35+
[InlineData("{ X : { $numberLong : -1 } }", "{ _id : 1, X : 2 }")]
36+
[InlineData("{ X : 0.0 }", "{ _id : 1 }")]
37+
[InlineData("{ X : 1.0 }", "{ _id : 1, X : 2 }")]
38+
[InlineData("{ X : -1.0 }", "{ _id : 1, X : 2 }")]
39+
public void Find_with_projections_that_should_work_on_all_server_versions(string projection, string expectedResult)
40+
{
41+
var collection = GetCollection("{ _id : 1, X : 2 }");
42+
43+
var find = collection
44+
.Find("{}")
45+
.Project(projection);
46+
47+
var result = find.Single();
48+
result.Should().Be(expectedResult);
49+
}
50+
51+
[Theory]
52+
[InlineData("{}", "{ A : { $slice : 2 } }", "{ _id : 1, A : [1, 2] }")]
53+
[InlineData("{}", "{ A : { $slice : -2 } }", "{ _id : 1, A : [2, 3] }")]
54+
[InlineData("{}", "{ A : { $slice : [1, 2] } }", "{ _id : 1, A : [2, 3] }")]
55+
[InlineData("{}", "{ A : { $slice : [-3, 2] } }", "{ _id : 1, A : [1, 2] }")]
56+
[InlineData("{}", "{ A : { $elemMatch : { $gt : 1 } } }", "{ _id : 1, A : [2] }")]
57+
[InlineData("{ A : { $gt : 1 } }", "{ 'A.$' : true }", "{ _id : 1, A : [2] }")]
58+
[InlineData("{ A : { $gt : 1 } }", "{ 'A.$' : 1 }", "{ _id : 1, A : [2] }")]
59+
[InlineData("{ A : { $gt : 1 } }", "{ 'A.$' : -1 }", "{ _id : 1, A : [2] }")]
60+
[InlineData("{ A : { $gt : 1 } }", "{ 'A.$' : { $numberLong : 1 } }", "{ _id : 1, A : [2] }")]
61+
[InlineData("{ A : { $gt : 1 } }", "{ 'A.$' : { $numberLong : -1 } }", "{ _id : 1, A : [2] }")]
62+
[InlineData("{ A : { $gt : 1 } }", "{ 'A.$' : 1.0 }", "{ _id : 1, A : [2] }")]
63+
[InlineData("{ A : { $gt : 1 } }", "{ 'A.$' : -1.0 }", "{ _id : 1, A : [2] }")]
64+
public void Find_with_array_projection_that_should_work_on_all_server_versions(string filter, string projection, string expectedResult)
65+
{
66+
var collection = GetCollection("{ _id : 1, A : [1, 2, 3] }");
67+
68+
var find = collection
69+
.Find(filter)
70+
.Project(projection);
71+
72+
var result = find.Single();
73+
result.Should().BeEquivalentTo(expectedResult); // order of result elements varies by server version
74+
}
75+
76+
[Theory]
77+
[InlineData("{ $text : { $search : 'coffee' } }", "{ score : { $meta : 'textScore' } }", "{ _id : 1, subject : 'coffee', score : 1.0 }")]
78+
public void Find_with_meta_projection_that_should_work_on_all_server_versions(string filter, string projection, string expectedResult)
79+
{
80+
var collection = GetCollection("{ _id : 1, subject : 'coffee' }");
81+
var keyDefinition = new IndexKeysDefinitionBuilder<BsonDocument>().Text("subject");
82+
var indexModel = new CreateIndexModel<BsonDocument>(keyDefinition);
83+
collection.Indexes.CreateOne(indexModel);
84+
85+
var find = collection
86+
.Find(filter)
87+
.Project(projection);
88+
89+
var result = find.Single();
90+
result.Should().Be(expectedResult);
91+
}
92+
93+
[Theory]
94+
[InlineData("{ X : 'abc' }", "{ _id : 1, X : 'abc' }")]
95+
[InlineData("{ X : '$Y' }", "{ _id : 1, X : 3 }")]
96+
[InlineData("{ X : { $add : ['$X', '$Y'] } }", "{ _id : 1, X : 5 }")]
97+
[InlineData("{ X : { $literal : true } }", "{ _id : 1, X : true }")]
98+
[InlineData("{ X : { $literal : 1 } }", "{ _id : 1, X : 1 }")]
99+
[InlineData("{ X : { $literal : { $numberLong : 1 } } }", "{ _id : 1, X : { $numberLong : 1 } }")]
100+
[InlineData("{ X : { $literal : 1.0 } }", "{ _id : 1, X : 1.0 }")]
101+
public void Find_with_projections_that_should_work_only_on_servers_newer_than_44(string projection, string expectedResult)
102+
{
103+
var collection = GetCollection("{ _id : 1, X : 2, Y : 3 }");
104+
105+
var find = collection
106+
.Find("{}")
107+
.Project(projection);
108+
109+
var wireVersion = CoreTestConfiguration.MaxWireVersion;
110+
if (Feature.FindProjectionExpressions.IsSupported(wireVersion))
111+
{
112+
var result = find.Single();
113+
result.Should().BeEquivalentTo(expectedResult); // order of result elements varies by server version
114+
}
115+
else
116+
{
117+
var exception = Record.Exception(() => find.Single());
118+
exception.Should().BeOfType<NotSupportedException>();
119+
exception.Message.Should().Contain("is not supported with find on servers prior to version 4.4.");
120+
}
121+
}
122+
123+
[Theory]
124+
[InlineData("{ X : { Y : 1 } }", "{ _id : 1, X : { Y : 2 } }")]
125+
[InlineData("{ X : { Y : 0 } }", "{ _id : 1, X : { Z : 3 } }")]
126+
[InlineData("{ X : { Y : { $numberLong : 1 } } }", "{ _id : 1, X : { Y : 2 } }")]
127+
[InlineData("{ X : { Y : 1.0 } }", "{ _id : 1, X : { Y : 2 } }")]
128+
public void Find_with_nested_field_projections_that_should_work_only_on_servers_newer_than_44(string projection, string expectedResult)
129+
{
130+
var collection = GetCollection("{ _id : 1, X : { Y : 2, Z : 3 } }");
131+
132+
var find = collection
133+
.Find("{}")
134+
.Project(projection);
135+
136+
var wireVersion = CoreTestConfiguration.MaxWireVersion;
137+
if (Feature.FindProjectionExpressions.IsSupported(wireVersion))
138+
{
139+
var result = find.Single();
140+
result.Should().BeEquivalentTo(expectedResult); // order of result elements varies by server version
141+
}
142+
else
143+
{
144+
var exception = Record.Exception(() => find.Single());
145+
exception.Should().BeOfType<NotSupportedException>();
146+
exception.Message.Should().Contain("is not supported with find on servers prior to version 4.4.");
147+
}
148+
}
149+
150+
[Fact] // it is sufficient to test only one projection because the rest are tested using Find
151+
public void FindOneAndDelete_with_projections_that_should_work_only_on_servers_newer_than_44()
152+
{
153+
var collection = GetCollection("{ _id : 1, X : 2, Y : 3 }");
154+
var filter = "{ _id : 1 }";
155+
var options = new FindOneAndDeleteOptions<BsonDocument> { Projection = "{ Z : '$Y' }" };
156+
157+
var wireVersion = CoreTestConfiguration.MaxWireVersion;
158+
if (Feature.FindProjectionExpressions.IsSupported(wireVersion))
159+
{
160+
var result = collection.FindOneAndDelete(filter, options);
161+
result.Should().BeEquivalentTo("{ _id : 1, Z : 3 }"); // order of result elements varies by server version
162+
}
163+
else
164+
{
165+
var exception = Record.Exception(() => collection.FindOneAndDelete(filter, options));
166+
exception.Should().BeOfType<NotSupportedException>();
167+
exception.Message.Should().Contain("is not supported with find on servers prior to version 4.4.");
168+
}
169+
}
170+
171+
[Fact] // it is sufficient to test only one projection because the rest are tested using Find
172+
public void FindOneAndReplace_with_projections_that_should_work_only_on_servers_newer_than_44()
173+
{
174+
var collection = GetCollection("{ _id : 1, X : 2, Y : 3 }");
175+
var filter = "{ _id : 1 }";
176+
var replacement = BsonDocument.Parse("{ _id : 1, X : 4, Y : 5 }");
177+
var options = new FindOneAndReplaceOptions<BsonDocument> { Projection = "{ Z : '$Y' }", ReturnDocument = ReturnDocument.After };
178+
179+
var wireVersion = CoreTestConfiguration.MaxWireVersion;
180+
if (Feature.FindProjectionExpressions.IsSupported(wireVersion))
181+
{
182+
var result = collection.FindOneAndReplace(filter, replacement, options);
183+
result.Should().BeEquivalentTo("{ _id : 1, Z : 5 }"); // order of result elements varies by server version
184+
}
185+
else
186+
{
187+
var exception = Record.Exception(() => collection.FindOneAndReplace(filter, replacement, options));
188+
exception.Should().BeOfType<NotSupportedException>();
189+
exception.Message.Should().Contain("is not supported with find on servers prior to version 4.4.");
190+
}
191+
}
192+
193+
[Fact] // it is sufficient to test only one projection because the rest are tested using Find
194+
public void FindOneAndUpdate_with_projections_that_should_work_only_on_servers_newer_than_44()
195+
{
196+
var collection = GetCollection("{ _id : 1, X : 2, Y : 3 }");
197+
var filter = "{ _id : 1 }";
198+
var update = "{ $inc : { Y : 1 } }";
199+
var options = new FindOneAndUpdateOptions<BsonDocument> { Projection = "{ Z : '$Y' }", ReturnDocument = ReturnDocument.After };
200+
201+
var wireVersion = CoreTestConfiguration.MaxWireVersion;
202+
if (Feature.FindProjectionExpressions.IsSupported(wireVersion))
203+
{
204+
var result = collection.FindOneAndUpdate(filter, update, options);
205+
result.Should().BeEquivalentTo("{ _id : 1, Z : 4 }"); // order of result elements varies by server version
206+
}
207+
else
208+
{
209+
var exception = Record.Exception(() => collection.FindOneAndUpdate(filter, update, options));
210+
exception.Should().BeOfType<NotSupportedException>();
211+
exception.Message.Should().Contain("is not supported with find on servers prior to version 4.4.");
212+
}
213+
}
214+
215+
private IMongoCollection<BsonDocument> GetCollection(params string[] documents)
216+
{
217+
var collection = GetCollection<BsonDocument>("test");
218+
CreateCollection(
219+
collection,
220+
documents.Select(BsonDocument.Parse));
221+
return collection;
222+
}
223+
}
224+
}

0 commit comments

Comments
 (0)