diff --git a/driver-core/src/main/com/mongodb/ClientBulkWriteException.java b/driver-core/src/main/com/mongodb/ClientBulkWriteException.java index b964961d754..d4e25858eb0 100644 --- a/driver-core/src/main/com/mongodb/ClientBulkWriteException.java +++ b/driver-core/src/main/com/mongodb/ClientBulkWriteException.java @@ -25,6 +25,8 @@ import java.util.Optional; import static com.mongodb.assertions.Assertions.isTrueArgument; +import static com.mongodb.assertions.Assertions.notNull; +import static com.mongodb.internal.operation.ClientBulkWriteOperation.Exceptions.serverAddressFromException; import static java.util.Collections.emptyList; import static java.util.Collections.emptyMap; import static java.util.Collections.unmodifiableList; @@ -58,6 +60,8 @@ public final class ClientBulkWriteException extends MongoServerException { * @param writeErrors The {@linkplain #getWriteErrors() write errors}. * @param partialResult The {@linkplain #getPartialResult() partial result}. * @param serverAddress The {@linkplain MongoServerException#getServerAddress() server address}. + * If {@code error} is a {@link MongoServerException} or a {@link MongoSocketException}, then {@code serverAddress} + * must be equal to the {@link ServerAddress} they bear. */ public ClientBulkWriteException( @Nullable final MongoException error, @@ -65,7 +69,11 @@ public ClientBulkWriteException( @Nullable final Map writeErrors, @Nullable final ClientBulkWriteResult partialResult, final ServerAddress serverAddress) { - super(message(error, writeConcernErrors, writeErrors, partialResult, serverAddress), serverAddress); + super( + message( + error, writeConcernErrors, writeErrors, partialResult, + notNull("serverAddress", serverAddress)), + validateServerAddress(error, serverAddress)); isTrueArgument("At least one of `writeConcernErrors`, `writeErrors`, `partialResult` must be non-null or non-empty", !(writeConcernErrors == null || writeConcernErrors.isEmpty()) || !(writeErrors == null || writeErrors.isEmpty()) @@ -89,6 +97,14 @@ private static String message( + (partialResult == null ? "" : " Partial result: " + partialResult + "."); } + private static ServerAddress validateServerAddress(@Nullable final MongoException error, final ServerAddress serverAddress) { + serverAddressFromException(error).ifPresent(serverAddressFromError -> + isTrueArgument("`serverAddress` must be equal to that of the `error`", serverAddressFromError.equals(serverAddress))); + return error instanceof MongoServerException + ? ((MongoServerException) error).getServerAddress() + : serverAddress; + } + /** * The top-level error. That is an error that is neither a {@linkplain #getWriteConcernErrors() write concern error}, * nor is an {@linkplain #getWriteErrors() error of an individual write operation}. diff --git a/driver-core/src/main/com/mongodb/internal/operation/AsyncOperationHelper.java b/driver-core/src/main/com/mongodb/internal/operation/AsyncOperationHelper.java index b3781fc66ff..072ae8e0d9f 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/AsyncOperationHelper.java +++ b/driver-core/src/main/com/mongodb/internal/operation/AsyncOperationHelper.java @@ -322,7 +322,7 @@ static AsyncCallbackSupplier decorateReadWithRetriesAsync(final RetryStat static AsyncCallbackSupplier decorateWriteWithRetriesAsync(final RetryState retryState, final OperationContext operationContext, final AsyncCallbackSupplier asyncWriteFunction) { return new RetryingAsyncCallbackSupplier<>(retryState, onRetryableWriteAttemptFailure(operationContext), - CommandOperationHelper::shouldAttemptToRetryWrite, callback -> { + CommandOperationHelper::loggingShouldAttemptToRetryWriteAndAddRetryableLabel, callback -> { logRetryExecute(retryState, operationContext); asyncWriteFunction.get(callback); }); diff --git a/driver-core/src/main/com/mongodb/internal/operation/BulkWriteBatch.java b/driver-core/src/main/com/mongodb/internal/operation/BulkWriteBatch.java index 1bca4734eff..8da0f13e312 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/BulkWriteBatch.java +++ b/driver-core/src/main/com/mongodb/internal/operation/BulkWriteBatch.java @@ -64,7 +64,7 @@ import static com.mongodb.internal.bulk.WriteRequest.Type.REPLACE; import static com.mongodb.internal.bulk.WriteRequest.Type.UPDATE; import static com.mongodb.internal.operation.DocumentHelper.putIfNotNull; -import static com.mongodb.internal.operation.MixedBulkWriteOperation.commandWriteConcern; +import static com.mongodb.internal.operation.CommandOperationHelper.commandWriteConcern; import static com.mongodb.internal.operation.OperationHelper.LOGGER; import static com.mongodb.internal.operation.OperationHelper.isRetryableWrite; import static com.mongodb.internal.operation.WriteConcernHelper.createWriteConcernError; @@ -111,7 +111,7 @@ static BulkWriteBatch createBulkWriteBatch(final MongoNamespace namespace, } if (canRetryWrites && !writeRequestsAreRetryable) { canRetryWrites = false; - LOGGER.debug("retryWrites set but one or more writeRequests do not support retryable writes"); + logWriteModelDoesNotSupportRetries(); } return new BulkWriteBatch(namespace, connectionDescription, ordered, writeConcern, bypassDocumentValidation, canRetryWrites, new BulkWriteBatchCombiner(connectionDescription.getServerAddress(), ordered, writeConcern), @@ -385,4 +385,8 @@ private static boolean isRetryable(final WriteRequest writeRequest) { } return true; } + + static void logWriteModelDoesNotSupportRetries() { + LOGGER.debug("retryWrites set but one or more writeRequests do not support retryable writes"); + } } diff --git a/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java b/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java new file mode 100644 index 00000000000..b2f6b131723 --- /dev/null +++ b/driver-core/src/main/com/mongodb/internal/operation/ClientBulkWriteOperation.java @@ -0,0 +1,1053 @@ +/* + * 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.internal.operation; + +import com.mongodb.ClientBulkWriteException; +import com.mongodb.Function; +import com.mongodb.MongoClientSettings; +import com.mongodb.MongoCommandException; +import com.mongodb.MongoException; +import com.mongodb.MongoNamespace; +import com.mongodb.MongoServerException; +import com.mongodb.MongoSocketException; +import com.mongodb.MongoWriteConcernException; +import com.mongodb.ServerAddress; +import com.mongodb.WriteConcern; +import com.mongodb.WriteError; +import com.mongodb.assertions.Assertions; +import com.mongodb.bulk.WriteConcernError; +import com.mongodb.client.cursor.TimeoutMode; +import com.mongodb.client.model.bulk.ClientBulkWriteOptions; +import com.mongodb.client.model.bulk.ClientNamespacedReplaceOneModel; +import com.mongodb.client.model.bulk.ClientNamespacedUpdateOneModel; +import com.mongodb.client.model.bulk.ClientNamespacedWriteModel; +import com.mongodb.client.model.bulk.ClientBulkWriteResult; +import com.mongodb.client.model.bulk.ClientDeleteResult; +import com.mongodb.client.model.bulk.ClientInsertOneResult; +import com.mongodb.client.model.bulk.ClientUpdateResult; +import com.mongodb.connection.ConnectionDescription; +import com.mongodb.internal.TimeoutContext; +import com.mongodb.internal.async.function.RetryState; +import com.mongodb.internal.binding.ConnectionSource; +import com.mongodb.internal.binding.WriteBinding; +import com.mongodb.internal.client.model.bulk.AbstractClientDeleteModel; +import com.mongodb.internal.client.model.bulk.AbstractClientNamespacedWriteModel; +import com.mongodb.internal.client.model.bulk.AbstractClientUpdateModel; +import com.mongodb.internal.client.model.bulk.ClientWriteModel; +import com.mongodb.internal.client.model.bulk.ConcreteClientBulkWriteOptions; +import com.mongodb.internal.client.model.bulk.ConcreteClientDeleteManyModel; +import com.mongodb.internal.client.model.bulk.ConcreteClientDeleteOneModel; +import com.mongodb.internal.client.model.bulk.ConcreteClientDeleteOptions; +import com.mongodb.internal.client.model.bulk.ConcreteClientInsertOneModel; +import com.mongodb.internal.client.model.bulk.ConcreteClientNamespacedDeleteManyModel; +import com.mongodb.internal.client.model.bulk.ConcreteClientNamespacedDeleteOneModel; +import com.mongodb.internal.client.model.bulk.ConcreteClientNamespacedInsertOneModel; +import com.mongodb.internal.client.model.bulk.ConcreteClientNamespacedReplaceOneModel; +import com.mongodb.internal.client.model.bulk.ConcreteClientNamespacedUpdateManyModel; +import com.mongodb.internal.client.model.bulk.ConcreteClientNamespacedUpdateOneModel; +import com.mongodb.internal.client.model.bulk.ConcreteClientReplaceOneModel; +import com.mongodb.internal.client.model.bulk.ConcreteClientReplaceOptions; +import com.mongodb.internal.client.model.bulk.ConcreteClientUpdateManyModel; +import com.mongodb.internal.client.model.bulk.ConcreteClientUpdateOneModel; +import com.mongodb.internal.client.model.bulk.ConcreteClientUpdateOptions; +import com.mongodb.internal.client.model.bulk.AcknowledgedSummaryClientBulkWriteResult; +import com.mongodb.internal.client.model.bulk.AcknowledgedVerboseClientBulkWriteResult; +import com.mongodb.internal.client.model.bulk.ConcreteClientDeleteResult; +import com.mongodb.internal.client.model.bulk.ConcreteClientInsertOneResult; +import com.mongodb.internal.client.model.bulk.ConcreteClientUpdateResult; +import com.mongodb.internal.client.model.bulk.UnacknowledgedClientBulkWriteResult; +import com.mongodb.internal.connection.Connection; +import com.mongodb.internal.connection.IdHoldingBsonWriter; +import com.mongodb.internal.connection.MongoWriteConcernWithResponseException; +import com.mongodb.internal.connection.OperationContext; +import com.mongodb.internal.operation.retry.AttachmentKeys; +import com.mongodb.internal.session.SessionContext; +import com.mongodb.internal.validator.MappedFieldNameValidator; +import com.mongodb.internal.validator.NoOpFieldNameValidator; +import com.mongodb.internal.validator.ReplacingDocumentFieldNameValidator; +import com.mongodb.internal.validator.UpdateFieldNameValidator; +import com.mongodb.lang.Nullable; +import org.bson.BsonArray; +import org.bson.BsonDocument; +import org.bson.BsonDocumentWrapper; +import org.bson.BsonObjectId; +import org.bson.BsonValue; +import org.bson.BsonWriter; +import org.bson.FieldNameValidator; +import org.bson.codecs.Encoder; +import org.bson.codecs.EncoderContext; +import org.bson.codecs.configuration.CodecRegistry; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import static com.mongodb.assertions.Assertions.assertFalse; +import static com.mongodb.assertions.Assertions.assertNotNull; +import static com.mongodb.assertions.Assertions.assertTrue; +import static com.mongodb.assertions.Assertions.fail; +import static com.mongodb.internal.operation.BulkWriteBatch.logWriteModelDoesNotSupportRetries; +import static com.mongodb.internal.operation.CommandOperationHelper.initialRetryState; +import static com.mongodb.internal.operation.CommandOperationHelper.shouldAttemptToRetryWriteAndAddRetryableLabel; +import static com.mongodb.internal.operation.CommandOperationHelper.transformWriteException; +import static com.mongodb.internal.operation.CommandOperationHelper.commandWriteConcern; +import static com.mongodb.internal.operation.CommandOperationHelper.validateAndGetEffectiveWriteConcern; +import static com.mongodb.internal.operation.OperationHelper.isRetryableWrite; +import static com.mongodb.internal.operation.SyncOperationHelper.cursorDocumentToBatchCursor; +import static com.mongodb.internal.operation.SyncOperationHelper.decorateWriteWithRetries; +import static com.mongodb.internal.operation.SyncOperationHelper.withSourceAndConnection; +import static java.util.Collections.emptyList; +import static java.util.Collections.emptyMap; +import static java.util.Collections.singletonMap; +import static java.util.Spliterator.IMMUTABLE; +import static java.util.Spliterator.ORDERED; +import static java.util.Spliterators.spliteratorUnknownSize; +import static java.util.stream.Collectors.toList; +import static java.util.stream.Collectors.toSet; +import static java.util.stream.StreamSupport.stream; + +/** + * This class is not part of the public API and may be removed or changed at any time. + */ +public final class ClientBulkWriteOperation implements WriteOperation { + private static final ConcreteClientBulkWriteOptions EMPTY_OPTIONS = new ConcreteClientBulkWriteOptions(); + private static final String BULK_WRITE_COMMAND_NAME = "bulkWrite"; + private static final EncoderContext DEFAULT_ENCODER_CONTEXT = EncoderContext.builder().build(); + private static final EncoderContext COLLECTIBLE_DOCUMENT_ENCODER_CONTEXT = EncoderContext.builder() + .isEncodingCollectibleDocument(true).build(); + + private final List models; + private final ConcreteClientBulkWriteOptions options; + private final WriteConcern writeConcernSetting; + private final boolean retryWritesSetting; + private final CodecRegistry codecRegistry; + + /** + * @param retryWritesSetting See {@link MongoClientSettings#getRetryWrites()}. + */ + public ClientBulkWriteOperation( + final List models, + @Nullable final ClientBulkWriteOptions options, + final WriteConcern writeConcernSetting, + final boolean retryWritesSetting, + final CodecRegistry codecRegistry) { + this.models = models; + this.options = options == null ? EMPTY_OPTIONS : (ConcreteClientBulkWriteOptions) options; + this.writeConcernSetting = writeConcernSetting; + this.retryWritesSetting = retryWritesSetting; + this.codecRegistry = codecRegistry; + } + + @Override + public ClientBulkWriteResult execute(final WriteBinding binding) throws ClientBulkWriteException { + WriteConcern effectiveWriteConcern = validateAndGetEffectiveWriteConcern( + writeConcernSetting, binding.getOperationContext().getSessionContext()); + ResultAccumulator resultAccumulator = new ResultAccumulator(); + MongoException transformedTopLevelError = null; + try { + executeAllBatches(effectiveWriteConcern, binding, resultAccumulator); + } catch (MongoException topLevelError) { + transformedTopLevelError = transformWriteException(topLevelError); + } + return resultAccumulator.build(transformedTopLevelError, effectiveWriteConcern); + } + + /** + * To execute a batch means: + *
    + *
  • execute a `bulkWrite` command, which creates a cursor;
  • + *
  • consume the cursor, which may involve executing `getMore` commands.
  • + *
+ * + * @throws MongoException When a {@linkplain ClientBulkWriteException#getError() top-level error} happens. + */ + private void executeAllBatches( + final WriteConcern effectiveWriteConcern, + final WriteBinding binding, + final ResultAccumulator resultAccumulator) throws MongoException { + Integer nextBatchStartModelIndex = 0; + do { + nextBatchStartModelIndex = executeBatch(nextBatchStartModelIndex, effectiveWriteConcern, binding, resultAccumulator); + } while (nextBatchStartModelIndex != null); + } + + /** + * @return The start model index of the next batch, provided that the operation + * {@linkplain ExhaustiveBulkWriteCommandOkResponse#operationMayContinue(ConcreteClientBulkWriteOptions) may continue} + * and there are unexecuted models left. + */ + @Nullable + private Integer executeBatch( + final int batchStartModelIndex, + final WriteConcern effectiveWriteConcern, + final WriteBinding binding, + final ResultAccumulator resultAccumulator) { + List unexecutedModels = models.subList(batchStartModelIndex, models.size()); + OperationContext operationContext = binding.getOperationContext(); + SessionContext sessionContext = operationContext.getSessionContext(); + TimeoutContext timeoutContext = operationContext.getTimeoutContext(); + RetryState retryState = initialRetryState(retryWritesSetting, timeoutContext); + BatchEncoder batchEncoder = new BatchEncoder(); + Supplier retryingBatchExecutor = decorateWriteWithRetries( + retryState, operationContext, + // Each batch re-selects a server and re-checks out a connection because this is simpler, + // and it is allowed by https://jira.mongodb.org/browse/DRIVERS-2502. + // If connection pinning is required, {@code binding} handles that, + // and `ClientSession`, `TransactionContext` are aware of that. + () -> withSourceAndConnection(binding::getWriteConnectionSource, true, (connectionSource, connection) -> { + ConnectionDescription connectionDescription = connection.getDescription(); + boolean effectiveRetryWrites = isRetryableWrite(retryWritesSetting, effectiveWriteConcern, connectionDescription, sessionContext); + retryState.breakAndThrowIfRetryAnd(() -> !effectiveRetryWrites); + resultAccumulator.onNewServerAddress(connectionDescription.getServerAddress()); + retryState.attach(AttachmentKeys.maxWireVersion(), connectionDescription.getMaxWireVersion(), true) + .attach(AttachmentKeys.commandDescriptionSupplier(), () -> BULK_WRITE_COMMAND_NAME, false); + BsonDocumentWrapper lazilyEncodedBulkWriteCommand = createBulkWriteCommand( + effectiveRetryWrites, effectiveWriteConcern, sessionContext, unexecutedModels, batchEncoder, + () -> retryState.attach(AttachmentKeys.retryableCommandFlag(), true, true)); + return executeBulkWriteCommandAndExhaustOkResponse( + retryState, connectionSource, connection, lazilyEncodedBulkWriteCommand, unexecutedModels, + effectiveWriteConcern, operationContext); + }) + ); + try { + ExhaustiveBulkWriteCommandOkResponse bulkWriteCommandOkResponse = retryingBatchExecutor.get(); + return resultAccumulator.onBulkWriteCommandOkResponseOrNoResponse( + batchStartModelIndex, bulkWriteCommandOkResponse, batchEncoder.intoEncodedBatchInfo()); + } catch (MongoWriteConcernWithResponseException mongoWriteConcernWithOkResponseException) { + return resultAccumulator.onBulkWriteCommandOkResponseWithWriteConcernError( + batchStartModelIndex, mongoWriteConcernWithOkResponseException, batchEncoder.intoEncodedBatchInfo()); + } catch (MongoCommandException bulkWriteCommandException) { + resultAccumulator.onBulkWriteCommandErrorResponse(bulkWriteCommandException); + throw bulkWriteCommandException; + } catch (MongoException e) { + // The server does not have a chance to add "RetryableWriteError" label to `e`, + // and if it is the last attempt failure, `RetryingSyncSupplier` also may not have a chance + // to add the label. So we do that explicitly. + shouldAttemptToRetryWriteAndAddRetryableLabel(retryState, e); + resultAccumulator.onBulkWriteCommandErrorWithoutResponse(e); + throw e; + } + } + + /** + * @throws MongoWriteConcernWithResponseException This internal exception must be handled to avoid it being observed by an application. + * It {@linkplain MongoWriteConcernWithResponseException#getResponse() bears} the OK response to the {@code lazilyEncodedCommand}, + * which must be + * {@linkplain ResultAccumulator#onBulkWriteCommandOkResponseWithWriteConcernError(int, MongoWriteConcernWithResponseException, BatchEncoder.EncodedBatchInfo) accumulated} + * iff this exception is the failed result of retries. + */ + @Nullable + private ExhaustiveBulkWriteCommandOkResponse executeBulkWriteCommandAndExhaustOkResponse( + final RetryState retryState, + final ConnectionSource connectionSource, + final Connection connection, + final BsonDocumentWrapper lazilyEncodedCommand, + final List unexecutedModels, + final WriteConcern effectiveWriteConcern, + final OperationContext operationContext) throws MongoWriteConcernWithResponseException { + BsonDocument bulkWriteCommandOkResponse = connection.command( + "admin", + lazilyEncodedCommand, + FieldNameValidators.createUpdateModsFieldValidator(unexecutedModels), + null, + CommandResultDocumentCodec.create(codecRegistry.get(BsonDocument.class), CommandBatchCursorHelper.FIRST_BATCH), + operationContext, + effectiveWriteConcern.isAcknowledged(), + null, + null); + if (bulkWriteCommandOkResponse == null) { + return null; + } + List> cursorExhaustBatches = doWithRetriesDisabledForCommand(retryState, "getMore", () -> + exhaustBulkWriteCommandOkResponseCursor(connectionSource, connection, bulkWriteCommandOkResponse)); + ExhaustiveBulkWriteCommandOkResponse exhaustiveBulkWriteCommandOkResponse = new ExhaustiveBulkWriteCommandOkResponse( + bulkWriteCommandOkResponse, cursorExhaustBatches); + // `Connection.command` does not throw `MongoWriteConcernException`, so we have to construct it ourselves + MongoWriteConcernException writeConcernException = Exceptions.createWriteConcernException( + bulkWriteCommandOkResponse, connection.getDescription().getServerAddress()); + if (writeConcernException != null) { + throw new MongoWriteConcernWithResponseException(writeConcernException, exhaustiveBulkWriteCommandOkResponse); + } + return exhaustiveBulkWriteCommandOkResponse; + } + + private R doWithRetriesDisabledForCommand( + final RetryState retryState, + final String commandDescription, + final Supplier actionWithCommand) { + Optional originalRetryableCommandFlag = retryState.attachment(AttachmentKeys.retryableCommandFlag()); + Supplier originalCommandDescriptionSupplier = retryState.attachment(AttachmentKeys.commandDescriptionSupplier()) + .orElseThrow(Assertions::fail); + try { + retryState.attach(AttachmentKeys.retryableCommandFlag(), false, true) + .attach(AttachmentKeys.commandDescriptionSupplier(), () -> commandDescription, false); + return actionWithCommand.get(); + } finally { + originalRetryableCommandFlag.ifPresent(value -> retryState.attach(AttachmentKeys.retryableCommandFlag(), value, true)); + retryState.attach(AttachmentKeys.commandDescriptionSupplier(), originalCommandDescriptionSupplier, false); + } + } + + private List> exhaustBulkWriteCommandOkResponseCursor( + final ConnectionSource connectionSource, + final Connection connection, + final BsonDocument response) { + int serverDefaultCursorBatchSize = 0; + try (BatchCursor cursor = cursorDocumentToBatchCursor( + TimeoutMode.CURSOR_LIFETIME, + response, + serverDefaultCursorBatchSize, + codecRegistry.get(BsonDocument.class), + options.getComment().orElse(null), + connectionSource, + connection)) { + return stream(spliteratorUnknownSize(cursor, ORDERED | IMMUTABLE), false).collect(toList()); + } + } + + private BsonDocumentWrapper createBulkWriteCommand( + final boolean effectiveRetryWrites, + final WriteConcern effectiveWriteConcern, + final SessionContext sessionContext, + final List unexecutedModels, + final BatchEncoder batchEncoder, + final Runnable ifCommandIsRetryable) { + return new BsonDocumentWrapper<>( + BULK_WRITE_COMMAND_NAME, + new Encoder() { + @Override + public void encode(final BsonWriter writer, final String commandName, final EncoderContext encoderContext) { + batchEncoder.reset(); + writer.writeStartDocument(); + writer.writeInt32(commandName, 1); + writer.writeBoolean("errorsOnly", !options.isVerboseResults()); + writer.writeBoolean("ordered", options.isOrdered()); + options.isBypassDocumentValidation().ifPresent(value -> writer.writeBoolean("bypassDocumentValidation", value)); + options.getComment().ifPresent(value -> { + writer.writeName("comment"); + encodeUsingRegistry(writer, value); + }); + options.getLet().ifPresent(value -> { + writer.writeName("let"); + encodeUsingRegistry(writer, value); + }); + Function modelSupportsRetries = model -> + !(model instanceof ConcreteClientUpdateManyModel || model instanceof ConcreteClientDeleteManyModel); + assertFalse(unexecutedModels.isEmpty()); + LinkedHashMap indexedNamespaces = new LinkedHashMap<>(); + writer.writeStartArray("ops"); + boolean commandIsRetryable = effectiveRetryWrites; + for (int modelIndexInBatch = 0; modelIndexInBatch < unexecutedModels.size(); modelIndexInBatch++) { + AbstractClientNamespacedWriteModel modelWithNamespace = getNamespacedModel(unexecutedModels, modelIndexInBatch); + ClientWriteModel model = modelWithNamespace.getModel(); + if (commandIsRetryable && !modelSupportsRetries.apply(model)) { + commandIsRetryable = false; + logWriteModelDoesNotSupportRetries(); + } + int namespaceIndexInBatch = indexedNamespaces.computeIfAbsent( + modelWithNamespace.getNamespace(), k -> indexedNamespaces.size()); + batchEncoder.encodeWriteModel(writer, model, modelIndexInBatch, namespaceIndexInBatch); + } + writer.writeEndArray(); + writer.writeStartArray("nsInfo"); + indexedNamespaces.keySet().forEach(namespace -> { + writer.writeStartDocument(); + writer.writeString("ns", namespace.getFullName()); + writer.writeEndDocument(); + }); + writer.writeEndArray(); + if (commandIsRetryable) { + batchEncoder.encodeTxnNumber(writer, sessionContext); + ifCommandIsRetryable.run(); + } + commandWriteConcern(effectiveWriteConcern, sessionContext).ifPresent(value -> { + writer.writeName("writeConcern"); + encodeUsingRegistry(writer, value.asDocument()); + }); + writer.writeEndDocument(); + } + + @Override + public Class getEncoderClass() { + throw fail(); + } + } + ); + } + + private void encodeUsingRegistry(final BsonWriter writer, final T value) { + encodeUsingRegistry(writer, value, DEFAULT_ENCODER_CONTEXT); + } + + private void encodeUsingRegistry(final BsonWriter writer, final T value, final EncoderContext encoderContext) { + @SuppressWarnings("unchecked") + Encoder collationEncoder = (Encoder) codecRegistry.get(value.getClass()); + collationEncoder.encode(writer, value, encoderContext); + } + + private static AbstractClientNamespacedWriteModel getNamespacedModel( + final List models, final int index) { + return (AbstractClientNamespacedWriteModel) models.get(index); + } + + public static final class Exceptions { + public static Optional serverAddressFromException(@Nullable final MongoException exception) { + ServerAddress serverAddress = null; + if (exception instanceof MongoServerException) { + serverAddress = ((MongoServerException) exception).getServerAddress(); + } else if (exception instanceof MongoSocketException) { + serverAddress = ((MongoSocketException) exception).getServerAddress(); + } + return Optional.ofNullable(serverAddress); + } + + @Nullable + private static MongoWriteConcernException createWriteConcernException( + final BsonDocument response, + final ServerAddress serverAddress) { + final String writeConcernErrorFieldName = "writeConcernError"; + if (!response.containsKey(writeConcernErrorFieldName)) { + return null; + } + BsonDocument writeConcernErrorDocument = response.getDocument(writeConcernErrorFieldName); + WriteConcernError writeConcernError = WriteConcernHelper.createWriteConcernError(writeConcernErrorDocument); + Set errorLabels = response.getArray("errorLabels", new BsonArray()).stream() + .map(i -> i.asString().getValue()) + .collect(toSet()); + return new MongoWriteConcernException(writeConcernError, null, serverAddress, errorLabels); + } + } + + private static final class FieldNameValidators { + /** + * The server supports only the {@code update} individual write operation in the {@code ops} array field, while the driver supports + * {@link ClientNamespacedUpdateOneModel}, {@link ClientNamespacedUpdateOneModel}, {@link ClientNamespacedReplaceOneModel}. + * The difference between updating and replacing is only in the document specified via the {@code updateMods} field: + *
    + *
  • if the name of the first field starts with {@code '$'}, then the document is interpreted as specifying update operators;
  • + *
  • if the name of the first field does not start with {@code '$'}, then the document is interpreted as a replacement.
  • + *
+ */ + private static FieldNameValidator createUpdateModsFieldValidator(final List models) { + return new MappedFieldNameValidator( + NoOpFieldNameValidator.INSTANCE, + singletonMap("ops", new FieldNameValidators.OpsArrayFieldValidator(models))); + } + + static final class OpsArrayFieldValidator implements FieldNameValidator { + private static final Set OPERATION_DISCRIMINATOR_FIELD_NAMES = Stream.of("insert", "update", "delete").collect(toSet()); + + private final List models; + private final ReplacingUpdateModsFieldValidator replacingValidator; + private final UpdatingUpdateModsFieldValidator updatingValidator; + private int currentIndividualOperationIndex; + + OpsArrayFieldValidator(final List models) { + this.models = models; + replacingValidator = new ReplacingUpdateModsFieldValidator(); + updatingValidator = new UpdatingUpdateModsFieldValidator(); + currentIndividualOperationIndex = -1; + } + + @Override + public boolean validate(final String fieldName) { + if (OPERATION_DISCRIMINATOR_FIELD_NAMES.contains(fieldName)) { + currentIndividualOperationIndex++; + } + return true; + } + + @Override + public FieldNameValidator getValidatorForField(final String fieldName) { + if (fieldName.equals("updateMods")) { + return currentIndividualOperationIsReplace() ? replacingValidator.reset() : updatingValidator.reset(); + } + return NoOpFieldNameValidator.INSTANCE; + } + + private boolean currentIndividualOperationIsReplace() { + return getNamespacedModel(models, currentIndividualOperationIndex) instanceof ConcreteClientNamespacedReplaceOneModel; + } + } + + private static final class ReplacingUpdateModsFieldValidator implements FieldNameValidator { + private boolean firstFieldSinceLastReset; + + ReplacingUpdateModsFieldValidator() { + firstFieldSinceLastReset = true; + } + + @Override + public boolean validate(final String fieldName) { + if (firstFieldSinceLastReset) { + // we must validate only the first field, and leave the rest up to the server + firstFieldSinceLastReset = false; + return ReplacingDocumentFieldNameValidator.INSTANCE.validate(fieldName); + } + return true; + } + + @Override + public String getValidationErrorMessage(final String fieldName) { + return ReplacingDocumentFieldNameValidator.INSTANCE.getValidationErrorMessage(fieldName); + } + + @Override + public FieldNameValidator getValidatorForField(final String fieldName) { + return NoOpFieldNameValidator.INSTANCE; + } + + ReplacingUpdateModsFieldValidator reset() { + firstFieldSinceLastReset = true; + return this; + } + } + + private static final class UpdatingUpdateModsFieldValidator implements FieldNameValidator { + private final UpdateFieldNameValidator delegate; + private boolean firstFieldSinceLastReset; + + UpdatingUpdateModsFieldValidator() { + delegate = new UpdateFieldNameValidator(); + firstFieldSinceLastReset = true; + } + + @Override + public boolean validate(final String fieldName) { + if (firstFieldSinceLastReset) { + // we must validate only the first field, and leave the rest up to the server + firstFieldSinceLastReset = false; + return delegate.validate(fieldName); + } + return true; + } + + @Override + public String getValidationErrorMessage(final String fieldName) { + return delegate.getValidationErrorMessage(fieldName); + } + + @Override + public FieldNameValidator getValidatorForField(final String fieldName) { + return NoOpFieldNameValidator.INSTANCE; + } + + @Override + public void start() { + delegate.start(); + } + + @Override + public void end() { + delegate.end(); + } + + UpdatingUpdateModsFieldValidator reset() { + delegate.reset(); + firstFieldSinceLastReset = true; + return this; + } + } + } + + private static final class ExhaustiveBulkWriteCommandOkResponse { + /** + * The number of unsuccessful individual write operations. + */ + private final int nErrors; + private final int nInserted; + private final int nUpserted; + private final int nMatched; + private final int nModified; + private final int nDeleted; + private final List cursorExhaust; + + ExhaustiveBulkWriteCommandOkResponse( + final BsonDocument bulkWriteCommandOkResponse, + final List> cursorExhaustBatches) { + this.nErrors = bulkWriteCommandOkResponse.getInt32("nErrors").getValue(); + this.nInserted = bulkWriteCommandOkResponse.getInt32("nInserted").getValue(); + this.nUpserted = bulkWriteCommandOkResponse.getInt32("nUpserted").getValue(); + this.nMatched = bulkWriteCommandOkResponse.getInt32("nMatched").getValue(); + this.nModified = bulkWriteCommandOkResponse.getInt32("nModified").getValue(); + this.nDeleted = bulkWriteCommandOkResponse.getInt32("nDeleted").getValue(); + if (cursorExhaustBatches.isEmpty()) { + cursorExhaust = emptyList(); + } else if (cursorExhaustBatches.size() == 1) { + cursorExhaust = cursorExhaustBatches.get(0); + } else { + cursorExhaust = cursorExhaustBatches.stream().flatMap(Collection::stream).collect(toList()); + } + } + + boolean operationMayContinue(final ConcreteClientBulkWriteOptions options) { + return nErrors == 0 || !options.isOrdered(); + } + + int getNErrors() { + return nErrors; + } + + int getNInserted() { + return nInserted; + } + + int getNUpserted() { + return nUpserted; + } + + int getNMatched() { + return nMatched; + } + + int getNModified() { + return nModified; + } + + int getNDeleted() { + return nDeleted; + } + + List getCursorExhaust() { + return cursorExhaust; + } + } + + /** + * Accumulates results of the operation as it is being executed + * for {@linkplain #build(MongoException, WriteConcern) building} them when the operation completes. + */ + private final class ResultAccumulator { + @Nullable + private ServerAddress serverAddress; + private final ArrayList batchResults; + + ResultAccumulator() { + serverAddress = null; + batchResults = new ArrayList<>(); + } + + /** + *
    + *
  • Either builds and returns {@link ClientBulkWriteResult};
  • + *
  • or builds and throws {@link ClientBulkWriteException};
  • + *
  • or throws {@code topLevelError}.
  • + *
+ */ + ClientBulkWriteResult build(@Nullable final MongoException topLevelError, final WriteConcern effectiveWriteConcern) throws MongoException { + boolean verboseResultsSetting = options.isVerboseResults(); + boolean haveResponses = false; + boolean haveSuccessfulIndividualOperations = false; + long insertedCount = 0; + long upsertedCount = 0; + long matchedCount = 0; + long modifiedCount = 0; + long deletedCount = 0; + Map insertResults = verboseResultsSetting ? new HashMap<>() : emptyMap(); + Map updateResults = verboseResultsSetting ? new HashMap<>() : emptyMap(); + Map deleteResults = verboseResultsSetting ? new HashMap<>() : emptyMap(); + ArrayList writeConcernErrors = new ArrayList<>(); + Map writeErrors = new HashMap<>(); + for (BatchResult batchResult : batchResults) { + if (batchResult.hasResponse()) { + haveResponses = true; + MongoWriteConcernException writeConcernException = batchResult.getWriteConcernException(); + if (writeConcernException != null) { + writeConcernErrors.add(writeConcernException.getWriteConcernError()); + } + int batchStartModelIndex = batchResult.getBatchStartModelIndex(); + ExhaustiveBulkWriteCommandOkResponse response = batchResult.getResponse(); + haveSuccessfulIndividualOperations = haveSuccessfulIndividualOperations + || response.getNErrors() < batchResult.getBatchModelsCount(); + insertedCount += response.getNInserted(); + upsertedCount += response.getNUpserted(); + matchedCount += response.getNMatched(); + modifiedCount += response.getNModified(); + deletedCount += response.getNDeleted(); + Map insertModelDocumentIds = batchResult.getInsertModelDocumentIds(); + for (BsonDocument individualOperationResponse : response.getCursorExhaust()) { + int individualOperationIndexInBatch = individualOperationResponse.getInt32("idx").getValue(); + int writeModelIndex = batchStartModelIndex + individualOperationIndexInBatch; + if (individualOperationResponse.getNumber("ok").intValue() == 1) { + assertTrue(verboseResultsSetting); + AbstractClientNamespacedWriteModel writeModel = getNamespacedModel(models, writeModelIndex); + if (writeModel instanceof ConcreteClientNamespacedInsertOneModel) { + insertResults.put( + writeModelIndex, + new ConcreteClientInsertOneResult(insertModelDocumentIds.get(individualOperationIndexInBatch))); + } else if (writeModel instanceof ConcreteClientNamespacedUpdateOneModel + || writeModel instanceof ConcreteClientNamespacedUpdateManyModel + || writeModel instanceof ConcreteClientNamespacedReplaceOneModel) { + BsonDocument upsertedIdDocument = individualOperationResponse.getDocument("upserted", null); + updateResults.put( + writeModelIndex, + new ConcreteClientUpdateResult( + individualOperationResponse.getInt32("n").getValue(), + individualOperationResponse.getInt32("nModified").getValue(), + upsertedIdDocument == null ? null : upsertedIdDocument.get("_id"))); + } else if (writeModel instanceof ConcreteClientNamespacedDeleteOneModel + || writeModel instanceof ConcreteClientNamespacedDeleteManyModel) { + deleteResults.put( + writeModelIndex, + new ConcreteClientDeleteResult(individualOperationResponse.getInt32("n").getValue())); + } else { + fail(writeModel.getClass().toString()); + } + } else { + WriteError individualOperationWriteError = new WriteError( + individualOperationResponse.getInt32("code").getValue(), + individualOperationResponse.getString("errmsg").getValue(), + individualOperationResponse.getDocument("errInfo", new BsonDocument())); + writeErrors.put(writeModelIndex, individualOperationWriteError); + } + } + } + } + if (topLevelError == null && writeConcernErrors.isEmpty() && writeErrors.isEmpty()) { + if (effectiveWriteConcern.isAcknowledged()) { + AcknowledgedSummaryClientBulkWriteResult summaryResult = new AcknowledgedSummaryClientBulkWriteResult( + insertedCount, upsertedCount, matchedCount, modifiedCount, deletedCount); + return verboseResultsSetting + ? new AcknowledgedVerboseClientBulkWriteResult(summaryResult, insertResults, updateResults, deleteResults) + : summaryResult; + } else { + return UnacknowledgedClientBulkWriteResult.INSTANCE; + } + } else if (haveResponses) { + AcknowledgedSummaryClientBulkWriteResult partialSummaryResult = haveSuccessfulIndividualOperations + ? new AcknowledgedSummaryClientBulkWriteResult(insertedCount, upsertedCount, matchedCount, modifiedCount, deletedCount) + : null; + throw new ClientBulkWriteException( + topLevelError, + writeConcernErrors, + writeErrors, + verboseResultsSetting && partialSummaryResult != null + ? new AcknowledgedVerboseClientBulkWriteResult(partialSummaryResult, insertResults, updateResults, deleteResults) + : partialSummaryResult, + assertNotNull(serverAddress)); + } else { + throw assertNotNull(topLevelError); + } + } + + void onNewServerAddress(final ServerAddress serverAddress) { + this.serverAddress = serverAddress; + } + + @Nullable + Integer onBulkWriteCommandOkResponseOrNoResponse( + final int batchStartModelIndex, + @Nullable + final ExhaustiveBulkWriteCommandOkResponse response, + final BatchEncoder.EncodedBatchInfo encodedBatchInfo) { + return onBulkWriteCommandOkResponseOrNoResponse(batchStartModelIndex, response, null, encodedBatchInfo); + } + + /** + * @return See {@link #executeBatch(int, WriteConcern, WriteBinding, ResultAccumulator)}. + */ + @Nullable + Integer onBulkWriteCommandOkResponseWithWriteConcernError( + final int batchStartModelIndex, + final MongoWriteConcernWithResponseException exception, + final BatchEncoder.EncodedBatchInfo encodedBatchInfo) { + MongoWriteConcernException writeConcernException = (MongoWriteConcernException) exception.getCause(); + onNewServerAddress(writeConcernException.getServerAddress()); + ExhaustiveBulkWriteCommandOkResponse response = (ExhaustiveBulkWriteCommandOkResponse) exception.getResponse(); + return onBulkWriteCommandOkResponseOrNoResponse(batchStartModelIndex, response, writeConcernException, encodedBatchInfo); + } + + /** + * @return See {@link #executeBatch(int, WriteConcern, WriteBinding, ResultAccumulator)}. + */ + @Nullable + private Integer onBulkWriteCommandOkResponseOrNoResponse( + final int batchStartModelIndex, + @Nullable + final ExhaustiveBulkWriteCommandOkResponse response, + @Nullable + final MongoWriteConcernException writeConcernException, + final BatchEncoder.EncodedBatchInfo encodedBatchInfo) { + BatchResult batchResult = response == null + ? BatchResult.noResponse(batchStartModelIndex, encodedBatchInfo) + : BatchResult.okResponse(batchStartModelIndex, encodedBatchInfo, response, writeConcernException); + batchResults.add(batchResult); + int potentialNextBatchStartModelIndex = batchStartModelIndex + batchResult.getBatchModelsCount(); + return (response == null || response.operationMayContinue(options)) + ? potentialNextBatchStartModelIndex == models.size() ? null : potentialNextBatchStartModelIndex + : null; + } + + void onBulkWriteCommandErrorResponse(final MongoCommandException exception) { + onNewServerAddress(exception.getServerAddress()); + } + + void onBulkWriteCommandErrorWithoutResponse(final MongoException exception) { + Exceptions.serverAddressFromException(exception).ifPresent(this::onNewServerAddress); + } + } + + static final class BatchResult { + private final int batchStartModelIndex; + private final BatchEncoder.EncodedBatchInfo encodedBatchInfo; + @Nullable + private final ExhaustiveBulkWriteCommandOkResponse response; + @Nullable + private final MongoWriteConcernException writeConcernException; + + static BatchResult okResponse( + final int batchStartModelIndex, + final BatchEncoder.EncodedBatchInfo encodedBatchInfo, + final ExhaustiveBulkWriteCommandOkResponse response, + @Nullable final MongoWriteConcernException writeConcernException) { + return new BatchResult(batchStartModelIndex, encodedBatchInfo, assertNotNull(response), writeConcernException); + } + + static BatchResult noResponse(final int batchStartModelIndex, final BatchEncoder.EncodedBatchInfo encodedBatchInfo) { + return new BatchResult(batchStartModelIndex, encodedBatchInfo, null, null); + } + + private BatchResult( + final int batchStartModelIndex, + final BatchEncoder.EncodedBatchInfo encodedBatchInfo, + @Nullable final ExhaustiveBulkWriteCommandOkResponse response, + @Nullable final MongoWriteConcernException writeConcernException) { + this.batchStartModelIndex = batchStartModelIndex; + this.encodedBatchInfo = encodedBatchInfo; + this.response = response; + this.writeConcernException = writeConcernException; + } + + int getBatchStartModelIndex() { + return batchStartModelIndex; + } + + /** + * @see BatchEncoder.EncodedBatchInfo#getModelsCount() + */ + int getBatchModelsCount() { + return encodedBatchInfo.getModelsCount(); + } + + boolean hasResponse() { + return response != null; + } + + ExhaustiveBulkWriteCommandOkResponse getResponse() { + return assertNotNull(response); + } + + @Nullable + MongoWriteConcernException getWriteConcernException() { + assertTrue(hasResponse()); + return writeConcernException; + } + + /** + * @see BatchEncoder.EncodedBatchInfo#getInsertModelDocumentIds() + */ + Map getInsertModelDocumentIds() { + assertTrue(hasResponse()); + return encodedBatchInfo.getInsertModelDocumentIds(); + } + } + + /** + * Exactly one instance must be used per {@linkplain #executeBatch(int, WriteConcern, WriteBinding, ResultAccumulator) batch}. + */ + private final class BatchEncoder { + private EncodedBatchInfo encodedBatchInfo; + + BatchEncoder() { + encodedBatchInfo = new EncodedBatchInfo(); + } + + /** + * Must be called at most once. + * Must not be called before calling {@link #encodeWriteModel(BsonWriter, ClientWriteModel, int, int)} at least once. + * Renders {@code this} unusable. + */ + EncodedBatchInfo intoEncodedBatchInfo() { + EncodedBatchInfo result = assertNotNull(encodedBatchInfo); + encodedBatchInfo = null; + assertTrue(result.getModelsCount() > 0); + return result; + } + + void reset() { + // we must not reset anything but `modelsCount` + assertNotNull(encodedBatchInfo).modelsCount = 0; + } + + void encodeTxnNumber(final BsonWriter writer, final SessionContext sessionContext) { + EncodedBatchInfo localEncodedBatchInfo = assertNotNull(encodedBatchInfo); + if (localEncodedBatchInfo.txnNumber == EncodedBatchInfo.UNINITIALIZED_TXN_NUMBER) { + localEncodedBatchInfo.txnNumber = sessionContext.advanceTransactionNumber(); + } + writer.writeInt64("txnNumber", localEncodedBatchInfo.txnNumber); + } + + void encodeWriteModel( + final BsonWriter writer, + final ClientWriteModel model, + final int modelIndexInBatch, + final int namespaceIndexInBatch) { + assertNotNull(encodedBatchInfo).modelsCount++; + writer.writeStartDocument(); + if (model instanceof ConcreteClientInsertOneModel) { + writer.writeInt32("insert", namespaceIndexInBatch); + encodeWriteModelInternals(writer, (ConcreteClientInsertOneModel) model, modelIndexInBatch); + } else if (model instanceof ConcreteClientUpdateOneModel) { + writer.writeInt32("update", namespaceIndexInBatch); + writer.writeBoolean("multi", false); + encodeWriteModelInternals(writer, (ConcreteClientUpdateOneModel) model); + } else if (model instanceof ConcreteClientUpdateManyModel) { + writer.writeInt32("update", namespaceIndexInBatch); + writer.writeBoolean("multi", true); + encodeWriteModelInternals(writer, (ConcreteClientUpdateManyModel) model); + } else if (model instanceof ConcreteClientReplaceOneModel) { + writer.writeInt32("update", namespaceIndexInBatch); + encodeWriteModelInternals(writer, (ConcreteClientReplaceOneModel) model); + } else if (model instanceof ConcreteClientDeleteOneModel) { + writer.writeInt32("delete", namespaceIndexInBatch); + writer.writeBoolean("multi", false); + encodeWriteModelInternals(writer, (ConcreteClientDeleteOneModel) model); + } else if (model instanceof ConcreteClientDeleteManyModel) { + writer.writeInt32("delete", namespaceIndexInBatch); + writer.writeBoolean("multi", true); + encodeWriteModelInternals(writer, (ConcreteClientDeleteManyModel) model); + } else { + throw fail(model.getClass().toString()); + } + writer.writeEndDocument(); + } + + private void encodeWriteModelInternals(final BsonWriter writer, final ConcreteClientInsertOneModel model, final int modelIndexInBatch) { + writer.writeName("document"); + Object document = model.getDocument(); + @SuppressWarnings("unchecked") + Encoder documentEncoder = (Encoder) codecRegistry.get(document.getClass()); + assertNotNull(encodedBatchInfo).insertModelDocumentIds.compute(modelIndexInBatch, (k, knownModelDocumentId) -> { + IdHoldingBsonWriter documentIdHoldingBsonWriter = new IdHoldingBsonWriter( + writer, + // Reuse `knownModelDocumentId` if it may have been generated by `IdHoldingBsonWriter` in a previous attempt. + // If its type is not `BsonObjectId`, we know it could not have been generated. + knownModelDocumentId instanceof BsonObjectId ? knownModelDocumentId.asObjectId() : null); + documentEncoder.encode(documentIdHoldingBsonWriter, document, COLLECTIBLE_DOCUMENT_ENCODER_CONTEXT); + return documentIdHoldingBsonWriter.getId(); + }); + } + + private void encodeWriteModelInternals(final BsonWriter writer, final AbstractClientUpdateModel model) { + writer.writeName("filter"); + encodeUsingRegistry(writer, model.getFilter()); + model.getUpdate().ifPresent(value -> { + writer.writeName("updateMods"); + encodeUsingRegistry(writer, value); + }); + model.getUpdatePipeline().ifPresent(value -> { + writer.writeStartArray("updateMods"); + value.forEach(pipelineStage -> encodeUsingRegistry(writer, pipelineStage)); + writer.writeEndArray(); + }); + ConcreteClientUpdateOptions options = model.getOptions(); + options.getArrayFilters().ifPresent(value -> { + writer.writeStartArray("arrayFilters"); + value.forEach(filter -> encodeUsingRegistry(writer, filter)); + writer.writeEndArray(); + }); + options.getCollation().ifPresent(value -> { + writer.writeName("collation"); + encodeUsingRegistry(writer, value.asDocument()); + }); + options.getHint().ifPresent(hint -> { + writer.writeName("hint"); + encodeUsingRegistry(writer, hint); + }); + options.getHintString().ifPresent(value -> writer.writeString("hint", value)); + options.isUpsert().ifPresent(value -> writer.writeBoolean("upsert", value)); + } + + private void encodeWriteModelInternals(final BsonWriter writer, final ConcreteClientReplaceOneModel model) { + writer.writeBoolean("multi", false); + writer.writeName("filter"); + encodeUsingRegistry(writer, model.getFilter()); + writer.writeName("updateMods"); + encodeUsingRegistry(writer, model.getReplacement(), COLLECTIBLE_DOCUMENT_ENCODER_CONTEXT); + ConcreteClientReplaceOptions options = model.getOptions(); + options.getCollation().ifPresent(value -> { + writer.writeName("collation"); + encodeUsingRegistry(writer, value.asDocument()); + }); + options.getHint().ifPresent(value -> { + writer.writeName("hint"); + encodeUsingRegistry(writer, value); + }); + options.getHintString().ifPresent(value -> writer.writeString("hint", value)); + options.isUpsert().ifPresent(value -> writer.writeBoolean("upsert", value)); + } + + private void encodeWriteModelInternals(final BsonWriter writer, final AbstractClientDeleteModel model) { + writer.writeName("filter"); + encodeUsingRegistry(writer, model.getFilter()); + ConcreteClientDeleteOptions options = model.getOptions(); + options.getCollation().ifPresent(value -> { + writer.writeName("collation"); + encodeUsingRegistry(writer, value.asDocument()); + }); + options.getHint().ifPresent(value -> { + writer.writeName("hint"); + encodeUsingRegistry(writer, value); + }); + options.getHintString().ifPresent(value -> writer.writeString("hint", value)); + } + + final class EncodedBatchInfo { + private static final long UNINITIALIZED_TXN_NUMBER = -1; + + private long txnNumber; + private final HashMap insertModelDocumentIds; + private int modelsCount; + + private EncodedBatchInfo() { + insertModelDocumentIds = new HashMap<>(); + modelsCount = 0; + txnNumber = UNINITIALIZED_TXN_NUMBER; + } + + /** + * The key of each entry is the index of a model in the + * {@linkplain #executeBatch(int, WriteConcern, WriteBinding, ResultAccumulator) batch}, + * the value is either the "_id" field value from {@linkplain ConcreteClientInsertOneModel#getDocument()}, + * or the value we generated for this field if the field is absent. + */ + Map getInsertModelDocumentIds() { + return insertModelDocumentIds; + } + + int getModelsCount() { + return modelsCount; + } + } + } +} diff --git a/driver-core/src/main/com/mongodb/internal/operation/CommandOperationHelper.java b/driver-core/src/main/com/mongodb/internal/operation/CommandOperationHelper.java index 4c428131853..2861bcf9ad5 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/CommandOperationHelper.java +++ b/driver-core/src/main/com/mongodb/internal/operation/CommandOperationHelper.java @@ -25,6 +25,7 @@ import com.mongodb.MongoSecurityException; import com.mongodb.MongoServerException; import com.mongodb.MongoSocketException; +import com.mongodb.WriteConcern; import com.mongodb.assertions.Assertions; import com.mongodb.connection.ConnectionDescription; import com.mongodb.connection.ServerDescription; @@ -33,20 +34,40 @@ import com.mongodb.internal.connection.OperationContext; import com.mongodb.internal.operation.OperationHelper.ResourceSupplierInternalException; import com.mongodb.internal.operation.retry.AttachmentKeys; +import com.mongodb.internal.session.SessionContext; import com.mongodb.lang.Nullable; import org.bson.BsonDocument; import java.util.List; +import java.util.Optional; import java.util.function.BinaryOperator; import java.util.function.Supplier; import static com.mongodb.assertions.Assertions.assertFalse; +import static com.mongodb.assertions.Assertions.assertNotNull; import static com.mongodb.internal.operation.OperationHelper.LOGGER; import static java.lang.String.format; import static java.util.Arrays.asList; @SuppressWarnings("overloads") final class CommandOperationHelper { + static WriteConcern validateAndGetEffectiveWriteConcern(final WriteConcern writeConcernSetting, final SessionContext sessionContext) + throws MongoClientException { + boolean activeTransaction = sessionContext.hasActiveTransaction(); + WriteConcern effectiveWriteConcern = activeTransaction + ? WriteConcern.ACKNOWLEDGED + : writeConcernSetting; + if (sessionContext.hasSession() && !sessionContext.isImplicitSession() && !activeTransaction && !effectiveWriteConcern.isAcknowledged()) { + throw new MongoClientException("Unacknowledged writes are not supported when using an explicit session"); + } + return effectiveWriteConcern; + } + + static Optional commandWriteConcern(final WriteConcern effectiveWriteConcern, final SessionContext sessionContext) { + return effectiveWriteConcern.isServerDefault() || sessionContext.hasActiveTransaction() + ? Optional.empty() + : Optional.of(effectiveWriteConcern); + } interface CommandCreator { BsonDocument create( @@ -153,7 +174,26 @@ static boolean shouldAttemptToRetryRead(final RetryState retryState, final Throw return decision; } - static boolean shouldAttemptToRetryWrite(final RetryState retryState, final Throwable attemptFailure) { + static boolean loggingShouldAttemptToRetryWriteAndAddRetryableLabel(final RetryState retryState, final Throwable attemptFailure) { + Throwable attemptFailureNotToBeRetried = getAttemptFailureNotToRetryOrAddRetryableLabel(retryState, attemptFailure); + boolean decision = attemptFailureNotToBeRetried == null; + if (!decision && retryState.attachment(AttachmentKeys.retryableCommandFlag()).orElse(false)) { + logUnableToRetry( + retryState.attachment(AttachmentKeys.commandDescriptionSupplier()).orElse(null), + assertNotNull(attemptFailureNotToBeRetried)); + } + return decision; + } + + static boolean shouldAttemptToRetryWriteAndAddRetryableLabel(final RetryState retryState, final Throwable attemptFailure) { + return getAttemptFailureNotToRetryOrAddRetryableLabel(retryState, attemptFailure) != null; + } + + /** + * @return {@code null} if the decision is {@code true}. Otherwise, returns the {@link Throwable} that must not be retried. + */ + @Nullable + private static Throwable getAttemptFailureNotToRetryOrAddRetryableLabel(final RetryState retryState, final Throwable attemptFailure) { Throwable failure = attemptFailure instanceof ResourceSupplierInternalException ? attemptFailure.getCause() : attemptFailure; boolean decision = false; MongoException exceptionRetryableRegardlessOfCommand = null; @@ -170,11 +210,9 @@ static boolean shouldAttemptToRetryWrite(final RetryState retryState, final Thro } else if (decideRetryableAndAddRetryableWriteErrorLabel(failure, retryState.attachment(AttachmentKeys.maxWireVersion()) .orElse(null))) { decision = true; - } else { - logUnableToRetry(retryState.attachment(AttachmentKeys.commandDescriptionSupplier()).orElse(null), failure); } } - return decision; + return decision ? null : assertNotNull(failure); } static boolean isRetryWritesEnabled(@Nullable final BsonDocument command) { diff --git a/driver-core/src/main/com/mongodb/internal/operation/MixedBulkWriteOperation.java b/driver-core/src/main/com/mongodb/internal/operation/MixedBulkWriteOperation.java index a32ce6d5153..28742574eb4 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/MixedBulkWriteOperation.java +++ b/driver-core/src/main/com/mongodb/internal/operation/MixedBulkWriteOperation.java @@ -16,7 +16,6 @@ package com.mongodb.internal.operation; -import com.mongodb.MongoClientException; import com.mongodb.MongoException; import com.mongodb.MongoNamespace; import com.mongodb.WriteConcern; @@ -63,8 +62,10 @@ import static com.mongodb.internal.operation.AsyncOperationHelper.withAsyncSourceAndConnection; import static com.mongodb.internal.operation.CommandOperationHelper.addRetryableWriteErrorLabel; import static com.mongodb.internal.operation.CommandOperationHelper.logRetryExecute; +import static com.mongodb.internal.operation.CommandOperationHelper.loggingShouldAttemptToRetryWriteAndAddRetryableLabel; import static com.mongodb.internal.operation.CommandOperationHelper.onRetryableWriteAttemptFailure; import static com.mongodb.internal.operation.CommandOperationHelper.transformWriteException; +import static com.mongodb.internal.operation.CommandOperationHelper.validateAndGetEffectiveWriteConcern; import static com.mongodb.internal.operation.OperationHelper.LOGGER; import static com.mongodb.internal.operation.OperationHelper.isRetryableWrite; import static com.mongodb.internal.operation.OperationHelper.validateWriteRequests; @@ -164,7 +165,7 @@ private boolean shouldAttemptToRetryWrite(final RetryState retryState, final Thr if (bulkWriteTracker.lastAttempt()) { return false; } - boolean decision = CommandOperationHelper.shouldAttemptToRetryWrite(retryState, attemptFailure); + boolean decision = loggingShouldAttemptToRetryWriteAndAddRetryableLabel(retryState, attemptFailure); if (decision) { /* The attempt counter maintained by `RetryState` is updated after (in the happens-before order) testing a retry predicate, * and only if the predicate completes normally. Here we maintain attempt counters manually, and we emulate the @@ -274,7 +275,7 @@ private BulkWriteResult executeBulkWriteBatch( if (currentBulkWriteTracker.lastAttempt()) { addRetryableWriteErrorLabel(writeConcernBasedError, maxWireVersion); addErrorLabelsToWriteConcern(result.getDocument("writeConcernError"), writeConcernBasedError.getErrorLabels()); - } else if (CommandOperationHelper.shouldAttemptToRetryWrite(retryState, writeConcernBasedError)) { + } else if (loggingShouldAttemptToRetryWriteAndAddRetryableLabel(retryState, writeConcernBasedError)) { throw new MongoWriteConcernWithResponseException(writeConcernBasedError, result); } } @@ -328,7 +329,7 @@ private void executeBulkWriteBatchAsync( addRetryableWriteErrorLabel(writeConcernBasedError, maxWireVersion); addErrorLabelsToWriteConcern(result.getDocument("writeConcernError"), writeConcernBasedError.getErrorLabels()); - } else if (CommandOperationHelper.shouldAttemptToRetryWrite(retryState, writeConcernBasedError)) { + } else if (loggingShouldAttemptToRetryWriteAndAddRetryableLabel(retryState, writeConcernBasedError)) { iterationCallback.onResult(null, new MongoWriteConcernWithResponseException(writeConcernBasedError, result)); return; @@ -435,24 +436,6 @@ operationContext, shouldExpectResponse(batch, effectiveWriteConcern), batch.getPayload(), batch.getFieldNameValidator(), callback); } - private static WriteConcern validateAndGetEffectiveWriteConcern(final WriteConcern writeConcernSetting, final SessionContext sessionContext) - throws MongoClientException { - boolean activeTransaction = sessionContext.hasActiveTransaction(); - WriteConcern effectiveWriteConcern = activeTransaction - ? WriteConcern.ACKNOWLEDGED - : writeConcernSetting; - if (sessionContext.hasSession() && !sessionContext.isImplicitSession() && !activeTransaction && !effectiveWriteConcern.isAcknowledged()) { - throw new MongoClientException("Unacknowledged writes are not supported when using an explicit session"); - } - return effectiveWriteConcern; - } - - static Optional commandWriteConcern(final WriteConcern effectiveWriteConcern, final SessionContext sessionContext) { - return effectiveWriteConcern.isServerDefault() || sessionContext.hasActiveTransaction() - ? Optional.empty() - : Optional.of(effectiveWriteConcern); - } - private boolean shouldExpectResponse(final BulkWriteBatch batch, final WriteConcern effectiveWriteConcern) { return effectiveWriteConcern.isAcknowledged() || (ordered && batch.hasAnotherBatch()); } diff --git a/driver-core/src/main/com/mongodb/internal/operation/Operations.java b/driver-core/src/main/com/mongodb/internal/operation/Operations.java index 5ec696b61ce..b6fe8f4d19e 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/Operations.java +++ b/driver-core/src/main/com/mongodb/internal/operation/Operations.java @@ -54,6 +54,8 @@ import com.mongodb.client.model.UpdateOptions; import com.mongodb.client.model.ValidationOptions; import com.mongodb.client.model.WriteModel; +import com.mongodb.client.model.bulk.ClientBulkWriteOptions; +import com.mongodb.client.model.bulk.ClientNamespacedWriteModel; import com.mongodb.client.model.changestream.FullDocument; import com.mongodb.client.model.changestream.FullDocumentBeforeChange; import com.mongodb.internal.bulk.DeleteRequest; @@ -718,6 +720,12 @@ ChangeStreamOperation changeStream(final FullDocument fullDoc .retryReads(retryReads); } + ClientBulkWriteOperation clientBulkWriteOperation( + final List clientWriteModels, + @Nullable final ClientBulkWriteOptions options) { + return new ClientBulkWriteOperation(clientWriteModels, options, writeConcern, retryWrites, codecRegistry); + } + private Codec getCodec() { return codecRegistry.get(documentClass); } diff --git a/driver-core/src/main/com/mongodb/internal/operation/SyncOperationHelper.java b/driver-core/src/main/com/mongodb/internal/operation/SyncOperationHelper.java index 62da7cde2c8..0d50768d668 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/SyncOperationHelper.java +++ b/driver-core/src/main/com/mongodb/internal/operation/SyncOperationHelper.java @@ -303,7 +303,7 @@ static T createReadCommandAndExecute( static Supplier decorateWriteWithRetries(final RetryState retryState, final OperationContext operationContext, final Supplier writeFunction) { return new RetryingSyncSupplier<>(retryState, onRetryableWriteAttemptFailure(operationContext), - CommandOperationHelper::shouldAttemptToRetryWrite, () -> { + CommandOperationHelper::loggingShouldAttemptToRetryWriteAndAddRetryableLabel, () -> { logRetryExecute(retryState, operationContext); return writeFunction.get(); }); @@ -335,7 +335,7 @@ static CommandReadTransformer> singleBatchCurso } static BatchCursor cursorDocumentToBatchCursor(final TimeoutMode timeoutMode, final BsonDocument cursorDocument, - final int batchSize, final Decoder decoder, final BsonValue comment, final ConnectionSource source, + final int batchSize, final Decoder decoder, @Nullable final BsonValue comment, final ConnectionSource source, final Connection connection) { return new CommandBatchCursor<>(timeoutMode, cursorDocument, batchSize, 0, decoder, comment, source, connection); } diff --git a/driver-core/src/main/com/mongodb/internal/operation/SyncOperations.java b/driver-core/src/main/com/mongodb/internal/operation/SyncOperations.java index 73a83310d65..65a6a9fe82e 100644 --- a/driver-core/src/main/com/mongodb/internal/operation/SyncOperations.java +++ b/driver-core/src/main/com/mongodb/internal/operation/SyncOperations.java @@ -44,8 +44,11 @@ import com.mongodb.client.model.SearchIndexModel; import com.mongodb.client.model.UpdateOptions; import com.mongodb.client.model.WriteModel; +import com.mongodb.client.model.bulk.ClientBulkWriteOptions; +import com.mongodb.client.model.bulk.ClientNamespacedWriteModel; import com.mongodb.client.model.changestream.FullDocument; import com.mongodb.client.model.changestream.FullDocumentBeforeChange; +import com.mongodb.client.model.bulk.ClientBulkWriteResult; import com.mongodb.internal.TimeoutSettings; import com.mongodb.internal.client.model.AggregationLevel; import com.mongodb.internal.client.model.FindOptions; @@ -358,4 +361,10 @@ public ReadOperation> changeStream(final FullDocu return operations.changeStream(fullDocument, fullDocumentBeforeChange, pipeline, decoder, changeStreamLevel, batchSize, collation, comment, resumeToken, startAtOperationTime, startAfter, showExpandedEvents); } + + public WriteOperation clientBulkWriteOperation( + final List clientWriteModels, + @Nullable final ClientBulkWriteOptions options) { + return operations.clientBulkWriteOperation(clientWriteModels, options); + } } diff --git a/driver-core/src/main/com/mongodb/internal/session/SessionContext.java b/driver-core/src/main/com/mongodb/internal/session/SessionContext.java index 6c55c526d45..4a8902799ec 100644 --- a/driver-core/src/main/com/mongodb/internal/session/SessionContext.java +++ b/driver-core/src/main/com/mongodb/internal/session/SessionContext.java @@ -48,7 +48,7 @@ public interface SessionContext { /** * Advance the transaction number. * - * @return the next transaction number for the session + * @return the next non-negative transaction number for the session */ long advanceTransactionNumber(); diff --git a/driver-core/src/main/com/mongodb/internal/validator/UpdateFieldNameValidator.java b/driver-core/src/main/com/mongodb/internal/validator/UpdateFieldNameValidator.java index 40762bfb5fb..fc59b0cc312 100644 --- a/driver-core/src/main/com/mongodb/internal/validator/UpdateFieldNameValidator.java +++ b/driver-core/src/main/com/mongodb/internal/validator/UpdateFieldNameValidator.java @@ -48,7 +48,7 @@ public FieldNameValidator getValidatorForField(final String fieldName) { @Override public void start() { - encounteredField = false; + reset(); } @Override @@ -57,4 +57,9 @@ public void end() { throw new IllegalArgumentException("Invalid BSON document for an update. The document may not be empty."); } } + + public UpdateFieldNameValidator reset() { + encounteredField = false; + return this; + } } diff --git a/driver-core/src/test/resources/unified-test-format/command-monitoring/unacknowledged-client-bulkWrite.json b/driver-core/src/test/resources/unified-test-format/command-monitoring/unacknowledged-client-bulkWrite.json new file mode 100644 index 00000000000..b30e1540f45 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/command-monitoring/unacknowledged-client-bulkWrite.json @@ -0,0 +1,219 @@ +{ + "description": "unacknowledged-client-bulkWrite", + "schemaVersion": "1.7", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "serverless": "forbid" + } + ], + "createEntities": [ + { + "client": { + "id": "client", + "useMultipleMongoses": false, + "observeEvents": [ + "commandStartedEvent", + "commandSucceededEvent", + "commandFailedEvent" + ], + "uriOptions": { + "w": 0 + } + } + }, + { + "database": { + "id": "database", + "client": "client", + "databaseName": "command-monitoring-tests" + } + }, + { + "collection": { + "id": "collection", + "database": "database", + "collectionName": "test" + } + } + ], + "initialData": [ + { + "collectionName": "test", + "databaseName": "command-monitoring-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "_yamlAnchors": { + "namespace": "command-monitoring-tests.test" + }, + "tests": [ + { + "description": "A successful mixed client bulkWrite", + "operations": [ + { + "object": "client", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "command-monitoring-tests.test", + "document": { + "_id": 4, + "x": 44 + } + } + }, + { + "updateOne": { + "namespace": "command-monitoring-tests.test", + "filter": { + "_id": 3 + }, + "update": { + "$set": { + "x": 333 + } + } + } + } + ] + }, + "expectResult": { + "insertedCount": { + "$$unsetOrMatches": 0 + }, + "upsertedCount": { + "$$unsetOrMatches": 0 + }, + "matchedCount": { + "$$unsetOrMatches": 0 + }, + "modifiedCount": { + "$$unsetOrMatches": 0 + }, + "deletedCount": { + "$$unsetOrMatches": 0 + }, + "insertResults": { + "$$unsetOrMatches": {} + }, + "updateResults": { + "$$unsetOrMatches": {} + }, + "deleteResults": { + "$$unsetOrMatches": {} + } + } + }, + { + "object": "collection", + "name": "find", + "arguments": { + "filter": {} + }, + "expectResult": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 333 + }, + { + "_id": 4, + "x": 44 + } + ] + } + ], + "expectEvents": [ + { + "client": "client", + "ignoreExtraEvents": true, + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": true, + "ordered": true, + "ops": [ + { + "insert": 0, + "document": { + "_id": 4, + "x": 44 + } + }, + { + "update": 0, + "filter": { + "_id": 3 + }, + "updateMods": { + "$set": { + "x": 333 + } + }, + "multi": false + } + ], + "nsInfo": [ + { + "ns": "command-monitoring-tests.test" + } + ] + } + } + }, + { + "commandSucceededEvent": { + "commandName": "bulkWrite", + "reply": { + "ok": 1, + "nInserted": { + "$$exists": false + }, + "nMatched": { + "$$exists": false + }, + "nModified": { + "$$exists": false + }, + "nUpserted": { + "$$exists": false + }, + "nDeleted": { + "$$exists": false + } + } + } + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-delete-options.json b/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-delete-options.json new file mode 100644 index 00000000000..d9987897dcd --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-delete-options.json @@ -0,0 +1,268 @@ +{ + "description": "client bulkWrite delete options", + "schemaVersion": "1.4", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "serverless": "forbid" + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "_yamlAnchors": { + "namespace": "crud-tests.coll0", + "collation": { + "locale": "simple" + }, + "hint": "_id_" + }, + "tests": [ + { + "description": "client bulk write delete with collation", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "deleteOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": 1 + }, + "collation": { + "locale": "simple" + } + } + }, + { + "deleteMany": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": { + "$gt": 1 + } + }, + "collation": { + "locale": "simple" + } + } + } + ], + "verboseResults": true + }, + "expectResult": { + "insertedCount": 0, + "upsertedCount": 0, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 3, + "insertResults": {}, + "updateResults": {}, + "deleteResults": { + "0": { + "deletedCount": 1 + }, + "1": { + "deletedCount": 2 + } + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "ops": [ + { + "delete": 0, + "filter": { + "_id": 1 + }, + "collation": { + "locale": "simple" + }, + "multi": false + }, + { + "delete": 0, + "filter": { + "_id": { + "$gt": 1 + } + }, + "collation": { + "locale": "simple" + }, + "multi": true + } + ], + "nsInfo": [ + { + "ns": "crud-tests.coll0" + } + ] + } + } + } + ] + } + ], + "outcome": [ + { + "databaseName": "crud-tests", + "collectionName": "coll0", + "documents": [] + } + ] + }, + { + "description": "client bulk write delete with hint", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "deleteOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": 1 + }, + "hint": "_id_" + } + }, + { + "deleteMany": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": { + "$gt": 1 + } + }, + "hint": "_id_" + } + } + ], + "verboseResults": true + }, + "expectResult": { + "insertedCount": 0, + "upsertedCount": 0, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 3, + "insertResults": {}, + "updateResults": {}, + "deleteResults": { + "0": { + "deletedCount": 1 + }, + "1": { + "deletedCount": 2 + } + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "ops": [ + { + "delete": 0, + "filter": { + "_id": 1 + }, + "hint": "_id_", + "multi": false + }, + { + "delete": 0, + "filter": { + "_id": { + "$gt": 1 + } + }, + "hint": "_id_", + "multi": true + } + ] + } + } + } + ] + } + ], + "outcome": [ + { + "databaseName": "crud-tests", + "collectionName": "coll0", + "documents": [] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-errorResponse.json b/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-errorResponse.json new file mode 100644 index 00000000000..b828aad3b93 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-errorResponse.json @@ -0,0 +1,69 @@ +{ + "description": "client bulkWrite errorResponse", + "schemaVersion": "1.12", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "serverless": "forbid" + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": false + } + } + ], + "_yamlAnchors": { + "namespace": "crud-tests.coll0" + }, + "tests": [ + { + "description": "client bulkWrite operations support errorResponse assertions", + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "client0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "bulkWrite" + ], + "errorCode": 8 + } + } + } + }, + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 1 + } + } + } + ] + }, + "expectError": { + "errorCode": 8, + "errorResponse": { + "code": 8 + } + } + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-errors.json b/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-errors.json new file mode 100644 index 00000000000..8cc45bb5f2d --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-errors.json @@ -0,0 +1,455 @@ +{ + "description": "client bulkWrite errors", + "schemaVersion": "1.21", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "serverless": "forbid" + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent" + ], + "uriOptions": { + "retryWrites": false + }, + "useMultipleMongoses": false + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "_yamlAnchors": { + "namespace": "crud-tests.coll0", + "writeConcernErrorCode": 91, + "writeConcernErrorMessage": "Replication is being shut down", + "undefinedVarCode": 17276 + }, + "tests": [ + { + "description": "an individual operation fails during an ordered bulkWrite", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "deleteOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": 1 + } + } + }, + { + "deleteOne": { + "namespace": "crud-tests.coll0", + "filter": { + "$expr": { + "$eq": [ + "$_id", + "$$id2" + ] + } + } + } + }, + { + "deleteOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": 3 + } + } + } + ], + "verboseResults": true + }, + "expectError": { + "expectResult": { + "insertedCount": 0, + "upsertedCount": 0, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 1, + "insertResults": {}, + "updateResults": {}, + "deleteResults": { + "0": { + "deletedCount": 1 + } + } + }, + "writeErrors": { + "1": { + "code": 17276 + } + } + } + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ] + }, + { + "description": "an individual operation fails during an unordered bulkWrite", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "deleteOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": 1 + } + } + }, + { + "deleteOne": { + "namespace": "crud-tests.coll0", + "filter": { + "$expr": { + "$eq": [ + "$_id", + "$$id2" + ] + } + } + } + }, + { + "deleteOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": 3 + } + } + } + ], + "verboseResults": true, + "ordered": false + }, + "expectError": { + "expectResult": { + "insertedCount": 0, + "upsertedCount": 0, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 2, + "insertResults": {}, + "updateResults": {}, + "deleteResults": { + "0": { + "deletedCount": 1 + }, + "2": { + "deletedCount": 1 + } + } + }, + "writeErrors": { + "1": { + "code": 17276 + } + } + } + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 2, + "x": 22 + } + ] + } + ] + }, + { + "description": "detailed results are omitted from error when verboseResults is false", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "deleteOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": 1 + } + } + }, + { + "deleteOne": { + "namespace": "crud-tests.coll0", + "filter": { + "$expr": { + "$eq": [ + "$_id", + "$$id2" + ] + } + } + } + }, + { + "deleteOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": 3 + } + } + } + ], + "verboseResults": false + }, + "expectError": { + "expectResult": { + "insertedCount": 0, + "upsertedCount": 0, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 1, + "insertResults": { + "$$unsetOrMatches": {} + }, + "updateResults": { + "$$unsetOrMatches": {} + }, + "deleteResults": { + "$$unsetOrMatches": {} + } + }, + "writeErrors": { + "1": { + "code": 17276 + } + } + } + } + ] + }, + { + "description": "a top-level failure occurs during a bulkWrite", + "operations": [ + { + "object": "testRunner", + "name": "failPoint", + "arguments": { + "client": "client0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "bulkWrite" + ], + "errorCode": 8 + } + } + } + }, + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "x": 1 + } + } + } + ], + "verboseResults": true + }, + "expectError": { + "errorCode": 8 + } + } + ] + }, + { + "description": "a bulk write with only errors does not report a partial result", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "deleteOne": { + "namespace": "crud-tests.coll0", + "filter": { + "$expr": { + "$eq": [ + "$_id", + "$$id2" + ] + } + } + } + } + ], + "verboseResults": true + }, + "expectError": { + "expectResult": { + "$$unsetOrMatches": {} + }, + "writeErrors": { + "0": { + "code": 17276 + } + } + } + } + ] + }, + { + "description": "a write concern error occurs during a bulkWrite", + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "client0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "bulkWrite" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + } + } + }, + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 10 + } + } + } + ], + "verboseResults": true + }, + "expectError": { + "expectResult": { + "insertedCount": 1, + "upsertedCount": 0, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 0, + "insertResults": { + "0": { + "insertedId": 10 + } + }, + "updateResults": {}, + "deleteResults": {} + }, + "writeConcernErrors": [ + { + "code": 91, + "message": "Replication is being shut down" + } + ] + } + } + ] + }, + { + "description": "an empty list of write models is a client-side error", + "operations": [ + { + "name": "clientBulkWrite", + "object": "client0", + "arguments": { + "models": [], + "verboseResults": true + }, + "expectError": { + "isClientError": true + } + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-mixed-namespaces.json b/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-mixed-namespaces.json new file mode 100644 index 00000000000..55f06189233 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-mixed-namespaces.json @@ -0,0 +1,315 @@ +{ + "description": "client bulkWrite with mixed namespaces", + "schemaVersion": "1.4", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "serverless": "forbid" + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "db0" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + }, + { + "collection": { + "id": "collection1", + "database": "database0", + "collectionName": "coll1" + } + }, + { + "database": { + "id": "database1", + "client": "client0", + "databaseName": "db1" + } + }, + { + "collection": { + "id": "collection2", + "database": "database1", + "collectionName": "coll2" + } + } + ], + "initialData": [ + { + "databaseName": "db0", + "collectionName": "coll0", + "documents": [] + }, + { + "databaseName": "db0", + "collectionName": "coll1", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + }, + { + "databaseName": "db1", + "collectionName": "coll2", + "documents": [ + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ] + } + ], + "_yamlAnchors": { + "db0Coll0Namespace": "db0.coll0", + "db0Coll1Namespace": "db0.coll1", + "db1Coll2Namespace": "db1.coll2" + }, + "tests": [ + { + "description": "client bulkWrite with mixed namespaces", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "db0.coll0", + "document": { + "_id": 1 + } + } + }, + { + "insertOne": { + "namespace": "db0.coll0", + "document": { + "_id": 2 + } + } + }, + { + "updateOne": { + "namespace": "db0.coll1", + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + }, + { + "deleteOne": { + "namespace": "db1.coll2", + "filter": { + "_id": 3 + } + } + }, + { + "deleteOne": { + "namespace": "db0.coll1", + "filter": { + "_id": 2 + } + } + }, + { + "replaceOne": { + "namespace": "db1.coll2", + "filter": { + "_id": 4 + }, + "replacement": { + "x": 45 + } + } + } + ], + "verboseResults": true + }, + "expectResult": { + "insertedCount": 2, + "upsertedCount": 0, + "matchedCount": 2, + "modifiedCount": 2, + "deletedCount": 2, + "insertResults": { + "0": { + "insertedId": 1 + }, + "1": { + "insertedId": 2 + } + }, + "updateResults": { + "2": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedId": { + "$$exists": false + } + }, + "5": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedId": { + "$$exists": false + } + } + }, + "deleteResults": { + "3": { + "deletedCount": 1 + }, + "4": { + "deletedCount": 1 + } + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "bulkWrite": 1, + "ops": [ + { + "insert": 0, + "document": { + "_id": 1 + } + }, + { + "insert": 0, + "document": { + "_id": 2 + } + }, + { + "update": 1, + "filter": { + "_id": 1 + }, + "updateMods": { + "$inc": { + "x": 1 + } + }, + "multi": false + }, + { + "delete": 2, + "filter": { + "_id": 3 + }, + "multi": false + }, + { + "delete": 1, + "filter": { + "_id": 2 + }, + "multi": false + }, + { + "update": 2, + "filter": { + "_id": 4 + }, + "updateMods": { + "x": 45 + }, + "multi": false + } + ], + "nsInfo": [ + { + "ns": "db0.coll0" + }, + { + "ns": "db0.coll1" + }, + { + "ns": "db1.coll2" + } + ] + } + } + } + ] + } + ], + "outcome": [ + { + "databaseName": "db0", + "collectionName": "coll0", + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + } + ] + }, + { + "databaseName": "db0", + "collectionName": "coll1", + "documents": [ + { + "_id": 1, + "x": 12 + } + ] + }, + { + "databaseName": "db1", + "collectionName": "coll2", + "documents": [ + { + "_id": 4, + "x": 45 + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-options.json b/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-options.json new file mode 100644 index 00000000000..708fe4e85b0 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-options.json @@ -0,0 +1,716 @@ +{ + "description": "client bulkWrite top-level options", + "schemaVersion": "1.4", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "serverless": "forbid" + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "client": { + "id": "writeConcernClient", + "uriOptions": { + "w": 1 + }, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + } + ] + } + ], + "_yamlAnchors": { + "namespace": "crud-tests.coll0", + "comment": { + "bulk": "write" + }, + "let": { + "id1": 1, + "id2": 2 + }, + "writeConcern": { + "w": "majority" + } + }, + "tests": [ + { + "description": "client bulkWrite comment", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 3, + "x": 33 + } + } + } + ], + "comment": { + "bulk": "write" + }, + "verboseResults": true + }, + "expectResult": { + "insertedCount": 1, + "upsertedCount": 0, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 0, + "insertResults": { + "0": { + "insertedId": 3 + } + }, + "updateResults": {}, + "deleteResults": {} + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "comment": { + "bulk": "write" + }, + "ops": [ + { + "insert": 0, + "document": { + "_id": 3, + "x": 33 + } + } + ], + "nsInfo": [ + { + "ns": "crud-tests.coll0" + } + ] + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ] + }, + { + "description": "client bulkWrite bypassDocumentValidation", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 3, + "x": 33 + } + } + } + ], + "bypassDocumentValidation": true, + "verboseResults": true + }, + "expectResult": { + "insertedCount": 1, + "upsertedCount": 0, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 0, + "insertResults": { + "0": { + "insertedId": 3 + } + }, + "updateResults": {}, + "deleteResults": {} + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "bypassDocumentValidation": true, + "ops": [ + { + "insert": 0, + "document": { + "_id": 3, + "x": 33 + } + } + ], + "nsInfo": [ + { + "ns": "crud-tests.coll0" + } + ] + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ] + }, + { + "description": "client bulkWrite let", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "updateOne": { + "namespace": "crud-tests.coll0", + "filter": { + "$expr": { + "$eq": [ + "$_id", + "$$id1" + ] + } + }, + "update": { + "$inc": { + "x": 1 + } + } + } + }, + { + "deleteOne": { + "namespace": "crud-tests.coll0", + "filter": { + "$expr": { + "$eq": [ + "$_id", + "$$id2" + ] + } + } + } + } + ], + "let": { + "id1": 1, + "id2": 2 + }, + "verboseResults": true + }, + "expectResult": { + "insertedCount": 0, + "upsertedCount": 0, + "matchedCount": 1, + "modifiedCount": 1, + "deletedCount": 1, + "insertResults": {}, + "updateResults": { + "0": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedId": { + "$$exists": false + } + } + }, + "deleteResults": { + "1": { + "deletedCount": 1 + } + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "let": { + "id1": 1, + "id2": 2 + }, + "ops": [ + { + "update": 0, + "filter": { + "$expr": { + "$eq": [ + "$_id", + "$$id1" + ] + } + }, + "updateMods": { + "$inc": { + "x": 1 + } + }, + "multi": false + }, + { + "delete": 0, + "filter": { + "$expr": { + "$eq": [ + "$_id", + "$$id2" + ] + } + }, + "multi": false + } + ], + "nsInfo": [ + { + "ns": "crud-tests.coll0" + } + ] + } + } + } + ] + } + ], + "outcome": [ + { + "databaseName": "crud-tests", + "collectionName": "coll0", + "documents": [ + { + "_id": 1, + "x": 12 + } + ] + } + ] + }, + { + "description": "client bulkWrite bypassDocumentValidation: false is sent", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 3, + "x": 33 + } + } + } + ], + "bypassDocumentValidation": false, + "verboseResults": true + }, + "expectResult": { + "insertedCount": 1, + "upsertedCount": 0, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 0, + "insertResults": { + "0": { + "insertedId": 3 + } + }, + "updateResults": {}, + "deleteResults": {} + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "bypassDocumentValidation": false, + "ops": [ + { + "insert": 0, + "document": { + "_id": 3, + "x": 33 + } + } + ], + "nsInfo": [ + { + "ns": "crud-tests.coll0" + } + ] + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ] + }, + { + "description": "client bulkWrite writeConcern", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 3, + "x": 33 + } + } + } + ], + "writeConcern": { + "w": "majority" + }, + "verboseResults": true + }, + "expectResult": { + "insertedCount": 1, + "upsertedCount": 0, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 0, + "insertResults": { + "0": { + "insertedId": 3 + } + }, + "updateResults": {}, + "deleteResults": {} + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "writeConcern": { + "w": "majority" + }, + "ops": [ + { + "insert": 0, + "document": { + "_id": 3, + "x": 33 + } + } + ], + "nsInfo": [ + { + "ns": "crud-tests.coll0" + } + ] + } + } + } + ] + } + ] + }, + { + "description": "client bulkWrite inherits writeConcern from client", + "operations": [ + { + "object": "writeConcernClient", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 3, + "x": 33 + } + } + } + ], + "verboseResults": true + }, + "expectResult": { + "insertedCount": 1, + "upsertedCount": 0, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 0, + "insertResults": { + "0": { + "insertedId": 3 + } + }, + "updateResults": {}, + "deleteResults": {} + } + } + ], + "expectEvents": [ + { + "client": "writeConcernClient", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "writeConcern": { + "w": 1 + }, + "ops": [ + { + "insert": 0, + "document": { + "_id": 3, + "x": 33 + } + } + ], + "nsInfo": [ + { + "ns": "crud-tests.coll0" + } + ] + } + } + } + ] + } + ] + }, + { + "description": "client bulkWrite writeConcern option overrides client writeConcern", + "operations": [ + { + "object": "writeConcernClient", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 3, + "x": 33 + } + } + } + ], + "writeConcern": { + "w": "majority" + }, + "verboseResults": true + }, + "expectResult": { + "insertedCount": 1, + "upsertedCount": 0, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 0, + "insertResults": { + "0": { + "insertedId": 3 + } + }, + "updateResults": {}, + "deleteResults": {} + } + } + ], + "expectEvents": [ + { + "client": "writeConcernClient", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "writeConcern": { + "w": "majority" + }, + "ops": [ + { + "insert": 0, + "document": { + "_id": 3, + "x": 33 + } + } + ], + "nsInfo": [ + { + "ns": "crud-tests.coll0" + } + ] + } + } + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-ordered.json b/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-ordered.json new file mode 100644 index 00000000000..6fb10d992f0 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-ordered.json @@ -0,0 +1,291 @@ +{ + "description": "client bulkWrite with ordered option", + "schemaVersion": "1.4", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "serverless": "forbid" + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [] + } + ], + "_yamlAnchors": { + "namespace": "crud-tests.coll0" + }, + "tests": [ + { + "description": "client bulkWrite with ordered: false", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 1, + "x": 11 + } + } + } + ], + "verboseResults": true, + "ordered": false + }, + "expectResult": { + "insertedCount": 1, + "upsertedCount": 0, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 0, + "insertResults": { + "0": { + "insertedId": 1 + } + }, + "updateResults": {}, + "deleteResults": {} + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": false, + "ops": [ + { + "insert": 0, + "document": { + "_id": 1, + "x": 11 + } + } + ], + "nsInfo": [ + { + "ns": "crud-tests.coll0" + } + ] + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + } + ] + } + ] + }, + { + "description": "client bulkWrite with ordered: true", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 1, + "x": 11 + } + } + } + ], + "verboseResults": true, + "ordered": true + }, + "expectResult": { + "insertedCount": 1, + "upsertedCount": 0, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 0, + "insertResults": { + "0": { + "insertedId": 1 + } + }, + "updateResults": {}, + "deleteResults": {} + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "ops": [ + { + "insert": 0, + "document": { + "_id": 1, + "x": 11 + } + } + ], + "nsInfo": [ + { + "ns": "crud-tests.coll0" + } + ] + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + } + ] + } + ] + }, + { + "description": "client bulkWrite defaults to ordered: true", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 1, + "x": 11 + } + } + } + ], + "verboseResults": true + }, + "expectResult": { + "insertedCount": 1, + "upsertedCount": 0, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 0, + "insertResults": { + "0": { + "insertedId": 1 + } + }, + "updateResults": {}, + "deleteResults": {} + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "ops": [ + { + "insert": 0, + "document": { + "_id": 1, + "x": 11 + } + } + ], + "nsInfo": [ + { + "ns": "crud-tests.coll0" + } + ] + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-results.json b/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-results.json new file mode 100644 index 00000000000..accf5a9cbf5 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-results.json @@ -0,0 +1,833 @@ +{ + "description": "client bulkWrite results", + "schemaVersion": "1.4", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "serverless": "forbid" + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 5, + "x": 55 + }, + { + "_id": 6, + "x": 66 + }, + { + "_id": 7, + "x": 77 + } + ] + } + ], + "_yamlAnchors": { + "namespace": "crud-tests.coll0" + }, + "tests": [ + { + "description": "client bulkWrite with verboseResults: true returns detailed results", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 8, + "x": 88 + } + } + }, + { + "updateOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + }, + { + "updateMany": { + "namespace": "crud-tests.coll0", + "filter": { + "$and": [ + { + "_id": { + "$gt": 1 + } + }, + { + "_id": { + "$lte": 3 + } + } + ] + }, + "update": { + "$inc": { + "x": 2 + } + } + } + }, + { + "replaceOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": 4 + }, + "replacement": { + "x": 44 + }, + "upsert": true + } + }, + { + "deleteOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": 5 + } + } + }, + { + "deleteMany": { + "namespace": "crud-tests.coll0", + "filter": { + "$and": [ + { + "_id": { + "$gt": 5 + } + }, + { + "_id": { + "$lte": 7 + } + } + ] + } + } + } + ], + "verboseResults": true + }, + "expectResult": { + "insertedCount": 1, + "upsertedCount": 1, + "matchedCount": 3, + "modifiedCount": 3, + "deletedCount": 3, + "insertResults": { + "0": { + "insertedId": 8 + } + }, + "updateResults": { + "1": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedId": { + "$$exists": false + } + }, + "2": { + "matchedCount": 2, + "modifiedCount": 2, + "upsertedId": { + "$$exists": false + } + }, + "3": { + "matchedCount": 1, + "modifiedCount": 0, + "upsertedId": 4 + } + }, + "deleteResults": { + "4": { + "deletedCount": 1 + }, + "5": { + "deletedCount": 2 + } + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "ops": [ + { + "insert": 0, + "document": { + "_id": 8, + "x": 88 + } + }, + { + "update": 0, + "filter": { + "_id": 1 + }, + "updateMods": { + "$inc": { + "x": 1 + } + }, + "multi": false + }, + { + "update": 0, + "filter": { + "$and": [ + { + "_id": { + "$gt": 1 + } + }, + { + "_id": { + "$lte": 3 + } + } + ] + }, + "updateMods": { + "$inc": { + "x": 2 + } + }, + "multi": true + }, + { + "update": 0, + "filter": { + "_id": 4 + }, + "updateMods": { + "x": 44 + }, + "upsert": true, + "multi": false + }, + { + "delete": 0, + "filter": { + "_id": 5 + }, + "multi": false + }, + { + "delete": 0, + "filter": { + "$and": [ + { + "_id": { + "$gt": 5 + } + }, + { + "_id": { + "$lte": 7 + } + } + ] + }, + "multi": true + } + ], + "nsInfo": [ + { + "ns": "crud-tests.coll0" + } + ] + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 12 + }, + { + "_id": 2, + "x": 24 + }, + { + "_id": 3, + "x": 35 + }, + { + "_id": 4, + "x": 44 + }, + { + "_id": 8, + "x": 88 + } + ] + } + ] + }, + { + "description": "client bulkWrite with verboseResults: false omits detailed results", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 8, + "x": 88 + } + } + }, + { + "updateOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + }, + { + "updateMany": { + "namespace": "crud-tests.coll0", + "filter": { + "$and": [ + { + "_id": { + "$gt": 1 + } + }, + { + "_id": { + "$lte": 3 + } + } + ] + }, + "update": { + "$inc": { + "x": 2 + } + } + } + }, + { + "replaceOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": 4 + }, + "replacement": { + "x": 44 + }, + "upsert": true + } + }, + { + "deleteOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": 5 + } + } + }, + { + "deleteMany": { + "namespace": "crud-tests.coll0", + "filter": { + "$and": [ + { + "_id": { + "$gt": 5 + } + }, + { + "_id": { + "$lte": 7 + } + } + ] + } + } + } + ], + "verboseResults": false + }, + "expectResult": { + "insertedCount": 1, + "upsertedCount": 1, + "matchedCount": 3, + "modifiedCount": 3, + "deletedCount": 3, + "insertResults": { + "$$unsetOrMatches": {} + }, + "updateResults": { + "$$unsetOrMatches": {} + }, + "deleteResults": { + "$$unsetOrMatches": {} + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": true, + "ordered": true, + "ops": [ + { + "insert": 0, + "document": { + "_id": 8, + "x": 88 + } + }, + { + "update": 0, + "filter": { + "_id": 1 + }, + "updateMods": { + "$inc": { + "x": 1 + } + }, + "multi": false + }, + { + "update": 0, + "filter": { + "$and": [ + { + "_id": { + "$gt": 1 + } + }, + { + "_id": { + "$lte": 3 + } + } + ] + }, + "updateMods": { + "$inc": { + "x": 2 + } + }, + "multi": true + }, + { + "update": 0, + "filter": { + "_id": 4 + }, + "updateMods": { + "x": 44 + }, + "upsert": true, + "multi": false + }, + { + "delete": 0, + "filter": { + "_id": 5 + }, + "multi": false + }, + { + "delete": 0, + "filter": { + "$and": [ + { + "_id": { + "$gt": 5 + } + }, + { + "_id": { + "$lte": 7 + } + } + ] + }, + "multi": true + } + ], + "nsInfo": [ + { + "ns": "crud-tests.coll0" + } + ] + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 12 + }, + { + "_id": 2, + "x": 24 + }, + { + "_id": 3, + "x": 35 + }, + { + "_id": 4, + "x": 44 + }, + { + "_id": 8, + "x": 88 + } + ] + } + ] + }, + { + "description": "client bulkWrite defaults to verboseResults: false", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "crud-tests.coll0", + "document": { + "_id": 8, + "x": 88 + } + } + }, + { + "updateOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + }, + { + "updateMany": { + "namespace": "crud-tests.coll0", + "filter": { + "$and": [ + { + "_id": { + "$gt": 1 + } + }, + { + "_id": { + "$lte": 3 + } + } + ] + }, + "update": { + "$inc": { + "x": 2 + } + } + } + }, + { + "replaceOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": 4 + }, + "replacement": { + "x": 44 + }, + "upsert": true + } + }, + { + "deleteOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": 5 + } + } + }, + { + "deleteMany": { + "namespace": "crud-tests.coll0", + "filter": { + "$and": [ + { + "_id": { + "$gt": 5 + } + }, + { + "_id": { + "$lte": 7 + } + } + ] + } + } + } + ] + }, + "expectResult": { + "insertedCount": 1, + "upsertedCount": 1, + "matchedCount": 3, + "modifiedCount": 3, + "deletedCount": 3, + "insertResults": { + "$$unsetOrMatches": {} + }, + "updateResults": { + "$$unsetOrMatches": {} + }, + "deleteResults": { + "$$unsetOrMatches": {} + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": true, + "ordered": true, + "ops": [ + { + "insert": 0, + "document": { + "_id": 8, + "x": 88 + } + }, + { + "update": 0, + "filter": { + "_id": 1 + }, + "updateMods": { + "$inc": { + "x": 1 + } + }, + "multi": false + }, + { + "update": 0, + "filter": { + "$and": [ + { + "_id": { + "$gt": 1 + } + }, + { + "_id": { + "$lte": 3 + } + } + ] + }, + "updateMods": { + "$inc": { + "x": 2 + } + }, + "multi": true + }, + { + "update": 0, + "filter": { + "_id": 4 + }, + "updateMods": { + "x": 44 + }, + "upsert": true, + "multi": false + }, + { + "delete": 0, + "filter": { + "_id": 5 + }, + "multi": false + }, + { + "delete": 0, + "filter": { + "$and": [ + { + "_id": { + "$gt": 5 + } + }, + { + "_id": { + "$lte": 7 + } + } + ] + }, + "multi": true + } + ], + "nsInfo": [ + { + "ns": "crud-tests.coll0" + } + ] + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 12 + }, + { + "_id": 2, + "x": 24 + }, + { + "_id": 3, + "x": 35 + }, + { + "_id": 4, + "x": 44 + }, + { + "_id": 8, + "x": 88 + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-update-options.json b/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-update-options.json new file mode 100644 index 00000000000..ce6241c6812 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-update-options.json @@ -0,0 +1,949 @@ +{ + "description": "client bulkWrite update options", + "schemaVersion": "1.4", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "serverless": "forbid" + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "array": [ + 1, + 2, + 3 + ] + }, + { + "_id": 2, + "array": [ + 1, + 2, + 3 + ] + }, + { + "_id": 3, + "array": [ + 1, + 2, + 3 + ] + }, + { + "_id": 4, + "array": [ + 1, + 2, + 3 + ] + } + ] + } + ], + "_yamlAnchors": { + "namespace": "crud-tests.coll0", + "collation": { + "locale": "simple" + }, + "hint": "_id_" + }, + "tests": [ + { + "description": "client bulkWrite update with arrayFilters", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "updateOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": 1 + }, + "update": { + "$set": { + "array.$[i]": 4 + } + }, + "arrayFilters": [ + { + "i": { + "$gte": 2 + } + } + ] + } + }, + { + "updateMany": { + "namespace": "crud-tests.coll0", + "filter": { + "$and": [ + { + "_id": { + "$gt": 1 + } + }, + { + "_id": { + "$lte": 3 + } + } + ] + }, + "update": { + "$set": { + "array.$[i]": 5 + } + }, + "arrayFilters": [ + { + "i": { + "$gte": 2 + } + } + ] + } + } + ], + "verboseResults": true + }, + "expectResult": { + "insertedCount": 0, + "upsertedCount": 0, + "matchedCount": 3, + "modifiedCount": 3, + "deletedCount": 0, + "insertResults": {}, + "updateResults": { + "0": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedId": { + "$$exists": false + } + }, + "1": { + "matchedCount": 2, + "modifiedCount": 2, + "upsertedId": { + "$$exists": false + } + } + }, + "deleteResults": {} + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "ops": [ + { + "update": 0, + "filter": { + "_id": 1 + }, + "updateMods": { + "$set": { + "array.$[i]": 4 + } + }, + "arrayFilters": [ + { + "i": { + "$gte": 2 + } + } + ], + "multi": false + }, + { + "update": 0, + "filter": { + "$and": [ + { + "_id": { + "$gt": 1 + } + }, + { + "_id": { + "$lte": 3 + } + } + ] + }, + "updateMods": { + "$set": { + "array.$[i]": 5 + } + }, + "arrayFilters": [ + { + "i": { + "$gte": 2 + } + } + ], + "multi": true + } + ], + "nsInfo": [ + { + "ns": "crud-tests.coll0" + } + ] + } + } + } + ] + } + ], + "outcome": [ + { + "databaseName": "crud-tests", + "collectionName": "coll0", + "documents": [ + { + "_id": 1, + "array": [ + 1, + 4, + 4 + ] + }, + { + "_id": 2, + "array": [ + 1, + 5, + 5 + ] + }, + { + "_id": 3, + "array": [ + 1, + 5, + 5 + ] + }, + { + "_id": 4, + "array": [ + 1, + 2, + 3 + ] + } + ] + } + ] + }, + { + "description": "client bulkWrite update with collation", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "updateOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": 1 + }, + "update": { + "$set": { + "array": [ + 1, + 2, + 4 + ] + } + }, + "collation": { + "locale": "simple" + } + } + }, + { + "updateMany": { + "namespace": "crud-tests.coll0", + "filter": { + "$and": [ + { + "_id": { + "$gt": 1 + } + }, + { + "_id": { + "$lte": 3 + } + } + ] + }, + "update": { + "$set": { + "array": [ + 1, + 2, + 5 + ] + } + }, + "collation": { + "locale": "simple" + } + } + }, + { + "replaceOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": 4 + }, + "replacement": { + "array": [ + 1, + 2, + 6 + ] + }, + "collation": { + "locale": "simple" + } + } + } + ], + "verboseResults": true + }, + "expectResult": { + "insertedCount": 0, + "upsertedCount": 0, + "matchedCount": 4, + "modifiedCount": 4, + "deletedCount": 0, + "insertResults": {}, + "updateResults": { + "0": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedId": { + "$$exists": false + } + }, + "1": { + "matchedCount": 2, + "modifiedCount": 2, + "upsertedId": { + "$$exists": false + } + }, + "2": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedId": { + "$$exists": false + } + } + }, + "deleteResults": {} + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "ops": [ + { + "update": 0, + "filter": { + "_id": 1 + }, + "updateMods": { + "$set": { + "array": [ + 1, + 2, + 4 + ] + } + }, + "collation": { + "locale": "simple" + }, + "multi": false + }, + { + "update": 0, + "filter": { + "$and": [ + { + "_id": { + "$gt": 1 + } + }, + { + "_id": { + "$lte": 3 + } + } + ] + }, + "updateMods": { + "$set": { + "array": [ + 1, + 2, + 5 + ] + } + }, + "collation": { + "locale": "simple" + }, + "multi": true + }, + { + "update": 0, + "filter": { + "_id": 4 + }, + "updateMods": { + "array": [ + 1, + 2, + 6 + ] + }, + "collation": { + "locale": "simple" + }, + "multi": false + } + ], + "nsInfo": [ + { + "ns": "crud-tests.coll0" + } + ] + } + } + } + ] + } + ], + "outcome": [ + { + "databaseName": "crud-tests", + "collectionName": "coll0", + "documents": [ + { + "_id": 1, + "array": [ + 1, + 2, + 4 + ] + }, + { + "_id": 2, + "array": [ + 1, + 2, + 5 + ] + }, + { + "_id": 3, + "array": [ + 1, + 2, + 5 + ] + }, + { + "_id": 4, + "array": [ + 1, + 2, + 6 + ] + } + ] + } + ] + }, + { + "description": "client bulkWrite update with hint", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "updateOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": 1 + }, + "update": { + "$set": { + "array": [ + 1, + 2, + 4 + ] + } + }, + "hint": "_id_" + } + }, + { + "updateMany": { + "namespace": "crud-tests.coll0", + "filter": { + "$and": [ + { + "_id": { + "$gt": 1 + } + }, + { + "_id": { + "$lte": 3 + } + } + ] + }, + "update": { + "$set": { + "array": [ + 1, + 2, + 5 + ] + } + }, + "hint": "_id_" + } + }, + { + "replaceOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": 4 + }, + "replacement": { + "array": [ + 1, + 2, + 6 + ] + }, + "hint": "_id_" + } + } + ], + "verboseResults": true + }, + "expectResult": { + "insertedCount": 0, + "upsertedCount": 0, + "matchedCount": 4, + "modifiedCount": 4, + "deletedCount": 0, + "insertResults": {}, + "updateResults": { + "0": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedId": { + "$$exists": false + } + }, + "1": { + "matchedCount": 2, + "modifiedCount": 2, + "upsertedId": { + "$$exists": false + } + }, + "2": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedId": { + "$$exists": false + } + } + }, + "deleteResults": {} + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "ops": [ + { + "update": 0, + "filter": { + "_id": 1 + }, + "updateMods": { + "$set": { + "array": [ + 1, + 2, + 4 + ] + } + }, + "hint": "_id_", + "multi": false + }, + { + "update": 0, + "filter": { + "$and": [ + { + "_id": { + "$gt": 1 + } + }, + { + "_id": { + "$lte": 3 + } + } + ] + }, + "updateMods": { + "$set": { + "array": [ + 1, + 2, + 5 + ] + } + }, + "hint": "_id_", + "multi": true + }, + { + "update": 0, + "filter": { + "_id": 4 + }, + "updateMods": { + "array": [ + 1, + 2, + 6 + ] + }, + "hint": "_id_", + "multi": false + } + ], + "nsInfo": [ + { + "ns": "crud-tests.coll0" + } + ] + } + } + } + ] + } + ], + "outcome": [ + { + "databaseName": "crud-tests", + "collectionName": "coll0", + "documents": [ + { + "_id": 1, + "array": [ + 1, + 2, + 4 + ] + }, + { + "_id": 2, + "array": [ + 1, + 2, + 5 + ] + }, + { + "_id": 3, + "array": [ + 1, + 2, + 5 + ] + }, + { + "_id": 4, + "array": [ + 1, + 2, + 6 + ] + } + ] + } + ] + }, + { + "description": "client bulkWrite update with upsert", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "updateOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": 5 + }, + "update": { + "$set": { + "array": [ + 1, + 2, + 4 + ] + } + }, + "upsert": true + } + }, + { + "replaceOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": 6 + }, + "replacement": { + "array": [ + 1, + 2, + 6 + ] + }, + "upsert": true + } + } + ], + "verboseResults": true + }, + "expectResult": { + "insertedCount": 0, + "upsertedCount": 2, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 0, + "insertResults": {}, + "updateResults": { + "0": { + "matchedCount": 1, + "modifiedCount": 0, + "upsertedId": 5 + }, + "1": { + "matchedCount": 1, + "modifiedCount": 0, + "upsertedId": 6 + } + }, + "deleteResults": {} + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "ops": [ + { + "update": 0, + "filter": { + "_id": 5 + }, + "updateMods": { + "$set": { + "array": [ + 1, + 2, + 4 + ] + } + }, + "upsert": true, + "multi": false + }, + { + "update": 0, + "filter": { + "_id": 6 + }, + "updateMods": { + "array": [ + 1, + 2, + 6 + ] + }, + "upsert": true, + "multi": false + } + ], + "nsInfo": [ + { + "ns": "crud-tests.coll0" + } + ] + } + } + } + ] + } + ], + "outcome": [ + { + "databaseName": "crud-tests", + "collectionName": "coll0", + "documents": [ + { + "_id": 1, + "array": [ + 1, + 2, + 3 + ] + }, + { + "_id": 2, + "array": [ + 1, + 2, + 3 + ] + }, + { + "_id": 3, + "array": [ + 1, + 2, + 3 + ] + }, + { + "_id": 4, + "array": [ + 1, + 2, + 3 + ] + }, + { + "_id": 5, + "array": [ + 1, + 2, + 4 + ] + }, + { + "_id": 6, + "array": [ + 1, + 2, + 6 + ] + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-update-pipeline.json b/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-update-pipeline.json new file mode 100644 index 00000000000..9dba5ee6c57 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-update-pipeline.json @@ -0,0 +1,258 @@ +{ + "description": "client bulkWrite update pipeline", + "schemaVersion": "1.4", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "serverless": "forbid" + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 1 + }, + { + "_id": 2, + "x": 2 + } + ] + } + ], + "_yamlAnchors": { + "namespace": "crud-tests.coll0" + }, + "tests": [ + { + "description": "client bulkWrite updateOne with pipeline", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "updateOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": 1 + }, + "update": [ + { + "$addFields": { + "foo": 1 + } + } + ] + } + } + ], + "verboseResults": true + }, + "expectResult": { + "insertedCount": 0, + "upsertedCount": 0, + "matchedCount": 1, + "modifiedCount": 1, + "deletedCount": 0, + "insertResults": {}, + "updateResults": { + "0": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedId": { + "$$exists": false + } + } + }, + "deleteResults": {} + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "ops": [ + { + "update": 0, + "filter": { + "_id": 1 + }, + "updateMods": [ + { + "$addFields": { + "foo": 1 + } + } + ], + "multi": false + } + ], + "nsInfo": [ + { + "ns": "crud-tests.coll0" + } + ] + } + } + } + ] + } + ], + "outcome": [ + { + "databaseName": "crud-tests", + "collectionName": "coll0", + "documents": [ + { + "_id": 1, + "x": 1, + "foo": 1 + }, + { + "_id": 2, + "x": 2 + } + ] + } + ] + }, + { + "description": "client bulkWrite updateMany with pipeline", + "operations": [ + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "updateMany": { + "namespace": "crud-tests.coll0", + "filter": {}, + "update": [ + { + "$addFields": { + "foo": 1 + } + } + ] + } + } + ], + "verboseResults": true + }, + "expectResult": { + "insertedCount": 0, + "upsertedCount": 0, + "matchedCount": 2, + "modifiedCount": 2, + "deletedCount": 0, + "insertResults": {}, + "updateResults": { + "0": { + "matchedCount": 2, + "modifiedCount": 2, + "upsertedId": { + "$$exists": false + } + } + }, + "deleteResults": {} + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "ops": [ + { + "update": 0, + "filter": {}, + "updateMods": [ + { + "$addFields": { + "foo": 1 + } + } + ], + "multi": true + } + ], + "nsInfo": [ + { + "ns": "crud-tests.coll0" + } + ] + } + } + } + ] + } + ], + "outcome": [ + { + "databaseName": "crud-tests", + "collectionName": "coll0", + "documents": [ + { + "_id": 1, + "x": 1, + "foo": 1 + }, + { + "_id": 2, + "x": 2, + "foo": 1 + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-update-validation.json b/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-update-validation.json new file mode 100644 index 00000000000..617e711338a --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/crud/client-bulkWrite-update-validation.json @@ -0,0 +1,216 @@ +{ + "description": "client-bulkWrite-update-validation", + "schemaVersion": "1.1", + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "crud-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "_yamlAnchors": { + "namespace": "crud-tests.coll0" + }, + "tests": [ + { + "description": "client bulkWrite replaceOne prohibits atomic modifiers", + "operations": [ + { + "name": "clientBulkWrite", + "object": "client0", + "arguments": { + "models": [ + { + "replaceOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": 1 + }, + "replacement": { + "$set": { + "x": 22 + } + } + } + } + ] + }, + "expectError": { + "isClientError": true + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ] + }, + { + "description": "client bulkWrite updateOne requires atomic modifiers", + "operations": [ + { + "name": "clientBulkWrite", + "object": "client0", + "arguments": { + "models": [ + { + "updateOne": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": 1 + }, + "update": { + "x": 22 + } + } + } + ] + }, + "expectError": { + "isClientError": true + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ] + }, + { + "description": "client bulkWrite updateMany requires atomic modifiers", + "operations": [ + { + "name": "clientBulkWrite", + "object": "client0", + "arguments": { + "models": [ + { + "updateMany": { + "namespace": "crud-tests.coll0", + "filter": { + "_id": { + "$gt": 1 + } + }, + "update": { + "x": 44 + } + } + } + ] + }, + "expectError": { + "isClientError": true + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "crud-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/retryable-writes/client-bulkWrite-clientErrors.json b/driver-core/src/test/resources/unified-test-format/retryable-writes/client-bulkWrite-clientErrors.json new file mode 100644 index 00000000000..d16e0c9c8d6 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/retryable-writes/client-bulkWrite-clientErrors.json @@ -0,0 +1,351 @@ +{ + "description": "client bulkWrite retryable writes with client errors", + "schemaVersion": "1.21", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "topologies": [ + "replicaset", + "sharded", + "load-balanced" + ], + "serverless": "forbid" + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent" + ], + "useMultipleMongoses": false + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "retryable-writes-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "retryable-writes-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "_yamlAnchors": { + "namespace": "retryable-writes-tests.coll0" + }, + "tests": [ + { + "description": "client bulkWrite with one network error succeeds after retry", + "operations": [ + { + "object": "testRunner", + "name": "failPoint", + "arguments": { + "client": "client0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "bulkWrite" + ], + "closeConnection": true + } + } + } + }, + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "retryable-writes-tests.coll0", + "document": { + "_id": 4, + "x": 44 + } + } + } + ], + "verboseResults": true + }, + "expectResult": { + "insertedCount": 1, + "upsertedCount": 0, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 0, + "insertResults": { + "0": { + "insertedId": 4 + } + }, + "updateResults": {}, + "deleteResults": {} + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "ops": [ + { + "insert": 0, + "document": { + "_id": 4, + "x": 44 + } + } + ], + "nsInfo": [ + { + "ns": "retryable-writes-tests.coll0" + } + ], + "lsid": { + "$$exists": true + }, + "txnNumber": { + "$$exists": true + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "ops": [ + { + "insert": 0, + "document": { + "_id": 4, + "x": 44 + } + } + ], + "nsInfo": [ + { + "ns": "retryable-writes-tests.coll0" + } + ], + "lsid": { + "$$exists": true + }, + "txnNumber": { + "$$exists": true + } + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "retryable-writes-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 4, + "x": 44 + } + ] + } + ] + }, + { + "description": "client bulkWrite with two network errors fails after retry", + "operations": [ + { + "object": "testRunner", + "name": "failPoint", + "arguments": { + "client": "client0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 2 + }, + "data": { + "failCommands": [ + "bulkWrite" + ], + "closeConnection": true + } + } + } + }, + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "retryable-writes-tests.coll0", + "document": { + "_id": 4, + "x": 44 + } + } + } + ], + "verboseResults": true + }, + "expectError": { + "isClientError": true, + "errorLabelsContain": [ + "RetryableWriteError" + ] + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "ops": [ + { + "insert": 0, + "document": { + "_id": 4, + "x": 44 + } + } + ], + "nsInfo": [ + { + "ns": "retryable-writes-tests.coll0" + } + ], + "lsid": { + "$$exists": true + }, + "txnNumber": { + "$$exists": true + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "ops": [ + { + "insert": 0, + "document": { + "_id": 4, + "x": 44 + } + } + ], + "nsInfo": [ + { + "ns": "retryable-writes-tests.coll0" + } + ], + "lsid": { + "$$exists": true + }, + "txnNumber": { + "$$exists": true + } + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "retryable-writes-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/retryable-writes/client-bulkWrite-serverErrors.json b/driver-core/src/test/resources/unified-test-format/retryable-writes/client-bulkWrite-serverErrors.json new file mode 100644 index 00000000000..f58c82bcc73 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/retryable-writes/client-bulkWrite-serverErrors.json @@ -0,0 +1,873 @@ +{ + "description": "client bulkWrite retryable writes", + "schemaVersion": "1.21", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "topologies": [ + "replicaset", + "sharded", + "load-balanced" + ], + "serverless": "forbid" + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent" + ], + "useMultipleMongoses": false + } + }, + { + "client": { + "id": "clientRetryWritesFalse", + "uriOptions": { + "retryWrites": false + }, + "observeEvents": [ + "commandStartedEvent" + ], + "useMultipleMongoses": false + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "retryable-writes-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + } + ], + "initialData": [ + { + "collectionName": "coll0", + "databaseName": "retryable-writes-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + } + ] + } + ], + "_yamlAnchors": { + "namespace": "retryable-writes-tests.coll0" + }, + "tests": [ + { + "description": "client bulkWrite with no multi: true operations succeeds after retryable top-level error", + "operations": [ + { + "object": "testRunner", + "name": "failPoint", + "arguments": { + "client": "client0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "bulkWrite" + ], + "errorCode": 189, + "errorLabels": [ + "RetryableWriteError" + ] + } + } + } + }, + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "retryable-writes-tests.coll0", + "document": { + "_id": 4, + "x": 44 + } + } + }, + { + "updateOne": { + "namespace": "retryable-writes-tests.coll0", + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + }, + { + "replaceOne": { + "namespace": "retryable-writes-tests.coll0", + "filter": { + "_id": 2 + }, + "replacement": { + "x": 222 + } + } + }, + { + "deleteOne": { + "namespace": "retryable-writes-tests.coll0", + "filter": { + "_id": 3 + } + } + } + ], + "verboseResults": true + }, + "expectResult": { + "insertedCount": 1, + "upsertedCount": 0, + "matchedCount": 2, + "modifiedCount": 2, + "deletedCount": 1, + "insertResults": { + "0": { + "insertedId": 4 + } + }, + "updateResults": { + "1": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedId": { + "$$exists": false + } + }, + "2": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedId": { + "$$exists": false + } + } + }, + "deleteResults": { + "3": { + "deletedCount": 1 + } + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "ops": [ + { + "insert": 0, + "document": { + "_id": 4, + "x": 44 + } + }, + { + "update": 0, + "filter": { + "_id": 1 + }, + "updateMods": { + "$inc": { + "x": 1 + } + }, + "multi": false + }, + { + "update": 0, + "filter": { + "_id": 2 + }, + "updateMods": { + "x": 222 + }, + "multi": false + }, + { + "delete": 0, + "filter": { + "_id": 3 + }, + "multi": false + } + ], + "nsInfo": [ + { + "ns": "retryable-writes-tests.coll0" + } + ], + "lsid": { + "$$exists": true + }, + "txnNumber": { + "$$exists": true + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "ops": [ + { + "insert": 0, + "document": { + "_id": 4, + "x": 44 + } + }, + { + "update": 0, + "filter": { + "_id": 1 + }, + "updateMods": { + "$inc": { + "x": 1 + } + }, + "multi": false + }, + { + "update": 0, + "filter": { + "_id": 2 + }, + "updateMods": { + "x": 222 + }, + "multi": false + }, + { + "delete": 0, + "filter": { + "_id": 3 + }, + "multi": false + } + ], + "nsInfo": [ + { + "ns": "retryable-writes-tests.coll0" + } + ], + "lsid": { + "$$exists": true + }, + "txnNumber": { + "$$exists": true + } + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "retryable-writes-tests", + "documents": [ + { + "_id": 1, + "x": 12 + }, + { + "_id": 2, + "x": 222 + }, + { + "_id": 4, + "x": 44 + } + ] + } + ] + }, + { + "description": "client bulkWrite with multi: true operations fails after retryable top-level error", + "operations": [ + { + "object": "testRunner", + "name": "failPoint", + "arguments": { + "client": "client0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "bulkWrite" + ], + "errorCode": 189, + "errorLabels": [ + "RetryableWriteError" + ] + } + } + } + }, + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "updateMany": { + "namespace": "retryable-writes-tests.coll0", + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + }, + { + "deleteMany": { + "namespace": "retryable-writes-tests.coll0", + "filter": { + "_id": 3 + } + } + } + ] + }, + "expectError": { + "errorCode": 189, + "errorLabelsContain": [ + "RetryableWriteError" + ] + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": true, + "ordered": true, + "ops": [ + { + "update": 0, + "filter": { + "_id": 1 + }, + "updateMods": { + "$inc": { + "x": 1 + } + }, + "multi": true + }, + { + "delete": 0, + "filter": { + "_id": 3 + }, + "multi": true + } + ], + "nsInfo": [ + { + "ns": "retryable-writes-tests.coll0" + } + ] + } + } + } + ] + } + ] + }, + { + "description": "client bulkWrite with no multi: true operations succeeds after retryable writeConcernError", + "operations": [ + { + "object": "testRunner", + "name": "failPoint", + "arguments": { + "client": "client0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "bulkWrite" + ], + "errorLabels": [ + "RetryableWriteError" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + } + } + }, + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "retryable-writes-tests.coll0", + "document": { + "_id": 4, + "x": 44 + } + } + }, + { + "updateOne": { + "namespace": "retryable-writes-tests.coll0", + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + }, + { + "replaceOne": { + "namespace": "retryable-writes-tests.coll0", + "filter": { + "_id": 2 + }, + "replacement": { + "x": 222 + } + } + }, + { + "deleteOne": { + "namespace": "retryable-writes-tests.coll0", + "filter": { + "_id": 3 + } + } + } + ], + "verboseResults": true + }, + "expectResult": { + "insertedCount": 1, + "upsertedCount": 0, + "matchedCount": 2, + "modifiedCount": 2, + "deletedCount": 1, + "insertResults": { + "0": { + "insertedId": 4 + } + }, + "updateResults": { + "1": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedId": { + "$$exists": false + } + }, + "2": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedId": { + "$$exists": false + } + } + }, + "deleteResults": { + "3": { + "deletedCount": 1 + } + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "ops": [ + { + "insert": 0, + "document": { + "_id": 4, + "x": 44 + } + }, + { + "update": 0, + "filter": { + "_id": 1 + }, + "updateMods": { + "$inc": { + "x": 1 + } + }, + "multi": false + }, + { + "update": 0, + "filter": { + "_id": 2 + }, + "updateMods": { + "x": 222 + }, + "multi": false + }, + { + "delete": 0, + "filter": { + "_id": 3 + }, + "multi": false + } + ], + "nsInfo": [ + { + "ns": "retryable-writes-tests.coll0" + } + ], + "lsid": { + "$$exists": true + }, + "txnNumber": { + "$$exists": true + } + } + } + }, + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "ops": [ + { + "insert": 0, + "document": { + "_id": 4, + "x": 44 + } + }, + { + "update": 0, + "filter": { + "_id": 1 + }, + "updateMods": { + "$inc": { + "x": 1 + } + }, + "multi": false + }, + { + "update": 0, + "filter": { + "_id": 2 + }, + "updateMods": { + "x": 222 + }, + "multi": false + }, + { + "delete": 0, + "filter": { + "_id": 3 + }, + "multi": false + } + ], + "nsInfo": [ + { + "ns": "retryable-writes-tests.coll0" + } + ], + "lsid": { + "$$exists": true + }, + "txnNumber": { + "$$exists": true + } + } + } + } + ] + } + ] + }, + { + "description": "client bulkWrite with multi: true operations fails after retryable writeConcernError", + "operations": [ + { + "object": "testRunner", + "name": "failPoint", + "arguments": { + "client": "client0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "bulkWrite" + ], + "errorLabels": [ + "RetryableWriteError" + ], + "writeConcernError": { + "code": 91, + "errmsg": "Replication is being shut down" + } + } + } + } + }, + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "updateMany": { + "namespace": "retryable-writes-tests.coll0", + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + }, + { + "deleteMany": { + "namespace": "retryable-writes-tests.coll0", + "filter": { + "_id": 3 + } + } + } + ] + }, + "expectError": { + "writeConcernErrors": [ + { + "code": 91, + "message": "Replication is being shut down" + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": true, + "ordered": true, + "ops": [ + { + "update": 0, + "filter": { + "_id": 1 + }, + "updateMods": { + "$inc": { + "x": 1 + } + }, + "multi": true + }, + { + "delete": 0, + "filter": { + "_id": 3 + }, + "multi": true + } + ], + "nsInfo": [ + { + "ns": "retryable-writes-tests.coll0" + } + ] + } + } + } + ] + } + ] + }, + { + "description": "client bulkWrite with retryWrites: false does not retry", + "operations": [ + { + "object": "testRunner", + "name": "failPoint", + "arguments": { + "client": "clientRetryWritesFalse", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "bulkWrite" + ], + "errorCode": 189, + "errorLabels": [ + "RetryableWriteError" + ] + } + } + } + }, + { + "object": "clientRetryWritesFalse", + "name": "clientBulkWrite", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "retryable-writes-tests.coll0", + "document": { + "_id": 4, + "x": 44 + } + } + } + ] + }, + "expectError": { + "errorCode": 189, + "errorLabelsContain": [ + "RetryableWriteError" + ] + } + } + ], + "expectEvents": [ + { + "client": "clientRetryWritesFalse", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": true, + "ordered": true, + "ops": [ + { + "insert": 0, + "document": { + "_id": 4, + "x": 44 + } + } + ], + "nsInfo": [ + { + "ns": "retryable-writes-tests.coll0" + } + ] + } + } + } + ] + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/retryable-writes/handshakeError.json b/driver-core/src/test/resources/unified-test-format/retryable-writes/handshakeError.json index df37bd72322..93cb2e849ec 100644 --- a/driver-core/src/test/resources/unified-test-format/retryable-writes/handshakeError.json +++ b/driver-core/src/test/resources/unified-test-format/retryable-writes/handshakeError.json @@ -1,6 +1,6 @@ { "description": "retryable writes handshake failures", - "schemaVersion": "1.3", + "schemaVersion": "1.4", "runOnRequirements": [ { "minServerVersion": "4.2", @@ -53,6 +53,224 @@ } ], "tests": [ + { + "description": "client.clientBulkWrite succeeds after retryable handshake network error", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "serverless": "forbid" + } + ], + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "client", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 2 + }, + "data": { + "failCommands": [ + "ping", + "saslContinue" + ], + "closeConnection": true + } + } + } + }, + { + "name": "runCommand", + "object": "database", + "arguments": { + "commandName": "ping", + "command": { + "ping": 1 + } + }, + "expectError": { + "isError": true + } + }, + { + "name": "clientBulkWrite", + "object": "client", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "retryable-writes-handshake-tests.coll", + "document": { + "_id": 8, + "x": 88 + } + } + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client", + "eventType": "cmap", + "events": [ + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + } + ] + }, + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "ping": 1 + }, + "databaseName": "retryable-writes-handshake-tests" + } + }, + { + "commandFailedEvent": { + "commandName": "ping" + } + }, + { + "commandStartedEvent": { + "commandName": "bulkWrite" + } + }, + { + "commandSucceededEvent": { + "commandName": "bulkWrite" + } + } + ] + } + ] + }, + { + "description": "client.clientBulkWrite succeeds after retryable handshake server error (ShutdownInProgress)", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "serverless": "forbid" + } + ], + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "client", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 2 + }, + "data": { + "failCommands": [ + "ping", + "saslContinue" + ], + "closeConnection": true + } + } + } + }, + { + "name": "runCommand", + "object": "database", + "arguments": { + "commandName": "ping", + "command": { + "ping": 1 + } + }, + "expectError": { + "isError": true + } + }, + { + "name": "clientBulkWrite", + "object": "client", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "retryable-writes-handshake-tests.coll", + "document": { + "_id": 8, + "x": 88 + } + } + } + ] + } + } + ], + "expectEvents": [ + { + "client": "client", + "eventType": "cmap", + "events": [ + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + }, + { + "connectionCheckOutStartedEvent": {} + } + ] + }, + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "command": { + "ping": 1 + }, + "databaseName": "retryable-writes-handshake-tests" + } + }, + { + "commandFailedEvent": { + "commandName": "ping" + } + }, + { + "commandStartedEvent": { + "commandName": "bulkWrite" + } + }, + { + "commandSucceededEvent": { + "commandName": "bulkWrite" + } + } + ] + } + ] + }, { "description": "collection.insertOne succeeds after retryable handshake network error", "operations": [ diff --git a/driver-core/src/test/resources/unified-test-format/server-selection/logging/operation-id.json b/driver-core/src/test/resources/unified-test-format/server-selection/logging/operation-id.json index 276e4b8d6d9..72ebff60d80 100644 --- a/driver-core/src/test/resources/unified-test-format/server-selection/logging/operation-id.json +++ b/driver-core/src/test/resources/unified-test-format/server-selection/logging/operation-id.json @@ -47,6 +47,9 @@ } } ], + "_yamlAnchors": { + "namespace": "logging-tests.server-selection" + }, "tests": [ { "description": "Successful bulkWrite operation: log messages have operationIds", @@ -224,6 +227,192 @@ ] } ] + }, + { + "description": "Successful client bulkWrite operation: log messages have operationIds", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "serverless": "forbid" + } + ], + "operations": [ + { + "name": "waitForEvent", + "object": "testRunner", + "arguments": { + "client": "client", + "event": { + "topologyDescriptionChangedEvent": {} + }, + "count": 2 + } + }, + { + "name": "clientBulkWrite", + "object": "client", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "logging-tests.server-selection", + "document": { + "x": 1 + } + } + } + ] + } + } + ], + "expectLogMessages": [ + { + "client": "client", + "messages": [ + { + "level": "debug", + "component": "serverSelection", + "data": { + "message": "Server selection started", + "operationId": { + "$$type": [ + "int", + "long" + ] + }, + "operation": "bulkWrite" + } + }, + { + "level": "debug", + "component": "serverSelection", + "data": { + "message": "Server selection succeeded", + "operationId": { + "$$type": [ + "int", + "long" + ] + }, + "operation": "bulkWrite" + } + } + ] + } + ] + }, + { + "description": "Failed client bulkWrite operation: log messages have operationIds", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "serverless": "forbid" + } + ], + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "failPointClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": "alwaysOn", + "data": { + "failCommands": [ + "hello", + "ismaster" + ], + "appName": "loggingClient", + "closeConnection": true + } + } + } + }, + { + "name": "waitForEvent", + "object": "testRunner", + "arguments": { + "client": "client", + "event": { + "serverDescriptionChangedEvent": { + "newDescription": { + "type": "Unknown" + } + } + }, + "count": 1 + } + }, + { + "name": "clientBulkWrite", + "object": "client", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "logging-tests.server-selection", + "document": { + "x": 1 + } + } + } + ] + }, + "expectError": { + "isClientError": true + } + } + ], + "expectLogMessages": [ + { + "client": "client", + "messages": [ + { + "level": "debug", + "component": "serverSelection", + "data": { + "message": "Server selection started", + "operationId": { + "$$type": [ + "int", + "long" + ] + }, + "operation": "bulkWrite" + } + }, + { + "level": "info", + "component": "serverSelection", + "data": { + "message": "Waiting for suitable server to become available", + "operationId": { + "$$type": [ + "int", + "long" + ] + }, + "operation": "bulkWrite" + } + }, + { + "level": "debug", + "component": "serverSelection", + "data": { + "message": "Server selection failed", + "operationId": { + "$$type": [ + "int", + "long" + ] + }, + "operation": "bulkWrite" + } + } + ] + } + ] } ] } diff --git a/driver-core/src/test/resources/unified-test-format/transactions/client-bulkWrite.json b/driver-core/src/test/resources/unified-test-format/transactions/client-bulkWrite.json new file mode 100644 index 00000000000..4a8d013f8d5 --- /dev/null +++ b/driver-core/src/test/resources/unified-test-format/transactions/client-bulkWrite.json @@ -0,0 +1,593 @@ +{ + "description": "client bulkWrite transactions", + "schemaVersion": "1.4", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "topologies": [ + "replicaset", + "sharded", + "load-balanced" + ], + "serverless": "forbid" + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "transaction-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "coll0" + } + }, + { + "session": { + "id": "session0", + "client": "client0" + } + }, + { + "client": { + "id": "client_with_wmajority", + "uriOptions": { + "w": "majority" + }, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "session": { + "id": "session_with_wmajority", + "client": "client_with_wmajority" + } + } + ], + "_yamlAnchors": { + "namespace": "transaction-tests.coll0" + }, + "initialData": [ + { + "databaseName": "transaction-tests", + "collectionName": "coll0", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 5, + "x": 55 + }, + { + "_id": 6, + "x": 66 + }, + { + "_id": 7, + "x": 77 + } + ] + } + ], + "tests": [ + { + "description": "client bulkWrite in a transaction", + "operations": [ + { + "object": "session0", + "name": "startTransaction" + }, + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "session": "session0", + "models": [ + { + "insertOne": { + "namespace": "transaction-tests.coll0", + "document": { + "_id": 8, + "x": 88 + } + } + }, + { + "updateOne": { + "namespace": "transaction-tests.coll0", + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + } + } + }, + { + "updateMany": { + "namespace": "transaction-tests.coll0", + "filter": { + "$and": [ + { + "_id": { + "$gt": 1 + } + }, + { + "_id": { + "$lte": 3 + } + } + ] + }, + "update": { + "$inc": { + "x": 2 + } + } + } + }, + { + "replaceOne": { + "namespace": "transaction-tests.coll0", + "filter": { + "_id": 4 + }, + "replacement": { + "x": 44 + }, + "upsert": true + } + }, + { + "deleteOne": { + "namespace": "transaction-tests.coll0", + "filter": { + "_id": 5 + } + } + }, + { + "deleteMany": { + "namespace": "transaction-tests.coll0", + "filter": { + "$and": [ + { + "_id": { + "$gt": 5 + } + }, + { + "_id": { + "$lte": 7 + } + } + ] + } + } + } + ], + "verboseResults": true + }, + "expectResult": { + "insertedCount": 1, + "upsertedCount": 1, + "matchedCount": 3, + "modifiedCount": 3, + "deletedCount": 3, + "insertResults": { + "0": { + "insertedId": 8 + } + }, + "updateResults": { + "1": { + "matchedCount": 1, + "modifiedCount": 1, + "upsertedId": { + "$$exists": false + } + }, + "2": { + "matchedCount": 2, + "modifiedCount": 2, + "upsertedId": { + "$$exists": false + } + }, + "3": { + "matchedCount": 1, + "modifiedCount": 0, + "upsertedId": 4 + } + }, + "deleteResults": { + "4": { + "deletedCount": 1 + }, + "5": { + "deletedCount": 2 + } + } + } + }, + { + "object": "session0", + "name": "commitTransaction" + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "lsid": { + "$$sessionLsid": "session0" + }, + "txnNumber": 1, + "startTransaction": true, + "autocommit": false, + "writeConcern": { + "$$exists": false + }, + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "ops": [ + { + "insert": 0, + "document": { + "_id": 8, + "x": 88 + } + }, + { + "update": 0, + "filter": { + "_id": 1 + }, + "updateMods": { + "$inc": { + "x": 1 + } + }, + "multi": false + }, + { + "update": 0, + "filter": { + "$and": [ + { + "_id": { + "$gt": 1 + } + }, + { + "_id": { + "$lte": 3 + } + } + ] + }, + "updateMods": { + "$inc": { + "x": 2 + } + }, + "multi": true + }, + { + "update": 0, + "filter": { + "_id": 4 + }, + "updateMods": { + "x": 44 + }, + "upsert": true, + "multi": false + }, + { + "delete": 0, + "filter": { + "_id": 5 + }, + "multi": false + }, + { + "delete": 0, + "filter": { + "$and": [ + { + "_id": { + "$gt": 5 + } + }, + { + "_id": { + "$lte": 7 + } + } + ] + }, + "multi": true + } + ], + "nsInfo": [ + { + "ns": "transaction-tests.coll0" + } + ] + } + } + }, + { + "commandStartedEvent": { + "commandName": "commitTransaction", + "databaseName": "admin", + "command": { + "commitTransaction": 1, + "lsid": { + "$$sessionLsid": "session0" + }, + "txnNumber": 1, + "startTransaction": { + "$$exists": false + }, + "autocommit": false, + "writeConcern": { + "$$exists": false + } + } + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "transaction-tests", + "documents": [ + { + "_id": 1, + "x": 12 + }, + { + "_id": 2, + "x": 24 + }, + { + "_id": 3, + "x": 35 + }, + { + "_id": 4, + "x": 44 + }, + { + "_id": 8, + "x": 88 + } + ] + } + ] + }, + { + "description": "client writeConcern ignored for client bulkWrite in transaction", + "operations": [ + { + "object": "session_with_wmajority", + "name": "startTransaction", + "arguments": { + "writeConcern": { + "w": 1 + } + } + }, + { + "object": "client_with_wmajority", + "name": "clientBulkWrite", + "arguments": { + "session": "session_with_wmajority", + "models": [ + { + "insertOne": { + "namespace": "transaction-tests.coll0", + "document": { + "_id": 8, + "x": 88 + } + } + } + ] + }, + "expectResult": { + "insertedCount": 1, + "upsertedCount": 0, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 0, + "insertResults": { + "$$unsetOrMatches": {} + }, + "updateResults": { + "$$unsetOrMatches": {} + }, + "deleteResults": { + "$$unsetOrMatches": {} + } + } + }, + { + "object": "session_with_wmajority", + "name": "commitTransaction" + } + ], + "expectEvents": [ + { + "client": "client_with_wmajority", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "lsid": { + "$$sessionLsid": "session_with_wmajority" + }, + "txnNumber": 1, + "startTransaction": true, + "autocommit": false, + "writeConcern": { + "$$exists": false + }, + "bulkWrite": 1, + "errorsOnly": true, + "ordered": true, + "ops": [ + { + "insert": 0, + "document": { + "_id": 8, + "x": 88 + } + } + ], + "nsInfo": [ + { + "ns": "transaction-tests.coll0" + } + ] + } + } + }, + { + "commandStartedEvent": { + "command": { + "commitTransaction": 1, + "lsid": { + "$$sessionLsid": "session_with_wmajority" + }, + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": { + "$$exists": false + }, + "autocommit": false, + "writeConcern": { + "w": 1 + } + }, + "commandName": "commitTransaction", + "databaseName": "admin" + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "coll0", + "databaseName": "transaction-tests", + "documents": [ + { + "_id": 1, + "x": 11 + }, + { + "_id": 2, + "x": 22 + }, + { + "_id": 3, + "x": 33 + }, + { + "_id": 5, + "x": 55 + }, + { + "_id": 6, + "x": 66 + }, + { + "_id": 7, + "x": 77 + }, + { + "_id": 8, + "x": 88 + } + ] + } + ] + }, + { + "description": "client bulkWrite with writeConcern in a transaction causes a transaction error", + "operations": [ + { + "object": "session0", + "name": "startTransaction" + }, + { + "object": "client0", + "name": "clientBulkWrite", + "arguments": { + "session": "session0", + "writeConcern": { + "w": 1 + }, + "models": [ + { + "insertOne": { + "namespace": "transaction-tests.coll0", + "document": { + "_id": 8, + "x": 88 + } + } + } + ] + }, + "expectError": { + "isClientError": true, + "errorContains": "Cannot set write concern after starting a transaction" + } + } + ] + } + ] +} diff --git a/driver-core/src/test/resources/unified-test-format/transactions/mongos-pin-auto.json b/driver-core/src/test/resources/unified-test-format/transactions/mongos-pin-auto.json index 93eac8bb773..27db5204011 100644 --- a/driver-core/src/test/resources/unified-test-format/transactions/mongos-pin-auto.json +++ b/driver-core/src/test/resources/unified-test-format/transactions/mongos-pin-auto.json @@ -2004,6 +2004,104 @@ } ] }, + { + "description": "remain pinned after non-transient Interrupted error on clientBulkWrite bulkWrite", + "operations": [ + { + "object": "session0", + "name": "startTransaction" + }, + { + "object": "collection0", + "name": "insertOne", + "arguments": { + "session": "session0", + "document": { + "_id": 3 + } + }, + "expectResult": { + "$$unsetOrMatches": { + "insertedId": { + "$$unsetOrMatches": 3 + } + } + } + }, + { + "name": "targetedFailPoint", + "object": "testRunner", + "arguments": { + "session": "session0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "bulkWrite" + ], + "errorCode": 11601 + } + } + } + }, + { + "name": "clientBulkWrite", + "object": "client0", + "arguments": { + "session": "session0", + "models": [ + { + "insertOne": { + "namespace": "database0.collection0", + "document": { + "_id": 8, + "x": 88 + } + } + } + ] + }, + "expectError": { + "errorLabelsOmit": [ + "TransientTransactionError" + ] + } + }, + { + "object": "testRunner", + "name": "assertSessionPinned", + "arguments": { + "session": "session0" + } + }, + { + "object": "session0", + "name": "abortTransaction" + } + ], + "outcome": [ + { + "collectionName": "test", + "databaseName": "transaction-tests", + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + } + ] + } + ], + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ] + }, { "description": "unpin after transient connection error on insertOne insert", "operations": [ @@ -5175,6 +5273,202 @@ ] } ] + }, + { + "description": "unpin after transient connection error on clientBulkWrite bulkWrite", + "operations": [ + { + "object": "session0", + "name": "startTransaction" + }, + { + "object": "collection0", + "name": "insertOne", + "arguments": { + "session": "session0", + "document": { + "_id": 3 + } + }, + "expectResult": { + "$$unsetOrMatches": { + "insertedId": { + "$$unsetOrMatches": 3 + } + } + } + }, + { + "name": "targetedFailPoint", + "object": "testRunner", + "arguments": { + "session": "session0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "bulkWrite" + ], + "closeConnection": true + } + } + } + }, + { + "name": "clientBulkWrite", + "object": "client0", + "arguments": { + "session": "session0", + "models": [ + { + "insertOne": { + "namespace": "database0.collection0", + "document": { + "_id": 8, + "x": 88 + } + } + } + ] + }, + "expectError": { + "errorLabelsContain": [ + "TransientTransactionError" + ] + } + }, + { + "object": "testRunner", + "name": "assertSessionUnpinned", + "arguments": { + "session": "session0" + } + }, + { + "object": "session0", + "name": "abortTransaction" + } + ], + "outcome": [ + { + "collectionName": "test", + "databaseName": "transaction-tests", + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + } + ] + } + ], + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ] + }, + { + "description": "unpin after transient ShutdownInProgress error on clientBulkWrite bulkWrite", + "operations": [ + { + "object": "session0", + "name": "startTransaction" + }, + { + "object": "collection0", + "name": "insertOne", + "arguments": { + "session": "session0", + "document": { + "_id": 3 + } + }, + "expectResult": { + "$$unsetOrMatches": { + "insertedId": { + "$$unsetOrMatches": 3 + } + } + } + }, + { + "name": "targetedFailPoint", + "object": "testRunner", + "arguments": { + "session": "session0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "bulkWrite" + ], + "errorCode": 91 + } + } + } + }, + { + "name": "clientBulkWrite", + "object": "client0", + "arguments": { + "session": "session0", + "models": [ + { + "insertOne": { + "namespace": "database0.collection0", + "document": { + "_id": 8, + "x": 88 + } + } + } + ] + }, + "expectError": { + "errorLabelsContain": [ + "TransientTransactionError" + ] + } + }, + { + "object": "testRunner", + "name": "assertSessionUnpinned", + "arguments": { + "session": "session0" + } + }, + { + "object": "session0", + "name": "abortTransaction" + } + ], + "outcome": [ + { + "collectionName": "test", + "databaseName": "transaction-tests", + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + } + ] + } + ], + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ] } ] } diff --git a/driver-core/src/test/resources/versioned-api/crud-api-version-1.json b/driver-core/src/test/resources/versioned-api/crud-api-version-1.json index a387d0587e0..23ef59a6d98 100644 --- a/driver-core/src/test/resources/versioned-api/crud-api-version-1.json +++ b/driver-core/src/test/resources/versioned-api/crud-api-version-1.json @@ -50,7 +50,8 @@ }, "apiDeprecationErrors": true } - ] + ], + "namespace": "versioned-api-tests.test" }, "initialData": [ { @@ -426,6 +427,86 @@ } ] }, + { + "description": "client bulkWrite appends declared API version", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "serverless": "forbid" + } + ], + "operations": [ + { + "name": "clientBulkWrite", + "object": "client", + "arguments": { + "models": [ + { + "insertOne": { + "namespace": "versioned-api-tests.test", + "document": { + "_id": 6, + "x": 6 + } + } + } + ], + "verboseResults": true + }, + "expectResult": { + "insertedCount": 1, + "upsertedCount": 0, + "matchedCount": 0, + "modifiedCount": 0, + "deletedCount": 0, + "insertResults": { + "0": { + "insertedId": 6 + } + }, + "updateResults": {}, + "deleteResults": {} + } + } + ], + "expectEvents": [ + { + "client": "client", + "events": [ + { + "commandStartedEvent": { + "commandName": "bulkWrite", + "databaseName": "admin", + "command": { + "bulkWrite": 1, + "errorsOnly": false, + "ordered": true, + "ops": [ + { + "insert": 0, + "document": { + "_id": 6, + "x": 6 + } + } + ], + "nsInfo": [ + { + "ns": "versioned-api-tests.test" + } + ], + "apiVersion": "1", + "apiStrict": { + "$$unsetOrMatches": false + }, + "apiDeprecationErrors": true + } + } + } + ] + } + ] + }, { "description": "countDocuments appends declared API version", "operations": [ diff --git a/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/CrudProseTest.java b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/CrudProseTest.java new file mode 100644 index 00000000000..81d88e6fdb0 --- /dev/null +++ b/driver-reactive-streams/src/test/functional/com/mongodb/reactivestreams/client/CrudProseTest.java @@ -0,0 +1,31 @@ +/* + * 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.reactivestreams.client; + +import com.mongodb.MongoClientSettings; +import com.mongodb.client.MongoClient; +import com.mongodb.reactivestreams.client.syncadapter.SyncMongoClient; + +/** + * See + * CRUD Prose Tests. + */ +final class CrudProseTest extends com.mongodb.client.CrudProseTest { + @Override + protected MongoClient createMongoClient(final MongoClientSettings.Builder mongoClientSettingsBuilder) { + return new SyncMongoClient(MongoClients.create(mongoClientSettingsBuilder.build())); + } +} diff --git a/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java b/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java index d6eded9cda6..9c0033e42a7 100644 --- a/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java +++ b/driver-sync/src/main/com/mongodb/client/internal/MongoClusterImpl.java @@ -31,7 +31,6 @@ import com.mongodb.ServerApi; import com.mongodb.TransactionOptions; import com.mongodb.WriteConcern; -import com.mongodb.assertions.Assertions; import com.mongodb.client.ChangeStreamIterable; import com.mongodb.client.ClientSession; import com.mongodb.client.ListDatabasesIterable; @@ -55,6 +54,7 @@ import com.mongodb.internal.connection.ReadConcernAwareNoOpSessionContext; import com.mongodb.internal.operation.OperationHelper; import com.mongodb.internal.operation.ReadOperation; +import com.mongodb.internal.operation.SyncOperations; import com.mongodb.internal.operation.WriteOperation; import com.mongodb.internal.session.ServerSessionPool; import com.mongodb.lang.Nullable; @@ -73,6 +73,7 @@ import static com.mongodb.MongoException.UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL; import static com.mongodb.ReadPreference.primary; import static com.mongodb.assertions.Assertions.isTrue; +import static com.mongodb.assertions.Assertions.isTrueArgument; import static com.mongodb.assertions.Assertions.notNull; import static com.mongodb.internal.TimeoutContext.createTimeoutContext; @@ -97,6 +98,7 @@ final class MongoClusterImpl implements MongoCluster { private final TimeoutSettings timeoutSettings; private final UuidRepresentation uuidRepresentation; private final WriteConcern writeConcern; + private final SyncOperations operations; MongoClusterImpl( @Nullable final AutoEncryptionSettings autoEncryptionSettings, final Cluster cluster, final CodecRegistry codecRegistry, @@ -121,6 +123,16 @@ final class MongoClusterImpl implements MongoCluster { this.timeoutSettings = timeoutSettings; this.uuidRepresentation = uuidRepresentation; this.writeConcern = writeConcern; + operations = new SyncOperations<>( + null, + BsonDocument.class, + readPreference, + codecRegistry, + readConcern, + writeConcern, + retryWrites, + retryReads, + timeoutSettings); } @Override @@ -317,7 +329,8 @@ public ChangeStreamIterable watch(final ClientSession clientS public ClientBulkWriteResult bulkWrite( final List clientWriteModels) throws ClientBulkWriteException { notNull("clientWriteModels", clientWriteModels); - throw Assertions.fail("BULK-TODO implement"); + isTrueArgument("`clientWriteModels` must not be empty", !clientWriteModels.isEmpty()); + return executeBulkWrite(null, clientWriteModels, null); } @Override @@ -325,8 +338,9 @@ public ClientBulkWriteResult bulkWrite( final List clientWriteModels, final ClientBulkWriteOptions options) throws ClientBulkWriteException { notNull("clientWriteModels", clientWriteModels); + isTrueArgument("`clientWriteModels` must not be empty", !clientWriteModels.isEmpty()); notNull("options", options); - throw Assertions.fail("BULK-TODO implement"); + return executeBulkWrite(null, clientWriteModels, options); } @Override @@ -335,7 +349,8 @@ public ClientBulkWriteResult bulkWrite( final List clientWriteModels) throws ClientBulkWriteException { notNull("clientSession", clientSession); notNull("clientWriteModels", clientWriteModels); - throw Assertions.fail("BULK-TODO implement"); + isTrueArgument("`clientWriteModels` must not be empty", !clientWriteModels.isEmpty()); + return executeBulkWrite(clientSession, clientWriteModels, null); } @Override @@ -345,8 +360,9 @@ public ClientBulkWriteResult bulkWrite( final ClientBulkWriteOptions options) throws ClientBulkWriteException { notNull("clientSession", clientSession); notNull("clientWriteModels", clientWriteModels); + isTrueArgument("`clientWriteModels` must not be empty", !clientWriteModels.isEmpty()); notNull("options", options); - throw Assertions.fail("BULK-TODO implement"); + return executeBulkWrite(clientSession, clientWriteModels, options); } private ListDatabasesIterable createListDatabasesIterable(@Nullable final ClientSession clientSession, final Class clazz) { @@ -366,6 +382,14 @@ private ChangeStreamIterable createChangeStreamIterable(@Null retryReads, timeoutSettings); } + private ClientBulkWriteResult executeBulkWrite( + @Nullable final ClientSession clientSession, + final List clientWriteModels, + @Nullable final ClientBulkWriteOptions options) { + isTrue("`autoEncryptionSettings` is null, as bulkWrite does not currently support automatic encryption", autoEncryptionSettings == null); + return operationExecutor.execute(operations.clientBulkWriteOperation(clientWriteModels, options), readConcern, clientSession); + } + final class OperationExecutorImpl implements OperationExecutor { private final TimeoutSettings executorTimeoutSettings; diff --git a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java index 418f874aabe..b42bbb3c7a6 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/AbstractClientSideOperationsTimeoutProseTest.java @@ -700,6 +700,14 @@ public void test10CustomTestWithTransactionUsesASingleTimeoutWithLock() { } } + @DisplayName("11. Multi-batch bulkWrites") + @Test + void test11MultiBatchBulkWrites() { + assumeTrue(serverVersionAtLeast(8, 0)); + assumeFalse(isServerlessTest()); + assumeTrue(Runtime.getRuntime().availableProcessors() < 1, "BULK-TODO implement prose test https://github.com/mongodb/specifications/blob/master/source/client-side-operations-timeout/tests/README.md#11-multi-batch-bulkwrites"); + } + /** * Not a prose spec test. However, it is additional test case for better coverage. */ diff --git a/driver-sync/src/test/functional/com/mongodb/client/CrudProseTest.java b/driver-sync/src/test/functional/com/mongodb/client/CrudProseTest.java index 5d3907bb210..05dae50e563 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/CrudProseTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/CrudProseTest.java @@ -16,181 +16,411 @@ package com.mongodb.client; +import com.mongodb.AutoEncryptionSettings; import com.mongodb.MongoBulkWriteException; +import com.mongodb.MongoClientException; +import com.mongodb.MongoClientSettings; +import com.mongodb.MongoNamespace; import com.mongodb.MongoWriteConcernException; import com.mongodb.MongoWriteException; -import com.mongodb.ServerAddress; +import com.mongodb.WriteConcern; +import com.mongodb.assertions.Assertions; import com.mongodb.client.model.CreateCollectionOptions; import com.mongodb.client.model.Filters; +import com.mongodb.client.model.InsertOneModel; import com.mongodb.client.model.ValidationOptions; +import com.mongodb.client.model.bulk.ClientNamespacedWriteModel; +import com.mongodb.client.model.bulk.ClientBulkWriteResult; import com.mongodb.event.CommandListener; import com.mongodb.event.CommandStartedEvent; import org.bson.BsonArray; import org.bson.BsonDocument; +import org.bson.BsonDocumentWrapper; import org.bson.BsonInt32; import org.bson.BsonString; import org.bson.BsonValue; import org.bson.Document; +import org.bson.RawBsonDocument; +import org.bson.codecs.configuration.CodecRegistry; import org.bson.codecs.pojo.PojoCodecProvider; -import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; +import java.time.Duration; +import java.util.ArrayList; +import java.util.HashMap; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import java.util.function.Supplier; +import java.util.stream.Stream; import static com.mongodb.ClusterFixture.isDiscoverableReplicaSet; +import static com.mongodb.ClusterFixture.isServerlessTest; +import static com.mongodb.ClusterFixture.isStandalone; import static com.mongodb.ClusterFixture.serverVersionAtLeast; import static com.mongodb.MongoClientSettings.getDefaultCodecRegistry; +import static com.mongodb.client.Fixture.getDefaultDatabaseName; import static com.mongodb.client.Fixture.getMongoClientSettingsBuilder; -import static java.lang.String.format; -import static java.util.Arrays.asList; +import static com.mongodb.client.Fixture.getPrimary; +import static com.mongodb.client.model.bulk.ClientBulkWriteOptions.clientBulkWriteOptions; +import static com.mongodb.client.model.bulk.ClientNamespacedWriteModel.insertOne; +import static java.lang.String.join; +import static java.util.Collections.nCopies; import static java.util.Collections.singletonList; +import static java.util.Collections.singletonMap; import static org.bson.codecs.configuration.CodecRegistries.fromProviders; import static org.bson.codecs.configuration.CodecRegistries.fromRegistries; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.Assumptions.assumeFalse; import static org.junit.jupiter.api.Assumptions.assumeTrue; +import static org.junit.jupiter.params.provider.Arguments.arguments; /** - * See https://github.com/mongodb/specifications/blob/master/source/crud/tests/README.rst#prose-tests + * See + * CRUD Prose Tests. */ -public class CrudProseTest extends DatabaseTestCase { - private BsonDocument failPointDocument; - - @BeforeEach - @Override - public void setUp() { - super.setUp(); - } - - /** - * 1. WriteConcernError.details exposes writeConcernError.errInfo - */ +public class CrudProseTest { + @DisplayName("1. WriteConcernError.details exposes writeConcernError.errInfo") @Test - public void testWriteConcernErrInfoIsPropagated() { + @SuppressWarnings("try") + void testWriteConcernErrInfoIsPropagated() throws InterruptedException { assumeTrue(isDiscoverableReplicaSet() && serverVersionAtLeast(4, 0)); - - try { - setFailPoint(); - collection.insertOne(Document.parse("{ x: 1 }")); - } catch (MongoWriteConcernException e) { - assertEquals(e.getWriteConcernError().getCode(), 100); - assertEquals("UnsatisfiableWriteConcern", e.getWriteConcernError().getCodeName()); - assertEquals(e.getWriteConcernError().getDetails(), new BsonDocument("writeConcern", + BsonDocument failPointDocument = new BsonDocument("configureFailPoint", new BsonString("failCommand")) + .append("mode", new BsonDocument("times", new BsonInt32(1))) + .append("data", new BsonDocument("failCommands", new BsonArray(singletonList(new BsonString("insert")))) + .append("writeConcernError", new BsonDocument("code", new BsonInt32(100)) + .append("codeName", new BsonString("UnsatisfiableWriteConcern")) + .append("errmsg", new BsonString("Not enough data-bearing nodes")) + .append("errInfo", new BsonDocument("writeConcern", new BsonDocument("w", new BsonInt32(2)) + .append("wtimeout", new BsonInt32(0)) + .append("provenance", new BsonString("clientSupplied")))))); + try (MongoClient client = createMongoClient(getMongoClientSettingsBuilder()); + FailPoint ignored = FailPoint.enable(failPointDocument, getPrimary())) { + MongoWriteConcernException actual = assertThrows(MongoWriteConcernException.class, () -> + droppedCollection(client, Document.class).insertOne(Document.parse("{ x: 1 }"))); + assertEquals(actual.getWriteConcernError().getCode(), 100); + assertEquals("UnsatisfiableWriteConcern", actual.getWriteConcernError().getCodeName()); + assertEquals(actual.getWriteConcernError().getDetails(), new BsonDocument("writeConcern", new BsonDocument("w", new BsonInt32(2)) .append("wtimeout", new BsonInt32(0)) .append("provenance", new BsonString("clientSupplied")))); - } catch (Exception ex) { - fail(format("Incorrect exception thrown in test: %s", ex.getClass())); - } finally { - disableFailPoint(); } } - /** - * 2. WriteError.details exposes writeErrors[].errInfo - */ + @DisplayName("2. WriteError.details exposes writeErrors[].errInfo") @Test - public void testWriteErrorDetailsIsPropagated() { - getCollectionHelper().create(getCollectionName(), - new CreateCollectionOptions() - .validationOptions(new ValidationOptions() - .validator(Filters.type("x", "string")))); - - try { - collection.insertOne(new Document("x", 1)); - fail("Should throw, as document doesn't match schema"); - } catch (MongoWriteException e) { - // These assertions doesn't do exactly what's required by the specification, but it's simpler to implement and nearly as - // effective - assertTrue(e.getMessage().contains("Write error")); - assertNotNull(e.getError().getDetails()); - if (serverVersionAtLeast(5, 0)) { - assertFalse(e.getError().getDetails().isEmpty()); + void testWriteErrorDetailsIsPropagated() { + try (MongoClient client = createMongoClient(getMongoClientSettingsBuilder())) { + MongoCollection collection = droppedCollection(client, Document.class); + droppedDatabase(client).createCollection( + collection.getNamespace().getCollectionName(), + new CreateCollectionOptions().validationOptions(new ValidationOptions().validator(Filters.type("x", "string")))); + assertAll( + () -> { + MongoWriteException actual = assertThrows(MongoWriteException.class, () -> + collection.insertOne(new Document("x", 1))); + // These assertions don't do exactly what's required by the specification, + // but it's simpler to implement and nearly as effective. + assertTrue(actual.getMessage().contains("Write error")); + assertNotNull(actual.getError().getDetails()); + if (serverVersionAtLeast(5, 0)) { + assertFalse(actual.getError().getDetails().isEmpty()); + } + }, + () -> { + MongoBulkWriteException actual = assertThrows(MongoBulkWriteException.class, () -> + collection.insertMany(singletonList(new Document("x", 1)))); + // These assertions don't do exactly what's required by the specification, + // but it's simpler to implement and nearly as effective. + assertTrue(actual.getMessage().contains("Write errors")); + assertEquals(1, actual.getWriteErrors().size()); + if (serverVersionAtLeast(5, 0)) { + assertFalse(actual.getWriteErrors().get(0).getDetails().isEmpty()); + } + } + ); + + } + } + + @DisplayName("3. MongoClient.bulkWrite batch splits a writeModels input with greater than maxWriteBatchSize operations") + @Test + void testBulkWriteSplitsWhenExceedingMaxWriteBatchSize() { + assumeTrue(serverVersionAtLeast(8, 0)); + assumeFalse(isServerlessTest()); + assumeTrue(Runtime.getRuntime().availableProcessors() < 1, "BULK-TODO implement prose test https://github.com/mongodb/specifications/blob/master/source/crud/tests/README.md#3-mongoclientbulkwrite-batch-splits-a-writemodels-input-with-greater-than-maxwritebatchsize-operations"); + } + + @DisplayName("4. MongoClient.bulkWrite batch splits when an ops payload exceeds maxMessageSizeBytes") + @Test + void testBulkWriteSplitsWhenExceedingMaxMessageSizeBytes() { + assumeTrue(serverVersionAtLeast(8, 0)); + assumeFalse(isServerlessTest()); + assumeTrue(Runtime.getRuntime().availableProcessors() < 1, "BULK-TODO implement prose test https://github.com/mongodb/specifications/blob/master/source/crud/tests/README.md#4-mongoclientbulkwrite-batch-splits-when-an-ops-payload-exceeds-maxmessagesizebytes"); + } + + @DisplayName("5. MongoClient.bulkWrite collects WriteConcernErrors across batches") + @Test + void testBulkWriteCollectsWriteConcernErrorsAcrossBatches() { + assumeTrue(serverVersionAtLeast(8, 0)); + assumeFalse(isServerlessTest()); + assumeTrue(Runtime.getRuntime().availableProcessors() < 1, "BULK-TODO implement prose test https://github.com/mongodb/specifications/blob/master/source/crud/tests/README.md#5-mongoclientbulkwrite-collects-writeconcernerrors-across-batches"); + } + + @DisplayName("6. MongoClient.bulkWrite handles individual WriteErrors across batches") + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void testBulkWriteHandlesWriteErrorsAcrossBatches(final boolean ordered) { + assumeTrue(serverVersionAtLeast(8, 0)); + assumeFalse(isServerlessTest()); + assumeTrue(Runtime.getRuntime().availableProcessors() < 1, "BULK-TODO implement prose test https://github.com/mongodb/specifications/blob/master/source/crud/tests/README.md#6-mongoclientbulkwrite-handles-individual-writeerrors-across-batches"); + } + + @DisplayName("7. MongoClient.bulkWrite handles a cursor requiring a getMore") + @Test + void testBulkWriteHandlesCursorRequiringGetMore() { + assumeTrue(serverVersionAtLeast(8, 0)); + assumeFalse(isServerlessTest()); + assumeFalse(isStandalone()); + assumeTrue(Runtime.getRuntime().availableProcessors() < 1, "BULK-TODO implement batch splitting https://jira.mongodb.org/browse/JAVA-5529"); + ArrayList startedBulkWriteCommandEvents = new ArrayList<>(); + CommandListener commandListener = new CommandListener() { + @Override + public void commandStarted(final CommandStartedEvent event) { + if (event.getCommandName().equals("bulkWrite")) { + startedBulkWriteCommandEvents.add(event); + } } + }; + try (MongoClient client = createMongoClient(getMongoClientSettingsBuilder().addCommandListener(commandListener))) { + int maxWriteBatchSize = droppedDatabase(client).runCommand(new Document("hello", 1)).getInteger("maxWriteBatchSize"); + ClientBulkWriteResult result = client.bulkWrite(nCopies( + maxWriteBatchSize + 1, + ClientNamespacedWriteModel.insertOne(namespace(), new Document("a", "b")))); + assertEquals(maxWriteBatchSize + 1, result.getInsertedCount()); + assertEquals(2, startedBulkWriteCommandEvents.size()); + CommandStartedEvent firstEvent = startedBulkWriteCommandEvents.get(0); + CommandStartedEvent secondEvent = startedBulkWriteCommandEvents.get(1); + assertEquals(maxWriteBatchSize, firstEvent.getCommand().getArray("ops").size()); + assertEquals(1, secondEvent.getCommand().getArray("ops").size()); + assertEquals(firstEvent.getOperationId(), secondEvent.getOperationId()); } + } - try { - collection.insertMany(asList(new Document("x", 1))); - fail("Should throw, as document doesn't match schema"); - } catch (MongoBulkWriteException e) { - // These assertions doesn't do exactly what's required by the specification, but it's simpler to implement and nearly as - // effective - assertTrue(e.getMessage().contains("Write errors")); - assertEquals(1, e.getWriteErrors().size()); - if (serverVersionAtLeast(5, 0)) { - assertFalse(e.getWriteErrors().get(0).getDetails().isEmpty()); + @DisplayName("8. MongoClient.bulkWrite handles a cursor requiring getMore within a transaction") + @Test + void testBulkWriteHandlesCursorRequiringGetMoreWithinTransaction() { + assumeTrue(serverVersionAtLeast(8, 0)); + assumeFalse(isServerlessTest()); + assumeTrue(Runtime.getRuntime().availableProcessors() < 1, "BULK-TODO implement prose test https://github.com/mongodb/specifications/blob/master/source/crud/tests/README.md#8-mongoclientbulkwrite-handles-a-cursor-requiring-getmore-within-a-transaction"); + } + + @DisplayName("10. MongoClient.bulkWrite returns error for unacknowledged too-large insert") + @ParameterizedTest + @ValueSource(strings = {"insert", "replace"}) + void testBulkWriteErrorsForUnacknowledgedTooLargeInsert(final String operationType) { + assumeTrue(serverVersionAtLeast(8, 0)); + assumeFalse(isServerlessTest()); + assumeTrue(Runtime.getRuntime().availableProcessors() < 1, "BULK-TODO implement maxBsonObjectSize validation https://jira.mongodb.org/browse/JAVA-5529"); + try (MongoClient client = createMongoClient(getMongoClientSettingsBuilder() + .writeConcern(WriteConcern.ACKNOWLEDGED))) { + int maxBsonObjectSize = droppedDatabase(client).runCommand(new Document("hello", 1)).getInteger("maxBsonObjectSize"); + Document document = new Document("a", join("", nCopies(maxBsonObjectSize, "b"))); + ClientNamespacedWriteModel model; + switch (operationType) { + case "insert": { + model = ClientNamespacedWriteModel.insertOne(namespace(), document); + break; + } + case "replace": { + model = ClientNamespacedWriteModel.replaceOne(namespace(), Filters.empty(), document); + break; + } + default: { + throw Assertions.fail(operationType); + } } + assertThrows(MongoClientException.class, () -> client.bulkWrite(singletonList(model))); + } + } + + @DisplayName("11. MongoClient.bulkWrite batch splits when the addition of a new namespace exceeds the maximum message size") + @Test + void testBulkWriteSplitsWhenExceedingMaxMessageSizeBytesDueToNsInfo() { + assumeTrue(serverVersionAtLeast(8, 0)); + assumeFalse(isServerlessTest()); + assumeTrue(Runtime.getRuntime().availableProcessors() < 1, "BULK-TODO implement prose test https://github.com/mongodb/specifications/blob/master/source/crud/tests/README.md#11-mongoclientbulkwrite-batch-splits-when-the-addition-of-a-new-namespace-exceeds-the-maximum-message-size"); + } + + @DisplayName("12. MongoClient.bulkWrite returns an error if no operations can be added to ops") + @Test + void testBulkWriteSplitsErrorsForTooLargeOpsOrNsInfo() { + assumeTrue(serverVersionAtLeast(8, 0)); + assumeFalse(isServerlessTest()); + assumeTrue(Runtime.getRuntime().availableProcessors() < 1, "BULK-TODO implement prose test https://github.com/mongodb/specifications/blob/master/source/crud/tests/README.md#12-mongoclientbulkwrite-returns-an-error-if-no-operations-can-be-added-to-ops"); + } + + @DisplayName("13. MongoClient.bulkWrite returns an error if auto-encryption is configured") + @Test + void testBulkWriteErrorsForAutoEncryption() { + assumeTrue(serverVersionAtLeast(8, 0)); + assumeFalse(isServerlessTest()); + HashMap awsKmsProviderProperties = new HashMap<>(); + awsKmsProviderProperties.put("accessKeyId", "foo"); + awsKmsProviderProperties.put("secretAccessKey", "bar"); + try (MongoClient client = createMongoClient(getMongoClientSettingsBuilder() + .autoEncryptionSettings(AutoEncryptionSettings.builder() + .keyVaultNamespace(namespace().getFullName()) + .kmsProviders(singletonMap("aws", awsKmsProviderProperties)) + .build()))) { + assertTrue( + assertThrows( + IllegalStateException.class, + () -> client.bulkWrite(singletonList(ClientNamespacedWriteModel.insertOne(namespace(), new Document("a", "b"))))) + .getMessage().contains("bulkWrite does not currently support automatic encryption")); } } /** * This test is not from the specification. */ - @Test - @SuppressWarnings("try") - void insertMustGenerateIdAtMostOnce() throws ExecutionException, InterruptedException { + @ParameterizedTest + @MethodSource("insertMustGenerateIdAtMostOnceArgs") + void insertMustGenerateIdAtMostOnce( + final Class documentClass, + final boolean expectIdGenerated, + final Supplier documentSupplier) { + assumeTrue(serverVersionAtLeast(8, 0)); assumeTrue(isDiscoverableReplicaSet()); - ServerAddress primaryServerAddress = Fixture.getPrimary(); + assertAll( + () -> assertInsertMustGenerateIdAtMostOnce("insert", documentClass, expectIdGenerated, + (client, collection) -> collection.insertOne(documentSupplier.get()).getInsertedId()), + () -> assertInsertMustGenerateIdAtMostOnce("insert", documentClass, expectIdGenerated, + (client, collection) -> collection.bulkWrite( + singletonList(new InsertOneModel<>(documentSupplier.get()))) + .getInserts().get(0).getId()), + () -> assertInsertMustGenerateIdAtMostOnce("bulkWrite", documentClass, expectIdGenerated, + (client, collection) -> client.bulkWrite( + singletonList(insertOne(collection.getNamespace(), documentSupplier.get())), + clientBulkWriteOptions().verboseResults(true)) + .getVerboseResults().orElseThrow(Assertions::fail).getInsertResults().get(0).getInsertedId().orElse(null)) + ); + } + + private static Stream insertMustGenerateIdAtMostOnceArgs() { + CodecRegistry codecRegistry = fromRegistries( + getDefaultCodecRegistry(), + fromProviders(PojoCodecProvider.builder().automatic(true).build())); + return Stream.of( + arguments(MyDocument.class, true, (Supplier) MyDocument::new), + arguments(Document.class, true, (Supplier) Document::new), + arguments(BsonDocument.class, true, (Supplier) BsonDocument::new), + arguments( + BsonDocumentWrapper.class, true, + (Supplier>) () -> + new BsonDocumentWrapper<>(new MyDocument(), codecRegistry.get(MyDocument.class))), + arguments( + RawBsonDocument.class, false, + (Supplier) () -> + new RawBsonDocument(new MyDocument(), codecRegistry.get(MyDocument.class))) + ); + } + + @SuppressWarnings("try") + private void assertInsertMustGenerateIdAtMostOnce( + final String commandName, + final Class documentClass, + final boolean expectIdGenerated, + final BiFunction, BsonValue> insertOperation) + throws ExecutionException, InterruptedException, TimeoutException { CompletableFuture futureIdGeneratedByFirstInsertAttempt = new CompletableFuture<>(); CompletableFuture futureIdGeneratedBySecondInsertAttempt = new CompletableFuture<>(); CommandListener commandListener = new CommandListener() { @Override public void commandStarted(final CommandStartedEvent event) { - if (event.getCommandName().equals("insert")) { - BsonValue generatedId = event.getCommand().getArray("documents").get(0).asDocument().get("_id"); + Consumer generatedIdConsumer = generatedId -> { if (!futureIdGeneratedByFirstInsertAttempt.isDone()) { futureIdGeneratedByFirstInsertAttempt.complete(generatedId); } else { futureIdGeneratedBySecondInsertAttempt.complete(generatedId); } + }; + switch (event.getCommandName()) { + case "insert": { + Assertions.assertTrue(commandName.equals("insert")); + generatedIdConsumer.accept(event.getCommand().getArray("documents").get(0).asDocument().get("_id")); + break; + } + case "bulkWrite": { + Assertions.assertTrue(commandName.equals("bulkWrite")); + generatedIdConsumer.accept(event.getCommand().getArray("ops").get(0).asDocument().getDocument("document").get("_id")); + break; + } + default: { + // nothing to do + } } } }; BsonDocument failPointDocument = new BsonDocument("configureFailPoint", new BsonString("failCommand")) .append("mode", new BsonDocument("times", new BsonInt32(1))) .append("data", new BsonDocument() - .append("failCommands", new BsonArray(singletonList(new BsonString("insert")))) + .append("failCommands", new BsonArray(singletonList(new BsonString(commandName)))) .append("errorLabels", new BsonArray(singletonList(new BsonString("RetryableWriteError")))) .append("writeConcernError", new BsonDocument("code", new BsonInt32(91)) .append("errmsg", new BsonString("Replication is being shut down")))); - try (MongoClient client = MongoClients.create(getMongoClientSettingsBuilder() + try (MongoClient client = createMongoClient(getMongoClientSettingsBuilder() .retryWrites(true) .addCommandListener(commandListener) .applyToServerSettings(builder -> builder.heartbeatFrequency(50, TimeUnit.MILLISECONDS)) - .build()); - FailPoint ignored = FailPoint.enable(failPointDocument, primaryServerAddress)) { - MongoCollection coll = client.getDatabase(database.getName()) - .getCollection(collection.getNamespace().getCollectionName(), MyDocument.class) - .withCodecRegistry(fromRegistries( - getDefaultCodecRegistry(), - fromProviders(PojoCodecProvider.builder().automatic(true).build()))); - BsonValue insertedId = coll.insertOne(new MyDocument()).getInsertedId(); - BsonValue idGeneratedByFirstInsertAttempt = futureIdGeneratedByFirstInsertAttempt.get(); + .codecRegistry(fromRegistries( + getDefaultCodecRegistry(), + fromProviders(PojoCodecProvider.builder().automatic(true).build())))); + FailPoint ignored = FailPoint.enable(failPointDocument, getPrimary())) { + MongoCollection collection = droppedCollection(client, documentClass); + BsonValue insertedId = insertOperation.apply(client, collection); + if (expectIdGenerated) { + assertNotNull(insertedId); + } else { + assertNull(insertedId); + } + Duration timeout = Duration.ofSeconds(10); + BsonValue idGeneratedByFirstInsertAttempt = futureIdGeneratedByFirstInsertAttempt.get(timeout.toMillis(), TimeUnit.MILLISECONDS); assertEquals(idGeneratedByFirstInsertAttempt, insertedId); - assertEquals(idGeneratedByFirstInsertAttempt, futureIdGeneratedBySecondInsertAttempt.get()); + assertEquals(idGeneratedByFirstInsertAttempt, futureIdGeneratedBySecondInsertAttempt.get(timeout.toMillis(), TimeUnit.MILLISECONDS)); } } - private void setFailPoint() { - failPointDocument = new BsonDocument("configureFailPoint", new BsonString("failCommand")) - .append("mode", new BsonDocument("times", new BsonInt32(1))) - .append("data", new BsonDocument("failCommands", new BsonArray(asList(new BsonString("insert")))) - .append("writeConcernError", new BsonDocument("code", new BsonInt32(100)) - .append("codeName", new BsonString("UnsatisfiableWriteConcern")) - .append("errmsg", new BsonString("Not enough data-bearing nodes")) - .append("errInfo", new BsonDocument("writeConcern", new BsonDocument("w", new BsonInt32(2)) - .append("wtimeout", new BsonInt32(0)) - .append("provenance", new BsonString("clientSupplied")))))); - getCollectionHelper().runAdminCommand(failPointDocument); + protected MongoClient createMongoClient(final MongoClientSettings.Builder mongoClientSettingsBuilder) { + return MongoClients.create(mongoClientSettingsBuilder.build()); + } + + private MongoCollection droppedCollection(final MongoClient client, final Class documentClass) { + return droppedDatabase(client).getCollection(namespace().getCollectionName(), documentClass); + } + + private MongoDatabase droppedDatabase(final MongoClient client) { + MongoDatabase database = client.getDatabase(namespace().getDatabaseName()); + database.drop(); + return database; } - private void disableFailPoint() { - getCollectionHelper().runAdminCommand(failPointDocument.append("mode", new BsonString("off"))); + private MongoNamespace namespace() { + return new MongoNamespace(getDefaultDatabaseName(), getClass().getSimpleName()); } public static final class MyDocument { diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/ErrorMatcher.java b/driver-sync/src/test/functional/com/mongodb/client/unified/ErrorMatcher.java index 75d264487f8..d82e4c6beb1 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/ErrorMatcher.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/ErrorMatcher.java @@ -16,6 +16,7 @@ package com.mongodb.client.unified; +import com.mongodb.ClientBulkWriteException; import com.mongodb.MongoBulkWriteException; import com.mongodb.MongoClientException; import com.mongodb.MongoCommandException; @@ -27,23 +28,33 @@ import com.mongodb.MongoSocketException; import com.mongodb.MongoWriteConcernException; import com.mongodb.MongoWriteException; +import com.mongodb.WriteError; +import com.mongodb.bulk.WriteConcernError; import org.bson.BsonDocument; +import org.bson.BsonInt32; +import org.bson.BsonString; import org.bson.BsonValue; import java.util.HashSet; +import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.Set; +import static java.lang.Integer.parseInt; import static java.util.Arrays.asList; +import static java.util.stream.Collectors.toList; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -import static org.spockframework.util.Assert.fail; +import static org.junit.jupiter.api.Assertions.fail; final class ErrorMatcher { private static final Set EXPECTED_ERROR_FIELDS = new HashSet<>( asList("isError", "expectError", "isClientError", "errorCode", "errorCodeName", "errorContains", "errorResponse", - "isClientError", "isTimeoutError", "errorLabelsOmit", "errorLabelsContain", "expectResult")); + "isClientError", "isTimeoutError", "errorLabelsOmit", "errorLabelsContain", + "writeErrors", "writeConcernErrors", "expectResult")); private final AssertionContext context; private final ValueMatcher valueMatcher; @@ -134,13 +145,55 @@ void assertErrorsMatch(final BsonDocument expectedError, final Exception e) { mongoException.hasErrorLabel(cur.asString().getValue())); } } + if (expectedError.containsKey("writeErrors")) { + assertTrue(context.getMessage("Exception must be of type ClientBulkWriteException when checking for write errors"), + e instanceof ClientBulkWriteException); + BsonDocument writeErrors = expectedError.getDocument("writeErrors"); + ClientBulkWriteException actualException = (ClientBulkWriteException) e; + Map actualWriteErrors = actualException.getWriteErrors(); + assertEquals("The number of write errors must match", writeErrors.size(), actualWriteErrors.size()); + writeErrors.forEach((index, writeError) -> { + WriteError actualWriteError = actualWriteErrors.get(parseInt(index)); + assertNotNull("Expected a write error with index " + index, actualWriteError); + valueMatcher.assertValuesMatch(writeError, toMatchableValue(actualWriteError)); + }); + } + if (expectedError.containsKey("writeConcernErrors")) { + assertTrue(context.getMessage("Exception must be of type ClientBulkWriteException when checking for write errors"), + e instanceof ClientBulkWriteException); + List writeConcernErrors = expectedError.getArray("writeConcernErrors").stream() + .map(BsonValue::asDocument).collect(toList()); + ClientBulkWriteException actualException = (ClientBulkWriteException) e; + List actualWriteConcernErrors = actualException.getWriteConcernErrors(); + assertEquals("The number of write concern errors must match", writeConcernErrors.size(), actualWriteConcernErrors.size()); + for (int index = 0; index < writeConcernErrors.size(); index++) { + BsonDocument writeConcernError = writeConcernErrors.get(index); + WriteConcernError actualWriteConcernError = actualWriteConcernErrors.get(index); + valueMatcher.assertValuesMatch(writeConcernError, toMatchableValue(actualWriteConcernError)); + } + } if (expectedError.containsKey("expectResult")) { - // Neither MongoBulkWriteException nor MongoSocketException includes information about the successful writes, so this - // is the only check that can currently be done - assertTrue(context.getMessage("Exception must be of type MongoBulkWriteException or MongoSocketException " - + "when checking for results, but actual type is " + e.getClass().getSimpleName()), - e instanceof MongoBulkWriteException || e instanceof MongoSocketException); + assertTrue(context.getMessage("Exception must be of type" + + " MongoBulkWriteException, or MongoSocketException, or ClientBulkWriteException" + + " when checking for results, but actual type is " + e.getClass().getSimpleName()), + e instanceof MongoBulkWriteException || e instanceof ClientBulkWriteException || e instanceof MongoSocketException); + // neither `MongoBulkWriteException` nor `MongoSocketException` includes information about the successful individual operations + if (e instanceof ClientBulkWriteException) { + BsonDocument actualPartialResult = ((ClientBulkWriteException) e).getPartialResult() + .map(UnifiedCrudHelper::toMatchableValue) + .orElse(new BsonDocument()); + valueMatcher.assertValuesMatch(expectedError.getDocument("expectResult"), actualPartialResult); + } } context.pop(); } + + private static BsonDocument toMatchableValue(final WriteError writeError) { + return new BsonDocument("code", new BsonInt32(writeError.getCode())); + } + + private static BsonDocument toMatchableValue(final WriteConcernError writeConcernError) { + return new BsonDocument("code", new BsonInt32(writeConcernError.getCode())) + .append("message", new BsonString(writeConcernError.getMessage())); + } } diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedCrudHelper.java b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedCrudHelper.java index 041f016510f..03afe429cbc 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedCrudHelper.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedCrudHelper.java @@ -27,6 +27,7 @@ import com.mongodb.TagSet; import com.mongodb.TransactionOptions; import com.mongodb.WriteConcern; +import com.mongodb.assertions.Assertions; import com.mongodb.bulk.BulkWriteResult; import com.mongodb.client.AggregateIterable; import com.mongodb.client.ChangeStreamIterable; @@ -75,6 +76,11 @@ import com.mongodb.client.model.UpdateOneModel; import com.mongodb.client.model.UpdateOptions; import com.mongodb.client.model.WriteModel; +import com.mongodb.client.model.bulk.ClientBulkWriteOptions; +import com.mongodb.client.model.bulk.ClientDeleteOptions; +import com.mongodb.client.model.bulk.ClientReplaceOptions; +import com.mongodb.client.model.bulk.ClientUpdateOptions; +import com.mongodb.client.model.bulk.ClientNamespacedWriteModel; import com.mongodb.client.model.changestream.ChangeStreamDocument; import com.mongodb.client.model.changestream.FullDocument; import com.mongodb.client.model.changestream.FullDocumentBeforeChange; @@ -82,7 +88,10 @@ import com.mongodb.client.result.InsertManyResult; import com.mongodb.client.result.InsertOneResult; import com.mongodb.client.result.UpdateResult; +import com.mongodb.client.model.bulk.ClientBulkWriteResult; +import com.mongodb.client.model.bulk.ClientUpdateResult; import com.mongodb.lang.NonNull; +import com.mongodb.lang.Nullable; import org.bson.BsonArray; import org.bson.BsonDocument; import org.bson.BsonDocumentWriter; @@ -101,15 +110,23 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Supplier; +import static com.mongodb.client.model.bulk.ClientBulkWriteOptions.clientBulkWriteOptions; +import static com.mongodb.client.model.bulk.ClientDeleteOptions.clientDeleteOptions; +import static com.mongodb.client.model.bulk.ClientReplaceOptions.clientReplaceOptions; +import static com.mongodb.client.model.bulk.ClientUpdateOptions.clientUpdateOptions; +import static java.lang.String.format; import static java.util.Arrays.asList; +import static java.util.Collections.singleton; import static java.util.Objects.requireNonNull; import static java.util.stream.Collectors.toList; @@ -215,6 +232,7 @@ private OperationResult resultOf(final Supplier operationResult) { } } + @Nullable private ClientSession getSession(final BsonDocument arguments) { if (arguments.containsKey("session")) { return entities.getSession(arguments.getString("session").asString().getValue()); @@ -1760,6 +1778,234 @@ public OperationResult createChangeStreamCursor(final BsonDocument operation) { }); } + public OperationResult clientBulkWrite(final BsonDocument operation) { + Set unexpectedOperationKeys = singleton("saveResultAsEntity"); + if (operation.keySet().stream().anyMatch(unexpectedOperationKeys::contains)) { + throw new UnsupportedOperationException("Unexpected field in operation. One of " + unexpectedOperationKeys); + } + String clientId = operation.getString("object").getValue(); + MongoCluster cluster = entities.getClient(clientId); + BsonDocument arguments = operation.getDocument("arguments"); + ClientSession session = getSession(arguments); + List models = arguments.getArray("models").stream() + .map(BsonValue::asDocument) + .map(UnifiedCrudHelper::toClientNamespacedWriteModel) + .collect(toList()); + ClientBulkWriteOptions options = clientBulkWriteOptions(); + for (Map.Entry entry : arguments.entrySet()) { + String key = entry.getKey(); + BsonValue argument = entry.getValue(); + switch (key) { + case "models": + case "session": + break; + case "writeConcern": + cluster = cluster.withWriteConcern(asWriteConcern(argument.asDocument())); + break; + case "ordered": + options.ordered(argument.asBoolean().getValue()); + break; + case "bypassDocumentValidation": + options.bypassDocumentValidation(argument.asBoolean().getValue()); + break; + case "let": + options.let(argument.asDocument()); + break; + case "comment": + options.comment(argument); + break; + case "verboseResults": + options.verboseResults(argument.asBoolean().getValue()); + break; + default: + throw new UnsupportedOperationException(format("Unsupported argument: key=%s, argument=%s", key, argument)); + } + } + MongoCluster clusterWithWriteConcern = cluster; + return resultOf(() -> { + if (session == null) { + return toMatchableValue(clusterWithWriteConcern.bulkWrite(models, options)); + } else { + return toMatchableValue(clusterWithWriteConcern.bulkWrite(session, models, options)); + } + }); + } + + private static ClientNamespacedWriteModel toClientNamespacedWriteModel(final BsonDocument model) { + String modelType = model.getFirstKey(); + BsonDocument arguments = model.getDocument(modelType); + MongoNamespace namespace = new MongoNamespace(arguments.getString("namespace").getValue()); + switch (modelType) { + case "insertOne": + Set expectedArguments = new HashSet<>(asList("namespace", "document")); + if (!expectedArguments.containsAll(arguments.keySet())) { + // for other `modelType`s a conceptually similar check is done when creating their options objects + throw new UnsupportedOperationException("Unsupported argument, one of: " + arguments.keySet()); + } + return ClientNamespacedWriteModel.insertOne( + namespace, + arguments.getDocument("document")); + case "replaceOne": + return ClientNamespacedWriteModel.replaceOne( + namespace, + arguments.getDocument("filter"), + arguments.getDocument("replacement"), + getClientReplaceOptions(arguments)); + case "updateOne": + return arguments.isDocument("update") + ? ClientNamespacedWriteModel.updateOne( + namespace, + arguments.getDocument("filter"), + arguments.getDocument("update"), + getClientUpdateOptions(arguments)) + : ClientNamespacedWriteModel.updateOne( + namespace, + arguments.getDocument("filter"), + arguments.getArray("update").stream().map(BsonValue::asDocument).collect(toList()), + getClientUpdateOptions(arguments)); + case "updateMany": + return arguments.isDocument("update") + ? ClientNamespacedWriteModel.updateMany( + namespace, + arguments.getDocument("filter"), + arguments.getDocument("update"), + getClientUpdateOptions(arguments)) + : ClientNamespacedWriteModel.updateMany( + namespace, + arguments.getDocument("filter"), + arguments.getArray("update").stream().map(BsonValue::asDocument).collect(toList()), + getClientUpdateOptions(arguments)); + case "deleteOne": + return ClientNamespacedWriteModel.deleteOne( + namespace, + arguments.getDocument("filter"), + getClientDeleteOptions(arguments)); + case "deleteMany": + return ClientNamespacedWriteModel.deleteMany( + namespace, + arguments.getDocument("filter"), + getClientDeleteOptions(arguments)); + default: + throw new UnsupportedOperationException("Unsupported client write model type: " + modelType); + } + } + + private static ClientReplaceOptions getClientReplaceOptions(final BsonDocument arguments) { + ClientReplaceOptions options = clientReplaceOptions(); + arguments.forEach((key, argument) -> { + switch (key) { + case "namespace": + case "filter": + case "replacement": + break; + case "collation": + options.collation(asCollation(argument.asDocument())); + break; + case "hint": + if (argument.isDocument()) { + options.hint(argument.asDocument()); + } else { + options.hintString(argument.asString().getValue()); + } + break; + case "upsert": + options.upsert(argument.asBoolean().getValue()); + break; + default: + throw new UnsupportedOperationException(format("Unsupported argument: key=%s, argument=%s", key, argument)); + } + }); + return options; + } + + private static ClientUpdateOptions getClientUpdateOptions(final BsonDocument arguments) { + ClientUpdateOptions options = clientUpdateOptions(); + arguments.forEach((key, argument) -> { + switch (key) { + case "namespace": + case "filter": + case "update": + break; + case "arrayFilters": + options.arrayFilters(argument.asArray().stream().map(BsonValue::asDocument).collect(toList())); + break; + case "collation": + options.collation(asCollation(argument.asDocument())); + break; + case "hint": + if (argument.isDocument()) { + options.hint(argument.asDocument()); + } else { + options.hintString(argument.asString().getValue()); + } + break; + case "upsert": + options.upsert(argument.asBoolean().getValue()); + break; + default: + throw new UnsupportedOperationException(format("Unsupported argument: key=%s, argument=%s", key, argument)); + } + }); + return options; + } + + private static ClientDeleteOptions getClientDeleteOptions(final BsonDocument arguments) { + ClientDeleteOptions options = clientDeleteOptions(); + arguments.forEach((key, argument) -> { + switch (key) { + case "namespace": + case "filter": + break; + case "collation": + options.collation(asCollation(argument.asDocument())); + break; + case "hint": + if (argument.isDocument()) { + options.hint(argument.asDocument()); + } else { + options.hintString(argument.asString().getValue()); + } + break; + default: + throw new UnsupportedOperationException(format("Unsupported argument: key=%s, argument=%s", key, argument)); + } + }); + return options; + } + + static BsonDocument toMatchableValue(final ClientBulkWriteResult result) { + BsonDocument expected = new BsonDocument(); + if (result.isAcknowledged()) { + expected.append("insertedCount", new BsonInt64(result.getInsertedCount())) + .append("upsertedCount", new BsonInt64(result.getUpsertedCount())) + .append("matchedCount", new BsonInt64(result.getMatchedCount())) + .append("modifiedCount", new BsonInt64(result.getModifiedCount())) + .append("deletedCount", new BsonInt64(result.getDeletedCount())); + result.getVerboseResults().ifPresent(verbose -> + expected.append("insertResults", new BsonDocument(verbose.getInsertResults().entrySet().stream() + .map(entry -> new BsonElement( + entry.getKey().toString(), + new BsonDocument("insertedId", entry.getValue().getInsertedId().orElseThrow(Assertions::fail)))) + .collect(toList()))) + .append("updateResults", new BsonDocument(verbose.getUpdateResults().entrySet().stream() + .map(entry -> { + ClientUpdateResult updateResult = entry.getValue(); + BsonDocument updateResultDocument = new BsonDocument( + "matchedCount", new BsonInt64(updateResult.getMatchedCount())) + .append("modifiedCount", new BsonInt64(updateResult.getModifiedCount())); + updateResult.getUpsertedId().ifPresent(upsertedId -> updateResultDocument.append("upsertedId", upsertedId)); + return new BsonElement(entry.getKey().toString(), updateResultDocument); + }) + .collect(toList()))) + .append("deleteResults", new BsonDocument(verbose.getDeleteResults().entrySet().stream() + .map(entry -> new BsonElement( + entry.getKey().toString(), + new BsonDocument("deletedCount", new BsonInt64(entry.getValue().getDeletedCount())))) + .collect(toList())))); + } + return expected; + } + public OperationResult executeIterateUntilDocumentOrError(final BsonDocument operation) { String id = operation.getString("object").getValue(); MongoCursor cursor = entities.getCursor(id); diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java index 58ad07034ec..642999547e9 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTest.java @@ -229,7 +229,8 @@ public void setUp( || schemaVersion.equals("1.16") || schemaVersion.equals("1.17") || schemaVersion.equals("1.18") - || schemaVersion.equals("1.19"), + || schemaVersion.equals("1.19") + || schemaVersion.equals("1.21"), String.format("Unsupported schema version %s", schemaVersion)); if (runOnRequirements != null) { assumeTrue(runOnRequirementsMet(runOnRequirements, getMongoClientSettings(), getServerVersion()), @@ -538,6 +539,8 @@ private OperationResult executeOperation(final UnifiedTestContext context, final return crudHelper.createFindCursor(operation); case "createChangeStream": return crudHelper.createChangeStreamCursor(operation); + case "clientBulkWrite": + return crudHelper.clientBulkWrite(operation); case "close": return crudHelper.close(operation); case "iterateUntilDocumentOrError": diff --git a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTransactionsTest.java b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTransactionsTest.java index 5acf74cd972..216fdf099d3 100644 --- a/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTransactionsTest.java +++ b/driver-sync/src/test/functional/com/mongodb/client/unified/UnifiedTransactionsTest.java @@ -36,6 +36,8 @@ protected void skips(final String fileDescription, final String testDescription) assumeFalse(fileDescription.equals("read-concern") && testDescription.equals("distinct ignores collection readConcern")); assumeFalse(fileDescription.equals("reads") && testDescription.equals("distinct")); } + // `MongoCluster.getWriteConcern`/`MongoCollection.getWriteConcern` are silently ignored in a transaction + assumeFalse(testDescription.equals("client bulkWrite with writeConcern in a transaction causes a transaction error")); } private static Collection data() throws URISyntaxException, IOException {