diff --git a/driver-core/src/main/com/mongodb/client/model/changestream/ChangeStreamDocument.java b/driver-core/src/main/com/mongodb/client/model/changestream/ChangeStreamDocument.java index d9db11d6de..ad71ca794f 100644 --- a/driver-core/src/main/com/mongodb/client/model/changestream/ChangeStreamDocument.java +++ b/driver-core/src/main/com/mongodb/client/model/changestream/ChangeStreamDocument.java @@ -46,6 +46,11 @@ public final class ChangeStreamDocument { @BsonId() private final BsonDocument resumeToken; private final BsonDocument namespaceDocument; + + @BsonProperty("nsType") + private final String namespaceTypeString; + @BsonIgnore + private final NamespaceType namespaceType; private final BsonDocument destinationNamespaceDocument; private final TDocument fullDocument; private final TDocument fullDocumentBeforeChange; @@ -66,9 +71,10 @@ public final class ChangeStreamDocument { /** * Creates a new instance * - * @param operationTypeString the operation type + * @param operationType the operation type * @param resumeToken the resume token * @param namespaceDocument the BsonDocument representing the namespace + * @param namespaceType the namespace type * @param destinationNamespaceDocument the BsonDocument representing the destinatation namespace * @param fullDocument the full document * @param fullDocumentBeforeChange the full document before change @@ -85,9 +91,10 @@ public final class ChangeStreamDocument { */ @BsonCreator public ChangeStreamDocument( - @Nullable @BsonProperty("operationType") final String operationTypeString, + @Nullable @BsonProperty("operationType") final String operationType, @BsonProperty("resumeToken") final BsonDocument resumeToken, @Nullable @BsonProperty("ns") final BsonDocument namespaceDocument, + @Nullable @BsonProperty("nsType") final String namespaceType, @Nullable @BsonProperty("to") final BsonDocument destinationNamespaceDocument, @Nullable @BsonProperty("fullDocument") final TDocument fullDocument, @Nullable @BsonProperty("fullDocumentBeforeChange") final TDocument fullDocumentBeforeChange, @@ -101,12 +108,14 @@ public ChangeStreamDocument( @Nullable @BsonProperty final BsonDocument extraElements) { this.resumeToken = resumeToken; this.namespaceDocument = namespaceDocument; + this.namespaceTypeString = namespaceType; + this.namespaceType = namespaceTypeString == null ? null : NamespaceType.fromString(namespaceType); this.destinationNamespaceDocument = destinationNamespaceDocument; this.fullDocumentBeforeChange = fullDocumentBeforeChange; this.documentKey = documentKey; this.fullDocument = fullDocument; this.clusterTime = clusterTime; - this.operationTypeString = operationTypeString; + this.operationTypeString = operationType; this.operationType = operationTypeString == null ? null : OperationType.fromString(operationTypeString); this.updateDescription = updateDescription; this.txnNumber = txnNumber; @@ -134,6 +143,8 @@ public BsonDocument getResumeToken() { * * @return the namespace. If the namespaceDocument is null or if it is missing either the 'db' or 'coll' keys, * then this will return null. + * @see #getNamespaceType() + * @see #getNamespaceTypeString() */ @BsonIgnore @Nullable @@ -156,6 +167,8 @@ public MongoNamespace getNamespace() { * * @return the namespaceDocument * @since 3.8 + * @see #getNamespaceType() + * @see #getNamespaceTypeString() */ @BsonProperty("ns") @Nullable @@ -163,6 +176,40 @@ public BsonDocument getNamespaceDocument() { return namespaceDocument; } + /** + * Returns the type of the newly created namespace object as a String, derived from the "nsType" field in a change stream document. + *

+ * This method is useful when using a driver release that has not yet been updated to include a newer namespace type in the + * {@link NamespaceType} enum. In that case, {@link #getNamespaceType()} will return {@link NamespaceType#OTHER} and this method can + * be used to retrieve the actual namespace type as a string value. + *

+ * May return null only if $changeStreamSplitLargeEvent is used. + * + * @return the namespace type as a string + * @since 5.6 + * @mongodb.server.release 8.1 + * @see #getNamespaceType() + * @see #getNamespaceDocument() + */ + @Nullable + public String getNamespaceTypeString() { + return namespaceTypeString; + } + + /** + * Returns the type of the newly created namespace object, derived from the "nsType" field in a change stream document. + * + * @return the namespace type. + * @since 5.6 + * @mongodb.server.release 8.1 + * @see #getNamespaceTypeString() + * @see #getNamespaceDocument() + */ + @Nullable + public NamespaceType getNamespaceType() { + return namespaceType; + } + /** * Returns the destination namespace, derived from the "to" field in a change stream document. * diff --git a/driver-core/src/main/com/mongodb/client/model/changestream/NamespaceType.java b/driver-core/src/main/com/mongodb/client/model/changestream/NamespaceType.java new file mode 100644 index 0000000000..02f9514f7a --- /dev/null +++ b/driver-core/src/main/com/mongodb/client/model/changestream/NamespaceType.java @@ -0,0 +1,78 @@ +/* + * Copyright 2008-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. + */ + +package com.mongodb.client.model.changestream; + +import com.mongodb.lang.Nullable; + +/** + * Represents the type of the newly created namespace object in change stream events. + *

+ * Only present for operations of type {@code create} and when the {@code showExpandedEvents} + * change stream option is enabled. + *

+ * + * @since 5.6 + * @mongodb.server.release 8.1 + */ +public enum NamespaceType { + COLLECTION("collection"), + TIMESERIES("timeseries"), + VIEW("view"), + /** + * The other namespace type. + * + *

A placeholder for newer namespace types issued by the server. + * Users encountering OTHER namespace types are advised to update the driver to get the actual namespace type.

+ */ + OTHER("other"); + + private final String value; + NamespaceType(final String namespaceTypeName) { + this.value = namespaceTypeName; + } + + /** + * @return the String representation of the namespace type + */ + public String getValue() { + return value; + } + + /** + * Returns the ChangeStreamNamespaceType from the string value. + * + * @param namespaceTypeName the string value. + * @return the namespace type. + */ + public static NamespaceType fromString(@Nullable final String namespaceTypeName) { + if (namespaceTypeName != null) { + for (NamespaceType namespaceType : NamespaceType.values()) { + if (namespaceTypeName.equals(namespaceType.value)) { + return namespaceType; + } + } + } + return OTHER; + } + + @Override + public String toString() { + return "NamespaceType{" + + "value='" + value + "'" + + "}"; + } +} diff --git a/driver-core/src/main/com/mongodb/client/model/changestream/OperationType.java b/driver-core/src/main/com/mongodb/client/model/changestream/OperationType.java index c7ca148e6f..3c04973fa1 100644 --- a/driver-core/src/main/com/mongodb/client/model/changestream/OperationType.java +++ b/driver-core/src/main/com/mongodb/client/model/changestream/OperationType.java @@ -16,6 +16,8 @@ package com.mongodb.client.model.changestream; +import com.mongodb.lang.Nullable; + /** * The {@code $changeStream} operation type. * @@ -95,9 +97,9 @@ public String getValue() { * Returns the ChangeStreamOperationType from the string value. * * @param operationTypeName the string value. - * @return the read concern + * @return the operation type. */ - public static OperationType fromString(final String operationTypeName) { + public static OperationType fromString(@Nullable final String operationTypeName) { if (operationTypeName != null) { for (OperationType operationType : OperationType.values()) { if (operationTypeName.equals(operationType.value)) { diff --git a/driver-core/src/test/unit/com/mongodb/client/model/changestream/ChangeStreamDocumentCodecSpecification.groovy b/driver-core/src/test/unit/com/mongodb/client/model/changestream/ChangeStreamDocumentCodecSpecification.groovy index 585338a074..09576c9429 100644 --- a/driver-core/src/test/unit/com/mongodb/client/model/changestream/ChangeStreamDocumentCodecSpecification.groovy +++ b/driver-core/src/test/unit/com/mongodb/client/model/changestream/ChangeStreamDocumentCodecSpecification.groovy @@ -61,6 +61,7 @@ class ChangeStreamDocumentCodecSpecification extends Specification { new ChangeStreamDocument(OperationType.INSERT.value, BsonDocument.parse('{token: true}'), BsonDocument.parse('{db: "engineering", coll: "users"}'), + NamespaceType.COLLECTION.value, null, Document.parse('{_id: 1, userName: "alice123", name: "Alice"}'), null, @@ -73,6 +74,7 @@ class ChangeStreamDocumentCodecSpecification extends Specification { new ChangeStreamDocument(OperationType.UPDATE.value, BsonDocument.parse('{token: true}'), BsonDocument.parse('{db: "engineering", coll: "users"}'), + NamespaceType.COLLECTION.value, null, null, null, @@ -84,6 +86,7 @@ class ChangeStreamDocumentCodecSpecification extends Specification { new ChangeStreamDocument(OperationType.UPDATE.value, BsonDocument.parse('{token: true}'), BsonDocument.parse('{db: "engineering", coll: "users"}'), + NamespaceType.COLLECTION.value, null, Document.parse('{_id: 1, userName: "alice123", name: "Alice"}'), Document.parse('{_id: 1, userName: "alice1234", name: "Alice"}'), @@ -96,6 +99,7 @@ class ChangeStreamDocumentCodecSpecification extends Specification { new ChangeStreamDocument(OperationType.REPLACE.value, BsonDocument.parse('{token: true}'), BsonDocument.parse('{db: "engineering", coll: "users"}'), + NamespaceType.COLLECTION.value, null, Document.parse('{_id: 1, userName: "alice123", name: "Alice"}'), Document.parse('{_id: 1, userName: "alice1234", name: "Alice"}'), @@ -106,6 +110,7 @@ class ChangeStreamDocumentCodecSpecification extends Specification { new ChangeStreamDocument(OperationType.DELETE.value, BsonDocument.parse('{token: true}'), BsonDocument.parse('{db: "engineering", coll: "users"}'), + NamespaceType.COLLECTION.value, null, null, Document.parse('{_id: 1, userName: "alice123", name: "Alice"}'), @@ -116,6 +121,7 @@ class ChangeStreamDocumentCodecSpecification extends Specification { new ChangeStreamDocument(OperationType.DROP.value, BsonDocument.parse('{token: true}'), BsonDocument.parse('{db: "engineering", coll: "users"}'), + NamespaceType.COLLECTION.value, null, null, null, @@ -126,6 +132,7 @@ class ChangeStreamDocumentCodecSpecification extends Specification { new ChangeStreamDocument(OperationType.RENAME.value, BsonDocument.parse('{token: true}'), BsonDocument.parse('{db: "engineering", coll: "users"}'), + NamespaceType.COLLECTION.value, BsonDocument.parse('{db: "engineering", coll: "people"}'), null, null, @@ -140,6 +147,7 @@ class ChangeStreamDocumentCodecSpecification extends Specification { null, null, null, + null, new BsonTimestamp(1234, 2), null, null, null, null, null, null ), @@ -150,12 +158,14 @@ class ChangeStreamDocumentCodecSpecification extends Specification { null, null, null, + null, new BsonTimestamp(1234, 2), null, null, null, null, null, null ), new ChangeStreamDocument(OperationType.INSERT.value, BsonDocument.parse('{token: true}'), BsonDocument.parse('{db: "engineering", coll: "users"}'), + NamespaceType.COLLECTION.value, null, Document.parse('{_id: 1, userName: "alice123", name: "Alice"}'), null, @@ -180,6 +190,7 @@ class ChangeStreamDocumentCodecSpecification extends Specification { db: 'engineering', coll: 'users' }, + nsType: 'collection', documentKey: { userName: 'alice123', _id: 1 @@ -204,6 +215,7 @@ class ChangeStreamDocumentCodecSpecification extends Specification { db: 'engineering', coll: 'users' }, + nsType: 'collection', documentKey: { _id: 1 }, @@ -225,6 +237,7 @@ class ChangeStreamDocumentCodecSpecification extends Specification { db: 'engineering', coll: 'users' }, + nsType: 'collection', documentKey: { _id: 1 }, @@ -261,6 +274,7 @@ class ChangeStreamDocumentCodecSpecification extends Specification { db: 'engineering', coll: 'users' }, + nsType: 'collection', documentKey: { _id: 1 }, @@ -285,6 +299,7 @@ class ChangeStreamDocumentCodecSpecification extends Specification { db: 'engineering', coll: 'users' }, + nsType: 'collection', documentKey: { _id: 1 }, @@ -304,6 +319,7 @@ class ChangeStreamDocumentCodecSpecification extends Specification { db: 'engineering', coll: 'users' } + nsType: 'collection', } ''', ''' @@ -315,6 +331,7 @@ class ChangeStreamDocumentCodecSpecification extends Specification { db: 'engineering', coll: 'users' }, + nsType: 'collection', to: { db: 'engineering', coll: 'people' @@ -347,6 +364,7 @@ class ChangeStreamDocumentCodecSpecification extends Specification { db: 'engineering', coll: 'users' }, + nsType: 'collection', documentKey: { userName: 'alice123', _id: 1 diff --git a/driver-core/src/test/unit/com/mongodb/client/model/changestream/ChangeStreamDocumentSpecification.groovy b/driver-core/src/test/unit/com/mongodb/client/model/changestream/ChangeStreamDocumentSpecification.groovy index 9a1c8fc4ac..da6b147513 100644 --- a/driver-core/src/test/unit/com/mongodb/client/model/changestream/ChangeStreamDocumentSpecification.groovy +++ b/driver-core/src/test/unit/com/mongodb/client/model/changestream/ChangeStreamDocumentSpecification.groovy @@ -35,6 +35,7 @@ class ChangeStreamDocumentSpecification extends Specification { def resumeToken = RawBsonDocument.parse('{token: true}') def namespaceDocument = BsonDocument.parse('{db: "databaseName", coll: "collectionName"}') def namespace = new MongoNamespace('databaseName.collectionName') + def namespaceType = NamespaceType.COLLECTION def destinationNamespaceDocument = BsonDocument.parse('{db: "databaseName2", coll: "collectionName2"}') def destinationNamespace = new MongoNamespace('databaseName2.collectionName2') def fullDocument = BsonDocument.parse('{key: "value for fullDocument"}') @@ -50,8 +51,11 @@ class ChangeStreamDocumentSpecification extends Specification { def extraElements = new BsonDocument('extra', BsonBoolean.TRUE) when: - def changeStreamDocument = new ChangeStreamDocument(operationType.value, resumeToken, namespaceDocument, - destinationNamespaceDocument, fullDocument, fullDocumentBeforeChange, documentKey, clusterTime, updateDesc, txnNumber, + def changeStreamDocument = new ChangeStreamDocument(operationType.value, resumeToken, + namespaceDocument, namespaceType.value, + destinationNamespaceDocument, fullDocument, + fullDocumentBeforeChange, documentKey, + clusterTime, updateDesc, txnNumber, lsid, wallTime, splitEvent, extraElements) then: @@ -62,6 +66,8 @@ class ChangeStreamDocumentSpecification extends Specification { changeStreamDocument.getClusterTime() == clusterTime changeStreamDocument.getNamespace() == namespace changeStreamDocument.getNamespaceDocument() == namespaceDocument + changeStreamDocument.getNamespaceType() == namespaceType + changeStreamDocument.getNamespaceTypeString() == namespaceType.value changeStreamDocument.getDestinationNamespace() == destinationNamespace changeStreamDocument.getDestinationNamespaceDocument() == destinationNamespaceDocument changeStreamDocument.getOperationTypeString() == operationType.value @@ -88,12 +94,15 @@ class ChangeStreamDocumentSpecification extends Specification { def splitEvent = new SplitEvent(1, 2) def extraElements = new BsonDocument('extra', BsonBoolean.TRUE) def changeStreamDocumentNullNamespace = new ChangeStreamDocument(operationType.value, resumeToken, - (BsonDocument) null, (BsonDocument) null, fullDocument, fullDocumentBeforeChange, documentKey, clusterTime, updateDesc, + (BsonDocument) null, null, (BsonDocument) null, fullDocument, fullDocumentBeforeChange, + documentKey, clusterTime, updateDesc, null, null, wallTime, splitEvent, extraElements) expect: changeStreamDocumentNullNamespace.getDatabaseName() == null changeStreamDocumentNullNamespace.getNamespace() == null + changeStreamDocumentNullNamespace.getNamespaceType() == null + changeStreamDocumentNullNamespace.getNamespaceTypeString() == null changeStreamDocumentNullNamespace.getNamespaceDocument() == null changeStreamDocumentNullNamespace.getDestinationNamespace() == null changeStreamDocumentNullNamespace.getDestinationNamespaceDocument() == null @@ -113,15 +122,18 @@ class ChangeStreamDocumentSpecification extends Specification { def splitEvent = new SplitEvent(1, 2) def extraElements = new BsonDocument('extra', BsonBoolean.TRUE) - def changeStreamDocument = new ChangeStreamDocument(null, resumeToken, namespaceDocument, + def changeStreamDocument = new ChangeStreamDocument(null, resumeToken, namespaceDocument, null, (BsonDocument) null, fullDocument, fullDocumentBeforeChange, documentKey, clusterTime, updateDesc, null, null, wallTime, splitEvent, extraElements) def changeStreamDocumentEmptyNamespace = new ChangeStreamDocument(null, resumeToken, - namespaceDocumentEmpty, (BsonDocument) null, fullDocument, fullDocumentBeforeChange, documentKey, clusterTime, updateDesc, + namespaceDocumentEmpty, null, (BsonDocument) null, fullDocument, fullDocumentBeforeChange, + documentKey, clusterTime, updateDesc, null, null, wallTime, splitEvent, extraElements) expect: changeStreamDocument.getNamespace() == null + changeStreamDocument.getNamespaceType() == null + changeStreamDocument.getNamespaceTypeString() == null changeStreamDocument.getDatabaseName() == 'databaseName' changeStreamDocument.getOperationTypeString() == null changeStreamDocument.getOperationType() == null diff --git a/driver-sync/src/test/functional/com/mongodb/client/ChangeStreamProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/ChangeStreamProseTest.java index b283bcfd74..51c2da53b0 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/ChangeStreamProseTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/ChangeStreamProseTest.java @@ -25,8 +25,10 @@ import com.mongodb.client.model.Aggregates; import com.mongodb.client.model.ChangeStreamPreAndPostImagesOptions; import com.mongodb.client.model.CreateCollectionOptions; +import com.mongodb.client.model.TimeSeriesOptions; import com.mongodb.client.model.changestream.ChangeStreamDocument; import com.mongodb.client.model.changestream.FullDocumentBeforeChange; +import com.mongodb.client.model.changestream.NamespaceType; import com.mongodb.client.model.changestream.SplitEvent; import com.mongodb.internal.operation.AggregateResponseBatchCursor; import org.bson.BsonArray; @@ -44,6 +46,7 @@ import static com.mongodb.client.CrudTestHelper.repeat; import static com.mongodb.client.model.Updates.set; import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -355,6 +358,72 @@ public void test19SplitChangeStreamEvents() { } } + /** + * Not a prose spec test. However, it is additional test case for better coverage. + */ + @Test + public void testNameSpaceTypePresentChangeStreamEvents() { + assumeTrue(serverVersionAtLeast(8, 1)); + collection.drop(); + + ChangeStreamIterable changeStream = database + .watch() + .fullDocumentBeforeChange(FullDocumentBeforeChange.REQUIRED) + .showExpandedEvents(true); + + try (MongoChangeStreamCursor> cursor = changeStream.cursor()) { + + TimeSeriesOptions timeSeriesOptions = new TimeSeriesOptions("timestampFieldName"); + database.createCollection( + "timeSeriesCollection", + new CreateCollectionOptions().timeSeriesOptions(timeSeriesOptions) + ); + database.createCollection( + getClass().getName(), + new CreateCollectionOptions().changeStreamPreAndPostImagesOptions( + new ChangeStreamPreAndPostImagesOptions(true))); + database.createView( + "view", + "timeSeriesCollection", + singletonList(Document.parse("{ $match: { field: 1 } }")) + ); + + ChangeStreamDocument e1 = Assertions.assertNotNull(cursor.tryNext()); + ChangeStreamDocument e2 = Assertions.assertNotNull(cursor.tryNext()); + ChangeStreamDocument e3 = Assertions.assertNotNull(cursor.tryNext()); + + assertEquals(NamespaceType.TIMESERIES, e1.getNamespaceType()); + assertEquals(NamespaceType.TIMESERIES.getValue(), e1.getNamespaceTypeString()); + assertEquals(NamespaceType.COLLECTION, e2.getNamespaceType()); + assertEquals(NamespaceType.COLLECTION.getValue(), e2.getNamespaceTypeString()); + assertEquals(NamespaceType.VIEW, e3.getNamespaceType()); + assertEquals(NamespaceType.VIEW.getValue(), e3.getNamespaceTypeString()); + } + } + + /** + * Not a prose spec test. However, it is additional test case for better coverage. + */ + @Test + public void testNameSpaceTypeAbsentChangeStreamEvents() { + assumeTrue(serverVersionAtLeast(8, 1)); + collection.drop(); + + ChangeStreamIterable changeStream = database + .watch() + .fullDocumentBeforeChange(FullDocumentBeforeChange.REQUIRED); + + try (MongoChangeStreamCursor> cursor = changeStream.cursor()) { + + collection.insertOne(new Document("test", new BsonString("test"))); + + ChangeStreamDocument e1 = Assertions.assertNotNull(cursor.tryNext()); + + assertNull(e1.getNamespaceType()); + assertNull(e1.getNamespaceTypeString()); + } + } + private void setFailPoint(final String command, final int errCode) { failPointDocument = new BsonDocument("configureFailPoint", new BsonString("failCommand")) .append("mode", new BsonDocument("times", new BsonInt32(1))) diff --git a/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java b/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java index 2b0544f0c5..9ba11974f3 100644 --- a/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java +++ b/driver-sync/src/test/functional/com/mongodb/internal/connection/OidcAuthenticationProseTests.java @@ -581,7 +581,7 @@ public void testh2p2HumanCallbackReturnsMissingData() { "accessToken can not be null"); } - // not a prose test + // Not a prose test @Test public void testRefreshTokenAbsent() { // additionally, check validation for refresh in machine workflow: