Skip to content

Add disambiguatedPaths field to ChangeStreamDocument #991

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

Merged
merged 2 commits into from
Aug 24, 2022
Merged
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 @@ -36,6 +36,7 @@ public final class UpdateDescription {
private final List<String> removedFields;
private final BsonDocument updatedFields;
private final List<TruncatedArray> truncatedArrays;
private final BsonDocument disambiguatedPaths;

/**
* Creates a new instance
Expand All @@ -57,14 +58,33 @@ public UpdateDescription(@Nullable final List<String> removedFields,
* If {@code null}, then {@link #getTruncatedArrays()} returns an {@linkplain List#isEmpty() empty} {@link List}.
* @since 4.3
*/
public UpdateDescription(
@Nullable final List<String> removedFields,
@Nullable final BsonDocument updatedFields,
@Nullable final List<TruncatedArray> truncatedArrays) {
this(removedFields, updatedFields, truncatedArrays, null);
}

/**
* @param removedFields Names of the fields that were removed.
* @param updatedFields Information about the updated fields.
* @param truncatedArrays Information about the updated fields of the {@linkplain org.bson.BsonType#ARRAY array} type
* when the changes are reported as truncations. If {@code null}, then {@link #getTruncatedArrays()} returns
* an {@linkplain List#isEmpty() empty} {@link List}.
* @param disambiguatedPaths a document containing a map that associates an update path to an array containing the path components
* used in the update document.
* @since 4.8
*/
@BsonCreator
public UpdateDescription(
@Nullable @BsonProperty("removedFields") final List<String> removedFields,
@Nullable @BsonProperty("updatedFields") final BsonDocument updatedFields,
@Nullable @BsonProperty("truncatedArrays") final List<TruncatedArray> truncatedArrays) {
@Nullable @BsonProperty("truncatedArrays") final List<TruncatedArray> truncatedArrays,
@Nullable @BsonProperty("disambiguatedPaths") final BsonDocument disambiguatedPaths) {
this.removedFields = removedFields;
this.updatedFields = updatedFields;
this.truncatedArrays = truncatedArrays == null ? emptyList() : truncatedArrays;
this.disambiguatedPaths = disambiguatedPaths;
}

/**
Expand Down Expand Up @@ -122,6 +142,35 @@ public List<TruncatedArray> getTruncatedArrays() {
return truncatedArrays;
}

/**
* A document containing a map that associates an update path to an array containing the path components used in the update document.
*
* <p>
* This data can be used in combination with the other fields in an `UpdateDescription` to determine the actual path in the document
* that was updated. This is necessary in cases where a key contains dot-separated strings (i.e., <code>{"a.b": "c"}</code>) or a
* document contains a numeric literal string key (i.e., <code>{ "a": { "0": "a" } }</code>. Note that in this
* scenario, the numeric key can't be the top level key, because <code>{ "0": "a" }</code> is not ambiguous - update paths
* would simply be <code>'0'</code> which is unambiguous because BSON documents cannot have arrays at the top level.).
* </p>
* <p>
* Each entry in the document maps an update path to an array which contains the actual path used when the document was updated. For
* example, given a document with the following shape <code>{ "a": { "0": 0 } }</code> and an update of
* <code>{ $inc: { "a.0": 1 } }</code>, <code>disambiguatedPaths</code> would look like the following:
* <code> { "a.0": ["a", "0"] }</code>.
* </p>
* <p>
* In each array, all elements will be returned as strings, except for array indices, which will be returned as 32-bit integers.
* </p>
*
* @return the disambiguated paths as a BSON document, which may be null
* @since 4.8
* @mongodb.server.release 6.1
*/
@Nullable
public BsonDocument getDisambiguatedPaths() {
return disambiguatedPaths;
}

/**
* @return {@code true} if and only if all of the following is true for the compared objects
* <ul>
Expand All @@ -146,12 +195,13 @@ public boolean equals(final Object o) {
UpdateDescription that = (UpdateDescription) o;
return Objects.equals(removedFields, that.removedFields)
&& Objects.equals(updatedFields, that.updatedFields)
&& Objects.equals(truncatedArrays, that.truncatedArrays);
&& Objects.equals(truncatedArrays, that.truncatedArrays)
&& Objects.equals(disambiguatedPaths, that.disambiguatedPaths);
}

@Override
public int hashCode() {
return Objects.hash(removedFields, updatedFields, truncatedArrays);
return Objects.hash(removedFields, updatedFields, truncatedArrays, disambiguatedPaths);
}

@Override
Expand All @@ -160,6 +210,7 @@ public String toString() {
+ "removedFields=" + removedFields
+ ", updatedFields=" + updatedFields
+ ", truncatedArrays=" + truncatedArrays
+ ", disambiguatedPaths=" + disambiguatedPaths
+ "}";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
{
"description": "disambiguatedPaths",
"schemaVersion": "1.3",
"createEntities": [
{
"client": {
"id": "client0",
"useMultipleMongoses": false
}
},
{
"database": {
"id": "database0",
"client": "client0",
"databaseName": "database0"
}
},
{
"collection": {
"id": "collection0",
"database": "database0",
"collectionName": "collection0"
}
}
],
"runOnRequirements": [
{
"minServerVersion": "6.1.0",
"topologies": [
"replicaset",
"sharded-replicaset",
"load-balanced",
"sharded"
]
}
],
"initialData": [
{
"collectionName": "collection0",
"databaseName": "database0",
"documents": []
}
],
"tests": [
{
"description": "disambiguatedPaths is not present when showExpandedEvents is false/unset",
"operations": [
{
"name": "insertOne",
"object": "collection0",
"arguments": {
"document": {
"_id": 1,
"a": {
"1": 1
}
}
}
},
{
"name": "createChangeStream",
"object": "collection0",
"arguments": {
"pipeline": []
},
"saveResultAsEntity": "changeStream0"
},
{
"name": "updateOne",
"object": "collection0",
"arguments": {
"filter": {
"_id": 1
},
"update": {
"$set": {
"a.1": 2
}
}
}
},
{
"name": "iterateUntilDocumentOrError",
"object": "changeStream0",
"expectResult": {
"operationType": "update",
"ns": {
"db": "database0",
"coll": "collection0"
},
"updateDescription": {
"updatedFields": {
"$$exists": true
},
"removedFields": {
"$$exists": true
},
"truncatedArrays": {
"$$exists": true
},
"disambiguatedPaths": {
"$$exists": false
}
}
}
}
]
},
{
"description": "disambiguatedPaths is present on updateDescription when an ambiguous path is present",
"operations": [
{
"name": "insertOne",
"object": "collection0",
"arguments": {
"document": {
"_id": 1,
"a": {
"1": 1
}
}
}
},
{
"name": "createChangeStream",
"object": "collection0",
"arguments": {
"pipeline": [],
"showExpandedEvents": true
},
"saveResultAsEntity": "changeStream0"
},
{
"name": "updateOne",
"object": "collection0",
"arguments": {
"filter": {
"_id": 1
},
"update": {
"$set": {
"a.1": 2
}
}
}
},
{
"name": "iterateUntilDocumentOrError",
"object": "changeStream0",
"expectResult": {
"operationType": "update",
"ns": {
"db": "database0",
"coll": "collection0"
},
"updateDescription": {
"updatedFields": {
"$$exists": true
},
"removedFields": {
"$$exists": true
},
"truncatedArrays": {
"$$exists": true
},
"disambiguatedPaths": {
"a.1": [
"a",
"1"
]
}
}
}
}
]
},
{
"description": "disambiguatedPaths returns array indices as integers",
"operations": [
{
"name": "insertOne",
"object": "collection0",
"arguments": {
"document": {
"_id": 1,
"a": [
{
"1": 1
}
]
}
}
},
{
"name": "createChangeStream",
"object": "collection0",
"arguments": {
"pipeline": [],
"showExpandedEvents": true
},
"saveResultAsEntity": "changeStream0"
},
{
"name": "updateOne",
"object": "collection0",
"arguments": {
"filter": {
"_id": 1
},
"update": {
"$set": {
"a.0.1": 2
}
}
}
},
{
"name": "iterateUntilDocumentOrError",
"object": "changeStream0",
"expectResult": {
"operationType": "update",
"ns": {
"db": "database0",
"coll": "collection0"
},
"updateDescription": {
"updatedFields": {
"$$exists": true
},
"removedFields": {
"$$exists": true
},
"truncatedArrays": {
"$$exists": true
},
"disambiguatedPaths": {
"a.0.1": [
"a",
{
"$$type": "int"
},
"1"
]
}
}
}
}
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,17 +26,19 @@ class UpdateDescriptionSpecification extends Specification {

def 'should create the expected UpdateDescription'() {
when:
def description = new UpdateDescription(removedFields, updatedFields, truncatedArrays)
def description = new UpdateDescription(removedFields, updatedFields, truncatedArrays, disambiguatedPaths)

then:
description.getRemovedFields() == removedFields
description.getUpdatedFields() == updatedFields
description.getTruncatedArrays() == (truncatedArrays ?: emptyList())
description.getDisambiguatedPaths() == disambiguatedPaths

where:
removedFields | updatedFields | truncatedArrays
['a', 'b'] | null | null
null | BsonDocument.parse('{c: 1}') | []
['a', 'b'] | BsonDocument.parse('{c: 1}') | singletonList(new TruncatedArray('d', 1))
removedFields | updatedFields | truncatedArrays | disambiguatedPaths
['a', 'b'] | null | null | null
null | BsonDocument.parse('{c: 1}') | [] | null
['a', 'b'] | BsonDocument.parse('{c: 1}') | singletonList(new TruncatedArray('d', 1)) | null
['a', 'b'] | BsonDocument.parse('{c: 1}') | singletonList(new TruncatedArray('d', 1)) | BsonDocument.parse('{e: 1}')
}
}