Skip to content

Commit 0634e83

Browse files
authored
CSHARP-5513: Fix ChangeStream DisambiguatedPaths implementation (#1630)
1 parent 30d59a5 commit 0634e83

8 files changed

+188
-33
lines changed

src/MongoDB.Driver/Core/ChangeStreamDocument.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,8 @@ public ChangeStreamDocument(
121121
/// Added in MongoDB version 6.1.0.
122122
/// </para>
123123
/// </remarks>
124-
public BsonDocument DisambiguatedPaths => GetValue<BsonDocument>(nameof(DisambiguatedPaths), null);
124+
[Obsolete("DisambiguatedPaths is obsolete and will be removed in a future version. Use <see cref=\"ChangeStreamDocument{TDocument}.UpdateDescription.DisambiguatedPaths\"/> instead.")]
125+
public BsonDocument DisambiguatedPaths => null;
125126

126127
/// <summary>
127128
/// Gets the document key.

src/MongoDB.Driver/Core/ChangeStreamDocumentSerializer.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,8 @@ public ChangeStreamDocumentSerializer(
4242

4343
RegisterMember("ClusterTime", "clusterTime", BsonTimestampSerializer.Instance);
4444
RegisterMember("CollectionNamespace", "ns", ChangeStreamDocumentCollectionNamespaceSerializer.Instance);
45-
RegisterMember("CollectionUuid", "ui", GuidSerializer.StandardInstance);
45+
RegisterMember("CollectionUuid", "collectionUUID", GuidSerializer.StandardInstance);
4646
RegisterMember("DatabaseNamespace", "ns", ChangeStreamDocumentDatabaseNamespaceSerializer.Instance);
47-
RegisterMember("DisambiguatedPaths", "disambiguatedPaths", BsonDocumentSerializer.Instance);
4847
RegisterMember("DocumentKey", "documentKey", BsonDocumentSerializer.Instance);
4948
RegisterMember("FullDocument", "fullDocument", _documentSerializer);
5049
RegisterMember("FullDocumentBeforeChange", "fullDocumentBeforeChange", _documentSerializer);

src/MongoDB.Driver/Core/ChangeStreamUpdateDescription.cs

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ namespace MongoDB.Driver
2727
public sealed class ChangeStreamUpdateDescription
2828
{
2929
// private fields
30+
private readonly BsonDocument _disambiguatedPaths;
3031
private readonly string[] _removedFields;
3132
private readonly BsonArray _truncatedArrays;
3233
private readonly BsonDocument _updatedFields;
@@ -54,12 +55,60 @@ public ChangeStreamUpdateDescription(
5455
BsonDocument updatedFields,
5556
string[] removedFields,
5657
BsonArray truncatedArrays)
58+
: this(updatedFields, removedFields, truncatedArrays, null)
59+
{
60+
}
61+
62+
/// <summary>
63+
/// Initializes a new instance of the <see cref="ChangeStreamUpdateDescription" /> class.
64+
/// </summary>
65+
/// <param name="updatedFields">The updated fields.</param>
66+
/// <param name="removedFields">The removed fields.</param>
67+
/// <param name="truncatedArrays">The truncated arrays.</param>
68+
/// <param name="disambiguatedPaths">The DisambiguatedPaths document.</param>
69+
public ChangeStreamUpdateDescription(
70+
BsonDocument updatedFields,
71+
string[] removedFields,
72+
BsonArray truncatedArrays,
73+
BsonDocument disambiguatedPaths)
5774
{
5875
_updatedFields = Ensure.IsNotNull(updatedFields, nameof(updatedFields));
5976
_removedFields = Ensure.IsNotNull(removedFields, nameof(removedFields));
6077
_truncatedArrays = truncatedArrays; // can be null
78+
_disambiguatedPaths = disambiguatedPaths;
6179
}
6280

81+
/// <summary>
82+
/// Gets the disambiguated paths if present.
83+
/// </summary>
84+
/// <value>
85+
/// The disambiguated paths.
86+
/// </value>
87+
/// <remarks>
88+
/// <para>
89+
/// A document containing a map that associates an update path to an array containing the path components used in the update document. This data
90+
/// can be used in combination with the other fields in an <see cref="ChangeStreamDocument{TDocument}.UpdateDescription"/> to determine the
91+
/// actual path in the document that was updated. This is necessary in cases where a key contains dot-separated strings (i.e. <c>{ "a.b": "c" }</c>) or
92+
/// a document contains a numeric literal string key (i.e. <c>{ "a": { "0": "a" } }</c>). Note that in this scenario, the numeric key can't be the top
93+
/// level key because <c>{ "0": "a" }</c> is not ambiguous - update paths would simply be <c>'0'</c> which is unambiguous because BSON documents cannot have
94+
/// arrays at the top level. Each entry in the document maps an update path to an array which contains the actual path used when the document
95+
/// was updated. For example, given a document with the following shape <c>{ "a": { "0": 0 } }</c> and an update of <c>{ $inc: { "a.0": 1 } }</c>,
96+
/// <see cref="ChangeStreamDocument{TDocument}.DisambiguatedPaths"/> would look like the following:
97+
/// </para>
98+
/// <code>
99+
/// {
100+
/// "a.0": ["a", "0"]
101+
/// }
102+
/// </code>
103+
/// <para>
104+
/// In each array, all elements will be returned as strings with the exception of array indices, which will be returned as 32-bit integers.
105+
/// </para>
106+
/// <para>
107+
/// Added in MongoDB version 6.1.0.
108+
/// </para>
109+
/// </remarks>
110+
public BsonDocument DisambiguatedPaths => _disambiguatedPaths;
111+
63112
// public properties
64113
/// <summary>
65114
/// Gets the removed fields.
@@ -98,7 +147,8 @@ public override bool Equals(object obj)
98147
return
99148
_removedFields.SequenceEqual(other._removedFields) &&
100149
_updatedFields.Equals(other._updatedFields) &&
101-
object.Equals(_truncatedArrays, other._truncatedArrays);
150+
object.Equals(_truncatedArrays, other._truncatedArrays) &&
151+
object.Equals(_disambiguatedPaths, other._disambiguatedPaths);
102152
}
103153

104154
/// <inheritdoc />
@@ -108,6 +158,7 @@ public override int GetHashCode()
108158
.HashElements(_removedFields)
109159
.Hash(_updatedFields)
110160
.Hash(_truncatedArrays)
161+
.Hash(_disambiguatedPaths)
111162
.GetHashCode();
112163
}
113164
}

src/MongoDB.Driver/Core/ChangeStreamUpdateDescriptionSerializer.cs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ protected override ChangeStreamUpdateDescription DeserializeValue(BsonDeserializ
4646
{
4747
var reader = context.Reader;
4848

49+
BsonDocument disambiguatedPaths = null;
4950
BsonDocument updatedFields = null;
5051
string[] removedFields = null;
5152
BsonArray truncatedArrays = null;
@@ -56,6 +57,10 @@ protected override ChangeStreamUpdateDescription DeserializeValue(BsonDeserializ
5657
var fieldName = reader.ReadName();
5758
switch (fieldName)
5859
{
60+
case "disambiguatedPaths":
61+
disambiguatedPaths = BsonDocumentSerializer.Instance.Deserialize(context);
62+
break;
63+
5964
case "updatedFields":
6065
updatedFields = BsonDocumentSerializer.Instance.Deserialize(context);
6166
break;
@@ -74,7 +79,7 @@ protected override ChangeStreamUpdateDescription DeserializeValue(BsonDeserializ
7479
}
7580
reader.ReadEndDocument();
7681

77-
return new ChangeStreamUpdateDescription(updatedFields, removedFields, truncatedArrays);
82+
return new ChangeStreamUpdateDescription(updatedFields, removedFields, truncatedArrays, disambiguatedPaths);
7883
}
7984

8085
/// <inheritdoc />
@@ -92,6 +97,13 @@ protected override void SerializeValue(BsonSerializationContext context, BsonSer
9297
writer.WriteName("truncatedArrays");
9398
BsonArraySerializer.Instance.Serialize(context, value.TruncatedArrays);
9499
}
100+
101+
if (value.DisambiguatedPaths != null)
102+
{
103+
writer.WriteName("disambiguatedPaths");
104+
BsonDocumentSerializer.Instance.Serialize(context, value.DisambiguatedPaths);
105+
}
106+
95107
writer.WriteEndDocument();
96108
}
97109
}

tests/MongoDB.Driver.Tests/Core/ChangeStreamDocumentSerializerTests.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,11 @@ public void constructor_should_initialize_instance()
3636
var result = new ChangeStreamDocumentSerializer<BsonDocument>(documentSerializer);
3737

3838
result._documentSerializer().Should().BeSameAs(documentSerializer);
39-
result._memberSerializationInfo().Count.Should().Be(15);
39+
result._memberSerializationInfo().Count.Should().Be(14);
4040
AssertRegisteredMember(result, "ClusterTime", "clusterTime", BsonTimestampSerializer.Instance);
4141
AssertRegisteredMember(result, "CollectionNamespace", "ns", ChangeStreamDocumentCollectionNamespaceSerializer.Instance);
42-
AssertRegisteredMember(result, "CollectionUuid", "ui", GuidSerializer.StandardInstance);
42+
AssertRegisteredMember(result, "CollectionUuid", "collectionUUID", GuidSerializer.StandardInstance);
4343
AssertRegisteredMember(result, "DatabaseNamespace", "ns", ChangeStreamDocumentDatabaseNamespaceSerializer.Instance);
44-
AssertRegisteredMember(result, "DisambiguatedPaths", "disambiguatedPaths", BsonDocumentSerializer.Instance);
4544
AssertRegisteredMember(result, "DocumentKey", "documentKey", BsonDocumentSerializer.Instance);
4645
AssertRegisteredMember(result, "FullDocument", "fullDocument", documentSerializer);
4746
AssertRegisteredMember(result, "FullDocumentBeforeChange", "fullDocumentBeforeChange", documentSerializer);

tests/MongoDB.Driver.Tests/Core/ChangeStreamDocumentTests.cs

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,7 @@ public void CollectionNamespace_should_allow_extra_elements()
173173
public void CollectionUuid_should_return_expected_result()
174174
{
175175
var value = Guid.NewGuid();
176-
var backingDocument = new BsonDocument { { "other", 1 }, { "ui", new BsonBinaryData(value, GuidRepresentation.Standard) } };
176+
var backingDocument = new BsonDocument { { "other", 1 }, { "collectionUUID", new BsonBinaryData(value, GuidRepresentation.Standard) } };
177177
var subject = CreateSubject(backingDocument: backingDocument);
178178

179179
var result = subject.CollectionUuid;
@@ -246,29 +246,6 @@ public void DatabaseNamespace_should_return_null_if_invalid(string databaseName)
246246
result.Should().BeNull();
247247
}
248248

249-
[Fact]
250-
public void DisambiguatedPaths_should_return_expected_result()
251-
{
252-
var value = new BsonDocument("a.0", new BsonArray {"a", "0"});
253-
var backingDocument = new BsonDocument { { "other", 1 }, { "disambiguatedPaths", value } };
254-
var subject = CreateSubject(backingDocument: backingDocument);
255-
256-
var result = subject.DisambiguatedPaths;
257-
258-
result.Should().Be(value);
259-
}
260-
261-
[Fact]
262-
public void DisambiguatedPaths_should_return_null_if_not_present()
263-
{
264-
var backingDocument = new BsonDocument { { "other", 1 } };
265-
var subject = CreateSubject(backingDocument: backingDocument);
266-
267-
var result = subject.DisambiguatedPaths;
268-
269-
result.Should().BeNull();
270-
}
271-
272249
[Fact]
273250
public void DocumentKey_should_return_expected_result()
274251
{
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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 MongoDB.Bson;
19+
using MongoDB.Bson.Serialization.Serializers;
20+
using MongoDB.Driver.Linq.Linq3Implementation.Misc;
21+
22+
namespace MongoDB.Driver.Tests.UnifiedTestOperations
23+
{
24+
public class UnifiedChangeStreamDocumentConverter
25+
{
26+
private static readonly string[] __changeStreamFields = ["_id", "clusterTime", "documentKey", "fullDocument", "fullDocumentBeforeChange", "ns", "operationDescription", "operationType", "to", "splitEvent", "collectionUUID", "updateDescription", "wallTime"];
27+
private static readonly string[] __nsFields = ["db", "coll"];
28+
29+
public static BsonDocument Convert(ChangeStreamDocument<BsonDocument> changeStreamDocument)
30+
{
31+
var result = new BsonDocument()
32+
{
33+
{ "_id", changeStreamDocument.ResumeToken },
34+
{ "clusterTime", changeStreamDocument.ClusterTime, changeStreamDocument.ClusterTime != null },
35+
{ "documentKey", changeStreamDocument.DocumentKey, changeStreamDocument.DocumentKey != null },
36+
{ "operationDescription", changeStreamDocument.OperationDescription, changeStreamDocument.OperationDescription != null },
37+
{ "to", () => SerializationHelper.SerializeValue(ChangeStreamDocumentCollectionNamespaceSerializer.Instance, changeStreamDocument.RenameTo), changeStreamDocument.RenameTo != null },
38+
{ "splitEvent", () => SerializationHelper.SerializeValue(ChangeStreamSplitEventSerializer.Instance, changeStreamDocument.SplitEvent), changeStreamDocument.SplitEvent != null },
39+
{ "collectionUUID", () => SerializationHelper.SerializeValue(GuidSerializer.StandardInstance, changeStreamDocument.CollectionUuid), changeStreamDocument.CollectionUuid != null },
40+
{ "updateDescription", () => SerializationHelper.SerializeValue(ChangeStreamUpdateDescriptionSerializer.Instance, changeStreamDocument.UpdateDescription), changeStreamDocument.UpdateDescription != null },
41+
{ "wallTime", () => SerializationHelper.SerializeValue(DateTimeSerializer.UtcInstance, changeStreamDocument.WallTime), changeStreamDocument.WallTime != null }
42+
};
43+
AppendDocumentFieldValue(result, changeStreamDocument, "fullDocument");
44+
AppendDocumentFieldValue(result, changeStreamDocument, "fullDocumentBeforeChange");
45+
AppendNsFieldValue(result, changeStreamDocument);
46+
AppendOperationTypeFieldValue(result, changeStreamDocument);
47+
48+
// map the rest of change stream document
49+
result.AddRange(changeStreamDocument.BackingDocument.Elements.Where(e => !__changeStreamFields.Contains(e.Name)));
50+
51+
return result;
52+
}
53+
54+
private static void AppendDocumentFieldValue(BsonDocument document, ChangeStreamDocument<BsonDocument> changeStreamResult, string fieldName)
55+
{
56+
BsonValue value = fieldName == "fullDocument" ? changeStreamResult.FullDocument : changeStreamResult.FullDocumentBeforeChange;
57+
if (value == null && !changeStreamResult.BackingDocument.Contains(fieldName))
58+
{
59+
return;
60+
}
61+
62+
if (value == null)
63+
{
64+
value = BsonNull.Value;
65+
}
66+
67+
document.Add(fieldName, value);
68+
}
69+
70+
private static void AppendNsFieldValue(BsonDocument document, ChangeStreamDocument<BsonDocument> changeStreamDocument)
71+
{
72+
BsonDocument nsFieldValue;
73+
if (changeStreamDocument.CollectionNamespace != null)
74+
{
75+
nsFieldValue = (BsonDocument)SerializationHelper.SerializeValue(ChangeStreamDocumentCollectionNamespaceSerializer.Instance, changeStreamDocument.CollectionNamespace);
76+
}
77+
else if (changeStreamDocument.DatabaseNamespace != null)
78+
{
79+
nsFieldValue = (BsonDocument)SerializationHelper.SerializeValue(ChangeStreamDocumentDatabaseNamespaceSerializer.Instance, changeStreamDocument.DatabaseNamespace);
80+
}
81+
else if (changeStreamDocument.BackingDocument.Contains("ns"))
82+
{
83+
nsFieldValue = new BsonDocument();
84+
}
85+
else
86+
{
87+
return;
88+
}
89+
90+
// map the rest of ns document
91+
nsFieldValue.AddRange(changeStreamDocument.BackingDocument["ns"].AsBsonDocument.Where(e => !__nsFields.Contains(e.Name)));
92+
document.Add("ns", nsFieldValue);
93+
}
94+
95+
private static void AppendOperationTypeFieldValue(BsonDocument document, ChangeStreamDocument<BsonDocument> changeStreamDocument)
96+
{
97+
BsonValue value = null;
98+
if (Enum.IsDefined(typeof(ChangeStreamOperationType), changeStreamDocument.OperationType))
99+
{
100+
value = SerializationHelper.SerializeValue(ChangeStreamOperationTypeSerializer.Instance, changeStreamDocument.OperationType);
101+
}
102+
else if (changeStreamDocument.BackingDocument.Contains("operationType"))
103+
{
104+
value = changeStreamDocument.BackingDocument["operationType"];
105+
}
106+
107+
if (value == null)
108+
{
109+
return;
110+
}
111+
112+
document.Add("operationType", value);
113+
}
114+
}
115+
}
116+

tests/MongoDB.Driver.Tests/UnifiedTestOperations/UnifiedIterateUntilDocumentOrErrorOperation.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ public class UnifiedIterateUntilDocumentOrErrorOperationResultConverter
103103
public BsonDocument Convert<T>(T value) =>
104104
value switch
105105
{
106-
ChangeStreamDocument<BsonDocument> changeStreamResult => changeStreamResult.BackingDocument,
106+
ChangeStreamDocument<BsonDocument> changeStreamResult => UnifiedChangeStreamDocumentConverter.Convert(changeStreamResult),
107107
BsonDocument bsonDocument => bsonDocument,
108108
_ => throw new FormatException($"Unsupported enumerator document {value.GetType().Name}.")
109109
};

0 commit comments

Comments
 (0)