diff --git a/src/common/common-macros-private.h b/src/common/common-macros-private.h index 360a0f51ead..97d8d34b1c9 100644 --- a/src/common/common-macros-private.h +++ b/src/common/common-macros-private.h @@ -12,4 +12,19 @@ #define MONGOC_DEBUG_ASSERT(statement) ((void) 0) #endif +// `MC_ENABLE_CONVERSION_WARNING_BEGIN` enables -Wconversion to check for potentially unsafe integer conversions. +// The `bson_in_range_*` functions can help address these warnings by ensuring a cast is within bounds. +#if (__GNUC__ > 4) || (__GNUC__ == 4 && __GNUC_MINOR__ >= 6) // gcc 4.6 added support for "diagnostic push". +#define MC_ENABLE_CONVERSION_WARNING_BEGIN \ + _Pragma ("GCC diagnostic push") _Pragma ("GCC diagnostic warning \"-Wconversion\"") +#define MC_ENABLE_CONVERSION_WARNING_END _Pragma ("GCC diagnostic pop") +#elif defined(__clang__) +#define MC_ENABLE_CONVERSION_WARNING_BEGIN \ + _Pragma ("clang diagnostic push") _Pragma ("clang diagnostic warning \"-Wconversion\"") +#define MC_ENABLE_CONVERSION_WARNING_END _Pragma ("clang diagnostic pop") +#else +#define MC_ENABLE_CONVERSION_WARNING_BEGIN +#define MC_ENABLE_CONVERSION_WARNING_END +#endif + #endif diff --git a/src/common/common-thread.c b/src/common/common-thread.c index 9b26f6e9567..8985ca7efd3 100644 --- a/src/common/common-thread.c +++ b/src/common/common-thread.c @@ -24,7 +24,7 @@ mcommon_thread_create (bson_thread_t *thread, BSON_THREAD_FUN_TYPE (func), void { BSON_ASSERT_PARAM (thread); BSON_ASSERT_PARAM (func); - BSON_ASSERT (arg || true); // optional. + BSON_OPTIONAL_PARAM (arg); // optional. return pthread_create (thread, NULL, func, arg); } int @@ -47,7 +47,7 @@ mcommon_thread_create (bson_thread_t *thread, BSON_THREAD_FUN_TYPE (func), void { BSON_ASSERT_PARAM (thread); BSON_ASSERT_PARAM (func); - BSON_ASSERT (arg || true); // optional. + BSON_OPTIONAL_PARAM (arg); // optional. *thread = (HANDLE) _beginthreadex (NULL, 0, func, arg, 0, NULL); if (0 == *thread) { diff --git a/src/libbson/src/bson/bson-macros.h b/src/libbson/src/bson/bson-macros.h index f9263914f1f..bed204b851c 100644 --- a/src/libbson/src/bson/bson-macros.h +++ b/src/libbson/src/bson/bson-macros.h @@ -232,6 +232,10 @@ } \ } while (0) +// `BSON_OPTIONAL_PARAM` is a documentation-only macro to document X may be NULL. +// Useful in combination with `BSON_ASSERT_PARAM` to document and assert pointer parameters. +#define BSON_OPTIONAL_PARAM(param) (void) 0 + /* obsolete macros, preserved for compatibility */ #define BSON_STATIC_ASSERT(s) BSON_STATIC_ASSERT_ (s, __LINE__) #define BSON_STATIC_ASSERT_JOIN(a, b) BSON_STATIC_ASSERT_JOIN2 (a, b) diff --git a/src/libmongoc/CMakeLists.txt b/src/libmongoc/CMakeLists.txt index 74c541f94b6..f93301f3007 100644 --- a/src/libmongoc/CMakeLists.txt +++ b/src/libmongoc/CMakeLists.txt @@ -658,6 +658,7 @@ set (SOURCES ${SOURCES} ${PROJECT_SOURCE_DIR}/src/mongoc/mongoc-stream-tls-openssl-bio.c ${PROJECT_SOURCE_DIR}/src/mongoc/mongoc-openssl.c ${PROJECT_SOURCE_DIR}/src/mongoc/mongoc-ocsp-cache.c + ${PROJECT_SOURCE_DIR}/src/mongoc/mongoc-bulkwrite.c ) set (HEADERS @@ -666,6 +667,7 @@ set (HEADERS ${PROJECT_SOURCE_DIR}/src/mongoc/mongoc.h ${PROJECT_SOURCE_DIR}/src/mongoc/mongoc-apm.h ${PROJECT_SOURCE_DIR}/src/mongoc/mongoc-bulk-operation.h + ${PROJECT_SOURCE_DIR}/src/mongoc/mongoc-bulkwrite.h ${PROJECT_SOURCE_DIR}/src/mongoc/mongoc-change-stream.h ${PROJECT_SOURCE_DIR}/src/mongoc/mongoc-client.h ${PROJECT_SOURCE_DIR}/src/mongoc/mongoc-client-pool.h @@ -713,6 +715,7 @@ set (HEADERS ${PROJECT_SOURCE_DIR}/src/mongoc/mongoc-rand.h ${PROJECT_SOURCE_DIR}/src/mongoc/mongoc-stream-tls.h ${PROJECT_SOURCE_DIR}/src/mongoc/mongoc-ssl.h + ${PROJECT_SOURCE_DIR}/src/mongoc/mongoc-bulkwrite.h ) set (HEADERS_FORWARDING @@ -1012,6 +1015,7 @@ set (test-libmongoc-sources ${PROJECT_SOURCE_DIR}/tests/test-mongoc-background-monitoring.c ${PROJECT_SOURCE_DIR}/tests/test-mongoc-buffer.c ${PROJECT_SOURCE_DIR}/tests/test-mongoc-bulk.c + ${PROJECT_SOURCE_DIR}/tests/test-mongoc-bulkwrite.c ${PROJECT_SOURCE_DIR}/tests/test-mongoc-change-stream.c ${PROJECT_SOURCE_DIR}/tests/test-mongoc-client-pool.c ${PROJECT_SOURCE_DIR}/tests/test-mongoc-client-session.c @@ -1226,6 +1230,7 @@ if (ENABLE_EXAMPLES AND ENABLE_SHARED) mongoc_add_example (mongoc-ping ${PROJECT_SOURCE_DIR}/examples/mongoc-ping.c) mongoc_add_example (mongoc-tail ${PROJECT_SOURCE_DIR}/examples/mongoc-tail.c) mongoc_add_example (example-collection-command ${PROJECT_SOURCE_DIR}/examples/example-collection-command.c) + mongoc_add_example (example-bulkwrite ${PROJECT_SOURCE_DIR}/examples/example-bulkwrite.c) # examples/aggregation/ mongoc_add_example (aggregation1 ${PROJECT_SOURCE_DIR}/examples/aggregation/aggregation1.c) diff --git a/src/libmongoc/examples/example-bulkwrite.c b/src/libmongoc/examples/example-bulkwrite.c new file mode 100644 index 00000000000..8b7f935e0d0 --- /dev/null +++ b/src/libmongoc/examples/example-bulkwrite.c @@ -0,0 +1,87 @@ +// example-bulkwrite shows use of `mongoc_client_bulkwrite`. + +#include + +#define HANDLE_ERROR(...) \ + if (1) { \ + fprintf (stderr, __VA_ARGS__); \ + fprintf (stderr, "\n"); \ + goto fail; \ + } else \ + (void) 0 + +int +main (int argc, char *argv[]) +{ + bool ok = false; + + mongoc_init (); + + bson_error_t error; + mongoc_client_t *client = mongoc_client_new ("mongodb://localhost:27017"); + mongoc_bulkwriteopts_t *bwo = mongoc_bulkwriteopts_new (); + mongoc_bulkwriteopts_set_verboseresults (bwo, true); + mongoc_bulkwrite_t *bw = mongoc_client_bulkwrite_new (client); + + // Insert a document to `db.coll1` + { + bson_t *doc = BCON_NEW ("foo", "bar"); + if (!mongoc_bulkwrite_append_insertone (bw, "db.coll1", doc, NULL, &error)) { + HANDLE_ERROR ("Error appending insert one: %s", error.message); + } + bson_destroy (doc); + } + // Insert a document to `db.coll2` + { + bson_t *doc = BCON_NEW ("foo", "baz"); + if (!mongoc_bulkwrite_append_insertone (bw, "db.coll2", doc, NULL, &error)) { + HANDLE_ERROR ("Error appending insert one: %s", error.message); + } + bson_destroy (doc); + } + + mongoc_bulkwritereturn_t bwr = mongoc_bulkwrite_execute (bw, bwo); + + // Print results. + { + BSON_ASSERT (bwr.res); // Has results. NULL only returned for unacknowledged writes. + printf ("Insert count : %" PRId64 "\n", mongoc_bulkwriteresult_insertedcount (bwr.res)); + const bson_t *ir = mongoc_bulkwriteresult_insertresults (bwr.res); + BSON_ASSERT (ir); // Has verbose results. NULL only returned if verbose results not requested. + char *ir_str = bson_as_relaxed_extended_json (ir, NULL); + printf ("Insert results : %s\n", ir_str); + bson_free (ir_str); + } + + // Print all error information. To observe: try setting the `_id` fields to cause a duplicate key error. + if (bwr.exc) { + const char *msg = "(none)"; + if (mongoc_bulkwriteexception_error (bwr.exc, &error)) { + msg = error.message; + } + const bson_t *we = mongoc_bulkwriteexception_writeerrors (bwr.exc); + char *we_str = bson_as_relaxed_extended_json (we, NULL); + const bson_t *wce = mongoc_bulkwriteexception_writeconcernerrors (bwr.exc); + char *wce_str = bson_as_relaxed_extended_json (wce, NULL); + const bson_t *er = mongoc_bulkwriteexception_errorreply (bwr.exc); + char *er_str = bson_as_relaxed_extended_json (er, NULL); + printf ("Top-level error : %s\n", msg); + printf ("Write errors : %s\n", we_str); + printf ("Write concern errors : %s\n", wce_str); + printf ("Error reply : %s\n", er_str); + bson_free (er_str); + bson_free (wce_str); + bson_free (we_str); + } + + mongoc_bulkwriteresult_destroy (bwr.res); + mongoc_bulkwriteexception_destroy (bwr.exc); + mongoc_bulkwrite_destroy (bw); + + ok = true; +fail: + mongoc_client_destroy (client); + mongoc_bulkwriteopts_destroy (bwo); + mongoc_cleanup (); + return ok ? EXIT_SUCCESS : EXIT_FAILURE; +} diff --git a/src/libmongoc/src/mongoc/mcd-nsinfo.c b/src/libmongoc/src/mongoc/mcd-nsinfo.c index 14730220a65..bceb44645dc 100644 --- a/src/libmongoc/src/mongoc/mcd-nsinfo.c +++ b/src/libmongoc/src/mongoc/mcd-nsinfo.c @@ -63,7 +63,7 @@ mcd_nsinfo_append (mcd_nsinfo_t *self, const char *ns, bson_error_t *error) { BSON_ASSERT_PARAM (self); BSON_ASSERT_PARAM (ns); - BSON_ASSERT (error || true); + BSON_OPTIONAL_PARAM (error); const int32_t ns_index = self->count; if (self->count == INT32_MAX) { diff --git a/src/libmongoc/src/mongoc/mcd-rpc.c b/src/libmongoc/src/mongoc/mcd-rpc.c index 863832db997..a5ebbf0b308 100644 --- a/src/libmongoc/src/mongoc/mcd-rpc.c +++ b/src/libmongoc/src/mongoc/mcd-rpc.c @@ -270,7 +270,7 @@ _consume_bson_objects (const uint8_t **ptr, size_t *remaining_bytes, int32_t *nu { BSON_ASSERT_PARAM (ptr); BSON_ASSERT_PARAM (remaining_bytes); - BSON_ASSERT (num_parsed || true); + BSON_OPTIONAL_PARAM (num_parsed); int32_t count = 0; @@ -814,7 +814,7 @@ mcd_rpc_message * mcd_rpc_message_from_data (const void *data, size_t length, const void **data_end) { BSON_ASSERT_PARAM (data); - BSON_ASSERT (data_end || true); + BSON_OPTIONAL_PARAM (data_end); mcd_rpc_message *rpc = bson_malloc (sizeof (mcd_rpc_message)); mcd_rpc_message *ret = NULL; @@ -838,7 +838,7 @@ mcd_rpc_message_from_data_in_place (mcd_rpc_message *rpc, const void *data, size { ASSERT_MCD_RPC_ACCESSOR_PRECONDITIONS; BSON_ASSERT_PARAM (data); - BSON_ASSERT (data_end || true); + BSON_OPTIONAL_PARAM (data_end); bool ret = false; diff --git a/src/libmongoc/src/mongoc/mongoc-bulkwrite.c b/src/libmongoc/src/mongoc/mongoc-bulkwrite.c new file mode 100644 index 00000000000..766d170a574 --- /dev/null +++ b/src/libmongoc/src/mongoc/mongoc-bulkwrite.c @@ -0,0 +1,1860 @@ +/* + * Copyright 2024-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. + */ + +#include + +#include +#include // MC_ENABLE_CONVERSION_WARNING_BEGIN +#include +#include +#include +#include +#include +#include +#include // _mongoc_write_error_handle_labels +#include +#include // _mongoc_iter_document_as_bson +#include + +MC_ENABLE_CONVERSION_WARNING_BEGIN + +struct _mongoc_bulkwriteopts_t { + mongoc_optional_t ordered; + mongoc_optional_t bypassdocumentvalidation; + bson_t *let; + mongoc_write_concern_t *writeconcern; + mongoc_optional_t verboseresults; + bson_t *comment; + bson_t *extra; + uint32_t serverid; +}; + +// `set_bson_opt` sets `*dst` by copying `src`. If `src` is NULL, `dst` is cleared. +static void +set_bson_opt (bson_t **dst, const bson_t *src) +{ + BSON_ASSERT_PARAM (dst); + bson_destroy (*dst); + *dst = NULL; + if (src) { + *dst = bson_copy (src); + } +} + +static void +set_hint_opt (bson_value_t *dst, const bson_value_t *src) +{ + BSON_ASSERT_PARAM (dst); + bson_value_destroy (dst); + *dst = (bson_value_t){0}; + if (src) { + bson_value_copy (src, dst); + } +} + +mongoc_bulkwriteopts_t * +mongoc_bulkwriteopts_new (void) +{ + return bson_malloc0 (sizeof (mongoc_bulkwriteopts_t)); +} +void +mongoc_bulkwriteopts_set_ordered (mongoc_bulkwriteopts_t *self, bool ordered) +{ + BSON_ASSERT_PARAM (self); + mongoc_optional_set_value (&self->ordered, ordered); +} +void +mongoc_bulkwriteopts_set_bypassdocumentvalidation (mongoc_bulkwriteopts_t *self, bool bypassdocumentvalidation) +{ + BSON_ASSERT_PARAM (self); + mongoc_optional_set_value (&self->bypassdocumentvalidation, bypassdocumentvalidation); +} +void +mongoc_bulkwriteopts_set_let (mongoc_bulkwriteopts_t *self, const bson_t *let) +{ + BSON_ASSERT_PARAM (self); + BSON_OPTIONAL_PARAM (let); + set_bson_opt (&self->let, let); +} +void +mongoc_bulkwriteopts_set_writeconcern (mongoc_bulkwriteopts_t *self, const mongoc_write_concern_t *writeconcern) +{ + BSON_ASSERT_PARAM (self); + BSON_OPTIONAL_PARAM (writeconcern); + mongoc_write_concern_destroy (self->writeconcern); + self->writeconcern = mongoc_write_concern_copy (writeconcern); +} +void +mongoc_bulkwriteopts_set_verboseresults (mongoc_bulkwriteopts_t *self, bool verboseresults) +{ + BSON_ASSERT_PARAM (self); + mongoc_optional_set_value (&self->verboseresults, verboseresults); +} +void +mongoc_bulkwriteopts_set_comment (mongoc_bulkwriteopts_t *self, const bson_t *comment) +{ + BSON_ASSERT_PARAM (self); + BSON_OPTIONAL_PARAM (comment); + set_bson_opt (&self->comment, comment); +} +void +mongoc_bulkwriteopts_set_extra (mongoc_bulkwriteopts_t *self, const bson_t *extra) +{ + BSON_ASSERT_PARAM (self); + BSON_OPTIONAL_PARAM (extra); + set_bson_opt (&self->extra, extra); +} +void +mongoc_bulkwriteopts_set_serverid (mongoc_bulkwriteopts_t *self, uint32_t serverid) +{ + BSON_ASSERT_PARAM (self); + self->serverid = serverid; +} +void +mongoc_bulkwriteopts_destroy (mongoc_bulkwriteopts_t *self) +{ + if (!self) { + return; + } + bson_destroy (self->extra); + bson_destroy (self->comment); + mongoc_write_concern_destroy (self->writeconcern); + bson_destroy (self->let); + bson_free (self); +} + +typedef enum { MODEL_OP_INSERT, MODEL_OP_UPDATE, MODEL_OP_DELETE } model_op_t; +typedef struct { + model_op_t op; + bson_iter_t id_iter; + char *ns; +} modeldata_t; + +struct _mongoc_bulkwrite_t { + mongoc_client_t *client; + // `executed` is set to true once `mongoc_bulkwrite_execute` is called. + // `mongoc_bulkwrite_t` may not be executed more than once. + bool executed; + // `ops` is a document sequence. + mongoc_buffer_t ops; + size_t n_ops; + // `arrayof_modeldata` is an array of `modeldata_t` sized to the number of models. It stores per-model data. + mongoc_array_t arrayof_modeldata; + // `max_insert_len` tracks the maximum length of any document to-be inserted. + uint32_t max_insert_len; + // `has_multi_write` is true if there are any multi-document update or delete operations. Multi-document + // writes are ineligible for retryable writes. + bool has_multi_write; + int64_t operation_id; + mongoc_client_session_t *session; +}; + + +// `mongoc_client_bulkwrite_new` creates a new bulk write operation. +mongoc_bulkwrite_t * +mongoc_client_bulkwrite_new (mongoc_client_t *self) +{ + BSON_ASSERT_PARAM (self); + mongoc_bulkwrite_t *bw = bson_malloc0 (sizeof (mongoc_bulkwrite_t)); + bw->client = self; + _mongoc_buffer_init (&bw->ops, NULL, 0, NULL, NULL); + _mongoc_array_init (&bw->arrayof_modeldata, sizeof (modeldata_t)); + bw->operation_id = ++self->cluster.operation_id; + return bw; +} + +void +mongoc_bulkwrite_destroy (mongoc_bulkwrite_t *self) +{ + if (!self) { + return; + } + for (size_t i = 0; i < self->arrayof_modeldata.len; i++) { + modeldata_t md = _mongoc_array_index (&self->arrayof_modeldata, modeldata_t, i); + bson_free (md.ns); + } + _mongoc_array_destroy (&self->arrayof_modeldata); + _mongoc_buffer_destroy (&self->ops); + bson_free (self); +} + +struct _mongoc_bulkwrite_insertoneopts_t { + // No fields yet. Include an unused placeholder to prevent compile errors due to an empty struct. + int unused; +}; + +mongoc_bulkwrite_insertoneopts_t * +mongoc_bulkwrite_insertoneopts_new (void) +{ + return bson_malloc0 (sizeof (mongoc_bulkwrite_insertoneopts_t)); +} + +void +mongoc_bulkwrite_insertoneopts_destroy (mongoc_bulkwrite_insertoneopts_t *self) +{ + if (!self) { + return; + } + bson_free (self); +} + +#define ERROR_IF_EXECUTED \ + if (self->executed) { \ + bson_set_error (error, MONGOC_ERROR_COMMAND, MONGOC_ERROR_COMMAND_INVALID_ARG, "bulk write already executed"); \ + return false; \ + } else \ + (void) 0 + +bool +mongoc_bulkwrite_append_insertone (mongoc_bulkwrite_t *self, + const char *ns, + const bson_t *document, + const mongoc_bulkwrite_insertoneopts_t *opts, // may be NULL + bson_error_t *error) +{ + BSON_ASSERT_PARAM (self); + BSON_ASSERT_PARAM (ns); + BSON_ASSERT_PARAM (document); + BSON_ASSERT (document->len >= 5); + BSON_OPTIONAL_PARAM (opts); + BSON_OPTIONAL_PARAM (error); + + ERROR_IF_EXECUTED; + + bson_t op = BSON_INITIALIZER; + BSON_ASSERT (BSON_APPEND_INT32 (&op, "insert", -1)); // Append -1 as a placeholder. Will be overwritten later. + + // `persisted_id_offset` is the byte offset the `_id` in `op`. + uint32_t persisted_id_offset = 0; + { + // Refer: bsonspec.org for BSON format. + persisted_id_offset += 4; // Document length. + persisted_id_offset += 1; // BSON type for int32. + persisted_id_offset += strlen ("insert") + 1; // Key + 1 for NULL byte. + persisted_id_offset += 4; // int32 value. + persisted_id_offset += 1; // BSON type for document. + persisted_id_offset += strlen ("document") + 1; // Key + 1 for NULL byte. + } + + // If `document` does not contain `_id`, add one in the beginning. + bson_iter_t existing_id_iter; + if (!bson_iter_init_find (&existing_id_iter, document, "_id")) { + bson_t tmp = BSON_INITIALIZER; + bson_oid_t oid; + bson_oid_init (&oid, NULL); + BSON_ASSERT (BSON_APPEND_OID (&tmp, "_id", &oid)); + BSON_ASSERT (bson_concat (&tmp, document)); + BSON_ASSERT (BSON_APPEND_DOCUMENT (&op, "document", &tmp)); + self->max_insert_len = BSON_MAX (self->max_insert_len, tmp.len); + bson_destroy (&tmp); + persisted_id_offset += 4; // Document length. + } else { + BSON_ASSERT (BSON_APPEND_DOCUMENT (&op, "document", document)); + self->max_insert_len = BSON_MAX (self->max_insert_len, document->len); + // `existing_id_offset` is offset of `_id` in the input `document`. + const uint32_t existing_id_offset = bson_iter_offset (&existing_id_iter); + BSON_ASSERT (persisted_id_offset <= UINT32_MAX - existing_id_offset); + persisted_id_offset += existing_id_offset; + } + + BSON_ASSERT (_mongoc_buffer_append (&self->ops, bson_get_data (&op), op.len)); + + // Store an iterator to the document's `_id` in the persisted payload: + bson_iter_t persisted_id_iter; + { + BSON_ASSERT (bson_in_range_size_t_unsigned (op.len)); + size_t start = self->ops.len - (size_t) op.len; + BSON_ASSERT (bson_iter_init_from_data_at_offset ( + &persisted_id_iter, self->ops.data + start, (size_t) op.len, persisted_id_offset, strlen ("_id"))); + } + + self->n_ops++; + modeldata_t md = {.op = MODEL_OP_INSERT, .id_iter = persisted_id_iter, .ns = bson_strdup (ns)}; + _mongoc_array_append_val (&self->arrayof_modeldata, md); + bson_destroy (&op); + return true; +} + + +static bool +validate_update (const bson_t *update, bool *is_pipeline, bson_error_t *error) +{ + BSON_ASSERT_PARAM (update); + BSON_ASSERT_PARAM (is_pipeline); + BSON_OPTIONAL_PARAM (error); + + bson_iter_t iter; + *is_pipeline = _mongoc_document_is_pipeline (update); + if (*is_pipeline) { + return true; + } + + BSON_ASSERT (bson_iter_init (&iter, update)); + + if (bson_iter_next (&iter)) { + const char *key = bson_iter_key (&iter); + if (key[0] != '$') { + bson_set_error (error, + MONGOC_ERROR_COMMAND, + MONGOC_ERROR_COMMAND_INVALID_ARG, + "Invalid key '%s': update only works with $ operators" + " and pipelines", + key); + + return false; + } + } + return true; +} + +struct _mongoc_bulkwrite_updateoneopts_t { + bson_t *arrayfilters; + bson_t *collation; + bson_value_t hint; + mongoc_optional_t upsert; +}; + +mongoc_bulkwrite_updateoneopts_t * +mongoc_bulkwrite_updateoneopts_new (void) +{ + return bson_malloc0 (sizeof (mongoc_bulkwrite_updateoneopts_t)); +} +void +mongoc_bulkwrite_updateoneopts_set_arrayfilters (mongoc_bulkwrite_updateoneopts_t *self, const bson_t *arrayfilters) +{ + BSON_ASSERT_PARAM (self); + BSON_OPTIONAL_PARAM (arrayfilters); + set_bson_opt (&self->arrayfilters, arrayfilters); +} +void +mongoc_bulkwrite_updateoneopts_set_collation (mongoc_bulkwrite_updateoneopts_t *self, const bson_t *collation) +{ + BSON_ASSERT_PARAM (self); + BSON_OPTIONAL_PARAM (collation); + set_bson_opt (&self->collation, collation); +} +void +mongoc_bulkwrite_updateoneopts_set_hint (mongoc_bulkwrite_updateoneopts_t *self, const bson_value_t *hint) +{ + BSON_ASSERT_PARAM (self); + BSON_OPTIONAL_PARAM (hint); + set_hint_opt (&self->hint, hint); +} +void +mongoc_bulkwrite_updateoneopts_set_upsert (mongoc_bulkwrite_updateoneopts_t *self, bool upsert) +{ + BSON_ASSERT_PARAM (self); + mongoc_optional_set_value (&self->upsert, upsert); +} +void +mongoc_bulkwrite_updateoneopts_destroy (mongoc_bulkwrite_updateoneopts_t *self) +{ + if (!self) { + return; + } + bson_destroy (self->arrayfilters); + bson_destroy (self->collation); + bson_value_destroy (&self->hint); + bson_free (self); +} + +bool +mongoc_bulkwrite_append_updateone (mongoc_bulkwrite_t *self, + const char *ns, + const bson_t *filter, + const bson_t *update, + const mongoc_bulkwrite_updateoneopts_t *opts /* May be NULL */, + bson_error_t *error) +{ + BSON_ASSERT_PARAM (self); + BSON_ASSERT_PARAM (ns); + BSON_ASSERT_PARAM (filter); + BSON_ASSERT (filter->len >= 5); + BSON_ASSERT_PARAM (update); + BSON_ASSERT (update->len >= 5); + BSON_OPTIONAL_PARAM (opts); + BSON_OPTIONAL_PARAM (error); + + ERROR_IF_EXECUTED; + + mongoc_bulkwrite_updateoneopts_t defaults = {0}; + if (!opts) { + opts = &defaults; + } + + bool is_pipeline = false; + if (!validate_update (update, &is_pipeline, error)) { + return false; + } + + bson_t op = BSON_INITIALIZER; + BSON_ASSERT (BSON_APPEND_INT32 (&op, "update", -1)); // Append -1 as a placeholder. Will be overwritten later. + BSON_ASSERT (BSON_APPEND_DOCUMENT (&op, "filter", filter)); + if (is_pipeline) { + BSON_ASSERT (BSON_APPEND_ARRAY (&op, "updateMods", update)); + } else { + BSON_ASSERT (BSON_APPEND_DOCUMENT (&op, "updateMods", update)); + } + BSON_ASSERT (BSON_APPEND_BOOL (&op, "multi", false)); + if (opts->arrayfilters) { + BSON_ASSERT (BSON_APPEND_ARRAY (&op, "arrayFilters", opts->arrayfilters)); + } + if (opts->collation) { + BSON_ASSERT (BSON_APPEND_DOCUMENT (&op, "collation", opts->collation)); + } + if (opts->hint.value_type != BSON_TYPE_EOD) { + BSON_ASSERT (BSON_APPEND_VALUE (&op, "hint", &opts->hint)); + } + if (mongoc_optional_is_set (&opts->upsert)) { + BSON_ASSERT (BSON_APPEND_BOOL (&op, "upsert", mongoc_optional_value (&opts->upsert))); + } + + BSON_ASSERT (_mongoc_buffer_append (&self->ops, bson_get_data (&op), op.len)); + + self->n_ops++; + modeldata_t md = {.op = MODEL_OP_UPDATE, .ns = bson_strdup (ns)}; + _mongoc_array_append_val (&self->arrayof_modeldata, md); + bson_destroy (&op); + return true; +} + +struct _mongoc_bulkwrite_replaceoneopts_t { + bson_t *collation; + bson_value_t hint; + mongoc_optional_t upsert; +}; + +mongoc_bulkwrite_replaceoneopts_t * +mongoc_bulkwrite_replaceoneopts_new (void) +{ + return bson_malloc0 (sizeof (mongoc_bulkwrite_replaceoneopts_t)); +} +void +mongoc_bulkwrite_replaceoneopts_set_collation (mongoc_bulkwrite_replaceoneopts_t *self, const bson_t *collation) +{ + BSON_ASSERT_PARAM (self); + BSON_OPTIONAL_PARAM (collation); + set_bson_opt (&self->collation, collation); +} +void +mongoc_bulkwrite_replaceoneopts_set_hint (mongoc_bulkwrite_replaceoneopts_t *self, const bson_value_t *hint) +{ + BSON_ASSERT_PARAM (self); + BSON_OPTIONAL_PARAM (hint); + set_hint_opt (&self->hint, hint); +} +void +mongoc_bulkwrite_replaceoneopts_set_upsert (mongoc_bulkwrite_replaceoneopts_t *self, bool upsert) +{ + BSON_ASSERT_PARAM (self); + mongoc_optional_set_value (&self->upsert, upsert); +} +void +mongoc_bulkwrite_replaceoneopts_destroy (mongoc_bulkwrite_replaceoneopts_t *self) +{ + if (!self) { + return; + } + bson_destroy (self->collation); + bson_value_destroy (&self->hint); + bson_free (self); +} + +bool +validate_replace (const bson_t *doc, bson_error_t *error) +{ + BSON_OPTIONAL_PARAM (doc); + BSON_OPTIONAL_PARAM (error); + + bson_iter_t iter; + + BSON_ASSERT (bson_iter_init (&iter, doc)); + + if (bson_iter_next (&iter)) { + const char *key = bson_iter_key (&iter); + if (key[0] == '$') { + bson_set_error (error, + MONGOC_ERROR_COMMAND, + MONGOC_ERROR_COMMAND_INVALID_ARG, + "Invalid key '%s': replace prohibits $ operators", + key); + + return false; + } + } + + return true; +} + +bool +mongoc_bulkwrite_append_replaceone (mongoc_bulkwrite_t *self, + const char *ns, + const bson_t *filter, + const bson_t *replacement, + const mongoc_bulkwrite_replaceoneopts_t *opts /* May be NULL */, + bson_error_t *error) +{ + BSON_ASSERT_PARAM (self); + BSON_ASSERT_PARAM (ns); + BSON_ASSERT_PARAM (filter); + BSON_ASSERT (filter->len >= 5); + BSON_ASSERT_PARAM (replacement); + BSON_ASSERT (replacement->len >= 5); + BSON_OPTIONAL_PARAM (opts); + BSON_OPTIONAL_PARAM (error); + + ERROR_IF_EXECUTED; + + mongoc_bulkwrite_replaceoneopts_t defaults = {0}; + if (!opts) { + opts = &defaults; + } + + if (!validate_replace (replacement, error)) { + return false; + } + + bson_t op = BSON_INITIALIZER; + BSON_ASSERT (BSON_APPEND_INT32 (&op, "update", -1)); // Append -1 as a placeholder. Will be overwritten later. + BSON_ASSERT (BSON_APPEND_DOCUMENT (&op, "filter", filter)); + BSON_ASSERT (BSON_APPEND_DOCUMENT (&op, "updateMods", replacement)); + BSON_ASSERT (BSON_APPEND_BOOL (&op, "multi", false)); + if (opts->collation) { + BSON_ASSERT (BSON_APPEND_DOCUMENT (&op, "collation", opts->collation)); + } + if (opts->hint.value_type != BSON_TYPE_EOD) { + BSON_ASSERT (BSON_APPEND_VALUE (&op, "hint", &opts->hint)); + } + if (mongoc_optional_is_set (&opts->upsert)) { + BSON_ASSERT (BSON_APPEND_BOOL (&op, "upsert", mongoc_optional_value (&opts->upsert))); + } + + BSON_ASSERT (_mongoc_buffer_append (&self->ops, bson_get_data (&op), op.len)); + + self->n_ops++; + self->max_insert_len = BSON_MAX (self->max_insert_len, replacement->len); + modeldata_t md = {.op = MODEL_OP_UPDATE, .ns = bson_strdup (ns)}; + _mongoc_array_append_val (&self->arrayof_modeldata, md); + bson_destroy (&op); + return true; +} + +struct _mongoc_bulkwrite_updatemanyopts_t { + bson_t *arrayfilters; + bson_t *collation; + bson_value_t hint; + mongoc_optional_t upsert; +}; + +mongoc_bulkwrite_updatemanyopts_t * +mongoc_bulkwrite_updatemanyopts_new (void) +{ + return bson_malloc0 (sizeof (mongoc_bulkwrite_updatemanyopts_t)); +} +void +mongoc_bulkwrite_updatemanyopts_set_arrayfilters (mongoc_bulkwrite_updatemanyopts_t *self, const bson_t *arrayfilters) +{ + BSON_ASSERT_PARAM (self); + BSON_OPTIONAL_PARAM (arrayfilters); + set_bson_opt (&self->arrayfilters, arrayfilters); +} +void +mongoc_bulkwrite_updatemanyopts_set_collation (mongoc_bulkwrite_updatemanyopts_t *self, const bson_t *collation) +{ + BSON_ASSERT_PARAM (self); + BSON_OPTIONAL_PARAM (collation); + set_bson_opt (&self->collation, collation); +} +void +mongoc_bulkwrite_updatemanyopts_set_hint (mongoc_bulkwrite_updatemanyopts_t *self, const bson_value_t *hint) +{ + BSON_ASSERT_PARAM (self); + BSON_OPTIONAL_PARAM (hint); + set_hint_opt (&self->hint, hint); +} +void +mongoc_bulkwrite_updatemanyopts_set_upsert (mongoc_bulkwrite_updatemanyopts_t *self, bool upsert) +{ + BSON_ASSERT_PARAM (self); + mongoc_optional_set_value (&self->upsert, upsert); +} +void +mongoc_bulkwrite_updatemanyopts_destroy (mongoc_bulkwrite_updatemanyopts_t *self) +{ + if (!self) { + return; + } + bson_destroy (self->arrayfilters); + bson_destroy (self->collation); + bson_value_destroy (&self->hint); + bson_free (self); +} + +bool +mongoc_bulkwrite_append_updatemany (mongoc_bulkwrite_t *self, + const char *ns, + const bson_t *filter, + const bson_t *update, + const mongoc_bulkwrite_updatemanyopts_t *opts /* May be NULL */, + bson_error_t *error) +{ + BSON_ASSERT_PARAM (self); + BSON_ASSERT_PARAM (ns); + BSON_ASSERT_PARAM (filter); + BSON_ASSERT (filter->len >= 5); + BSON_ASSERT_PARAM (update); + BSON_ASSERT (update->len >= 5); + BSON_OPTIONAL_PARAM (opts); + BSON_OPTIONAL_PARAM (error); + + ERROR_IF_EXECUTED; + + mongoc_bulkwrite_updatemanyopts_t defaults = {0}; + if (!opts) { + opts = &defaults; + } + + bool is_pipeline = false; + if (!validate_update (update, &is_pipeline, error)) { + return false; + } + + bson_t op = BSON_INITIALIZER; + BSON_ASSERT (BSON_APPEND_INT32 (&op, "update", -1)); // Append -1 as a placeholder. Will be overwritten later. + BSON_ASSERT (BSON_APPEND_DOCUMENT (&op, "filter", filter)); + if (is_pipeline) { + BSON_ASSERT (BSON_APPEND_ARRAY (&op, "updateMods", update)); + } else { + BSON_ASSERT (BSON_APPEND_DOCUMENT (&op, "updateMods", update)); + } + BSON_ASSERT (BSON_APPEND_BOOL (&op, "multi", true)); + if (opts->arrayfilters) { + BSON_ASSERT (BSON_APPEND_ARRAY (&op, "arrayFilters", opts->arrayfilters)); + } + if (opts->collation) { + BSON_ASSERT (BSON_APPEND_DOCUMENT (&op, "collation", opts->collation)); + } + if (opts->hint.value_type != BSON_TYPE_EOD) { + BSON_ASSERT (BSON_APPEND_VALUE (&op, "hint", &opts->hint)); + } + if (mongoc_optional_is_set (&opts->upsert)) { + BSON_ASSERT (BSON_APPEND_BOOL (&op, "upsert", mongoc_optional_value (&opts->upsert))); + } + + BSON_ASSERT (_mongoc_buffer_append (&self->ops, bson_get_data (&op), op.len)); + + self->has_multi_write = true; + self->n_ops++; + modeldata_t md = {.op = MODEL_OP_UPDATE, .ns = bson_strdup (ns)}; + _mongoc_array_append_val (&self->arrayof_modeldata, md); + bson_destroy (&op); + return true; +} + +struct _mongoc_bulkwrite_deleteoneopts_t { + bson_t *collation; + bson_value_t hint; +}; + +mongoc_bulkwrite_deleteoneopts_t * +mongoc_bulkwrite_deleteoneopts_new (void) +{ + return bson_malloc0 (sizeof (mongoc_bulkwrite_deleteoneopts_t)); +} +void +mongoc_bulkwrite_deleteoneopts_set_collation (mongoc_bulkwrite_deleteoneopts_t *self, const bson_t *collation) +{ + BSON_ASSERT_PARAM (self); + BSON_OPTIONAL_PARAM (collation); + set_bson_opt (&self->collation, collation); +} +void +mongoc_bulkwrite_deleteoneopts_set_hint (mongoc_bulkwrite_deleteoneopts_t *self, const bson_value_t *hint) +{ + BSON_ASSERT_PARAM (self); + BSON_OPTIONAL_PARAM (hint); + set_hint_opt (&self->hint, hint); +} +void +mongoc_bulkwrite_deleteoneopts_destroy (mongoc_bulkwrite_deleteoneopts_t *self) +{ + if (!self) { + return; + } + bson_value_destroy (&self->hint); + bson_destroy (self->collation); + bson_free (self); +} + +bool +mongoc_bulkwrite_append_deleteone (mongoc_bulkwrite_t *self, + const char *ns, + const bson_t *filter, + const mongoc_bulkwrite_deleteoneopts_t *opts /* May be NULL */, + bson_error_t *error) +{ + BSON_ASSERT_PARAM (self); + BSON_ASSERT_PARAM (ns); + BSON_ASSERT_PARAM (filter); + BSON_ASSERT (filter->len >= 5); + BSON_OPTIONAL_PARAM (opts); + BSON_OPTIONAL_PARAM (error); + + ERROR_IF_EXECUTED; + + mongoc_bulkwrite_deleteoneopts_t defaults = {0}; + if (!opts) { + opts = &defaults; + } + + bson_t op = BSON_INITIALIZER; + BSON_ASSERT (BSON_APPEND_INT32 (&op, "delete", -1)); // Append -1 as a placeholder. Will be overwritten later. + BSON_ASSERT (BSON_APPEND_DOCUMENT (&op, "filter", filter)); + BSON_ASSERT (BSON_APPEND_BOOL (&op, "multi", false)); + if (opts->collation) { + BSON_ASSERT (BSON_APPEND_DOCUMENT (&op, "collation", opts->collation)); + } + if (opts->hint.value_type != BSON_TYPE_EOD) { + BSON_ASSERT (BSON_APPEND_VALUE (&op, "hint", &opts->hint)); + } + + BSON_ASSERT (_mongoc_buffer_append (&self->ops, bson_get_data (&op), op.len)); + + self->n_ops++; + modeldata_t md = {.op = MODEL_OP_DELETE, .ns = bson_strdup (ns)}; + _mongoc_array_append_val (&self->arrayof_modeldata, md); + bson_destroy (&op); + return true; +} + +struct _mongoc_bulkwrite_deletemanyopts_t { + bson_t *collation; + bson_value_t hint; +}; + +mongoc_bulkwrite_deletemanyopts_t * +mongoc_bulkwrite_deletemanyopts_new (void) +{ + return bson_malloc0 (sizeof (mongoc_bulkwrite_deletemanyopts_t)); +} +void +mongoc_bulkwrite_deletemanyopts_set_collation (mongoc_bulkwrite_deletemanyopts_t *self, const bson_t *collation) +{ + BSON_ASSERT_PARAM (self); + set_bson_opt (&self->collation, collation); +} +void +mongoc_bulkwrite_deletemanyopts_set_hint (mongoc_bulkwrite_deletemanyopts_t *self, const bson_value_t *hint) +{ + BSON_ASSERT_PARAM (self); + set_hint_opt (&self->hint, hint); +} +void +mongoc_bulkwrite_deletemanyopts_destroy (mongoc_bulkwrite_deletemanyopts_t *self) +{ + if (!self) { + return; + } + bson_value_destroy (&self->hint); + bson_destroy (self->collation); + bson_free (self); +} + +bool +mongoc_bulkwrite_append_deletemany (mongoc_bulkwrite_t *self, + const char *ns, + const bson_t *filter, + const mongoc_bulkwrite_deletemanyopts_t *opts /* May be NULL */, + bson_error_t *error) +{ + BSON_ASSERT_PARAM (self); + BSON_ASSERT_PARAM (ns); + BSON_ASSERT_PARAM (filter); + BSON_ASSERT (filter->len >= 5); + BSON_OPTIONAL_PARAM (opts); + BSON_OPTIONAL_PARAM (error); + + ERROR_IF_EXECUTED; + + mongoc_bulkwrite_deletemanyopts_t defaults = {0}; + if (!opts) { + opts = &defaults; + } + + bson_t op = BSON_INITIALIZER; + BSON_ASSERT (BSON_APPEND_INT32 (&op, "delete", -1)); // Append -1 as a placeholder. Will be overwritten later. + BSON_ASSERT (BSON_APPEND_DOCUMENT (&op, "filter", filter)); + BSON_ASSERT (BSON_APPEND_BOOL (&op, "multi", true)); + if (opts->collation) { + BSON_ASSERT (BSON_APPEND_DOCUMENT (&op, "collation", opts->collation)); + } + if (opts->hint.value_type != BSON_TYPE_EOD) { + BSON_ASSERT (BSON_APPEND_VALUE (&op, "hint", &opts->hint)); + } + + BSON_ASSERT (_mongoc_buffer_append (&self->ops, bson_get_data (&op), op.len)); + + self->has_multi_write = true; + self->n_ops++; + modeldata_t md = {.op = MODEL_OP_DELETE, .ns = bson_strdup (ns)}; + _mongoc_array_append_val (&self->arrayof_modeldata, md); + bson_destroy (&op); + return true; +} + + +struct _mongoc_bulkwriteresult_t { + int64_t insertedcount; + int64_t upsertedcount; + int64_t matchedcount; + int64_t modifiedcount; + int64_t deletedcount; + uint32_t serverid; + bson_t insertresults; + bson_t updateresults; + bson_t deleteresults; + bool verboseresults; +}; + +int64_t +mongoc_bulkwriteresult_insertedcount (const mongoc_bulkwriteresult_t *self) +{ + BSON_ASSERT_PARAM (self); + return self->insertedcount; +} + +int64_t +mongoc_bulkwriteresult_upsertedcount (const mongoc_bulkwriteresult_t *self) +{ + BSON_ASSERT_PARAM (self); + return self->upsertedcount; +} + +int64_t +mongoc_bulkwriteresult_matchedcount (const mongoc_bulkwriteresult_t *self) +{ + BSON_ASSERT_PARAM (self); + return self->matchedcount; +} + +int64_t +mongoc_bulkwriteresult_modifiedcount (const mongoc_bulkwriteresult_t *self) +{ + BSON_ASSERT_PARAM (self); + return self->modifiedcount; +} + +int64_t +mongoc_bulkwriteresult_deletedcount (const mongoc_bulkwriteresult_t *self) +{ + BSON_ASSERT_PARAM (self); + return self->deletedcount; +} + +const bson_t * +mongoc_bulkwriteresult_insertresults (const mongoc_bulkwriteresult_t *self) +{ + BSON_ASSERT_PARAM (self); + if (!self->verboseresults) { + return NULL; + } + return &self->insertresults; +} + +const bson_t * +mongoc_bulkwriteresult_updateresults (const mongoc_bulkwriteresult_t *self) +{ + BSON_ASSERT_PARAM (self); + if (!self->verboseresults) { + return NULL; + } + return &self->updateresults; +} + +const bson_t * +mongoc_bulkwriteresult_deleteresults (const mongoc_bulkwriteresult_t *self) +{ + BSON_ASSERT_PARAM (self); + if (!self->verboseresults) { + return NULL; + } + return &self->deleteresults; +} + +uint32_t +mongoc_bulkwriteresult_serverid (const mongoc_bulkwriteresult_t *self) +{ + BSON_ASSERT_PARAM (self); + return self->serverid; +} + +void +mongoc_bulkwriteresult_destroy (mongoc_bulkwriteresult_t *self) +{ + if (!self) { + return; + } + bson_destroy (&self->deleteresults); + bson_destroy (&self->updateresults); + bson_destroy (&self->insertresults); + bson_free (self); +} + +static mongoc_bulkwriteresult_t * +_bulkwriteresult_new (void) +{ + mongoc_bulkwriteresult_t *self = bson_malloc0 (sizeof (*self)); + bson_init (&self->insertresults); + bson_init (&self->updateresults); + bson_init (&self->deleteresults); + return self; +} + +static void +_bulkwriteresult_set_updateresult ( + mongoc_bulkwriteresult_t *self, int64_t n, int64_t nModified, const bson_value_t *upserted_id, size_t models_idx) +{ + BSON_ASSERT_PARAM (self); + BSON_OPTIONAL_PARAM (upserted_id); + + bson_t updateresult; + { + char *key = bson_strdup_printf ("%zu", models_idx); + BSON_APPEND_DOCUMENT_BEGIN (&self->updateresults, key, &updateresult); + bson_free (key); + } + + BSON_ASSERT (BSON_APPEND_INT64 (&updateresult, "matchedCount", n)); + BSON_ASSERT (BSON_APPEND_INT64 (&updateresult, "modifiedCount", nModified)); + if (upserted_id) { + BSON_ASSERT (BSON_APPEND_VALUE (&updateresult, "upsertedId", upserted_id)); + } + BSON_ASSERT (bson_append_document_end (&self->updateresults, &updateresult)); +} + +static void +_bulkwriteresult_set_deleteresult (mongoc_bulkwriteresult_t *self, int64_t n, size_t models_idx) +{ + BSON_ASSERT_PARAM (self); + + bson_t deleteresult; + { + char *key = bson_strdup_printf ("%zu", models_idx); + BSON_APPEND_DOCUMENT_BEGIN (&self->deleteresults, key, &deleteresult); + bson_free (key); + } + + BSON_ASSERT (BSON_APPEND_INT64 (&deleteresult, "deletedCount", n)); + BSON_ASSERT (bson_append_document_end (&self->deleteresults, &deleteresult)); +} + +static void +_bulkwriteresult_set_insertresult (mongoc_bulkwriteresult_t *self, const bson_iter_t *id_iter, size_t models_idx) +{ + BSON_ASSERT_PARAM (self); + BSON_ASSERT_PARAM (id_iter); + + bson_t insertresult; + { + char *key = bson_strdup_printf ("%zu", models_idx); + BSON_APPEND_DOCUMENT_BEGIN (&self->insertresults, key, &insertresult); + bson_free (key); + } + + BSON_ASSERT (BSON_APPEND_ITER (&insertresult, "insertedId", id_iter)); + BSON_ASSERT (bson_append_document_end (&self->insertresults, &insertresult)); +} + +struct _mongoc_bulkwriteexception_t { + bson_error_t error; + bson_t error_reply; + bson_t write_concern_errors; + size_t write_concern_errors_len; + bson_t write_errors; + // If `has_any_error` is false, the bulk write exception is not returned. + bool has_any_error; +}; + +static mongoc_bulkwriteexception_t * +_bulkwriteexception_new (void) +{ + mongoc_bulkwriteexception_t *self = bson_malloc0 (sizeof (*self)); + bson_init (&self->write_concern_errors); + bson_init (&self->write_errors); + bson_init (&self->error_reply); + return self; +} + +// Returns true if there was a top-level error. +bool +mongoc_bulkwriteexception_error (const mongoc_bulkwriteexception_t *self, bson_error_t *error) +{ + BSON_ASSERT_PARAM (self); + BSON_ASSERT_PARAM (error); + + if (self->error.code != 0) { + memcpy (error, &self->error, sizeof (*error)); + return true; + } + return false; // No top-level error. +} + +const bson_t * +mongoc_bulkwriteexception_writeerrors (const mongoc_bulkwriteexception_t *self) +{ + BSON_ASSERT_PARAM (self); + return &self->write_errors; +} + +const bson_t * +mongoc_bulkwriteexception_writeconcernerrors (const mongoc_bulkwriteexception_t *self) +{ + BSON_ASSERT_PARAM (self); + return &self->write_concern_errors; +} + +const bson_t * +mongoc_bulkwriteexception_errorreply (const mongoc_bulkwriteexception_t *self) +{ + BSON_ASSERT_PARAM (self); + return &self->error_reply; +} + +void +mongoc_bulkwriteexception_destroy (mongoc_bulkwriteexception_t *self) +{ + if (!self) { + return; + } + bson_destroy (&self->write_errors); + bson_destroy (&self->write_concern_errors); + bson_destroy (&self->error_reply); + bson_free (self); +} + +static void +_bulkwriteexception_set_error (mongoc_bulkwriteexception_t *self, bson_error_t *error) +{ + BSON_ASSERT_PARAM (self); + BSON_ASSERT_PARAM (error); + + BSON_ASSERT (error->code != 0); + memcpy (&self->error, error, sizeof (*error)); + self->has_any_error = true; +} + +static void +_bulkwriteexception_set_error_reply (mongoc_bulkwriteexception_t *self, const bson_t *error_reply) +{ + BSON_ASSERT_PARAM (self); + BSON_ASSERT_PARAM (error_reply); + + bson_copy_to (error_reply, &self->error_reply); + self->has_any_error = true; +} + +static void +_bulkwriteexception_append_writeconcernerror (mongoc_bulkwriteexception_t *self, + int32_t code, + const char *errmsg, + const bson_t *errInfo) +{ + BSON_ASSERT_PARAM (self); + BSON_ASSERT_PARAM (errmsg); + BSON_ASSERT_PARAM (errInfo); + + char *key = bson_strdup_printf ("%zu", self->write_concern_errors_len); + self->write_concern_errors_len++; + + bson_t write_concern_error; + BSON_ASSERT (BSON_APPEND_DOCUMENT_BEGIN (&self->write_concern_errors, key, &write_concern_error)); + BSON_ASSERT (BSON_APPEND_INT32 (&write_concern_error, "code", code)); + BSON_ASSERT (BSON_APPEND_UTF8 (&write_concern_error, "message", errmsg)); + BSON_ASSERT (BSON_APPEND_DOCUMENT (&write_concern_error, "details", errInfo)); + BSON_ASSERT (bson_append_document_end (&self->write_concern_errors, &write_concern_error)); + self->has_any_error = true; + bson_free (key); +} + +static void +_bulkwriteexception_set_writeerror ( + mongoc_bulkwriteexception_t *self, int32_t code, const char *errmsg, const bson_t *errInfo, size_t models_idx) +{ + BSON_ASSERT_PARAM (self); + BSON_ASSERT_PARAM (errmsg); + BSON_ASSERT_PARAM (errInfo); + + bson_t write_error; + { + char *key = bson_strdup_printf ("%zu", models_idx); + BSON_APPEND_DOCUMENT_BEGIN (&self->write_errors, key, &write_error); + bson_free (key); + } + + BSON_ASSERT (BSON_APPEND_INT32 (&write_error, "code", code)); + BSON_ASSERT (BSON_APPEND_UTF8 (&write_error, "message", errmsg)); + BSON_ASSERT (BSON_APPEND_DOCUMENT (&write_error, "details", errInfo)); + BSON_ASSERT (bson_append_document_end (&self->write_errors, &write_error)); + self->has_any_error = true; +} + +static bool +lookup_int32 (const bson_t *bson, const char *key, int32_t *out, const char *source, mongoc_bulkwriteexception_t *exc) +{ + BSON_ASSERT_PARAM (bson); + BSON_ASSERT_PARAM (key); + BSON_ASSERT_PARAM (out); + BSON_OPTIONAL_PARAM (source); + BSON_ASSERT_PARAM (exc); + + bson_iter_t iter; + if (bson_iter_init_find (&iter, bson, key) && BSON_ITER_HOLDS_INT32 (&iter)) { + *out = bson_iter_int32 (&iter); + return true; + } + bson_error_t error; + if (source) { + bson_set_error (&error, + MONGOC_ERROR_COMMAND, + MONGOC_ERROR_COMMAND_INVALID_ARG, + "expected to find int32 `%s` in %s, but did not", + key, + source); + } else { + bson_set_error (&error, + MONGOC_ERROR_COMMAND, + MONGOC_ERROR_COMMAND_INVALID_ARG, + "expected to find int32 `%s`, but did not", + key); + } + _bulkwriteexception_set_error (exc, &error); + return false; +} + +// `lookup_as_int64` looks for `key` as a BSON int32, int64, or double and returns as an int64_t. Doubles are truncated. +static int64_t +lookup_as_int64 ( + const bson_t *bson, const char *key, int64_t *out, const char *source, mongoc_bulkwriteexception_t *exc) +{ + BSON_ASSERT_PARAM (bson); + BSON_ASSERT_PARAM (key); + BSON_ASSERT_PARAM (out); + BSON_OPTIONAL_PARAM (source); + BSON_ASSERT_PARAM (exc); + + bson_iter_t iter; + if (bson_iter_init_find (&iter, bson, key) && BSON_ITER_HOLDS_NUMBER (&iter)) { + *out = bson_iter_as_int64 (&iter); + return true; + } + bson_error_t error; + if (source) { + bson_set_error (&error, + MONGOC_ERROR_COMMAND, + MONGOC_ERROR_COMMAND_INVALID_ARG, + "expected to find int32, int64, or double `%s` in %s, but did not", + key, + source); + } else { + bson_set_error (&error, + MONGOC_ERROR_COMMAND, + MONGOC_ERROR_COMMAND_INVALID_ARG, + "expected to find int32, int64, or double `%s`, but did not", + key); + } + _bulkwriteexception_set_error (exc, &error); + return false; +} + +static bool +lookup_string ( + const bson_t *bson, const char *key, const char **out, const char *source, mongoc_bulkwriteexception_t *exc) +{ + BSON_ASSERT_PARAM (bson); + BSON_ASSERT_PARAM (key); + BSON_ASSERT_PARAM (out); + BSON_OPTIONAL_PARAM (source); + BSON_ASSERT_PARAM (exc); + + bson_iter_t iter; + if (bson_iter_init_find (&iter, bson, key) && BSON_ITER_HOLDS_UTF8 (&iter)) { + *out = bson_iter_utf8 (&iter, NULL); + return true; + } + bson_error_t error; + if (source) { + bson_set_error (&error, + MONGOC_ERROR_COMMAND, + MONGOC_ERROR_COMMAND_INVALID_ARG, + "expected to find string `%s` in %s, but did not", + key, + source); + } else { + bson_set_error (&error, + MONGOC_ERROR_COMMAND, + MONGOC_ERROR_COMMAND_INVALID_ARG, + "expected to find string `%s`, but did not", + key); + } + _bulkwriteexception_set_error (exc, &error); + return false; +} + +// `_bulkwritereturn_apply_reply` applies the top-level fields of a server reply to the returned results. +static bool +_bulkwritereturn_apply_reply (mongoc_bulkwritereturn_t *self, const bson_t *cmd_reply) +{ + BSON_ASSERT_PARAM (self); + BSON_ASSERT_PARAM (cmd_reply); + + // Parse top-level fields. + // These fields are expected to be int32 as of server 8.0. However, drivers return the values as int64. + // Use `lookup_as_int64` to support other numeric types to future-proof. + int64_t nInserted; + if (!lookup_as_int64 (cmd_reply, "nInserted", &nInserted, NULL, self->exc)) { + return false; + } + self->res->insertedcount += nInserted; + + int64_t nMatched; + if (!lookup_as_int64 (cmd_reply, "nMatched", &nMatched, NULL, self->exc)) { + return false; + } + self->res->matchedcount += nMatched; + + int64_t nModified; + if (!lookup_as_int64 (cmd_reply, "nModified", &nModified, NULL, self->exc)) { + return false; + } + self->res->modifiedcount += nModified; + + int64_t nDeleted; + if (!lookup_as_int64 (cmd_reply, "nDeleted", &nDeleted, NULL, self->exc)) { + return false; + } + self->res->deletedcount += nDeleted; + + int64_t nUpserted; + if (!lookup_as_int64 (cmd_reply, "nUpserted", &nUpserted, NULL, self->exc)) { + return false; + } + self->res->upsertedcount += nUpserted; + + bson_error_t error; + bson_iter_t iter; + if (bson_iter_init_find (&iter, cmd_reply, "writeConcernError")) { + bson_iter_t wce_iter; + bson_t wce_bson; + + if (!_mongoc_iter_document_as_bson (&iter, &wce_bson, &error)) { + _bulkwriteexception_set_error (self->exc, &error); + _bulkwriteexception_set_error_reply (self->exc, cmd_reply); + return false; + } + + // Parse `code`. + int32_t code; + if (!lookup_int32 (&wce_bson, "code", &code, "writeConcernError", self->exc)) { + return false; + } + + // Parse `errmsg`. + const char *errmsg; + if (!lookup_string (&wce_bson, "errmsg", &errmsg, "writeConcernError", self->exc)) { + return false; + } + + // Parse optional `errInfo`. + bson_t errInfo = BSON_INITIALIZER; + if (bson_iter_init_find (&wce_iter, &wce_bson, "errInfo")) { + if (!_mongoc_iter_document_as_bson (&wce_iter, &errInfo, &error)) { + _bulkwriteexception_set_error (self->exc, &error); + _bulkwriteexception_set_error_reply (self->exc, cmd_reply); + } + } + + _bulkwriteexception_append_writeconcernerror (self->exc, code, errmsg, &errInfo); + } + + return true; +} + +// `_bulkwritereturn_apply_result` applies an individual cursor result to the returned results. +static bool +_bulkwritereturn_apply_result (mongoc_bulkwritereturn_t *self, + const bson_t *result, + size_t ops_doc_offset, + const mongoc_array_t *arrayof_modeldata) +{ + BSON_ASSERT_PARAM (self); + BSON_ASSERT_PARAM (result); + BSON_ASSERT_PARAM (arrayof_modeldata); + + bson_error_t error; + + // Parse for `ok`. + int64_t ok; + if (!lookup_as_int64 (result, "ok", &ok, "result", self->exc)) { + return false; + } + + // Parse `idx`. + int64_t idx; + { + if (!lookup_as_int64 (result, "idx", &idx, "result", self->exc)) { + return false; + } + if (idx < 0) { + bson_set_error (&error, + MONGOC_ERROR_COMMAND, + MONGOC_ERROR_COMMAND_INVALID_ARG, + "expected to find non-negative int64 `idx` in " + "result, but did not"); + _bulkwriteexception_set_error (self->exc, &error); + return false; + } + } + + BSON_ASSERT (bson_in_range_size_t_signed (idx)); + // `models_idx` is the index of the model that produced this result. + size_t models_idx = (size_t) idx + ops_doc_offset; + if (ok == 0) { + bson_iter_t result_iter; + + // Parse `code`. + int32_t code; + if (!lookup_int32 (result, "code", &code, "result", self->exc)) { + return false; + } + + // Parse `errmsg`. + const char *errmsg; + if (!lookup_string (result, "errmsg", &errmsg, "result", self->exc)) { + return false; + } + + // Parse optional `errInfo`. + bson_t errInfo = BSON_INITIALIZER; + if (bson_iter_init_find (&result_iter, result, "errInfo")) { + if (!_mongoc_iter_document_as_bson (&result_iter, &errInfo, &error)) { + _bulkwriteexception_set_error (self->exc, &error); + return false; + } + } + + // Store a copy of the write error. + _bulkwriteexception_set_writeerror (self->exc, code, errmsg, &errInfo, models_idx); + } else { + // This is a successful result of an individual operation. + // Server only reports successful results of individual + // operations when verbose results are requested + // (`errorsOnly: false` is sent). + + modeldata_t *md = &_mongoc_array_index (arrayof_modeldata, modeldata_t, models_idx); + // Check if model is an update. + switch (md->op) { + case MODEL_OP_UPDATE: { + bson_iter_t result_iter; + // Parse `n`. + int64_t n; + if (!lookup_as_int64 (result, "n", &n, "result", self->exc)) { + return false; + } + + // Parse `nModified`. + int64_t nModified; + if (!lookup_as_int64 (result, "nModified", &nModified, "result", self->exc)) { + return false; + } + + // Check for an optional `upserted._id`. + const bson_value_t *upserted_id = NULL; + bson_iter_t id_iter; + if (bson_iter_init_find (&result_iter, result, "upserted")) { + BSON_ASSERT (bson_iter_init (&result_iter, result)); + if (!bson_iter_find_descendant (&result_iter, "upserted._id", &id_iter)) { + bson_set_error (&error, + MONGOC_ERROR_COMMAND, + MONGOC_ERROR_COMMAND_INVALID_ARG, + "expected `upserted` to be a document " + "containing `_id`, but did not find `_id`"); + _bulkwriteexception_set_error (self->exc, &error); + return false; + } + upserted_id = bson_iter_value (&id_iter); + } + + _bulkwriteresult_set_updateresult (self->res, n, nModified, upserted_id, models_idx); + break; + } + case MODEL_OP_DELETE: { + // Parse `n`. + int64_t n; + if (!lookup_as_int64 (result, "n", &n, "result", self->exc)) { + return false; + } + + _bulkwriteresult_set_deleteresult (self->res, n, models_idx); + break; + } + case MODEL_OP_INSERT: { + _bulkwriteresult_set_insertresult (self->res, &md->id_iter, models_idx); + break; + } + default: + // Add an unreachable default case to silence `switch-default` warnings. + BSON_UNREACHABLE ("unexpected default"); + } + } + return true; +} + +BSON_EXPORT (void) +mongoc_bulkwrite_set_session (mongoc_bulkwrite_t *self, mongoc_client_session_t *session) +{ + BSON_ASSERT_PARAM (self); + BSON_OPTIONAL_PARAM (session); + + self->session = session; +} + +mongoc_bulkwritereturn_t +mongoc_bulkwrite_execute (mongoc_bulkwrite_t *self, const mongoc_bulkwriteopts_t *opts) +{ + BSON_ASSERT_PARAM (self); + BSON_OPTIONAL_PARAM (opts); + + mongoc_bulkwritereturn_t ret = {0}; + bson_error_t error = {0}; + mongoc_server_stream_t *ss = NULL; + bson_t cmd = BSON_INITIALIZER; + mongoc_cmd_parts_t parts = {{0}}; + mongoc_bulkwriteopts_t defaults = {{0}}; + + if (!opts) { + opts = &defaults; + } + bool is_acknowledged = false; + // Create empty result and exception to collect results/errors from batches. + ret.res = _bulkwriteresult_new (); + ret.exc = _bulkwriteexception_new (); + + if (self->executed) { + bson_set_error (&error, MONGOC_ERROR_COMMAND, MONGOC_ERROR_COMMAND_INVALID_ARG, "bulk write already executed"); + _bulkwriteexception_set_error (ret.exc, &error); + goto fail; + } + self->executed = true; + + if (self->n_ops == 0) { + bson_set_error ( + &error, MONGOC_ERROR_COMMAND, MONGOC_ERROR_COMMAND_INVALID_ARG, "cannot do `bulkWrite` with no models"); + _bulkwriteexception_set_error (ret.exc, &error); + goto fail; + } + + if (_mongoc_cse_is_enabled (self->client)) { + bson_set_error (&error, + MONGOC_ERROR_COMMAND, + MONGOC_ERROR_COMMAND_INVALID_ARG, + "bulkWrite does not currently support automatic encryption"); + _bulkwriteexception_set_error (ret.exc, &error); + goto fail; + } + + // Select a stream. + { + bson_t reply; + + if (opts->serverid) { + ss = mongoc_cluster_stream_for_server ( + &self->client->cluster, opts->serverid, true /* reconnect_ok */, self->session, &reply, &error); + } else { + ss = mongoc_cluster_stream_for_writes ( + &self->client->cluster, self->session, NULL /* deprioritized servers */, &reply, &error); + } + + if (!ss) { + _bulkwriteexception_set_error (ret.exc, &error); + _bulkwriteexception_set_error_reply (ret.exc, &reply); + bson_destroy (&reply); + goto fail; + } + } + + bool verboseresults = + mongoc_optional_is_set (&opts->verboseresults) ? mongoc_optional_value (&opts->verboseresults) : false; + ret.res->verboseresults = verboseresults; + + int32_t maxBsonObjectSize = mongoc_server_stream_max_bson_obj_size (ss); + // Create the payload 0. + { + BSON_ASSERT (BSON_APPEND_INT32 (&cmd, "bulkWrite", 1)); + // errorsOnly is default true. Set to false if verboseResults requested. + BSON_ASSERT (BSON_APPEND_BOOL (&cmd, "errorsOnly", !verboseresults)); + // ordered is default true. + BSON_ASSERT (BSON_APPEND_BOOL ( + &cmd, "ordered", (mongoc_optional_is_set (&opts->ordered)) ? mongoc_optional_value (&opts->ordered) : true)); + + if (opts->comment) { + BSON_ASSERT (BSON_APPEND_DOCUMENT (&cmd, "comment", opts->comment)); + } + + if (mongoc_optional_is_set (&opts->bypassdocumentvalidation)) { + BSON_ASSERT (BSON_APPEND_BOOL ( + &cmd, "bypassDocumentValidation", mongoc_optional_value (&opts->bypassdocumentvalidation))); + } + + if (opts->let) { + BSON_ASSERT (BSON_APPEND_DOCUMENT (&cmd, "let", opts->let)); + } + + // Add optional extra fields. + if (opts->extra) { + BSON_ASSERT (bson_concat (&cmd, opts->extra)); + } + + mongoc_cmd_parts_init (&parts, self->client, "admin", MONGOC_QUERY_NONE, &cmd); + parts.assembled.operation_id = self->operation_id; + + parts.allow_txn_number = MONGOC_CMD_PARTS_ALLOW_TXN_NUMBER_YES; // To append `lsid`. + if (self->has_multi_write) { + // Write commands that include multi-document operations are not + // retryable. + parts.allow_txn_number = MONGOC_CMD_PARTS_ALLOW_TXN_NUMBER_NO; + } + parts.is_write_command = true; // To append `txnNumber`. + + if (self->session) { + mongoc_cmd_parts_set_session (&parts, self->session); + } + + // Apply write concern: + { + const mongoc_write_concern_t *wc = self->client->write_concern; // Default to client. + if (opts->writeconcern) { + if (_mongoc_client_session_in_txn (self->session)) { + bson_set_error (&error, + MONGOC_ERROR_COMMAND, + MONGOC_ERROR_COMMAND_INVALID_ARG, + "Cannot set write concern after starting a transaction."); + _bulkwriteexception_set_error (ret.exc, &error); + goto fail; + } + wc = opts->writeconcern; + } + if (!mongoc_cmd_parts_set_write_concern (&parts, wc, &error)) { + _bulkwriteexception_set_error (ret.exc, &error); + goto fail; + } + if (!mongoc_write_concern_is_acknowledged (wc) && + bson_cmp_greater_us (self->max_insert_len, maxBsonObjectSize)) { + bson_set_error (&error, + MONGOC_ERROR_COMMAND, + MONGOC_ERROR_COMMAND_INVALID_ARG, + "Unacknowledged `bulkWrite` includes insert of size: %" PRIu32 + ", exceeding maxBsonObjectSize: %" PRId32, + self->max_insert_len, + maxBsonObjectSize); + _bulkwriteexception_set_error (ret.exc, &error); + goto fail; + } + is_acknowledged = mongoc_write_concern_is_acknowledged (wc); + } + + if (!mongoc_cmd_parts_assemble (&parts, ss, &error)) { + _bulkwriteexception_set_error (ret.exc, &error); + goto fail; + } + } + + int32_t maxWriteBatchSize = mongoc_server_stream_max_write_batch_size (ss); + int32_t maxMessageSizeBytes = mongoc_server_stream_max_msg_size (ss); + // `ops_doc_offset` is an offset into the `ops` document sequence. Counts the number of documents sent. + size_t ops_doc_offset = 0; + // `ops_byte_offset` is an offset into the `ops` document sequence. Counts the number of bytes sent. + size_t ops_byte_offset = 0; + // Calculate overhead of OP_MSG and the `bulkWrite` command. See bulk write specification for explanation. + size_t opmsg_overhead = 0; + { + opmsg_overhead += 1000; + // Add size of `bulkWrite` command. Exclude command-agnostic fields added in `mongoc_cmd_parts_assemble` (e.g. + // `txnNumber` and `lsid`). + opmsg_overhead += cmd.len; + } + + // Send one or more `bulkWrite` commands. Split input payload if necessary to satisfy server size limits. + while (true) { + bool has_write_errors = false; + bool batch_ok = false; + bson_t cmd_reply = BSON_INITIALIZER; + mongoc_cursor_t *reply_cursor = NULL; + // `ops_byte_len` is the number of documents from `ops` to send in this batch. + size_t ops_byte_len = 0; + // `ops_doc_len` is the number of bytes from `ops` to send in this batch. + size_t ops_doc_len = 0; + + if (ops_byte_offset == self->ops.len) { + // All write models were sent. + break; + } + + // Track the nsInfo entries to include in this batch. + mcd_nsinfo_t *nsinfo = mcd_nsinfo_new (); + + // Read as many documents from payload as possible. + while (true) { + if (ops_byte_offset + ops_byte_len >= self->ops.len) { + // All remaining ops are readied. + break; + } + + if (ops_doc_len >= maxWriteBatchSize) { + // Maximum number of operations are readied. + break; + } + + // Read length of next document. + uint32_t doc_len; + memcpy (&doc_len, self->ops.data + ops_byte_offset + ops_byte_len, 4); + doc_len = BSON_UINT32_FROM_LE (doc_len); + + // Check if adding this operation requires adding an `nsInfo` entry. + // `models_idx` is the index of the model that produced this result. + size_t models_idx = ops_doc_len + ops_doc_offset; + modeldata_t *md = &_mongoc_array_index (&self->arrayof_modeldata, modeldata_t, models_idx); + uint32_t nsinfo_bson_size = 0; + int32_t ns_index = mcd_nsinfo_find (nsinfo, md->ns); + if (ns_index == -1) { + // Need to append `nsInfo` entry. Append after checking that both the document and the `nsInfo` entry fit. + nsinfo_bson_size = mcd_nsinfo_get_bson_size (md->ns); + } + + if (opmsg_overhead + ops_byte_len + doc_len + nsinfo_bson_size > maxMessageSizeBytes) { + if (ops_byte_len == 0) { + // Could not even fit one document within an OP_MSG. + bson_set_error (&error, + MONGOC_ERROR_COMMAND, + MONGOC_ERROR_COMMAND_INVALID_ARG, + "unable to send document at index %zu. Sending " + "would exceed maxMessageSizeBytes=%" PRId32, + ops_doc_len, + maxMessageSizeBytes); + _bulkwriteexception_set_error (ret.exc, &error); + goto batch_fail; + } + break; + } + + // Check if a new `nsInfo` entry is needed. + if (ns_index == -1) { + ns_index = mcd_nsinfo_append (nsinfo, md->ns, &error); + if (ns_index == -1) { + _bulkwriteexception_set_error (ret.exc, &error); + goto batch_fail; + } + } + + // Overwrite the placeholder to the index of the `nsInfo` entry. + { + bson_iter_t nsinfo_iter; + bson_t doc; + BSON_ASSERT (bson_init_static (&doc, self->ops.data + ops_byte_offset + ops_byte_len, doc_len)); + // Find the index. + BSON_ASSERT (bson_iter_init (&nsinfo_iter, &doc)); + BSON_ASSERT (bson_iter_next (&nsinfo_iter)); + bson_iter_overwrite_int32 (&nsinfo_iter, ns_index); + } + + // Include document. + { + ops_byte_len += doc_len; + ops_doc_len += 1; + } + } + + // Send batch. + { + parts.assembled.payloads_count = 2; + + // Create the `nsInfo` payload. + { + mongoc_cmd_payload_t *payload = &parts.assembled.payloads[0]; + const mongoc_buffer_t *nsinfo_docseq = mcd_nsinfo_as_document_sequence (nsinfo); + payload->documents = nsinfo_docseq->data; + BSON_ASSERT (bson_in_range_int32_t_unsigned (nsinfo_docseq->len)); + payload->size = (int32_t) nsinfo_docseq->len; + payload->identifier = "nsInfo"; + } + + // Create the `ops` payload. + { + mongoc_cmd_payload_t *payload = &parts.assembled.payloads[1]; + payload->identifier = "ops"; + payload->documents = self->ops.data + ops_byte_offset; + BSON_ASSERT (bson_in_range_int32_t_unsigned (ops_byte_len)); + payload->size = (int32_t) ops_byte_len; + } + + // Check if stream is valid. A previous call to `mongoc_cluster_run_retryable_write` may have invalidated + // stream (e.g. due to processing an error). If invalid, select a new stream before processing more batches. + if (!mongoc_cluster_stream_valid (&self->client->cluster, parts.assembled.server_stream)) { + bson_t reply; + // Select a server and create a stream again. + mongoc_server_stream_cleanup (ss); + ss = mongoc_cluster_stream_for_writes ( + &self->client->cluster, NULL /* session */, NULL /* deprioritized servers */, &reply, &error); + + if (ss) { + parts.assembled.server_stream = ss; + } else { + _bulkwriteexception_set_error (ret.exc, &error); + _bulkwriteexception_set_error_reply (ret.exc, &reply); + bson_destroy (&reply); + goto batch_fail; + } + } + + // Send command. + { + mongoc_server_stream_t *new_ss = NULL; + bool ok = mongoc_cluster_run_retryable_write ( + &self->client->cluster, &parts.assembled, parts.is_retryable_write, &new_ss, &cmd_reply, &error); + if (new_ss) { + // A retry occurred. Save the newly created stream to use for subsequent commands. + mongoc_server_stream_cleanup (ss); + ss = new_ss; + parts.assembled.server_stream = ss; + } + + // Check for a command ('ok': 0) error. + if (!ok) { + if (error.code != 0) { + // The original error was a command ('ok': 0) error. + _bulkwriteexception_set_error (ret.exc, &error); + } + _bulkwriteexception_set_error_reply (ret.exc, &cmd_reply); + goto batch_fail; + } + } + + // Add to result and/or exception. + if (is_acknowledged) { + // Parse top-level fields. + if (!_bulkwritereturn_apply_reply (&ret, &cmd_reply)) { + goto batch_fail; + } + + // Construct reply cursor and read individual results. + { + bson_t cursor_opts = BSON_INITIALIZER; + { + uint32_t serverid = parts.assembled.server_stream->sd->id; + BSON_ASSERT (bson_in_range_int32_t_unsigned (serverid)); + int32_t serverid_i32 = (int32_t) serverid; + BSON_ASSERT (BSON_APPEND_INT32 (&cursor_opts, "serverId", serverid_i32)); + // Use same session if one was applied. + if (parts.assembled.session && + !mongoc_client_session_append (parts.assembled.session, &cursor_opts, &error)) { + _bulkwriteexception_set_error (ret.exc, &error); + _bulkwriteexception_set_error_reply (ret.exc, &cmd_reply); + goto batch_fail; + } + } + + // Construct the reply cursor. + reply_cursor = mongoc_cursor_new_from_command_reply_with_opts (self->client, &cmd_reply, &cursor_opts); + bson_destroy (&cursor_opts); + // `cmd_reply` is stolen. Clear it. + bson_init (&cmd_reply); + + // Ensure constructing cursor did not error. + { + const bson_t *error_document; + if (mongoc_cursor_error_document (reply_cursor, &error, &error_document)) { + _bulkwriteexception_set_error (ret.exc, &error); + if (error_document) { + _bulkwriteexception_set_error_reply (ret.exc, error_document); + } + goto batch_fail; + } + } + + // Iterate over cursor results. + const bson_t *result; + while (mongoc_cursor_next (reply_cursor, &result)) { + if (!_bulkwritereturn_apply_result (&ret, result, ops_doc_offset, &self->arrayof_modeldata)) { + goto batch_fail; + } + } + has_write_errors = !bson_empty (&ret.exc->write_errors); + // Ensure iterating cursor did not error. + { + const bson_t *error_document; + if (mongoc_cursor_error_document (reply_cursor, &error, &error_document)) { + _bulkwriteexception_set_error (ret.exc, &error); + if (error_document) { + _bulkwriteexception_set_error_reply (ret.exc, error_document); + } + goto batch_fail; + } + } + } + } + } + + ops_doc_offset += ops_doc_len; + ops_byte_offset += ops_byte_len; + batch_ok = true; + batch_fail: + mcd_nsinfo_destroy (nsinfo); + mongoc_cursor_destroy (reply_cursor); + bson_destroy (&cmd_reply); + if (!batch_ok) { + goto fail; + } + bool is_ordered = + mongoc_optional_is_set (&opts->ordered) ? mongoc_optional_value (&opts->ordered) : true; // default. + if (has_write_errors && is_ordered) { + // Ordered writes must not continue to send batches once an error is + // occurred. An individual write error is not a top-level error. + break; + } + } + +fail: + if (!is_acknowledged) { + mongoc_bulkwriteresult_destroy (ret.res); + ret.res = NULL; + } + if (parts.body) { + // Only clean-up if initialized. + mongoc_cmd_parts_cleanup (&parts); + } + bson_destroy (&cmd); + if (ret.res && ss) { + // Set the returned server ID to the most recently selected server. + ret.res->serverid = ss->sd->id; + } + mongoc_server_stream_cleanup (ss); + if (!ret.exc->has_any_error) { + mongoc_bulkwriteexception_destroy (ret.exc); + ret.exc = NULL; + } + return ret; +} + +MC_ENABLE_CONVERSION_WARNING_END diff --git a/src/libmongoc/src/mongoc/mongoc-bulkwrite.h b/src/libmongoc/src/mongoc/mongoc-bulkwrite.h new file mode 100644 index 00000000000..36bb0dee21f --- /dev/null +++ b/src/libmongoc/src/mongoc/mongoc-bulkwrite.h @@ -0,0 +1,254 @@ +/* + * Copyright 2024-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. + */ + +#include "mongoc-prelude.h" + +#ifndef MONGOC_BULKWRITE_H +#define MONGOC_BULKWRITE_H + +#include +#include + +BSON_BEGIN_DECLS + +typedef struct _mongoc_bulkwriteopts_t mongoc_bulkwriteopts_t; +MONGOC_EXPORT (mongoc_bulkwriteopts_t *) +mongoc_bulkwriteopts_new (void); +MONGOC_EXPORT (void) +mongoc_bulkwriteopts_set_ordered (mongoc_bulkwriteopts_t *self, bool ordered); +MONGOC_EXPORT (void) +mongoc_bulkwriteopts_set_bypassdocumentvalidation (mongoc_bulkwriteopts_t *self, bool bypassdocumentvalidation); +MONGOC_EXPORT (void) +mongoc_bulkwriteopts_set_let (mongoc_bulkwriteopts_t *self, const bson_t *let); +MONGOC_EXPORT (void) +mongoc_bulkwriteopts_set_writeconcern (mongoc_bulkwriteopts_t *self, const mongoc_write_concern_t *writeconcern); +MONGOC_EXPORT (void) +mongoc_bulkwriteopts_set_comment (mongoc_bulkwriteopts_t *self, const bson_t *comment); +MONGOC_EXPORT (void) +mongoc_bulkwriteopts_set_verboseresults (mongoc_bulkwriteopts_t *self, bool verboseresults); +// `mongoc_bulkwriteopts_set_extra` appends `extra` to bulkWrite command. +// It is intended to support future server options. +MONGOC_EXPORT (void) +mongoc_bulkwriteopts_set_extra (mongoc_bulkwriteopts_t *self, const bson_t *extra); +// `mongoc_bulkwriteopts_set_serverid` identifies which server to perform the operation. This is intended for use by +// wrapping drivers that select a server before running the operation. +MONGOC_EXPORT (void) +mongoc_bulkwriteopts_set_serverid (mongoc_bulkwriteopts_t *self, uint32_t serverid); +MONGOC_EXPORT (void) +mongoc_bulkwriteopts_destroy (mongoc_bulkwriteopts_t *self); + +typedef struct _mongoc_bulkwriteresult_t mongoc_bulkwriteresult_t; +MONGOC_EXPORT (int64_t) +mongoc_bulkwriteresult_insertedcount (const mongoc_bulkwriteresult_t *self); +MONGOC_EXPORT (int64_t) +mongoc_bulkwriteresult_upsertedcount (const mongoc_bulkwriteresult_t *self); +MONGOC_EXPORT (int64_t) +mongoc_bulkwriteresult_matchedcount (const mongoc_bulkwriteresult_t *self); +MONGOC_EXPORT (int64_t) +mongoc_bulkwriteresult_modifiedcount (const mongoc_bulkwriteresult_t *self); +MONGOC_EXPORT (int64_t) +mongoc_bulkwriteresult_deletedcount (const mongoc_bulkwriteresult_t *self); +// `mongoc_bulkwriteresult_insertresults` returns a BSON document mapping model indexes to insert results. +// Example: +// { +// "0" : { "insertedId" : "foo" }, +// "1" : { "insertedId" : "bar" } +// } +// Returns NULL if verbose results were not requested. +MONGOC_EXPORT (const bson_t *) +mongoc_bulkwriteresult_insertresults (const mongoc_bulkwriteresult_t *self); +// `mongoc_bulkwriteresult_updateresults` returns a BSON document mapping model indexes to update results. +// Example: +// { +// "0" : { "matchedCount" : 2, "modifiedCount" : 2 }, +// "1" : { "matchedCount" : 1, "modifiedCount" : 0, "upsertedId" : "foo" } +// } +// Returns NULL if verbose results were not requested. +MONGOC_EXPORT (const bson_t *) +mongoc_bulkwriteresult_updateresults (const mongoc_bulkwriteresult_t *self); +// `mongoc_bulkwriteresult_deleteresults` returns a BSON document mapping model indexes to delete results. +// Example: +// { +// "0" : { "deletedCount" : 1 }, +// "1" : { "deletedCount" : 2 } +// } +// Returns NULL if verbose results were not requested. +MONGOC_EXPORT (const bson_t *) +mongoc_bulkwriteresult_deleteresults (const mongoc_bulkwriteresult_t *self); +// `mongoc_bulkwriteresult_serverid` identifies the most recently selected server. This may differ from a +// previously set serverid if a retry occurred. This is intended for use by wrapping drivers that select a server before +// running the operation. +MONGOC_EXPORT (uint32_t) +mongoc_bulkwriteresult_serverid (const mongoc_bulkwriteresult_t *self); +MONGOC_EXPORT (void) +mongoc_bulkwriteresult_destroy (mongoc_bulkwriteresult_t *self); + +typedef struct _mongoc_bulkwriteexception_t mongoc_bulkwriteexception_t; +// Returns true if there was a top-level error. +MONGOC_EXPORT (bool) +mongoc_bulkwriteexception_error (const mongoc_bulkwriteexception_t *self, bson_error_t *error); +// `mongoc_bulkwriteexception_writeerrors` returns a BSON document mapping model indexes to write errors. +// Example: +// { +// "0" : { "code" : 123, "message" : "foo", "details" : { } }, +// "1" : { "code" : 456, "message" : "bar", "details" : { } } +// } +// Returns an empty document if there are no write errors. +MONGOC_EXPORT (const bson_t *) +mongoc_bulkwriteexception_writeerrors (const mongoc_bulkwriteexception_t *self); +// `mongoc_bulkwriteexception_writeconcernerrors` returns a BSON array of write concern errors. +// Example: +// [ +// { "code" : 123, "message" : "foo", "details" : { } }, +// { "code" : 456, "message" : "bar", "details" : { } } +// ] +// Returns an empty array if there are no write concern errors. +MONGOC_EXPORT (const bson_t *) +mongoc_bulkwriteexception_writeconcernerrors (const mongoc_bulkwriteexception_t *self); +// `mongoc_bulkwriteexception_errorreply` returns a possible server reply related to the error, or an empty document. +MONGOC_EXPORT (const bson_t *) +mongoc_bulkwriteexception_errorreply (const mongoc_bulkwriteexception_t *self); +MONGOC_EXPORT (void) +mongoc_bulkwriteexception_destroy (mongoc_bulkwriteexception_t *self); + +typedef struct _mongoc_bulkwrite_t mongoc_bulkwrite_t; +MONGOC_EXPORT (mongoc_bulkwrite_t *) +mongoc_client_bulkwrite_new (mongoc_client_t *self); +typedef struct _mongoc_bulkwrite_insertoneopts_t mongoc_bulkwrite_insertoneopts_t; +MONGOC_EXPORT (mongoc_bulkwrite_insertoneopts_t *) +mongoc_bulkwrite_insertoneopts_new (void); +MONGOC_EXPORT (void) +mongoc_bulkwrite_insertoneopts_destroy (mongoc_bulkwrite_insertoneopts_t *self); +MONGOC_EXPORT (bool) +mongoc_bulkwrite_append_insertone (mongoc_bulkwrite_t *self, + const char *ns, + const bson_t *document, + const mongoc_bulkwrite_insertoneopts_t *opts /* May be NULL */, + bson_error_t *error); + +typedef struct _mongoc_bulkwrite_updateoneopts_t mongoc_bulkwrite_updateoneopts_t; +MONGOC_EXPORT (mongoc_bulkwrite_updateoneopts_t *) +mongoc_bulkwrite_updateoneopts_new (void); +MONGOC_EXPORT (void) +mongoc_bulkwrite_updateoneopts_set_arrayfilters (mongoc_bulkwrite_updateoneopts_t *self, const bson_t *arrayfilters); +MONGOC_EXPORT (void) +mongoc_bulkwrite_updateoneopts_set_collation (mongoc_bulkwrite_updateoneopts_t *self, const bson_t *collation); +MONGOC_EXPORT (void) +mongoc_bulkwrite_updateoneopts_set_hint (mongoc_bulkwrite_updateoneopts_t *self, const bson_value_t *hint); +MONGOC_EXPORT (void) +mongoc_bulkwrite_updateoneopts_set_upsert (mongoc_bulkwrite_updateoneopts_t *self, bool upsert); +MONGOC_EXPORT (void) +mongoc_bulkwrite_updateoneopts_destroy (mongoc_bulkwrite_updateoneopts_t *self); +MONGOC_EXPORT (bool) +mongoc_bulkwrite_append_updateone (mongoc_bulkwrite_t *self, + const char *ns, + const bson_t *filter, + const bson_t *update, + const mongoc_bulkwrite_updateoneopts_t *opts /* May be NULL */, + bson_error_t *error); + +typedef struct _mongoc_bulkwrite_updatemanyopts_t mongoc_bulkwrite_updatemanyopts_t; +MONGOC_EXPORT (mongoc_bulkwrite_updatemanyopts_t *) +mongoc_bulkwrite_updatemanyopts_new (void); +MONGOC_EXPORT (void) +mongoc_bulkwrite_updatemanyopts_set_arrayfilters (mongoc_bulkwrite_updatemanyopts_t *self, const bson_t *arrayfilters); +MONGOC_EXPORT (void) +mongoc_bulkwrite_updatemanyopts_set_collation (mongoc_bulkwrite_updatemanyopts_t *self, const bson_t *collation); +MONGOC_EXPORT (void) +mongoc_bulkwrite_updatemanyopts_set_hint (mongoc_bulkwrite_updatemanyopts_t *self, const bson_value_t *hint); +MONGOC_EXPORT (void) +mongoc_bulkwrite_updatemanyopts_set_upsert (mongoc_bulkwrite_updatemanyopts_t *self, bool upsert); +MONGOC_EXPORT (void) +mongoc_bulkwrite_updatemanyopts_destroy (mongoc_bulkwrite_updatemanyopts_t *self); +MONGOC_EXPORT (bool) +mongoc_bulkwrite_append_updatemany (mongoc_bulkwrite_t *self, + const char *ns, + const bson_t *filter, + const bson_t *update, + const mongoc_bulkwrite_updatemanyopts_t *opts /* May be NULL */, + bson_error_t *error); + +typedef struct _mongoc_bulkwrite_replaceoneopts_t mongoc_bulkwrite_replaceoneopts_t; +MONGOC_EXPORT (mongoc_bulkwrite_replaceoneopts_t *) +mongoc_bulkwrite_replaceoneopts_new (void); +MONGOC_EXPORT (void) +mongoc_bulkwrite_replaceoneopts_set_collation (mongoc_bulkwrite_replaceoneopts_t *self, const bson_t *collation); +MONGOC_EXPORT (void) +mongoc_bulkwrite_replaceoneopts_set_hint (mongoc_bulkwrite_replaceoneopts_t *self, const bson_value_t *hint); +MONGOC_EXPORT (void) +mongoc_bulkwrite_replaceoneopts_set_upsert (mongoc_bulkwrite_replaceoneopts_t *self, bool upsert); +MONGOC_EXPORT (void) +mongoc_bulkwrite_replaceoneopts_destroy (mongoc_bulkwrite_replaceoneopts_t *self); +MONGOC_EXPORT (bool) +mongoc_bulkwrite_append_replaceone (mongoc_bulkwrite_t *self, + const char *ns, + const bson_t *filter, + const bson_t *replacement, + const mongoc_bulkwrite_replaceoneopts_t *opts /* May be NULL */, + bson_error_t *error); + +typedef struct _mongoc_bulkwrite_deleteoneopts_t mongoc_bulkwrite_deleteoneopts_t; +MONGOC_EXPORT (mongoc_bulkwrite_deleteoneopts_t *) +mongoc_bulkwrite_deleteoneopts_new (void); +MONGOC_EXPORT (void) +mongoc_bulkwrite_deleteoneopts_set_collation (mongoc_bulkwrite_deleteoneopts_t *self, const bson_t *collation); +MONGOC_EXPORT (void) +mongoc_bulkwrite_deleteoneopts_set_hint (mongoc_bulkwrite_deleteoneopts_t *self, const bson_value_t *hint); +MONGOC_EXPORT (void) +mongoc_bulkwrite_deleteoneopts_destroy (mongoc_bulkwrite_deleteoneopts_t *self); +MONGOC_EXPORT (bool) +mongoc_bulkwrite_append_deleteone (mongoc_bulkwrite_t *self, + const char *ns, + const bson_t *filter, + const mongoc_bulkwrite_deleteoneopts_t *opts /* May be NULL */, + bson_error_t *error); + +typedef struct _mongoc_bulkwrite_deletemanyopts_t mongoc_bulkwrite_deletemanyopts_t; +MONGOC_EXPORT (mongoc_bulkwrite_deletemanyopts_t *) +mongoc_bulkwrite_deletemanyopts_new (void); +MONGOC_EXPORT (void) +mongoc_bulkwrite_deletemanyopts_set_collation (mongoc_bulkwrite_deletemanyopts_t *self, const bson_t *collation); +MONGOC_EXPORT (void) +mongoc_bulkwrite_deletemanyopts_set_hint (mongoc_bulkwrite_deletemanyopts_t *self, const bson_value_t *hint); +MONGOC_EXPORT (void) +mongoc_bulkwrite_deletemanyopts_destroy (mongoc_bulkwrite_deletemanyopts_t *self); +MONGOC_EXPORT (bool) +mongoc_bulkwrite_append_deletemany (mongoc_bulkwrite_t *self, + const char *ns, + const bson_t *filter, + const mongoc_bulkwrite_deletemanyopts_t *opts /* May be NULL */, + bson_error_t *error); + + +// `mongoc_bulkwritereturn_t` may outlive `mongoc_bulkwrite_t`. +typedef struct { + mongoc_bulkwriteresult_t *res; // NULL if write was unacknowledged. + mongoc_bulkwriteexception_t *exc; // NULL if no error. +} mongoc_bulkwritereturn_t; + +// `mongoc_bulkwrite_set_session` sets an optional explicit session. +// `*session` may be modified when `mongoc_bulkwrite_execute` is called. +MONGOC_EXPORT (void) +mongoc_bulkwrite_set_session (mongoc_bulkwrite_t *self, mongoc_client_session_t *session); +// `mongoc_bulkwrite_execute` executes a bulk write operation. +MONGOC_EXPORT (mongoc_bulkwritereturn_t) +mongoc_bulkwrite_execute (mongoc_bulkwrite_t *self, const mongoc_bulkwriteopts_t *opts); +MONGOC_EXPORT (void) +mongoc_bulkwrite_destroy (mongoc_bulkwrite_t *self); + +BSON_END_DECLS + +#endif // MONGOC_BULKWRITE_H diff --git a/src/libmongoc/src/mongoc/mongoc-client-side-encryption.c b/src/libmongoc/src/mongoc/mongoc-client-side-encryption.c index ffe33494dbd..154699f3d4f 100644 --- a/src/libmongoc/src/mongoc/mongoc-client-side-encryption.c +++ b/src/libmongoc/src/mongoc/mongoc-client-side-encryption.c @@ -915,7 +915,7 @@ mongoc_client_encryption_encrypt_expression (mongoc_client_encryption_t *client_ BSON_ASSERT_PARAM (expr); BSON_ASSERT_PARAM (opts); BSON_ASSERT_PARAM (expr_encrypted); - BSON_ASSERT (error || true); + BSON_OPTIONAL_PARAM (error); bson_init (expr_encrypted); @@ -2661,7 +2661,7 @@ mongoc_client_encryption_encrypt_expression (mongoc_client_encryption_t *client_ BSON_ASSERT_PARAM (expr); BSON_ASSERT_PARAM (opts); BSON_ASSERT_PARAM (expr_out); - BSON_ASSERT (error || true); + BSON_OPTIONAL_PARAM (error); bson_init (expr_out); @@ -2774,9 +2774,9 @@ mongoc_client_encryption_create_encrypted_collection (mongoc_client_encryption_t BSON_ASSERT_PARAM (database); BSON_ASSERT_PARAM (name); BSON_ASSERT_PARAM (in_options); - BSON_ASSERT (opt_out_options || true); + BSON_OPTIONAL_PARAM (opt_out_options); BSON_ASSERT_PARAM (kms_provider); - BSON_ASSERT (error || true); + BSON_OPTIONAL_PARAM (error); mongoc_collection_t *ret = NULL; @@ -2889,8 +2889,8 @@ _init_1_encryptedField ( BSON_ASSERT_PARAM (out_field); BSON_ASSERT_PARAM (in_field); BSON_ASSERT_PARAM (fac); - BSON_ASSERT (fac_userdata || true); - BSON_ASSERT (error || true); + BSON_OPTIONAL_PARAM (fac_userdata); + BSON_OPTIONAL_PARAM (error); bsonVisitEach (*in_field, // If it is not a "keyId":null element, just copy it to the output. if (not(keyWithType ("keyId", null)), then (appendTo (*out_field), continue)), @@ -2922,8 +2922,8 @@ _init_encryptedFields ( BSON_ASSERT_PARAM (out_fields); BSON_ASSERT_PARAM (in_fields); BSON_ASSERT_PARAM (fac); - BSON_ASSERT (fac_userdata || true); - BSON_ASSERT (error || true); + BSON_OPTIONAL_PARAM (fac_userdata); + BSON_OPTIONAL_PARAM (error); // Ref to one encyrptedField bson_t cur_field; bsonVisitEach ( diff --git a/src/libmongoc/src/mongoc/mongoc-client.c b/src/libmongoc/src/mongoc/mongoc-client.c index 189579a2d8e..b7b284562c2 100644 --- a/src/libmongoc/src/mongoc/mongoc-client.c +++ b/src/libmongoc/src/mongoc/mongoc-client.c @@ -2243,8 +2243,8 @@ _mongoc_client_op_killcursors (mongoc_cluster_t *cluster, { BSON_ASSERT_PARAM (cluster); BSON_ASSERT_PARAM (server_stream); - BSON_ASSERT (db || true); - BSON_ASSERT (collection || true); + BSON_OPTIONAL_PARAM (db); + BSON_OPTIONAL_PARAM (collection); const bool has_ns = db && collection; const int64_t started = bson_get_monotonic_time (); diff --git a/src/libmongoc/src/mongoc/mongoc-cluster.c b/src/libmongoc/src/mongoc/mongoc-cluster.c index adea40f661c..503891c502b 100644 --- a/src/libmongoc/src/mongoc/mongoc-cluster.c +++ b/src/libmongoc/src/mongoc/mongoc-cluster.c @@ -2223,9 +2223,9 @@ mongoc_cluster_stream_for_server (mongoc_cluster_t *cluster, bson_error_t *error) { BSON_ASSERT_PARAM (cluster); - BSON_ASSERT (cs || true); - BSON_ASSERT (reply || true); - BSON_ASSERT (error || true); + BSON_OPTIONAL_PARAM (cs); + BSON_OPTIONAL_PARAM (reply); + BSON_OPTIONAL_PARAM (error); ENTRY; @@ -2585,11 +2585,11 @@ _mongoc_cluster_select_server_id (mongoc_client_session_t *cs, const mongoc_deprioritized_servers_t *ds, bson_error_t *error) { - BSON_ASSERT (cs || true); + BSON_OPTIONAL_PARAM (cs); BSON_ASSERT_PARAM (topology); - BSON_ASSERT (read_prefs || true); + BSON_OPTIONAL_PARAM (read_prefs); BSON_ASSERT_PARAM (must_use_primary); - BSON_ASSERT (error || true); + BSON_OPTIONAL_PARAM (error); uint32_t server_id; @@ -2643,10 +2643,10 @@ _mongoc_cluster_stream_for_optype (mongoc_cluster_t *cluster, bson_error_t *error) { BSON_ASSERT_PARAM (cluster); - BSON_ASSERT (read_prefs || true); - BSON_ASSERT (cs || true); - BSON_ASSERT (reply || true); - BSON_ASSERT (error || true); + BSON_OPTIONAL_PARAM (read_prefs); + BSON_OPTIONAL_PARAM (cs); + BSON_OPTIONAL_PARAM (reply); + BSON_OPTIONAL_PARAM (error); mongoc_server_stream_t *server_stream; uint32_t server_id; @@ -3669,7 +3669,7 @@ mongoc_cluster_run_retryable_write (mongoc_cluster_t *cluster, BSON_ASSERT_PARAM (cmd); BSON_ASSERT_PARAM (retry_server_stream); BSON_ASSERT_PARAM (reply); - BSON_ASSERT (error || true); + BSON_OPTIONAL_PARAM (error); bool ret; // `can_retry` is set to false on retry. A retry may only happen once. diff --git a/src/libmongoc/src/mongoc/mongoc-crypt.c b/src/libmongoc/src/mongoc/mongoc-crypt.c index c660aa1bbf9..b72a75dc841 100644 --- a/src/libmongoc/src/mongoc/mongoc-crypt.c +++ b/src/libmongoc/src/mongoc/mongoc-crypt.c @@ -1576,12 +1576,12 @@ _create_explicit_state_machine (_mongoc_crypt_t *crypt, { BSON_ASSERT_PARAM (crypt); BSON_ASSERT_PARAM (keyvault_coll); - BSON_ASSERT (algorithm || true); - BSON_ASSERT (keyid || true); - BSON_ASSERT (keyaltname || true); - BSON_ASSERT (query_type || true); - BSON_ASSERT (range_opts || true); - BSON_ASSERT (error || true); + BSON_OPTIONAL_PARAM (algorithm); + BSON_OPTIONAL_PARAM (keyid); + BSON_OPTIONAL_PARAM (keyaltname); + BSON_OPTIONAL_PARAM (query_type); + BSON_OPTIONAL_PARAM (range_opts); + BSON_OPTIONAL_PARAM (error); _state_machine_t *state_machine = NULL; bool ok = false; @@ -1685,14 +1685,14 @@ _mongoc_crypt_explicit_encrypt (_mongoc_crypt_t *crypt, { BSON_ASSERT_PARAM (crypt); BSON_ASSERT_PARAM (keyvault_coll); - BSON_ASSERT (algorithm || true); - BSON_ASSERT (keyid || true); - BSON_ASSERT (keyaltname || true); - BSON_ASSERT (query_type || true); - BSON_ASSERT (range_opts || true); + BSON_OPTIONAL_PARAM (algorithm); + BSON_OPTIONAL_PARAM (keyid); + BSON_OPTIONAL_PARAM (keyaltname); + BSON_OPTIONAL_PARAM (query_type); + BSON_OPTIONAL_PARAM (range_opts); BSON_ASSERT_PARAM (value_in); BSON_ASSERT_PARAM (value_out); - BSON_ASSERT (error || true); + BSON_OPTIONAL_PARAM (error); _state_machine_t *state_machine = NULL; bson_t *to_encrypt_doc = NULL; @@ -1760,14 +1760,14 @@ _mongoc_crypt_explicit_encrypt_expression (_mongoc_crypt_t *crypt, { BSON_ASSERT_PARAM (crypt); BSON_ASSERT_PARAM (keyvault_coll); - BSON_ASSERT (algorithm || true); - BSON_ASSERT (keyid || true); - BSON_ASSERT (keyaltname || true); - BSON_ASSERT (query_type || true); - BSON_ASSERT (range_opts || true); + BSON_OPTIONAL_PARAM (algorithm); + BSON_OPTIONAL_PARAM (keyid); + BSON_OPTIONAL_PARAM (keyaltname); + BSON_OPTIONAL_PARAM (query_type); + BSON_OPTIONAL_PARAM (range_opts); BSON_ASSERT_PARAM (expr_in); BSON_ASSERT_PARAM (expr_out); - BSON_ASSERT (error || true); + BSON_OPTIONAL_PARAM (error); _state_machine_t *state_machine = NULL; bson_t *to_encrypt_doc = NULL; diff --git a/src/libmongoc/src/mongoc/mongoc-database.c b/src/libmongoc/src/mongoc/mongoc-database.c index 4e4e28f98b6..428a2185f1d 100644 --- a/src/libmongoc/src/mongoc/mongoc-database.c +++ b/src/libmongoc/src/mongoc/mongoc-database.c @@ -1197,9 +1197,9 @@ _mongoc_get_collection_encryptedFields (mongoc_client_t *client, BSON_ASSERT_PARAM (client); BSON_ASSERT_PARAM (dbName); BSON_ASSERT_PARAM (collName); - BSON_ASSERT (opts || true); + BSON_OPTIONAL_PARAM (opts); BSON_ASSERT_PARAM (encryptedFields); - BSON_ASSERT (error || true); + BSON_OPTIONAL_PARAM (error); bson_init (encryptedFields); // Initially empty @@ -1248,8 +1248,8 @@ mongoc_database_create_collection (mongoc_database_t *database, { BSON_ASSERT_PARAM (database); BSON_ASSERT_PARAM (name); - BSON_ASSERT (opts || true); - BSON_ASSERT (error || true); + BSON_OPTIONAL_PARAM (opts); + BSON_OPTIONAL_PARAM (error); bson_t encryptedFields = BSON_INITIALIZER; if (!_mongoc_get_collection_encryptedFields (database->client, diff --git a/src/libmongoc/src/mongoc/mongoc-opts.c b/src/libmongoc/src/mongoc/mongoc-opts.c index aa2ad00573a..b296f50daae 100644 --- a/src/libmongoc/src/mongoc/mongoc-opts.c +++ b/src/libmongoc/src/mongoc/mongoc-opts.c @@ -23,7 +23,7 @@ _mongoc_insert_one_opts_parse ( { bson_iter_t iter; - BSON_ASSERT (client || true); // client may be NULL. + BSON_OPTIONAL_PARAM (client); // client may be NULL. mongoc_insert_one_opts->crud.writeConcern = NULL; mongoc_insert_one_opts->crud.write_concern_owned = false; @@ -130,7 +130,7 @@ _mongoc_insert_many_opts_parse ( { bson_iter_t iter; - BSON_ASSERT (client || true); // client may be NULL. + BSON_OPTIONAL_PARAM (client); // client may be NULL. mongoc_insert_many_opts->crud.writeConcern = NULL; mongoc_insert_many_opts->crud.write_concern_owned = false; @@ -247,7 +247,7 @@ _mongoc_delete_one_opts_parse ( { bson_iter_t iter; - BSON_ASSERT (client || true); // client may be NULL. + BSON_OPTIONAL_PARAM (client); // client may be NULL. mongoc_delete_one_opts->delete.crud.writeConcern = NULL; mongoc_delete_one_opts->delete.crud.write_concern_owned = false; @@ -377,7 +377,7 @@ _mongoc_delete_many_opts_parse ( { bson_iter_t iter; - BSON_ASSERT (client || true); // client may be NULL. + BSON_OPTIONAL_PARAM (client); // client may be NULL. mongoc_delete_many_opts->delete.crud.writeConcern = NULL; mongoc_delete_many_opts->delete.crud.write_concern_owned = false; @@ -507,7 +507,7 @@ _mongoc_update_one_opts_parse ( { bson_iter_t iter; - BSON_ASSERT (client || true); // client may be NULL. + BSON_OPTIONAL_PARAM (client); // client may be NULL. mongoc_update_one_opts->update.crud.writeConcern = NULL; mongoc_update_one_opts->update.crud.write_concern_owned = false; @@ -668,7 +668,7 @@ _mongoc_update_many_opts_parse ( { bson_iter_t iter; - BSON_ASSERT (client || true); // client may be NULL. + BSON_OPTIONAL_PARAM (client); // client may be NULL. mongoc_update_many_opts->update.crud.writeConcern = NULL; mongoc_update_many_opts->update.crud.write_concern_owned = false; @@ -829,7 +829,7 @@ _mongoc_replace_one_opts_parse ( { bson_iter_t iter; - BSON_ASSERT (client || true); // client may be NULL. + BSON_OPTIONAL_PARAM (client); // client may be NULL. mongoc_replace_one_opts->update.crud.writeConcern = NULL; mongoc_replace_one_opts->update.crud.write_concern_owned = false; @@ -979,7 +979,7 @@ _mongoc_bulk_opts_parse ( { bson_iter_t iter; - BSON_ASSERT (client || true); // client may be NULL. + BSON_OPTIONAL_PARAM (client); // client may be NULL. mongoc_bulk_opts->writeConcern = NULL; mongoc_bulk_opts->write_concern_owned = false; @@ -1082,7 +1082,7 @@ _mongoc_bulk_insert_opts_parse ( { bson_iter_t iter; - BSON_ASSERT (client || true); // client may be NULL. + BSON_OPTIONAL_PARAM (client); // client may be NULL. mongoc_bulk_insert_opts->validate = _mongoc_default_insert_vflags; bson_init (&mongoc_bulk_insert_opts->extra); @@ -1137,7 +1137,7 @@ _mongoc_bulk_update_one_opts_parse ( { bson_iter_t iter; - BSON_ASSERT (client || true); // client may be NULL. + BSON_OPTIONAL_PARAM (client); // client may be NULL. mongoc_bulk_update_one_opts->update.validate = _mongoc_default_update_vflags; bson_init (&mongoc_bulk_update_one_opts->update.collation); @@ -1245,7 +1245,7 @@ _mongoc_bulk_update_many_opts_parse ( { bson_iter_t iter; - BSON_ASSERT (client || true); // client may be NULL. + BSON_OPTIONAL_PARAM (client); // client may be NULL. mongoc_bulk_update_many_opts->update.validate = _mongoc_default_update_vflags; bson_init (&mongoc_bulk_update_many_opts->update.collation); @@ -1353,7 +1353,7 @@ _mongoc_bulk_replace_one_opts_parse ( { bson_iter_t iter; - BSON_ASSERT (client || true); // client may be NULL. + BSON_OPTIONAL_PARAM (client); // client may be NULL. mongoc_bulk_replace_one_opts->update.validate = _mongoc_default_replace_vflags; bson_init (&mongoc_bulk_replace_one_opts->update.collation); @@ -1450,7 +1450,7 @@ _mongoc_bulk_remove_one_opts_parse ( { bson_iter_t iter; - BSON_ASSERT (client || true); // client may be NULL. + BSON_OPTIONAL_PARAM (client); // client may be NULL. bson_init (&mongoc_bulk_remove_one_opts->remove.collation); memset (&mongoc_bulk_remove_one_opts->remove.hint, 0, sizeof (bson_value_t)); @@ -1527,7 +1527,7 @@ _mongoc_bulk_remove_many_opts_parse ( { bson_iter_t iter; - BSON_ASSERT (client || true); // client may be NULL. + BSON_OPTIONAL_PARAM (client); // client may be NULL. bson_init (&mongoc_bulk_remove_many_opts->remove.collation); memset (&mongoc_bulk_remove_many_opts->remove.hint, 0, sizeof (bson_value_t)); @@ -1604,7 +1604,7 @@ _mongoc_change_stream_opts_parse ( { bson_iter_t iter; - BSON_ASSERT (client || true); // client may be NULL. + BSON_OPTIONAL_PARAM (client); // client may be NULL. mongoc_change_stream_opts->batchSize = 0; bson_init (&mongoc_change_stream_opts->resumeAfter); @@ -1747,7 +1747,7 @@ _mongoc_create_index_opts_parse ( { bson_iter_t iter; - BSON_ASSERT (client || true); // client may be NULL. + BSON_OPTIONAL_PARAM (client); // client may be NULL. mongoc_create_index_opts->writeConcern = NULL; mongoc_create_index_opts->write_concern_owned = false; @@ -1823,7 +1823,7 @@ _mongoc_read_write_opts_parse ( { bson_iter_t iter; - BSON_ASSERT (client || true); // client may be NULL. + BSON_OPTIONAL_PARAM (client); // client may be NULL. bson_init (&mongoc_read_write_opts->readConcern); mongoc_read_write_opts->writeConcern = NULL; @@ -1931,7 +1931,7 @@ _mongoc_gridfs_bucket_opts_parse ( { bson_iter_t iter; - BSON_ASSERT (client || true); // client may be NULL. + BSON_OPTIONAL_PARAM (client); // client may be NULL. mongoc_gridfs_bucket_opts->bucketName = "fs"; mongoc_gridfs_bucket_opts->chunkSizeBytes = 261120; @@ -2028,7 +2028,7 @@ _mongoc_gridfs_bucket_upload_opts_parse ( { bson_iter_t iter; - BSON_ASSERT (client || true); // client may be NULL. + BSON_OPTIONAL_PARAM (client); // client may be NULL. mongoc_gridfs_bucket_upload_opts->chunkSizeBytes = 0; bson_init (&mongoc_gridfs_bucket_upload_opts->metadata); @@ -2099,7 +2099,7 @@ _mongoc_aggregate_opts_parse ( { bson_iter_t iter; - BSON_ASSERT (client || true); // client may be NULL. + BSON_OPTIONAL_PARAM (client); // client may be NULL. mongoc_aggregate_opts->readConcern = NULL; mongoc_aggregate_opts->writeConcern = NULL; @@ -2263,7 +2263,7 @@ _mongoc_find_and_modify_appended_opts_parse ( { bson_iter_t iter; - BSON_ASSERT (client || true); // client may be NULL. + BSON_OPTIONAL_PARAM (client); // client may be NULL. mongoc_find_and_modify_appended_opts->writeConcern = NULL; mongoc_find_and_modify_appended_opts->write_concern_owned = false; @@ -2372,7 +2372,7 @@ _mongoc_count_document_opts_parse ( { bson_iter_t iter; - BSON_ASSERT (client || true); // client may be NULL. + BSON_OPTIONAL_PARAM (client); // client may be NULL. bson_init (&mongoc_count_document_opts->readConcern); mongoc_count_document_opts->client_session = NULL; diff --git a/src/libmongoc/src/mongoc/mongoc-write-command.c b/src/libmongoc/src/mongoc/mongoc-write-command.c index 7f6606aa0de..c8cd7554c2a 100644 --- a/src/libmongoc/src/mongoc/mongoc-write-command.c +++ b/src/libmongoc/src/mongoc/mongoc-write-command.c @@ -561,7 +561,6 @@ _mongoc_write_opmsg (mongoc_write_command_t *command, int32_t max_msg_size; int32_t max_bson_obj_size; int32_t max_document_count; - uint32_t header; uint32_t payload_batch_size = 0; uint32_t payload_total_offset = 0; bool ship_it = false; @@ -618,18 +617,18 @@ _mongoc_write_opmsg (mongoc_write_command_t *command, EXIT; } - /* - * OP_MSG header == 16 byte - * + 4 bytes flagBits - * + 1 byte payload type = 1 - * + 1 byte payload type = 2 - * + 4 byte size of payload - * == 26 bytes opcode overhead - * + X Full command document {insert: "test", writeConcern: {...}} - * + Y command identifier ("documents", "deletes", "updates") ( + \0) - */ - - header = 26 + parts.assembled.command->len + gCommandFieldLens[command->type] + 1; + // Calculate overhead of OP_MSG data. See OP_MSG spec for description of fields. + uint32_t opmsg_overhead = 0; + { + opmsg_overhead += 16; // OP_MSG.MsgHeader + opmsg_overhead += 4; // OP_MSG.flagBits + opmsg_overhead += 1; // OP_MSG.Section[0].payloadType (0) + opmsg_overhead += parts.assembled.command->len; // OP_MSG.Section[0].payload.document + opmsg_overhead += 1; // OP_MSG.Section[1].payloadType (1) + opmsg_overhead += 4; // OP_MSG.Section[1].payload.size + opmsg_overhead += gCommandFieldLens[command->type] + 1; // OP_MSG.Section[1].payload.identifier + // OP_MSG.Section[1].payload.documents is omitted. Calculated below with remaining size. + } do { uint32_t ulen; @@ -646,7 +645,8 @@ _mongoc_write_opmsg (mongoc_write_command_t *command, result->failed = true; break; - } else if (bson_cmp_less_equal_us (payload_batch_size + header + ulen, max_msg_size) || document_count == 0) { + } else if (bson_cmp_less_equal_us (payload_batch_size + opmsg_overhead + ulen, max_msg_size) || + document_count == 0) { /* The current batch is still under max batch size in bytes */ payload_batch_size += ulen; diff --git a/src/libmongoc/src/mongoc/mongoc.h b/src/libmongoc/src/mongoc/mongoc.h index a43304cec7c..f21d3381075 100644 --- a/src/libmongoc/src/mongoc/mongoc.h +++ b/src/libmongoc/src/mongoc/mongoc.h @@ -25,6 +25,7 @@ #include "mongoc-macros.h" #include "mongoc-apm.h" #include "mongoc-bulk-operation.h" +#include "mongoc-bulkwrite.h" #include "mongoc-change-stream.h" #include "mongoc-client.h" #include "mongoc-client-pool.h" diff --git a/src/libmongoc/tests/TestSuite.c b/src/libmongoc/tests/TestSuite.c index d44a4af4e96..c7d8857c75a 100644 --- a/src/libmongoc/tests/TestSuite.c +++ b/src/libmongoc/tests/TestSuite.c @@ -1226,3 +1226,22 @@ bson_value_eq (const bson_value_t *a, const bson_value_t *b) bson_destroy (tmp_a); return ret; } + +const char * +test_bulkwriteexception_str (const mongoc_bulkwriteexception_t *bwe) +{ + bson_error_t _error; + const char *_msg = "(none)"; + if (mongoc_bulkwriteexception_error (bwe, &_error)) { + _msg = _error.message; + } + return tmp_str ("Bulk Write Exception:\n" + " Error : %s\n" + " Write Errors : %s\n" + " Write Concern Errors : %s\n" + " Error Reply : %s", + _msg, + tmp_json (mongoc_bulkwriteexception_writeerrors (bwe)), + tmp_json (mongoc_bulkwriteexception_writeconcernerrors (bwe)), + tmp_json (mongoc_bulkwriteexception_errorreply (bwe))); +} diff --git a/src/libmongoc/tests/TestSuite.h b/src/libmongoc/tests/TestSuite.h index 26d2161bb08..b84d8602023 100644 --- a/src/libmongoc/tests/TestSuite.h +++ b/src/libmongoc/tests/TestSuite.h @@ -577,6 +577,18 @@ bson_value_eq (const bson_value_t *a, const bson_value_t *b); } else \ (void) 0 +// `test_bulkwriteexception_tostring` returns a temporary string that does not need to be freed. +const char * +test_bulkwriteexception_str (const mongoc_bulkwriteexception_t *bwe); + +#define ASSERT_NO_BULKWRITEEXCEPTION(bwr) \ + if (bwr.exc) { \ + const char *_str = test_bulkwriteexception_str (bwr.exc); \ + test_error ("Expected no bulk write exception, but got:\n%s", _str); \ + } else \ + (void) 0 + + #define MAX_TEST_NAME_LENGTH 500 #define MAX_TEST_CHECK_FUNCS 10 diff --git a/src/libmongoc/tests/bsonutil/bson-match.c b/src/libmongoc/tests/bsonutil/bson-match.c index adf5ccfa986..80c182a2a55 100644 --- a/src/libmongoc/tests/bsonutil/bson-match.c +++ b/src/libmongoc/tests/bsonutil/bson-match.c @@ -32,7 +32,7 @@ struct _bson_matcher_t { special_functor_t *specials; }; -#define MATCH_ERR(format, ...) test_set_error (error, "match error at '%s': " format, path, __VA_ARGS__) +#define MATCH_ERR(format, ...) test_set_error (error, "match error at path: '%s': " format, path, __VA_ARGS__) static char * get_first_key (const bson_t *bson) @@ -425,7 +425,7 @@ bson_matcher_match (bson_matcher_t *matcher, } if (NULL == actual_val) { - MATCH_ERR ("key %s is not present", key); + MATCH_ERR ("key '%s' is not present", key); bson_val_destroy (expected_val); bson_val_destroy (actual_val); goto done; @@ -570,7 +570,9 @@ test_match (void) {"$$type array match", "{'a': {'$$type': ['string', 'int']}}", "{'a': 1}", true}, {"$$type array mismatch", "{'a': {'$$type': ['string', 'int']}}", "{'a': 1.2}", false}, {"extra keys in root ok", "{'a': 1}", "{'a': 1, 'b': 2}", true}, - {"extra keys in subdoc not ok", "{'a': {'b': 1}}", "{'a': {'b': 1, 'c': 2}}", false}}; + {"extra keys in subdoc not ok", "{'a': {'b': 1}}", "{'a': {'b': 1, 'c': 2}}", false}, + {"numeric type mismatch is ok", "{'a': 1}", "{'a': 1.0}", true}, + {"comparing number to string is an error", "{'a': 1}", "{'a': 'foo'}", false}}; int i; for (i = 0; i < sizeof (tests) / sizeof (testcase_t); i++) { diff --git a/src/libmongoc/tests/bsonutil/bson-val.c b/src/libmongoc/tests/bsonutil/bson-val.c index ab6875ac098..ea3719c1c4c 100644 --- a/src/libmongoc/tests/bsonutil/bson-val.c +++ b/src/libmongoc/tests/bsonutil/bson-val.c @@ -179,7 +179,11 @@ bson_val_eq (const bson_val_t *a, const bson_val_t *b, bson_val_comparison_flags vtype = a->type; if (vtype == BSON_TYPE_DOUBLE || vtype == BSON_TYPE_INT32 || vtype == BSON_TYPE_INT64) { if (flags & BSON_VAL_FLEXIBLE_NUMERICS) { - return bson_val_convert_int64 (a) == bson_val_convert_int64 (b); + bson_type_t vtype_b = b->type; + if (vtype_b == BSON_TYPE_INT32 || vtype_b == BSON_TYPE_INT64 || vtype_b == BSON_TYPE_DOUBLE) { + return bson_val_convert_int64 (a) == bson_val_convert_int64 (b); + } + // Otherwise fall through to propagate an error on a type mismatch. } } diff --git a/src/libmongoc/tests/json/command-logging-and-monitoring/monitoring/unacknowledged-client-bulkWrite.json b/src/libmongoc/tests/json/command-logging-and-monitoring/monitoring/unacknowledged-client-bulkWrite.json new file mode 100644 index 00000000000..d6153f444ec --- /dev/null +++ b/src/libmongoc/tests/json/command-logging-and-monitoring/monitoring/unacknowledged-client-bulkWrite.json @@ -0,0 +1,186 @@ +{ + "description": "unacknowledged-client-bulkWrite", + "schemaVersion": "1.0", + "createEntities": [ + { + "client": { + "id": "client", + "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": {} + } + } + } + ], + "expectEvents": [ + { + "client": "client", + "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/src/libmongoc/tests/json/crud/unified/client-bulkWrite-delete-options.json b/src/libmongoc/tests/json/crud/unified/client-bulkWrite-delete-options.json new file mode 100644 index 00000000000..5bdf2b124a3 --- /dev/null +++ b/src/libmongoc/tests/json/crud/unified/client-bulkWrite-delete-options.json @@ -0,0 +1,267 @@ +{ + "description": "client bulkWrite delete options", + "schemaVersion": "1.1", + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ], + "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/src/libmongoc/tests/json/crud/unified/client-bulkWrite-errorResponse.json b/src/libmongoc/tests/json/crud/unified/client-bulkWrite-errorResponse.json new file mode 100644 index 00000000000..edf2339d8a3 --- /dev/null +++ b/src/libmongoc/tests/json/crud/unified/client-bulkWrite-errorResponse.json @@ -0,0 +1,68 @@ +{ + "description": "client bulkWrite errorResponse", + "schemaVersion": "1.12", + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ], + "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/src/libmongoc/tests/json/crud/unified/client-bulkWrite-errors.json b/src/libmongoc/tests/json/crud/unified/client-bulkWrite-errors.json new file mode 100644 index 00000000000..0c38499732b --- /dev/null +++ b/src/libmongoc/tests/json/crud/unified/client-bulkWrite-errors.json @@ -0,0 +1,454 @@ +{ + "description": "client bulkWrite errors", + "schemaVersion": "1.20", + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ], + "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/src/libmongoc/tests/json/crud/unified/client-bulkWrite-mixed-namespaces.json b/src/libmongoc/tests/json/crud/unified/client-bulkWrite-mixed-namespaces.json new file mode 100644 index 00000000000..f90755dc850 --- /dev/null +++ b/src/libmongoc/tests/json/crud/unified/client-bulkWrite-mixed-namespaces.json @@ -0,0 +1,314 @@ +{ + "description": "client bulkWrite with mixed namespaces", + "schemaVersion": "1.1", + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ], + "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/src/libmongoc/tests/json/crud/unified/client-bulkWrite-options.json b/src/libmongoc/tests/json/crud/unified/client-bulkWrite-options.json new file mode 100644 index 00000000000..a1e6af3bf3b --- /dev/null +++ b/src/libmongoc/tests/json/crud/unified/client-bulkWrite-options.json @@ -0,0 +1,715 @@ +{ + "description": "client bulkWrite top-level options", + "schemaVersion": "1.1", + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ], + "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/src/libmongoc/tests/json/crud/unified/client-bulkWrite-ordered.json b/src/libmongoc/tests/json/crud/unified/client-bulkWrite-ordered.json new file mode 100644 index 00000000000..a55d6619b5e --- /dev/null +++ b/src/libmongoc/tests/json/crud/unified/client-bulkWrite-ordered.json @@ -0,0 +1,290 @@ +{ + "description": "client bulkWrite with ordered option", + "schemaVersion": "1.1", + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ], + "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/src/libmongoc/tests/json/crud/unified/client-bulkWrite-results.json b/src/libmongoc/tests/json/crud/unified/client-bulkWrite-results.json new file mode 100644 index 00000000000..97a9e50b217 --- /dev/null +++ b/src/libmongoc/tests/json/crud/unified/client-bulkWrite-results.json @@ -0,0 +1,832 @@ +{ + "description": "client bulkWrite results", + "schemaVersion": "1.1", + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ], + "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/src/libmongoc/tests/json/crud/unified/client-bulkWrite-update-options.json b/src/libmongoc/tests/json/crud/unified/client-bulkWrite-update-options.json new file mode 100644 index 00000000000..93a2774e5fa --- /dev/null +++ b/src/libmongoc/tests/json/crud/unified/client-bulkWrite-update-options.json @@ -0,0 +1,948 @@ +{ + "description": "client bulkWrite update options", + "schemaVersion": "1.1", + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ], + "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/src/libmongoc/tests/json/crud/unified/client-bulkWrite-update-pipeline.json b/src/libmongoc/tests/json/crud/unified/client-bulkWrite-update-pipeline.json new file mode 100644 index 00000000000..57b6c9c1ba4 --- /dev/null +++ b/src/libmongoc/tests/json/crud/unified/client-bulkWrite-update-pipeline.json @@ -0,0 +1,257 @@ +{ + "description": "client bulkWrite update pipeline", + "schemaVersion": "1.1", + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ], + "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/src/libmongoc/tests/json/crud/unified/client-bulkWrite-update-validation.json b/src/libmongoc/tests/json/crud/unified/client-bulkWrite-update-validation.json new file mode 100644 index 00000000000..1ac3e8d0488 --- /dev/null +++ b/src/libmongoc/tests/json/crud/unified/client-bulkWrite-update-validation.json @@ -0,0 +1,216 @@ +{ + "description": "client-bulkWrite-update-validation", + "schemaVersion": "1.0", + "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/src/libmongoc/tests/json/retryable_writes/unified/client-bulkWrite-clientErrors.json b/src/libmongoc/tests/json/retryable_writes/unified/client-bulkWrite-clientErrors.json new file mode 100644 index 00000000000..64b1988ffdf --- /dev/null +++ b/src/libmongoc/tests/json/retryable_writes/unified/client-bulkWrite-clientErrors.json @@ -0,0 +1,350 @@ +{ + "description": "client bulkWrite retryable writes with client errors", + "schemaVersion": "1.20", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "topologies": [ + "replicaset", + "sharded", + "load-balanced" + ] + } + ], + "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/src/libmongoc/tests/json/retryable_writes/unified/client-bulkWrite-serverErrors.json b/src/libmongoc/tests/json/retryable_writes/unified/client-bulkWrite-serverErrors.json new file mode 100644 index 00000000000..f9812241f3c --- /dev/null +++ b/src/libmongoc/tests/json/retryable_writes/unified/client-bulkWrite-serverErrors.json @@ -0,0 +1,872 @@ +{ + "description": "client bulkWrite retryable writes", + "schemaVersion": "1.20", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "topologies": [ + "replicaset", + "sharded", + "load-balanced" + ] + } + ], + "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/src/libmongoc/tests/json/retryable_writes/unified/handshakeError.json b/src/libmongoc/tests/json/retryable_writes/unified/handshakeError.json index df37bd72322..3c464637598 100644 --- a/src/libmongoc/tests/json/retryable_writes/unified/handshakeError.json +++ b/src/libmongoc/tests/json/retryable_writes/unified/handshakeError.json @@ -53,6 +53,222 @@ } ], "tests": [ + { + "description": "client.clientBulkWrite succeeds after retryable handshake network error", + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ], + "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" + } + ], + "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/src/libmongoc/tests/json/transactions/unified/client-bulkWrite.json b/src/libmongoc/tests/json/transactions/unified/client-bulkWrite.json new file mode 100644 index 00000000000..f8f1d97169d --- /dev/null +++ b/src/libmongoc/tests/json/transactions/unified/client-bulkWrite.json @@ -0,0 +1,592 @@ +{ + "description": "client bulkWrite transactions", + "schemaVersion": "1.3", + "runOnRequirements": [ + { + "minServerVersion": "8.0", + "topologies": [ + "replicaset", + "sharded", + "load-balanced" + ] + } + ], + "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/src/libmongoc/tests/json/transactions/unified/mongos-pin-auto.json b/src/libmongoc/tests/json/transactions/unified/mongos-pin-auto.json new file mode 100644 index 00000000000..27db5204011 --- /dev/null +++ b/src/libmongoc/tests/json/transactions/unified/mongos-pin-auto.json @@ -0,0 +1,5474 @@ +{ + "description": "mongos-pin-auto", + "schemaVersion": "1.4", + "runOnRequirements": [ + { + "minServerVersion": "4.1.8", + "topologies": [ + "sharded" + ], + "serverless": "forbid" + } + ], + "createEntities": [ + { + "client": { + "id": "client0", + "useMultipleMongoses": true, + "observeEvents": [ + "commandStartedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "transaction-tests" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "test" + } + }, + { + "session": { + "id": "session0", + "client": "client0" + } + } + ], + "initialData": [ + { + "collectionName": "test", + "databaseName": "transaction-tests", + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + } + ] + } + ], + "tests": [ + { + "description": "remain pinned after non-transient Interrupted error on insertOne", + "operations": [ + { + "object": "session0", + "name": "startTransaction" + }, + { + "object": "collection0", + "name": "insertOne", + "arguments": { + "session": "session0", + "document": { + "_id": 3 + } + }, + "expectResult": { + "$$unsetOrMatches": { + "insertedId": { + "$$unsetOrMatches": 3 + } + } + } + }, + { + "object": "testRunner", + "name": "targetedFailPoint", + "arguments": { + "session": "session0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 11601 + } + } + } + }, + { + "object": "collection0", + "name": "insertOne", + "arguments": { + "session": "session0", + "document": { + "_id": 4 + } + }, + "expectError": { + "errorLabelsOmit": [ + "TransientTransactionError", + "UnknownTransactionCommitResult" + ], + "errorCodeName": "Interrupted" + } + }, + { + "object": "testRunner", + "name": "assertSessionPinned", + "arguments": { + "session": "session0" + } + }, + { + "object": "session0", + "name": "commitTransaction" + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 3 + } + ], + "ordered": true, + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + }, + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": { + "$$exists": false + } + }, + "commandName": "insert", + "databaseName": "transaction-tests" + } + }, + { + "commandStartedEvent": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 4 + } + ], + "ordered": true, + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + }, + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": { + "$$exists": false + }, + "autocommit": false, + "writeConcern": { + "$$exists": false + } + }, + "commandName": "insert", + "databaseName": "transaction-tests" + } + }, + { + "commandStartedEvent": { + "command": { + "commitTransaction": 1, + "lsid": { + "$$sessionLsid": "session0" + }, + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": { + "$$exists": false + }, + "autocommit": false, + "writeConcern": { + "$$exists": false + }, + "recoveryToken": { + "$$exists": true + } + }, + "commandName": "commitTransaction", + "databaseName": "admin" + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "test", + "databaseName": "transaction-tests", + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + }, + { + "_id": 3 + } + ] + } + ] + }, + { + "description": "unpin after transient error within a transaction", + "operations": [ + { + "object": "session0", + "name": "startTransaction" + }, + { + "object": "collection0", + "name": "insertOne", + "arguments": { + "session": "session0", + "document": { + "_id": 3 + } + }, + "expectResult": { + "$$unsetOrMatches": { + "insertedId": { + "$$unsetOrMatches": 3 + } + } + } + }, + { + "object": "testRunner", + "name": "targetedFailPoint", + "arguments": { + "session": "session0", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "closeConnection": true + } + } + } + }, + { + "object": "collection0", + "name": "insertOne", + "arguments": { + "session": "session0", + "document": { + "_id": 4 + } + }, + "expectError": { + "errorLabelsContain": [ + "TransientTransactionError" + ], + "errorLabelsOmit": [ + "UnknownTransactionCommitResult" + ] + } + }, + { + "object": "testRunner", + "name": "assertSessionUnpinned", + "arguments": { + "session": "session0" + } + }, + { + "object": "session0", + "name": "abortTransaction" + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 3 + } + ], + "ordered": true, + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + }, + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": true, + "autocommit": false, + "writeConcern": { + "$$exists": false + } + }, + "commandName": "insert", + "databaseName": "transaction-tests" + } + }, + { + "commandStartedEvent": { + "command": { + "insert": "test", + "documents": [ + { + "_id": 4 + } + ], + "ordered": true, + "readConcern": { + "$$exists": false + }, + "lsid": { + "$$sessionLsid": "session0" + }, + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": { + "$$exists": false + }, + "autocommit": false, + "writeConcern": { + "$$exists": false + } + }, + "commandName": "insert", + "databaseName": "transaction-tests" + } + }, + { + "commandStartedEvent": { + "command": { + "abortTransaction": 1, + "lsid": { + "$$sessionLsid": "session0" + }, + "txnNumber": { + "$numberLong": "1" + }, + "startTransaction": { + "$$exists": false + }, + "autocommit": false, + "writeConcern": { + "$$exists": false + }, + "recoveryToken": { + "$$exists": true + } + }, + "commandName": "abortTransaction", + "databaseName": "admin" + } + } + ] + } + ], + "outcome": [ + { + "collectionName": "test", + "databaseName": "transaction-tests", + "documents": [ + { + "_id": 1 + }, + { + "_id": 2 + } + ] + } + ] + }, + { + "description": "remain pinned after non-transient Interrupted error on insertOne insert", + "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": [ + "insert" + ], + "errorCode": 11601 + } + } + } + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "session": "session0", + "document": { + "_id": 4 + } + }, + "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 + } + ] + } + ] + }, + { + "description": "remain pinned after non-transient Interrupted error on insertMany insert", + "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": [ + "insert" + ], + "errorCode": 11601 + } + } + } + }, + { + "name": "insertMany", + "object": "collection0", + "arguments": { + "session": "session0", + "documents": [ + { + "_id": 4 + }, + { + "_id": 5 + } + ] + }, + "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 + } + ] + } + ] + }, + { + "description": "remain pinned after non-transient Interrupted error on updateOne update", + "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": [ + "update" + ], + "errorCode": 11601 + } + } + } + }, + { + "name": "updateOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + } + }, + "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 + } + ] + } + ] + }, + { + "description": "remain pinned after non-transient Interrupted error on replaceOne update", + "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": [ + "update" + ], + "errorCode": 11601 + } + } + } + }, + { + "name": "replaceOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + }, + "replacement": { + "y": 1 + } + }, + "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 + } + ] + } + ] + }, + { + "description": "remain pinned after non-transient Interrupted error on updateMany update", + "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": [ + "update" + ], + "errorCode": 11601 + } + } + } + }, + { + "name": "updateMany", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": { + "$gte": 1 + } + }, + "update": { + "$set": { + "z": 1 + } + } + }, + "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 + } + ] + } + ] + }, + { + "description": "remain pinned after non-transient Interrupted error on deleteOne delete", + "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": [ + "delete" + ], + "errorCode": 11601 + } + } + } + }, + { + "name": "deleteOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "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 + } + ] + } + ] + }, + { + "description": "remain pinned after non-transient Interrupted error on deleteMany delete", + "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": [ + "delete" + ], + "errorCode": 11601 + } + } + } + }, + { + "name": "deleteMany", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": { + "$gte": 1 + } + } + }, + "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 + } + ] + } + ] + }, + { + "description": "remain pinned after non-transient Interrupted error on findOneAndDelete findAndModify", + "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": [ + "findAndModify" + ], + "errorCode": 11601 + } + } + } + }, + { + "name": "findOneAndDelete", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "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 + } + ] + } + ] + }, + { + "description": "remain pinned after non-transient Interrupted error on findOneAndUpdate findAndModify", + "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": [ + "findAndModify" + ], + "errorCode": 11601 + } + } + } + }, + { + "name": "findOneAndUpdate", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "returnDocument": "Before" + }, + "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 + } + ] + } + ] + }, + { + "description": "remain pinned after non-transient Interrupted error on findOneAndReplace findAndModify", + "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": [ + "findAndModify" + ], + "errorCode": 11601 + } + } + } + }, + { + "name": "findOneAndReplace", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + }, + "replacement": { + "y": 1 + }, + "returnDocument": "Before" + }, + "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 + } + ] + } + ] + }, + { + "description": "remain pinned after non-transient Interrupted error on bulkWrite insert", + "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": [ + "insert" + ], + "errorCode": 11601 + } + } + } + }, + { + "name": "bulkWrite", + "object": "collection0", + "arguments": { + "session": "session0", + "requests": [ + { + "insertOne": { + "document": { + "_id": 1 + } + } + } + ] + }, + "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 + } + ] + } + ] + }, + { + "description": "remain pinned after non-transient Interrupted error on bulkWrite update", + "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": [ + "update" + ], + "errorCode": 11601 + } + } + } + }, + { + "name": "bulkWrite", + "object": "collection0", + "arguments": { + "session": "session0", + "requests": [ + { + "updateOne": { + "filter": { + "_id": 1 + }, + "update": { + "$set": { + "x": 1 + } + } + } + } + ] + }, + "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 + } + ] + } + ] + }, + { + "description": "remain pinned after non-transient Interrupted error on bulkWrite delete", + "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": [ + "delete" + ], + "errorCode": 11601 + } + } + } + }, + { + "name": "bulkWrite", + "object": "collection0", + "arguments": { + "session": "session0", + "requests": [ + { + "deleteOne": { + "filter": { + "_id": 1 + } + } + } + ] + }, + "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 + } + ] + } + ] + }, + { + "description": "remain pinned after non-transient Interrupted error on find find", + "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": [ + "find" + ], + "errorCode": 11601 + } + } + } + }, + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "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 + } + ] + } + ] + }, + { + "description": "remain pinned after non-transient Interrupted error on countDocuments aggregate", + "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": [ + "aggregate" + ], + "errorCode": 11601 + } + } + } + }, + { + "name": "countDocuments", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": {} + }, + "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 + } + ] + } + ] + }, + { + "description": "remain pinned after non-transient Interrupted error on aggregate aggregate", + "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": [ + "aggregate" + ], + "errorCode": 11601 + } + } + } + }, + { + "name": "aggregate", + "object": "collection0", + "arguments": { + "session": "session0", + "pipeline": [] + }, + "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 + } + ] + } + ] + }, + { + "description": "remain pinned after non-transient Interrupted error on distinct distinct", + "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": [ + "distinct" + ], + "errorCode": 11601 + } + } + } + }, + { + "name": "distinct", + "object": "collection0", + "arguments": { + "session": "session0", + "fieldName": "_id", + "filter": {} + }, + "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 + } + ] + } + ] + }, + { + "description": "remain pinned after non-transient Interrupted error on runCommand insert", + "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": [ + "insert" + ], + "errorCode": 11601 + } + } + } + }, + { + "name": "runCommand", + "object": "database0", + "arguments": { + "session": "session0", + "commandName": "insert", + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ] + } + }, + "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 + } + ] + } + ] + }, + { + "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": [ + { + "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": [ + "insert" + ], + "closeConnection": true + } + } + } + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "session": "session0", + "document": { + "_id": 4 + } + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient ShutdownInProgress error on insertOne insert", + "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": [ + "insert" + ], + "errorCode": 91 + } + } + } + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "session": "session0", + "document": { + "_id": 4 + } + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient connection error on insertMany insert", + "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": [ + "insert" + ], + "closeConnection": true + } + } + } + }, + { + "name": "insertMany", + "object": "collection0", + "arguments": { + "session": "session0", + "documents": [ + { + "_id": 4 + }, + { + "_id": 5 + } + ] + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient ShutdownInProgress error on insertMany insert", + "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": [ + "insert" + ], + "errorCode": 91 + } + } + } + }, + { + "name": "insertMany", + "object": "collection0", + "arguments": { + "session": "session0", + "documents": [ + { + "_id": 4 + }, + { + "_id": 5 + } + ] + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient connection error on updateOne update", + "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": [ + "update" + ], + "closeConnection": true + } + } + } + }, + { + "name": "updateOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + } + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient ShutdownInProgress error on updateOne update", + "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": [ + "update" + ], + "errorCode": 91 + } + } + } + }, + { + "name": "updateOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + } + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient connection error on replaceOne update", + "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": [ + "update" + ], + "closeConnection": true + } + } + } + }, + { + "name": "replaceOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + }, + "replacement": { + "y": 1 + } + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient ShutdownInProgress error on replaceOne update", + "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": [ + "update" + ], + "errorCode": 91 + } + } + } + }, + { + "name": "replaceOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + }, + "replacement": { + "y": 1 + } + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient connection error on updateMany update", + "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": [ + "update" + ], + "closeConnection": true + } + } + } + }, + { + "name": "updateMany", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": { + "$gte": 1 + } + }, + "update": { + "$set": { + "z": 1 + } + } + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient ShutdownInProgress error on updateMany update", + "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": [ + "update" + ], + "errorCode": 91 + } + } + } + }, + { + "name": "updateMany", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": { + "$gte": 1 + } + }, + "update": { + "$set": { + "z": 1 + } + } + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient connection error on deleteOne delete", + "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": [ + "delete" + ], + "closeConnection": true + } + } + } + }, + { + "name": "deleteOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient ShutdownInProgress error on deleteOne delete", + "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": [ + "delete" + ], + "errorCode": 91 + } + } + } + }, + { + "name": "deleteOne", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient connection error on deleteMany delete", + "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": [ + "delete" + ], + "closeConnection": true + } + } + } + }, + { + "name": "deleteMany", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": { + "$gte": 1 + } + } + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient ShutdownInProgress error on deleteMany delete", + "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": [ + "delete" + ], + "errorCode": 91 + } + } + } + }, + { + "name": "deleteMany", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": { + "$gte": 1 + } + } + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient connection error on findOneAndDelete findAndModify", + "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": [ + "findAndModify" + ], + "closeConnection": true + } + } + } + }, + { + "name": "findOneAndDelete", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient ShutdownInProgress error on findOneAndDelete findAndModify", + "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": [ + "findAndModify" + ], + "errorCode": 91 + } + } + } + }, + { + "name": "findOneAndDelete", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient connection error on findOneAndUpdate findAndModify", + "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": [ + "findAndModify" + ], + "closeConnection": true + } + } + } + }, + { + "name": "findOneAndUpdate", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "returnDocument": "Before" + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient ShutdownInProgress error on findOneAndUpdate findAndModify", + "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": [ + "findAndModify" + ], + "errorCode": 91 + } + } + } + }, + { + "name": "findOneAndUpdate", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + }, + "update": { + "$inc": { + "x": 1 + } + }, + "returnDocument": "Before" + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient connection error on findOneAndReplace findAndModify", + "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": [ + "findAndModify" + ], + "closeConnection": true + } + } + } + }, + { + "name": "findOneAndReplace", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + }, + "replacement": { + "y": 1 + }, + "returnDocument": "Before" + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient ShutdownInProgress error on findOneAndReplace findAndModify", + "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": [ + "findAndModify" + ], + "errorCode": 91 + } + } + } + }, + { + "name": "findOneAndReplace", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + }, + "replacement": { + "y": 1 + }, + "returnDocument": "Before" + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient connection error on bulkWrite insert", + "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": [ + "insert" + ], + "closeConnection": true + } + } + } + }, + { + "name": "bulkWrite", + "object": "collection0", + "arguments": { + "session": "session0", + "requests": [ + { + "insertOne": { + "document": { + "_id": 1 + } + } + } + ] + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient ShutdownInProgress error on bulkWrite insert", + "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": [ + "insert" + ], + "errorCode": 91 + } + } + } + }, + { + "name": "bulkWrite", + "object": "collection0", + "arguments": { + "session": "session0", + "requests": [ + { + "insertOne": { + "document": { + "_id": 1 + } + } + } + ] + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient connection error on bulkWrite update", + "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": [ + "update" + ], + "closeConnection": true + } + } + } + }, + { + "name": "bulkWrite", + "object": "collection0", + "arguments": { + "session": "session0", + "requests": [ + { + "updateOne": { + "filter": { + "_id": 1 + }, + "update": { + "$set": { + "x": 1 + } + } + } + } + ] + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient ShutdownInProgress error on bulkWrite update", + "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": [ + "update" + ], + "errorCode": 91 + } + } + } + }, + { + "name": "bulkWrite", + "object": "collection0", + "arguments": { + "session": "session0", + "requests": [ + { + "updateOne": { + "filter": { + "_id": 1 + }, + "update": { + "$set": { + "x": 1 + } + } + } + } + ] + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient connection error on bulkWrite delete", + "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": [ + "delete" + ], + "closeConnection": true + } + } + } + }, + { + "name": "bulkWrite", + "object": "collection0", + "arguments": { + "session": "session0", + "requests": [ + { + "deleteOne": { + "filter": { + "_id": 1 + } + } + } + ] + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient ShutdownInProgress error on bulkWrite delete", + "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": [ + "delete" + ], + "errorCode": 91 + } + } + } + }, + { + "name": "bulkWrite", + "object": "collection0", + "arguments": { + "session": "session0", + "requests": [ + { + "deleteOne": { + "filter": { + "_id": 1 + } + } + } + ] + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient connection error on find find", + "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": [ + "find" + ], + "closeConnection": true + } + } + } + }, + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient ShutdownInProgress error on find find", + "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": [ + "find" + ], + "errorCode": 91 + } + } + } + }, + { + "name": "find", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": { + "_id": 1 + } + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient connection error on countDocuments aggregate", + "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": [ + "aggregate" + ], + "closeConnection": true + } + } + } + }, + { + "name": "countDocuments", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": {} + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient ShutdownInProgress error on countDocuments aggregate", + "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": [ + "aggregate" + ], + "errorCode": 91 + } + } + } + }, + { + "name": "countDocuments", + "object": "collection0", + "arguments": { + "session": "session0", + "filter": {} + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient connection error on aggregate aggregate", + "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": [ + "aggregate" + ], + "closeConnection": true + } + } + } + }, + { + "name": "aggregate", + "object": "collection0", + "arguments": { + "session": "session0", + "pipeline": [] + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient ShutdownInProgress error on aggregate aggregate", + "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": [ + "aggregate" + ], + "errorCode": 91 + } + } + } + }, + { + "name": "aggregate", + "object": "collection0", + "arguments": { + "session": "session0", + "pipeline": [] + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient connection error on distinct distinct", + "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": [ + "distinct" + ], + "closeConnection": true + } + } + } + }, + { + "name": "distinct", + "object": "collection0", + "arguments": { + "session": "session0", + "fieldName": "_id", + "filter": {} + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient ShutdownInProgress error on distinct distinct", + "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": [ + "distinct" + ], + "errorCode": 91 + } + } + } + }, + { + "name": "distinct", + "object": "collection0", + "arguments": { + "session": "session0", + "fieldName": "_id", + "filter": {} + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient connection error on runCommand insert", + "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": [ + "insert" + ], + "closeConnection": true + } + } + } + }, + { + "name": "runCommand", + "object": "database0", + "arguments": { + "session": "session0", + "commandName": "insert", + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ] + } + }, + "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 + } + ] + } + ] + }, + { + "description": "unpin after transient ShutdownInProgress error on runCommand insert", + "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": [ + "insert" + ], + "errorCode": 91 + } + } + } + }, + { + "name": "runCommand", + "object": "database0", + "arguments": { + "session": "session0", + "commandName": "insert", + "command": { + "insert": "test", + "documents": [ + { + "_id": 1 + } + ] + } + }, + "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 + } + ] + } + ] + }, + { + "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/src/libmongoc/tests/json/versioned_api/crud-api-version-1.json b/src/libmongoc/tests/json/versioned_api/crud-api-version-1.json index a387d0587e0..fe668620f82 100644 --- a/src/libmongoc/tests/json/versioned_api/crud-api-version-1.json +++ b/src/libmongoc/tests/json/versioned_api/crud-api-version-1.json @@ -50,7 +50,8 @@ }, "apiDeprecationErrors": true } - ] + ], + "namespace": "versioned-api-tests.test" }, "initialData": [ { @@ -426,6 +427,85 @@ } ] }, + { + "description": "client bulkWrite appends declared API version", + "runOnRequirements": [ + { + "minServerVersion": "8.0" + } + ], + "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/src/libmongoc/tests/test-conveniences.c b/src/libmongoc/tests/test-conveniences.c index 4dba1337a97..71b4878d8ab 100644 --- a/src/libmongoc/tests/test-conveniences.c +++ b/src/libmongoc/tests/test-conveniences.c @@ -737,10 +737,10 @@ match_json (const bson_t *doc, if (!matches) { char *as_string = doc ? bson_as_canonical_extended_json (doc, NULL) : NULL; fprintf (stderr, - "ASSERT_MATCH failed with document:\n\n" - "%s\n" - "pattern:\n%s\n" - "%s\n" + "ASSERT_MATCH failed:\n" + "document: %s\n" + "pattern : %s\n" + "error : %s\n" "%s:%d %s()\n", as_string ? as_string : "{}", double_quoted, diff --git a/src/libmongoc/tests/test-conveniences.h b/src/libmongoc/tests/test-conveniences.h index 580d5d11d3a..67886c721b6 100644 --- a/src/libmongoc/tests/test-conveniences.h +++ b/src/libmongoc/tests/test-conveniences.h @@ -207,6 +207,23 @@ match_json (const bson_t *doc, BSON_ASSERT (match_json (doc, false, __FILE__, __LINE__, BSON_FUNC, __VA_ARGS__)); \ } while (0) +#define ASSERT_EQUAL_BSON(expected, actual) \ + do { \ + bson_t *_expected_bson = expected, *_actual_bson = actual; \ + char *_expected_str, *_actual_str; \ + _expected_str = bson_as_canonical_extended_json (_expected_bson, NULL); \ + _actual_str = bson_as_canonical_extended_json (_actual_bson, NULL); \ + if (!bson_equal (_expected_bson, _actual_bson)) { \ + test_error ("BSON unequal: \n" \ + "Expected: %s\n" \ + "Got : %s", \ + _expected_str, \ + _actual_str); \ + } \ + bson_free (_actual_str); \ + bson_free (_expected_str); \ + } while (0) + bool mongoc_write_concern_append_bad (mongoc_write_concern_t *write_concern, bson_t *command); diff --git a/src/libmongoc/tests/test-libmongoc-main.c b/src/libmongoc/tests/test-libmongoc-main.c index bbd6e76913a..f38ab7431c7 100644 --- a/src/libmongoc/tests/test-libmongoc-main.c +++ b/src/libmongoc/tests/test-libmongoc-main.c @@ -154,6 +154,7 @@ main (int argc, char *argv[]) TEST_INSTALL (test_mcd_rpc_install); TEST_INSTALL (test_service_gcp_install); TEST_INSTALL (test_mcd_nsinfo_install); + TEST_INSTALL (test_bulkwrite_install); ret = TestSuite_Run (&suite); diff --git a/src/libmongoc/tests/test-mongoc-bulkwrite.c b/src/libmongoc/tests/test-mongoc-bulkwrite.c new file mode 100644 index 00000000000..7197b07882a --- /dev/null +++ b/src/libmongoc/tests/test-mongoc-bulkwrite.c @@ -0,0 +1,678 @@ +/* + * Copyright 2024-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. + */ + +// This file includes tests `mongoc_bulkwrite_t` for basic usage and libmongoc-specific behavior. +// The specification tests (prose and JSON) include more coverage of driver-agnostic behavior. + +#include +#include +#include +#include +#include + +static void +test_bulkwrite_insert (void *unused) +{ + BSON_UNUSED (unused); + + bson_error_t error; + bool ok; + mongoc_client_t *client = test_framework_new_default_client (); + + // Drop prior data. + { + mongoc_collection_t *coll = mongoc_client_get_collection (client, "db", "coll"); + mongoc_collection_drop (coll, NULL); // Ignore return. + mongoc_collection_destroy (coll); + } + + // Insert two documents with verbose results. + mongoc_bulkwrite_t *bw = mongoc_client_bulkwrite_new (client); + ok = mongoc_bulkwrite_append_insertone (bw, "db.coll", tmp_bson ("{'_id': 123}"), NULL /* opts */, &error); + ASSERT_OR_PRINT (ok, error); + ok = mongoc_bulkwrite_append_insertone (bw, "db.coll", tmp_bson ("{'_id': 456}"), NULL /* opts */, &error); + ASSERT_OR_PRINT (ok, error); + + // Do the bulk write. + mongoc_bulkwriteopts_t *opts = mongoc_bulkwriteopts_new (); + mongoc_bulkwriteopts_set_verboseresults (opts, true); + mongoc_bulkwritereturn_t bwr = mongoc_bulkwrite_execute (bw, opts); + + ASSERT_NO_BULKWRITEEXCEPTION (bwr); + + // Ensure results report IDs inserted. + { + ASSERT (bwr.res); + const bson_t *insertResults = mongoc_bulkwriteresult_insertresults (bwr.res); + ASSERT (insertResults); + ASSERT_MATCH (insertResults, BSON_STR ({"0" : {"insertedId" : 123}, "1" : {"insertedId" : 456}})); + } + + mongoc_bulkwriteexception_destroy (bwr.exc); + mongoc_bulkwriteresult_destroy (bwr.res); + mongoc_bulkwrite_destroy (bw); + mongoc_bulkwriteopts_destroy (opts); + mongoc_client_destroy (client); +} + +static void +test_bulkwrite_upsert_with_null (void *unused) +{ + BSON_UNUSED (unused); + + bson_error_t error; + bool ok; + mongoc_client_t *client = test_framework_new_default_client (); + + // Drop prior data. + { + mongoc_collection_t *coll = mongoc_client_get_collection (client, "db", "coll"); + mongoc_collection_drop (coll, NULL); // Ignore return. + mongoc_collection_destroy (coll); + } + + // Upsert document with an `_id` of null. + mongoc_bulkwrite_t *bw = mongoc_client_bulkwrite_new (client); + mongoc_bulkwrite_replaceoneopts_t *roo = mongoc_bulkwrite_replaceoneopts_new (); + mongoc_bulkwrite_replaceoneopts_set_upsert (roo, true); + ok = mongoc_bulkwrite_append_replaceone (bw, "db.coll", tmp_bson ("{}"), tmp_bson ("{'_id': null}"), roo, &error); + ASSERT_OR_PRINT (ok, error); + + // Do the bulk write. + mongoc_bulkwriteopts_t *opts = mongoc_bulkwriteopts_new (); + mongoc_bulkwriteopts_set_verboseresults (opts, true); + mongoc_bulkwritereturn_t bwr = mongoc_bulkwrite_execute (bw, opts); + + ASSERT_NO_BULKWRITEEXCEPTION (bwr); + + // Ensure results report null ID inserted. + { + ASSERT (bwr.res); + const bson_t *updateResults = mongoc_bulkwriteresult_updateresults (bwr.res); + ASSERT (updateResults); + ASSERT_MATCH (updateResults, BSON_STR ({"0" : {"matchedCount" : 1, "modifiedCount" : 0, "upsertedId" : null}})); + } + + mongoc_bulkwriteexception_destroy (bwr.exc); + mongoc_bulkwriteresult_destroy (bwr.res); + mongoc_bulkwrite_destroy (bw); + mongoc_bulkwriteopts_destroy (opts); + mongoc_bulkwrite_replaceoneopts_destroy (roo); + mongoc_client_destroy (client); +} + +static void +test_bulkwrite_writeError (void *unused) +{ + BSON_UNUSED (unused); + + bson_error_t error; + bool ok; + mongoc_client_t *client = test_framework_new_default_client (); + + // Drop prior data. + { + mongoc_collection_t *coll = mongoc_client_get_collection (client, "db", "coll"); + mongoc_collection_drop (coll, NULL); // Ignore return. + mongoc_collection_destroy (coll); + } + + // Insert two documents with verbose results. + mongoc_bulkwrite_t *bw = mongoc_client_bulkwrite_new (client); + ok = mongoc_bulkwrite_append_insertone (bw, "db.coll", tmp_bson ("{'_id': 123}"), NULL /* opts */, &error); + ASSERT_OR_PRINT (ok, error); + ok = mongoc_bulkwrite_append_insertone (bw, "db.coll", tmp_bson ("{'_id': 123}"), NULL /* opts */, &error); + ASSERT_OR_PRINT (ok, error); + + // Do the bulk write. + mongoc_bulkwriteopts_t *opts = mongoc_bulkwriteopts_new (); + mongoc_bulkwriteopts_set_verboseresults (opts, true); + mongoc_bulkwritereturn_t bwr = mongoc_bulkwrite_execute (bw, opts); + + // Expect an error. + ASSERT (bwr.exc); + const bson_t *ed = mongoc_bulkwriteexception_writeerrors (bwr.exc); + ASSERT_MATCH (ed, BSON_STR ({ + "1" : { + "code" : 11000, + "message" : "E11000 duplicate key error collection: db.coll index: _id_ dup key: { _id: 123 }", + "details" : {} + } + })); + + // Ensure results report only one ID inserted. + ASSERT (bwr.res); + const bson_t *insertResults = mongoc_bulkwriteresult_insertresults (bwr.res); + ASSERT (insertResults); + ASSERT_MATCH (insertResults, BSON_STR ({"0" : {"insertedId" : 123}})); + + mongoc_bulkwriteexception_destroy (bwr.exc); + mongoc_bulkwriteresult_destroy (bwr.res); + mongoc_bulkwrite_destroy (bw); + mongoc_bulkwriteopts_destroy (opts); + mongoc_client_destroy (client); +} + +static void +test_bulkwrite_session_with_unacknowledged (void *ctx) +{ + BSON_UNUSED (ctx); + bool ok; + bson_error_t error; + + mongoc_client_t *client = test_framework_new_default_client (); + mongoc_bulkwrite_t *bw = mongoc_client_bulkwrite_new (client); + mongoc_client_session_t *session = mongoc_client_start_session (client, NULL, &error); + ASSERT_OR_PRINT (session, error); + mongoc_write_concern_t *wc = mongoc_write_concern_new (); + mongoc_write_concern_set_w (wc, MONGOC_WRITE_CONCERN_W_UNACKNOWLEDGED); + mongoc_bulkwriteopts_t *opts = mongoc_bulkwriteopts_new (); + mongoc_bulkwriteopts_set_writeconcern (opts, wc); + + // Execute bulk write: + ok = mongoc_bulkwrite_append_insertone (bw, "db.coll", tmp_bson ("{}"), NULL, &error); + ASSERT_OR_PRINT (ok, error); + mongoc_bulkwrite_set_session (bw, session); + mongoc_bulkwritereturn_t ret = mongoc_bulkwrite_execute (bw, opts); + // Expect no result and an error: + ASSERT (!ret.res); + ASSERT (ret.exc); + ASSERT (mongoc_bulkwriteexception_error (ret.exc, &error)); + ASSERT_ERROR_CONTAINS (error, + MONGOC_ERROR_COMMAND, + MONGOC_ERROR_COMMAND_INVALID_ARG, + "Cannot use client session with unacknowledged command"); + mongoc_bulkwriteresult_destroy (ret.res); + mongoc_bulkwriteexception_destroy (ret.exc); + mongoc_client_session_destroy (session); + mongoc_bulkwriteopts_destroy (opts); + mongoc_bulkwrite_destroy (bw); + mongoc_client_destroy (client); + mongoc_write_concern_destroy (wc); +} + +static void +test_bulkwrite_double_execute (void *ctx) +{ + BSON_UNUSED (ctx); + bool ok; + bson_error_t error; + + mongoc_client_t *client = test_framework_new_default_client (); + mongoc_bulkwrite_t *bw = mongoc_client_bulkwrite_new (client); + ok = mongoc_bulkwrite_append_insertone (bw, "db.coll", tmp_bson ("{}"), NULL, &error); + ASSERT_OR_PRINT (ok, error); + // Execute. + { + mongoc_bulkwritereturn_t bwr = mongoc_bulkwrite_execute (bw, NULL); + ASSERT_NO_BULKWRITEEXCEPTION (bwr); + mongoc_bulkwriteresult_destroy (bwr.res); + mongoc_bulkwriteexception_destroy (bwr.exc); + } + + // Expect an error on reuse. + ASSERT (!mongoc_bulkwrite_append_insertone (bw, "db.coll", tmp_bson ("{}"), NULL, &error)); + ASSERT_ERROR_CONTAINS (error, MONGOC_ERROR_COMMAND, MONGOC_ERROR_COMMAND_INVALID_ARG, "bulk write already executed"); + memset (&error, 0, sizeof (error)); + + ASSERT (!mongoc_bulkwrite_append_updateone (bw, "db.coll", tmp_bson ("{}"), tmp_bson ("{}"), NULL, &error)); + ASSERT_ERROR_CONTAINS (error, MONGOC_ERROR_COMMAND, MONGOC_ERROR_COMMAND_INVALID_ARG, "bulk write already executed"); + memset (&error, 0, sizeof (error)); + + ASSERT (!mongoc_bulkwrite_append_updatemany (bw, "db.coll", tmp_bson ("{}"), tmp_bson ("{}"), NULL, &error)); + ASSERT_ERROR_CONTAINS (error, MONGOC_ERROR_COMMAND, MONGOC_ERROR_COMMAND_INVALID_ARG, "bulk write already executed"); + memset (&error, 0, sizeof (error)); + + ASSERT (!mongoc_bulkwrite_append_replaceone (bw, "db.coll", tmp_bson ("{}"), tmp_bson ("{}"), NULL, &error)); + ASSERT_ERROR_CONTAINS (error, MONGOC_ERROR_COMMAND, MONGOC_ERROR_COMMAND_INVALID_ARG, "bulk write already executed"); + memset (&error, 0, sizeof (error)); + + ASSERT (!mongoc_bulkwrite_append_deleteone (bw, "db.coll", tmp_bson ("{}"), NULL, &error)); + ASSERT_ERROR_CONTAINS (error, MONGOC_ERROR_COMMAND, MONGOC_ERROR_COMMAND_INVALID_ARG, "bulk write already executed"); + memset (&error, 0, sizeof (error)); + + ASSERT (!mongoc_bulkwrite_append_deletemany (bw, "db.coll", tmp_bson ("{}"), NULL, &error)); + ASSERT_ERROR_CONTAINS (error, MONGOC_ERROR_COMMAND, MONGOC_ERROR_COMMAND_INVALID_ARG, "bulk write already executed"); + memset (&error, 0, sizeof (error)); + + { + mongoc_bulkwritereturn_t bwr = mongoc_bulkwrite_execute (bw, NULL); + ASSERT (bwr.exc); + ASSERT (mongoc_bulkwriteexception_error (bwr.exc, &error)); + ASSERT_ERROR_CONTAINS ( + error, MONGOC_ERROR_COMMAND, MONGOC_ERROR_COMMAND_INVALID_ARG, "bulk write already executed"); + mongoc_bulkwriteexception_destroy (bwr.exc); + mongoc_bulkwriteresult_destroy (bwr.res); + } + + mongoc_bulkwrite_destroy (bw); + mongoc_client_destroy (client); +} + +static void +capture_last_bulkWrite_serverid (const mongoc_apm_command_started_t *event) +{ + if (0 == strcmp (mongoc_apm_command_started_get_command_name (event), "bulkWrite")) { + uint32_t *last_captured = mongoc_apm_command_started_get_context (event); + *last_captured = mongoc_apm_command_started_get_server_id (event); + } +} + +static void +test_bulkwrite_serverid (void *ctx) +{ + mongoc_client_t *client; + BSON_UNUSED (ctx); + bool ok; + bson_error_t error; + + client = test_framework_new_default_client (); + + // Get a server ID + uint32_t selected_serverid; + { + mongoc_server_description_t *sd = mongoc_client_select_server (client, true /* for_writes */, NULL, &error); + ASSERT_OR_PRINT (sd, error); + selected_serverid = mongoc_server_description_id (sd); + mongoc_server_description_destroy (sd); + } + + uint32_t last_captured = 0; + // Set callback to capture the serverid used for the last `bulkWrite` command. + { + mongoc_apm_callbacks_t *cbs = mongoc_apm_callbacks_new (); + mongoc_apm_set_command_started_cb (cbs, capture_last_bulkWrite_serverid); + mongoc_client_set_apm_callbacks (client, cbs, &last_captured); + mongoc_apm_callbacks_destroy (cbs); + } + + mongoc_bulkwrite_t *bw = mongoc_client_bulkwrite_new (client); + mongoc_bulkwriteopts_t *bwo = mongoc_bulkwriteopts_new (); + mongoc_bulkwriteopts_set_serverid (bwo, selected_serverid); + + ok = mongoc_bulkwrite_append_insertone (bw, "db.coll", tmp_bson ("{}"), NULL, &error); + ASSERT_OR_PRINT (ok, error); + // Execute. + { + mongoc_bulkwritereturn_t bwr = mongoc_bulkwrite_execute (bw, bwo); + ASSERT_NO_BULKWRITEEXCEPTION (bwr); + // Expect the selected server is reported as used. + uint32_t used_serverid = mongoc_bulkwriteresult_serverid (bwr.res); + ASSERT_CMPUINT32 (selected_serverid, ==, used_serverid); + mongoc_bulkwriteresult_destroy (bwr.res); + mongoc_bulkwriteexception_destroy (bwr.exc); + } + + // Expect the selected server is reported as used in command monitoring. + ASSERT_CMPUINT32 (last_captured, ==, selected_serverid); + + mongoc_bulkwriteopts_destroy (bwo); + mongoc_bulkwrite_destroy (bw); + mongoc_client_destroy (client); +} + +static void +test_bulkwrite_serverid_on_retry (void *ctx) +{ + BSON_UNUSED (ctx); + bool ok; + bson_error_t error; + + mongoc_uri_t *uri = test_framework_get_uri (); + ASSERT_OR_PRINT (test_framework_uri_apply_multi_mongos (uri, true, &error), error); + mongoc_client_t *client = mongoc_client_new_from_uri (uri); + test_framework_set_ssl_opts (client); + + // Get a server ID + uint32_t selected_serverid; + { + mongoc_server_description_t *sd = mongoc_client_select_server (client, true /* for_writes */, NULL, &error); + ASSERT_OR_PRINT (sd, error); + selected_serverid = mongoc_server_description_id (sd); + mongoc_server_description_destroy (sd); + } + + // Set failpoint on selected server. + { + bool ret = mongoc_client_command_simple_with_server_id ( + client, + "admin", + tmp_bson (BSON_STR ({ + "configureFailPoint" : "failCommand", + "mode" : {"times" : 1}, + "data" : {"failCommands" : ["bulkWrite"], "closeConnection" : true} + })), + NULL, + selected_serverid, + NULL, + &error); + ASSERT_OR_PRINT (ret, error); + } + + uint32_t last_captured = 0; + // Set callback to capture the serverid used for the last `bulkWrite` command. + { + mongoc_apm_callbacks_t *cbs = mongoc_apm_callbacks_new (); + mongoc_apm_set_command_started_cb (cbs, capture_last_bulkWrite_serverid); + mongoc_client_set_apm_callbacks (client, cbs, &last_captured); + mongoc_apm_callbacks_destroy (cbs); + } + + mongoc_bulkwrite_t *bw = mongoc_client_bulkwrite_new (client); + mongoc_bulkwriteopts_t *bwo = mongoc_bulkwriteopts_new (); + mongoc_bulkwriteopts_set_serverid (bwo, selected_serverid); + + ok = mongoc_bulkwrite_append_insertone (bw, "db.coll", tmp_bson ("{}"), NULL, &error); + ASSERT_OR_PRINT (ok, error); + // Execute. + { + mongoc_bulkwritereturn_t bwr = mongoc_bulkwrite_execute (bw, bwo); + ASSERT_NO_BULKWRITEEXCEPTION (bwr); + // Expect a different server was used due to retry. + uint32_t used_serverid = mongoc_bulkwriteresult_serverid (bwr.res); + ASSERT_CMPUINT32 (selected_serverid, !=, used_serverid); + mongoc_bulkwriteresult_destroy (bwr.res); + mongoc_bulkwriteexception_destroy (bwr.exc); + // Expect the used server was reported in command monitoring. + ASSERT_CMPUINT32 (last_captured, ==, used_serverid); + } + + mongoc_uri_destroy (uri); + mongoc_bulkwriteopts_destroy (bwo); + mongoc_bulkwrite_destroy (bw); + mongoc_client_destroy (client); +} + +static void +capture_last_bulkWrite_command (const mongoc_apm_command_started_t *event) +{ + if (0 == strcmp (mongoc_apm_command_started_get_command_name (event), "bulkWrite")) { + bson_t *last_captured = mongoc_apm_command_started_get_context (event); + bson_destroy (last_captured); + const bson_t *cmd = mongoc_apm_command_started_get_command (event); + bson_copy_to (cmd, last_captured); + } +} + +static void +test_bulkwrite_extra (void *ctx) +{ + mongoc_client_t *client; + BSON_UNUSED (ctx); + bool ok; + bson_error_t error; + + client = test_framework_new_default_client (); + + bson_t last_captured = BSON_INITIALIZER; + // Set callback to capture the last `bulkWrite` command. + { + mongoc_apm_callbacks_t *cbs = mongoc_apm_callbacks_new (); + mongoc_apm_set_command_started_cb (cbs, capture_last_bulkWrite_command); + mongoc_client_set_apm_callbacks (client, cbs, &last_captured); + mongoc_apm_callbacks_destroy (cbs); + } + + mongoc_bulkwrite_t *bw = mongoc_client_bulkwrite_new (client); + // Create bulk write. + { + ok = mongoc_bulkwrite_append_insertone (bw, "db.coll", tmp_bson ("{}"), NULL, &error); + ASSERT_OR_PRINT (ok, error); + } + + mongoc_bulkwriteopts_t *bwo = mongoc_bulkwriteopts_new (); + // Create bulk write options with extra options. + { + bson_t *extra = tmp_bson ("{'comment': 'foo'}"); + mongoc_bulkwriteopts_set_extra (bwo, extra); + } + + // Execute. + { + mongoc_bulkwritereturn_t bwr = mongoc_bulkwrite_execute (bw, bwo); + ASSERT_NO_BULKWRITEEXCEPTION (bwr); + mongoc_bulkwriteresult_destroy (bwr.res); + mongoc_bulkwriteexception_destroy (bwr.exc); + } + + // Expect `bulkWrite` command was sent with extra option. + ASSERT_MATCH (&last_captured, "{'comment': 'foo'}"); + + mongoc_bulkwriteopts_destroy (bwo); + mongoc_bulkwrite_destroy (bw); + bson_destroy (&last_captured); + mongoc_client_destroy (client); +} + +static void +test_bulkwrite_no_verbose_results (void *ctx) +{ + mongoc_client_t *client; + BSON_UNUSED (ctx); + bool ok; + bson_error_t error; + + client = test_framework_new_default_client (); + + mongoc_bulkwrite_t *bw = mongoc_client_bulkwrite_new (client); + // Create bulk write. + { + ok = mongoc_bulkwrite_append_insertone (bw, "db.coll", tmp_bson ("{}"), NULL, &error); + ASSERT_OR_PRINT (ok, error); + + ok = mongoc_bulkwrite_append_updateone ( + bw, "db.coll", tmp_bson ("{}"), tmp_bson ("{'$set': {'x': 1}}"), NULL, &error); + ASSERT_OR_PRINT (ok, error); + + ok = mongoc_bulkwrite_append_deleteone (bw, "db.coll", tmp_bson ("{}"), NULL, &error); + ASSERT_OR_PRINT (ok, error); + } + + + // Execute. + { + mongoc_bulkwritereturn_t bwr = mongoc_bulkwrite_execute (bw, NULL /* opts */); + ASSERT_NO_BULKWRITEEXCEPTION (bwr); + // Expect no verbose results. + ASSERT (NULL == mongoc_bulkwriteresult_insertresults (bwr.res)); + ASSERT (NULL == mongoc_bulkwriteresult_updateresults (bwr.res)); + ASSERT (NULL == mongoc_bulkwriteresult_deleteresults (bwr.res)); + mongoc_bulkwriteresult_destroy (bwr.res); + mongoc_bulkwriteexception_destroy (bwr.exc); + } + + mongoc_bulkwrite_destroy (bw); + mongoc_client_destroy (client); +} + +static void +capture_all_bulkWrite_commands (const mongoc_apm_command_started_t *event) +{ + if (0 == strcmp (mongoc_apm_command_started_get_command_name (event), "bulkWrite")) { + mongoc_array_t *captured = mongoc_apm_command_started_get_context (event); + bson_t *cmd = bson_copy (mongoc_apm_command_started_get_command (event)); + _mongoc_array_append_val (captured, cmd); + } +} + +// `test_bulkwrite_many_namespaces` tests a bulk write with many unique namespace entries. +// An early implementation used linear look-up for namespaces. It resulted in a very long test runtime (30 minutes+). +// A hash map was used to improve the look-up. +static void +test_bulkwrite_many_namespaces (void *ctx) +{ + mongoc_client_t *client; + BSON_UNUSED (ctx); + bool ok; + bson_error_t error; + + client = test_framework_new_default_client (); + + mongoc_array_t captured; + _mongoc_array_init (&captured, sizeof (bson_t *)); + // Set callback to capture all `bulkWrite` commands. + { + mongoc_apm_callbacks_t *cbs = mongoc_apm_callbacks_new (); + mongoc_apm_set_command_started_cb (cbs, capture_all_bulkWrite_commands); + mongoc_client_set_apm_callbacks (client, cbs, &captured); + mongoc_apm_callbacks_destroy (cbs); + } + + // Get `maxWriteBatchSize` from the server. + int32_t maxWriteBatchSize; + { + bson_t reply; + + ok = mongoc_client_command_simple (client, "admin", tmp_bson ("{'hello': 1}"), NULL, &reply, &error); + ASSERT_OR_PRINT (ok, error); + + maxWriteBatchSize = bson_lookup_int32 (&reply, "maxWriteBatchSize"); + bson_destroy (&reply); + } + + mongoc_bulkwrite_t *bw = mongoc_client_bulkwrite_new (client); + // Create bulk write large enough to split into two batches. Use a unique namespace per model. + { + for (int32_t i = 0; i < maxWriteBatchSize + 1; i++) { + char *ns = bson_strdup_printf ("db.coll%" PRId32, i); + ok = mongoc_bulkwrite_append_deleteone (bw, ns, tmp_bson ("{}"), NULL, &error); + ASSERT_OR_PRINT (ok, error); + bson_free (ns); + } + } + + // Execute. + { + mongoc_bulkwritereturn_t bwr = mongoc_bulkwrite_execute (bw, NULL /* opts */); + ASSERT_NO_BULKWRITEEXCEPTION (bwr); + mongoc_bulkwriteresult_destroy (bwr.res); + mongoc_bulkwriteexception_destroy (bwr.exc); + } + + + // Expect two `bulkWrite` commands were sent. + ASSERT_CMPSIZE_T (captured.len, ==, 2); + bson_t *first = _mongoc_array_index (&captured, bson_t *, 0); + // Expect the first contains maxWriteBatchSize `nsInfo` entries: + { + bson_t *nsInfo = bson_lookup_bson (first, "nsInfo"); + ASSERT_CMPUINT32 (bson_count_keys (nsInfo), ==, maxWriteBatchSize); + bson_destroy (nsInfo); + } + // Expect the second only contains one `nsInfo` entry: + bson_t *second = _mongoc_array_index (&captured, bson_t *, 1); + { + bson_t *nsInfo = bson_lookup_bson (second, "nsInfo"); + ASSERT_CMPUINT32 (bson_count_keys (nsInfo), ==, 1); + bson_destroy (nsInfo); + } + + for (size_t i = 0; i < captured.len; i++) { + bson_t *el = _mongoc_array_index (&captured, bson_t *, i); + bson_destroy (el); + } + _mongoc_array_destroy (&captured); + + mongoc_bulkwrite_destroy (bw); + mongoc_client_destroy (client); +} + + +void +test_bulkwrite_install (TestSuite *suite) +{ + TestSuite_AddFull (suite, + "/bulkwrite/insert", + test_bulkwrite_insert, + NULL /* dtor */, + NULL /* ctx */, + test_framework_skip_if_max_wire_version_less_than_25 // require server 8.0 + ); + + TestSuite_AddFull (suite, + "/bulkwrite/upsert_with_null", + test_bulkwrite_upsert_with_null, + NULL /* dtor */, + NULL /* ctx */, + test_framework_skip_if_max_wire_version_less_than_25 // require server 8.0 + ); + + TestSuite_AddFull (suite, + "/bulkwrite/writeError", + test_bulkwrite_writeError, + NULL /* dtor */, + NULL /* ctx */, + test_framework_skip_if_max_wire_version_less_than_25 // require server 8.0 + ); + + TestSuite_AddFull (suite, + "/bulkwrite/session_with_unacknowledged", + test_bulkwrite_session_with_unacknowledged, + NULL /* dtor */, + NULL /* ctx */, + test_framework_skip_if_max_wire_version_less_than_25, // require server 8.0 + test_framework_skip_if_no_sessions); + + TestSuite_AddFull (suite, + "/bulkwrite/double_execute", + test_bulkwrite_double_execute, + NULL /* dtor */, + NULL /* ctx */, + test_framework_skip_if_max_wire_version_less_than_25 // require server 8.0 + ); + + TestSuite_AddFull (suite, + "/bulkwrite/server_id", + test_bulkwrite_serverid, + NULL /* dtor */, + NULL /* ctx */, + test_framework_skip_if_max_wire_version_less_than_25 // require server 8.0 + ); + + TestSuite_AddFull (suite, + "/bulkwrite/server_id/on_retry", + test_bulkwrite_serverid_on_retry, + NULL /* dtor */, + NULL /* ctx */, + test_framework_skip_if_max_wire_version_less_than_25, // require server 8.0 + test_framework_skip_if_not_mongos // Requires multiple hosts that can accept writes. + ); + + TestSuite_AddFull (suite, + "/bulkwrite/extra", + test_bulkwrite_extra, + NULL /* dtor */, + NULL /* ctx */, + test_framework_skip_if_max_wire_version_less_than_25 // require server 8.0 + ); + + TestSuite_AddFull (suite, + "/bulkwrite/no_verbose_results", + test_bulkwrite_no_verbose_results, + NULL /* dtor */, + NULL /* ctx */, + test_framework_skip_if_max_wire_version_less_than_25 // require server 8.0 + ); + + TestSuite_AddFull (suite, + "/bulkwrite/many_namespaces", + test_bulkwrite_many_namespaces, + NULL /* dtor */, + NULL /* ctx */, + test_framework_skip_if_max_wire_version_less_than_25, // require server 8.0 + test_framework_skip_if_mongos // Creating 100k collections is very slow (~5 minutes) on mongos. + ); +} diff --git a/src/libmongoc/tests/test-mongoc-crud.c b/src/libmongoc/tests/test-mongoc-crud.c index c43d588735e..2bd01a84677 100644 --- a/src/libmongoc/tests/test-mongoc-crud.c +++ b/src/libmongoc/tests/test-mongoc-crud.c @@ -3,6 +3,7 @@ #include "json-test.h" #include "json-test-operations.h" #include "test-libmongoc.h" +#include "mongoc-bulkwrite.h" static bool crud_test_operation_cb (json_test_ctx_t *ctx, const bson_t *test, const bson_t *operation) @@ -146,6 +147,1140 @@ prose_test_2 (void *ctx) mongoc_client_destroy (client); } +typedef struct { + // `ops_counts` is a BSON document of this form: + // { "0": , "1": ... } + bson_t ops_counts; + // `operation_ids` is a BSON document of this form: + // { "0": , "1": ... } + bson_t operation_ids; + int numGetMore; + int numKillCursors; +} bulkWrite_ctx; + +// `bulkWrite_cb` records the number of `ops` in each sent `bulkWrite` to a BSON +// document of this form: +// { "0": , "1": ... } +static void +bulkWrite_cb (const mongoc_apm_command_started_t *event) +{ + bulkWrite_ctx *ctx = mongoc_apm_command_started_get_context (event); + const char *cmd_name = mongoc_apm_command_started_get_command_name (event); + + if (0 == strcmp (cmd_name, "bulkWrite")) { + const bson_t *cmd = mongoc_apm_command_started_get_command (event); + bson_iter_t ops_iter; + // Count the number of `ops`. + ASSERT (bson_iter_init_find (&ops_iter, cmd, "ops")); + bson_t ops; + bson_iter_bson (&ops_iter, &ops); + uint32_t ops_count = bson_count_keys (&ops); + // Record. + char *key = bson_strdup_printf ("%" PRIu32, bson_count_keys (&ctx->ops_counts)); + BSON_APPEND_INT64 (&ctx->ops_counts, key, ops_count); + BSON_APPEND_INT64 (&ctx->operation_ids, key, mongoc_apm_command_started_get_operation_id (event)); + bson_free (key); + } + + if (0 == strcmp (cmd_name, "getMore")) { + ctx->numGetMore++; + } + + if (0 == strcmp (cmd_name, "killCursors")) { + ctx->numKillCursors++; + } +} + +// `capture_bulkWrite_info` captures event data relevant to some bulk write prose tests. +static bulkWrite_ctx * +capture_bulkWrite_info (mongoc_client_t *client) +{ + bulkWrite_ctx *cb_ctx = bson_malloc0 (sizeof (*cb_ctx)); + bson_init (&cb_ctx->ops_counts); + bson_init (&cb_ctx->operation_ids); + mongoc_apm_callbacks_t *cbs = mongoc_apm_callbacks_new (); + mongoc_apm_set_command_started_cb (cbs, bulkWrite_cb); + mongoc_client_set_apm_callbacks (client, cbs, cb_ctx); + mongoc_apm_callbacks_destroy (cbs); + return cb_ctx; +} + +static void +bulkWrite_ctx_reset (bulkWrite_ctx *cb_ctx) +{ + bson_reinit (&cb_ctx->ops_counts); + bson_reinit (&cb_ctx->operation_ids); + cb_ctx->numGetMore = 0; + cb_ctx->numKillCursors = 0; +} + +static void +bulkWrite_ctx_destroy (bulkWrite_ctx *cb_ctx) +{ + if (!cb_ctx) { + return; + } + bson_destroy (&cb_ctx->ops_counts); + bson_destroy (&cb_ctx->operation_ids); + bson_free (cb_ctx); +} + +static void +prose_test_3 (void *ctx) +{ + /* + `MongoClient.bulkWrite` batch splits a `writeModels` input with greater than `maxWriteBatchSize` operations + */ + mongoc_client_t *client; + BSON_UNUSED (ctx); + bool ok; + bson_error_t error; + + client = test_framework_new_default_client (); + // Set callbacks to count the number of bulkWrite commands sent. + bulkWrite_ctx *cb_ctx = capture_bulkWrite_info (client); + + // Get `maxWriteBatchSize` from the server. + int32_t maxWriteBatchSize; + { + bson_t reply; + + ok = mongoc_client_command_simple (client, "admin", tmp_bson ("{'hello': 1}"), NULL, &reply, &error); + ASSERT_OR_PRINT (ok, error); + + maxWriteBatchSize = bson_lookup_int32 (&reply, "maxWriteBatchSize"); + bson_destroy (&reply); + } + + bson_t *doc = tmp_bson ("{'a': 'b'}"); + mongoc_bulkwrite_t *bw = mongoc_client_bulkwrite_new (client); + for (int32_t i = 0; i < maxWriteBatchSize + 1; i++) { + ok = mongoc_bulkwrite_append_insertone (bw, "db.coll", doc, NULL, &error); + ASSERT_OR_PRINT (ok, error); + } + + mongoc_bulkwritereturn_t ret = mongoc_bulkwrite_execute (bw, NULL /* options */); + ASSERT (ret.res); + ASSERT_CMPINT64 (mongoc_bulkwriteresult_insertedcount (ret.res), ==, maxWriteBatchSize + 1); + mongoc_bulkwriteexception_destroy (ret.exc); + mongoc_bulkwriteresult_destroy (ret.res); + + + // Assert first `bulkWrite` sends `maxWriteBatchSize` ops. + // Assert second `bulkWrite` sends 1 op. + bson_t expect = BSON_INITIALIZER; + BSON_APPEND_INT64 (&expect, "0", maxWriteBatchSize); + BSON_APPEND_INT64 (&expect, "1", 1); + ASSERT_EQUAL_BSON (&expect, &cb_ctx->ops_counts); + bson_destroy (&expect); + + // Assert both have the same `operation_id`. + int64_t operation_id_0 = bson_lookup_int64 (&cb_ctx->operation_ids, "0"); + int64_t operation_id_1 = bson_lookup_int64 (&cb_ctx->operation_ids, "1"); + ASSERT_CMPINT64 (operation_id_0, ==, operation_id_1); + + mongoc_bulkwrite_destroy (bw); + bulkWrite_ctx_destroy (cb_ctx); + mongoc_client_destroy (client); +} + +static char * +repeat_char (char c, int32_t count) +{ + ASSERT (bson_in_range_size_t_signed (count)); + char *str = bson_malloc (count + 1); + memset (str, c, count); + str[count] = '\0'; + return str; +} + +typedef struct { + int32_t maxBsonObjectSize; + int32_t maxMessageSizeBytes; + int32_t maxWriteBatchSize; +} server_limits_t; + +static server_limits_t +get_server_limits (mongoc_client_t *client) +{ + server_limits_t sl; + bson_error_t error; + bson_t reply; + ASSERT_OR_PRINT (mongoc_client_command_simple (client, "admin", tmp_bson ("{'hello': 1}"), NULL, &reply, &error), + error); + sl.maxBsonObjectSize = bson_lookup_int32 (&reply, "maxBsonObjectSize"); + sl.maxMessageSizeBytes = bson_lookup_int32 (&reply, "maxMessageSizeBytes"); + sl.maxWriteBatchSize = bson_lookup_int32 (&reply, "maxWriteBatchSize"); + bson_destroy (&reply); + return sl; +} + +static void +prose_test_4 (void *ctx) +{ + /* + `MongoClient.bulkWrite` batch splits when an `ops` payload exceeds `maxMessageSizeBytes` + */ + mongoc_client_t *client; + BSON_UNUSED (ctx); + bool ok; + bson_error_t error; + + client = test_framework_new_default_client (); + // Set callbacks to count the number of bulkWrite commands sent. + bulkWrite_ctx *cb_ctx = capture_bulkWrite_info (client); + + // Get `maxWriteBatchSize` and `maxBsonObjectSize` from the server. + server_limits_t sl = get_server_limits (client); + int32_t maxMessageSizeBytes = sl.maxMessageSizeBytes; + int32_t maxBsonObjectSize = sl.maxBsonObjectSize; + + + bson_t doc = BSON_INITIALIZER; + { + char *large_str = repeat_char ('b', (size_t) maxBsonObjectSize - 500); + BSON_APPEND_UTF8 (&doc, "a", large_str); + bson_free (large_str); + } + + mongoc_bulkwrite_t *bw = mongoc_client_bulkwrite_new (client); + int32_t numModels = (maxMessageSizeBytes / maxBsonObjectSize) + 1; + + for (int32_t i = 0; i < numModels; i++) { + ok = mongoc_bulkwrite_append_insertone (bw, "db.coll", &doc, NULL, &error); + ASSERT_OR_PRINT (ok, error); + } + + mongoc_bulkwritereturn_t ret = mongoc_bulkwrite_execute (bw, NULL /* options */); + ASSERT_NO_BULKWRITEEXCEPTION (ret); + ASSERT (ret.res); + ASSERT_CMPINT64 (mongoc_bulkwriteresult_insertedcount (ret.res), ==, numModels); + mongoc_bulkwriteexception_destroy (ret.exc); + mongoc_bulkwriteresult_destroy (ret.res); + + + // Assert two `bulkWrite`s were sent: + ASSERT_CMPUINT32 (2, ==, bson_count_keys (&cb_ctx->ops_counts)); + + // Assert first `bulkWrite` sends `numModels - 1` ops. + int64_t ops_count_0 = bson_lookup_int64 (&cb_ctx->ops_counts, "0"); + ASSERT_CMPINT64 (ops_count_0, ==, numModels - 1); + // Assert second `bulkWrite` sends 1 op. + int64_t ops_count_1 = bson_lookup_int64 (&cb_ctx->ops_counts, "1"); + ASSERT_CMPINT64 (ops_count_1, ==, 1); + + // Assert both have the same `operation_id`. + int64_t operation_id_0 = bson_lookup_int64 (&cb_ctx->operation_ids, "0"); + int64_t operation_id_1 = bson_lookup_int64 (&cb_ctx->operation_ids, "1"); + ASSERT_CMPINT64 (operation_id_0, ==, operation_id_1); + + mongoc_bulkwrite_destroy (bw); + + bulkWrite_ctx_destroy (cb_ctx); + bson_destroy (&doc); + mongoc_client_destroy (client); +} + +static void +prose_test_5 (void *ctx) +{ + /* + `MongoClient.bulkWrite` collects `WriteConcernError`s across batches + */ + mongoc_client_t *client; + BSON_UNUSED (ctx); + bool ok; + bson_error_t error; + + // Construct client with retryable writes disabled. + { + mongoc_uri_t *uri = test_framework_get_uri (); + mongoc_uri_set_option_as_bool (uri, MONGOC_URI_RETRYWRITES, false); + client = mongoc_client_new_from_uri (uri); + test_framework_set_ssl_opts (client); + mongoc_uri_destroy (uri); + } + + // Get `maxWriteBatchSize` from the server. + server_limits_t sl = get_server_limits (client); + int32_t maxWriteBatchSize = sl.maxWriteBatchSize; + + // Drop collection to clear prior data. + { + mongoc_collection_t *coll = mongoc_client_get_collection (client, "db", "coll"); + mongoc_collection_drop (coll, NULL); + mongoc_collection_destroy (coll); + } + + // Set callbacks to count the number of bulkWrite commands sent. + bulkWrite_ctx *cb_ctx = capture_bulkWrite_info (client); + + // Set failpoint + { + ok = mongoc_client_command_simple ( + client, + "admin", + tmp_bson (BSON_STR ({ + "configureFailPoint" : "failCommand", + "mode" : {"times" : 2}, + "data" : { + "failCommands" : ["bulkWrite"], + "writeConcernError" : {"code" : 91, "errmsg" : "Replication is being shut down"} + } + })), + NULL, + NULL, + &error); + ASSERT_OR_PRINT (ok, error); + } + + // Construct models. + mongoc_bulkwrite_t *bw = mongoc_client_bulkwrite_new (client); + { + bson_t doc = BSON_INITIALIZER; + BSON_APPEND_UTF8 (&doc, "a", "b"); + for (int32_t i = 0; i < maxWriteBatchSize + 1; i++) { + ok = mongoc_bulkwrite_append_insertone (bw, "db.coll", &doc, NULL, &error); + ASSERT_OR_PRINT (ok, error); + } + } + + mongoc_bulkwritereturn_t ret = mongoc_bulkwrite_execute (bw, NULL /* options */); + ASSERT (ret.exc); + + // Expect no top-level error. + if (mongoc_bulkwriteexception_error (ret.exc, &error)) { + test_error ("Expected no top-level error but got:\n%s", test_bulkwriteexception_str (ret.exc)); + } + + // Count write concern errors. + { + const bson_t *writeConcernErrors = mongoc_bulkwriteexception_writeconcernerrors (ret.exc); + ASSERT_CMPUINT32 (bson_count_keys (writeConcernErrors), ==, 2); + } + + // Assert partial results. + ASSERT_CMPINT64 (mongoc_bulkwriteresult_insertedcount (ret.res), ==, maxWriteBatchSize + 1); + + // Assert two batches were sent. + ASSERT_CMPUINT32 (bson_count_keys (&cb_ctx->ops_counts), ==, 2); + + bulkWrite_ctx_destroy (cb_ctx); + mongoc_bulkwriteexception_destroy (ret.exc); + mongoc_bulkwriteresult_destroy (ret.res); + mongoc_bulkwrite_destroy (bw); + mongoc_client_destroy (client); +} + + +static void +prose_test_6 (void *ctx) +{ + /* + `MongoClient.bulkWrite` handles individual `WriteError`s across batches + */ + mongoc_client_t *client; + BSON_UNUSED (ctx); + bool ok; + bson_error_t error; + + client = test_framework_new_default_client (); + // Get `maxWriteBatchSize` from the server. + server_limits_t sl = get_server_limits (client); + int32_t maxWriteBatchSize = sl.maxWriteBatchSize; + + // Drop collection to clear prior data. + mongoc_collection_t *coll = mongoc_client_get_collection (client, "db", "coll"); + mongoc_collection_drop (coll, NULL); + + + // Set callbacks to count the number of bulkWrite commands sent. + bulkWrite_ctx *cb_ctx = capture_bulkWrite_info (client); + + bson_t document = BSON_INITIALIZER; + BSON_APPEND_INT32 (&document, "_id", 1); + ok = mongoc_collection_insert_one (coll, &document, NULL, NULL, &error); + ASSERT_OR_PRINT (ok, error); + + + // Test Unordered + { + // Construct models. + mongoc_bulkwrite_t *bw = mongoc_client_bulkwrite_new (client); + + for (int32_t i = 0; i < maxWriteBatchSize + 1; i++) { + ok = mongoc_bulkwrite_append_insertone (bw, "db.coll", &document, NULL, &error); + ASSERT_OR_PRINT (ok, error); + } + + mongoc_bulkwriteopts_t *opts = mongoc_bulkwriteopts_new (); + mongoc_bulkwriteopts_set_ordered (opts, false); + mongoc_bulkwritereturn_t ret = mongoc_bulkwrite_execute (bw, opts); + ASSERT (ret.exc); + + if (mongoc_bulkwriteexception_error (ret.exc, &error)) { + test_error ("Expected no top-level error but got:\n%s", test_bulkwriteexception_str (ret.exc)); + } + + // Count write errors. + { + const bson_t *writeErrors = mongoc_bulkwriteexception_writeerrors (ret.exc); + ASSERT (bson_in_range_uint32_t_signed (maxWriteBatchSize + 1)); + ASSERT_CMPUINT32 (bson_count_keys (writeErrors), ==, (uint32_t) maxWriteBatchSize + 1); + } + + // Assert two batches were sent. + ASSERT_CMPUINT32 (bson_count_keys (&cb_ctx->ops_counts), ==, 2); + + mongoc_bulkwriteexception_destroy (ret.exc); + mongoc_bulkwriteresult_destroy (ret.res); + mongoc_bulkwriteopts_destroy (opts); + mongoc_bulkwrite_destroy (bw); + } + + // Reset state. + bulkWrite_ctx_reset (cb_ctx); + + // Test Ordered + { + // Construct models. + mongoc_bulkwrite_t *bw = mongoc_client_bulkwrite_new (client); + + for (int32_t i = 0; i < maxWriteBatchSize + 1; i++) { + ok = mongoc_bulkwrite_append_insertone (bw, "db.coll", &document, NULL, &error); + ASSERT_OR_PRINT (ok, error); + } + + + mongoc_bulkwriteopts_t *opts = mongoc_bulkwriteopts_new (); + mongoc_bulkwriteopts_set_ordered (opts, true); + mongoc_bulkwritereturn_t ret = mongoc_bulkwrite_execute (bw, opts); + ASSERT (ret.exc); + + if (mongoc_bulkwriteexception_error (ret.exc, &error)) { + test_error ("Expected no top-level error but got:\n%s", test_bulkwriteexception_str (ret.exc)); + } + + // Count write errors. + { + const bson_t *writeErrors = mongoc_bulkwriteexception_writeerrors (ret.exc); + ASSERT_CMPUINT32 (bson_count_keys (writeErrors), ==, 1); + } + + // Assert one batch was sent. + ASSERT_CMPUINT32 (bson_count_keys (&cb_ctx->ops_counts), ==, 1); + + mongoc_bulkwriteexception_destroy (ret.exc); + mongoc_bulkwriteresult_destroy (ret.res); + mongoc_bulkwriteopts_destroy (opts); + mongoc_bulkwrite_destroy (bw); + } + + bulkWrite_ctx_destroy (cb_ctx); + bson_destroy (&document); + mongoc_collection_destroy (coll); + mongoc_client_destroy (client); +} + +static void +prose_test_7 (void *ctx) +{ + /* + `MongoClient.bulkWrite` handles a cursor requiring a `getMore` + */ + mongoc_client_t *client; + BSON_UNUSED (ctx); + bool ok; + bson_error_t error; + + client = test_framework_new_default_client (); + // Get `maxBsonObjectSize` from the server. + server_limits_t sl = get_server_limits (client); + int32_t maxBsonObjectSize = sl.maxBsonObjectSize; + // Drop collection to clear prior data. + mongoc_collection_t *coll = mongoc_client_get_collection (client, "db", "coll"); + mongoc_collection_drop (coll, NULL); + + // Set callbacks to count the number of bulkWrite commands sent. + bulkWrite_ctx *cb_ctx = capture_bulkWrite_info (client); + + // Construct models. + mongoc_bulkwrite_t *bw = mongoc_client_bulkwrite_new (client); + size_t numModels = 0; + + mongoc_bulkwrite_updateoneopts_t *uo = mongoc_bulkwrite_updateoneopts_new (); + mongoc_bulkwrite_updateoneopts_set_upsert (uo, true); + bson_t *update = BCON_NEW ("$set", "{", "x", BCON_INT32 (1), "}"); + bson_t d1 = BSON_INITIALIZER; + { + char *large_str = repeat_char ('a', maxBsonObjectSize / 2); + BSON_APPEND_UTF8 (&d1, "_id", large_str); + bson_free (large_str); + } + + ok = mongoc_bulkwrite_append_updateone (bw, "db.coll", &d1, update, uo, &error); + ASSERT_OR_PRINT (ok, error); + numModels++; + + bson_t d2 = BSON_INITIALIZER; + { + char *large_str = repeat_char ('b', maxBsonObjectSize / 2); + BSON_APPEND_UTF8 (&d2, "_id", large_str); + bson_free (large_str); + } + + ok = mongoc_bulkwrite_append_updateone (bw, "db.coll", &d2, update, uo, &error); + ASSERT_OR_PRINT (ok, error); + numModels++; + + mongoc_bulkwriteopts_t *opts = mongoc_bulkwriteopts_new (); + mongoc_bulkwriteopts_set_verboseresults (opts, true); + mongoc_bulkwritereturn_t ret = mongoc_bulkwrite_execute (bw, opts); + + ASSERT_NO_BULKWRITEEXCEPTION (ret); + + ASSERT_CMPINT64 (mongoc_bulkwriteresult_upsertedcount (ret.res), ==, 2); + + // Check length of update results. + { + const bson_t *updateResults = mongoc_bulkwriteresult_updateresults (ret.res); + ASSERT_CMPSIZE_T ((size_t) bson_count_keys (updateResults), ==, numModels); + } + + ASSERT_CMPINT (cb_ctx->numGetMore, ==, 1); + + mongoc_bulkwriteopts_destroy (opts); + bson_destroy (&d2); + bson_destroy (&d1); + bson_destroy (update); + mongoc_bulkwriteexception_destroy (ret.exc); + mongoc_bulkwriteresult_destroy (ret.res); + mongoc_bulkwrite_updateoneopts_destroy (uo); + mongoc_bulkwrite_destroy (bw); + mongoc_collection_destroy (coll); + bulkWrite_ctx_destroy (cb_ctx); + mongoc_client_destroy (client); +} + +static void +prose_test_8 (void *ctx) +{ + /* + `MongoClient.bulkWrite` handles a cursor requiring `getMore` within a transaction + */ + mongoc_client_t *client; + BSON_UNUSED (ctx); + bool ok; + bson_error_t error; + + client = test_framework_new_default_client (); + // Get `maxBsonObjectSize` from the server. + server_limits_t sl = get_server_limits (client); + int32_t maxBsonObjectSize = sl.maxBsonObjectSize; + + // Drop collection to clear prior data. + mongoc_collection_t *coll = mongoc_client_get_collection (client, "db", "coll"); + mongoc_collection_drop (coll, NULL); + + // Set callbacks to count the number of bulkWrite commands sent. + bulkWrite_ctx *cb_ctx = capture_bulkWrite_info (client); + + // Construct models. + mongoc_bulkwrite_t *bw = mongoc_client_bulkwrite_new (client); + size_t numModels = 0; + + mongoc_bulkwrite_updateoneopts_t *uo = mongoc_bulkwrite_updateoneopts_new (); + mongoc_bulkwrite_updateoneopts_set_upsert (uo, true); + + bson_t *update = BCON_NEW ("$set", "{", "x", BCON_INT32 (1), "}"); + mongoc_client_session_t *sess = mongoc_client_start_session (client, NULL, &error); + ASSERT_OR_PRINT (sess, error); + ASSERT_OR_PRINT (mongoc_client_session_start_transaction (sess, NULL, &error), error); + + bson_t d1 = BSON_INITIALIZER; + { + char *large_str = repeat_char ('a', maxBsonObjectSize / 2); + BSON_APPEND_UTF8 (&d1, "_id", large_str); + bson_free (large_str); + } + + ok = mongoc_bulkwrite_append_updateone (bw, "db.coll", &d1, update, uo, &error); + ASSERT_OR_PRINT (ok, error); + numModels++; + + bson_t d2 = BSON_INITIALIZER; + { + char *large_str = repeat_char ('b', maxBsonObjectSize / 2); + BSON_APPEND_UTF8 (&d2, "_id", large_str); + bson_free (large_str); + } + + ok = mongoc_bulkwrite_append_updateone (bw, "db.coll", &d2, update, uo, &error); + ASSERT_OR_PRINT (ok, error); + numModels++; + + mongoc_bulkwriteopts_t *opts = mongoc_bulkwriteopts_new (); + mongoc_bulkwriteopts_set_verboseresults (opts, true); + mongoc_bulkwritereturn_t ret = mongoc_bulkwrite_execute (bw, opts); + + ASSERT_NO_BULKWRITEEXCEPTION (ret); + + ASSERT_CMPINT64 (mongoc_bulkwriteresult_upsertedcount (ret.res), ==, 2); + + ASSERT_CMPINT (cb_ctx->numGetMore, ==, 1); + + // Check length of update results. + { + const bson_t *updateResults = mongoc_bulkwriteresult_updateresults (ret.res); + ASSERT_CMPSIZE_T ((size_t) bson_count_keys (updateResults), ==, numModels); + } + + mongoc_bulkwrite_updateoneopts_destroy (uo); + mongoc_bulkwriteopts_destroy (opts); + bson_destroy (&d2); + bson_destroy (&d1); + bson_destroy (update); + mongoc_client_session_destroy (sess); + mongoc_bulkwriteexception_destroy (ret.exc); + mongoc_bulkwriteresult_destroy (ret.res); + mongoc_bulkwrite_destroy (bw); + mongoc_collection_destroy (coll); + bulkWrite_ctx_destroy (cb_ctx); + mongoc_client_destroy (client); +} + +static void +prose_test_9 (void *ctx) +{ + /* + `MongoClient.bulkWrite` handles a `getMore` error + */ + mongoc_client_t *client; + BSON_UNUSED (ctx); + bool ok; + bson_error_t error; + + client = test_framework_new_default_client (); + // Get `maxBsonObjectSize` from the server. + server_limits_t sl = get_server_limits (client); + int32_t maxBsonObjectSize = sl.maxBsonObjectSize; + + // Drop collection to clear prior data. + mongoc_collection_t *coll = mongoc_client_get_collection (client, "db", "coll"); + mongoc_collection_drop (coll, NULL); + + // Set callbacks to count the number of bulkWrite commands sent. + bulkWrite_ctx *cb_ctx = capture_bulkWrite_info (client); + + // Configure failpoint on `getMore`. + { + { + ok = mongoc_client_command_simple (client, + "admin", + tmp_bson (BSON_STR ({ + "configureFailPoint" : "failCommand", + "mode" : {"times" : 1}, + "data" : {"failCommands" : ["getMore"], "errorCode" : 8} + })), + NULL, + NULL, + &error); + ASSERT_OR_PRINT (ok, error); + } + } + + bson_t *update = BCON_NEW ("$set", "{", "x", BCON_INT32 (1), "}"); + + // Construct models. + mongoc_bulkwrite_t *bw = mongoc_client_bulkwrite_new (client); + size_t numModels = 0; + + mongoc_bulkwrite_updateoneopts_t *uo = mongoc_bulkwrite_updateoneopts_new (); + mongoc_bulkwrite_updateoneopts_set_upsert (uo, true); + + bson_t d1 = BSON_INITIALIZER; + { + char *large_str = repeat_char ('a', maxBsonObjectSize / 2); + BSON_APPEND_UTF8 (&d1, "_id", large_str); + bson_free (large_str); + } + + ok = mongoc_bulkwrite_append_updateone (bw, "db.coll", &d1, update, uo, &error); + ASSERT_OR_PRINT (ok, error); + numModels++; + + bson_t d2 = BSON_INITIALIZER; + { + char *large_str = repeat_char ('b', maxBsonObjectSize / 2); + BSON_APPEND_UTF8 (&d2, "_id", large_str); + bson_free (large_str); + } + + ok = mongoc_bulkwrite_append_updateone (bw, "db.coll", &d2, update, uo, &error); + ASSERT_OR_PRINT (ok, error); + numModels++; + + mongoc_bulkwriteopts_t *opts = mongoc_bulkwriteopts_new (); + mongoc_bulkwriteopts_set_verboseresults (opts, true); + mongoc_bulkwritereturn_t ret = mongoc_bulkwrite_execute (bw, opts); + ASSERT (ret.exc); + + if (!mongoc_bulkwriteexception_error (ret.exc, &error)) { + test_error ("Expected top-level error but got:\n%s", test_bulkwriteexception_str (ret.exc)); + } + ASSERT_ERROR_CONTAINS (error, MONGOC_ERROR_QUERY, 8, "Failing command via 'failCommand' failpoint"); + ASSERT (ret.res); + ASSERT_CMPSIZE_T ((size_t) mongoc_bulkwriteresult_upsertedcount (ret.res), ==, numModels); + + // Check length of update results. + { + const bson_t *updateResults = mongoc_bulkwriteresult_updateresults (ret.res); + ASSERT_CMPSIZE_T ((size_t) bson_count_keys (updateResults), ==, 1); + } + ASSERT_CMPINT (cb_ctx->numGetMore, ==, 1); + ASSERT_CMPINT (cb_ctx->numKillCursors, ==, 1); + + mongoc_bulkwrite_updateoneopts_destroy (uo); + mongoc_bulkwriteopts_destroy (opts); + bson_destroy (&d2); + bson_destroy (&d1); + bson_destroy (update); + mongoc_bulkwriteexception_destroy (ret.exc); + mongoc_bulkwriteresult_destroy (ret.res); + mongoc_bulkwrite_destroy (bw); + mongoc_collection_destroy (coll); + bulkWrite_ctx_destroy (cb_ctx); + mongoc_client_destroy (client); +} + + +static void +prose_test_10 (void *ctx) +{ + /* + `MongoClient.bulkWrite` returns error for unacknowledged too-large insert + */ + mongoc_client_t *client; + BSON_UNUSED (ctx); + bool ok; + bson_error_t error; + mongoc_write_concern_t *wc; + + client = test_framework_new_default_client (); + // Get `maxBsonObjectSize` from the server. + server_limits_t sl = get_server_limits (client); + int32_t maxBsonObjectSize = sl.maxBsonObjectSize; + + bson_t doc = BSON_INITIALIZER; + { + char *large_str = repeat_char ('b', maxBsonObjectSize); + BSON_APPEND_UTF8 (&doc, "a", large_str); + bson_free (large_str); + } + + + wc = mongoc_write_concern_new (); + mongoc_write_concern_set_w (wc, MONGOC_WRITE_CONCERN_W_UNACKNOWLEDGED); + mongoc_bulkwriteopts_t *opts = mongoc_bulkwriteopts_new (); + mongoc_bulkwriteopts_set_writeconcern (opts, wc); + + // Test a large insert. + { + mongoc_bulkwrite_t *bw = mongoc_client_bulkwrite_new (client); + ok = mongoc_bulkwrite_append_insertone (bw, "db.coll", &doc, NULL, &error); + ASSERT_OR_PRINT (ok, error); + + mongoc_bulkwritereturn_t ret = mongoc_bulkwrite_execute (bw, opts); + ASSERT (ret.exc); + if (!mongoc_bulkwriteexception_error (ret.exc, &error)) { + test_error ("Expected top-level error but got:\n%s", test_bulkwriteexception_str (ret.exc)); + } + ASSERT_ERROR_CONTAINS (error, MONGOC_ERROR_COMMAND, MONGOC_ERROR_COMMAND_INVALID_ARG, "of size"); + + mongoc_bulkwriteexception_destroy (ret.exc); + mongoc_bulkwriteresult_destroy (ret.res); + mongoc_bulkwrite_destroy (bw); + } + + // Test a large replace. + { + mongoc_bulkwrite_t *bw = mongoc_client_bulkwrite_new (client); + ok = mongoc_bulkwrite_append_replaceone (bw, "db.coll", tmp_bson ("{}"), &doc, NULL, &error); + ASSERT_OR_PRINT (ok, error); + + mongoc_bulkwritereturn_t ret = mongoc_bulkwrite_execute (bw, opts); + ASSERT (ret.exc); + if (!mongoc_bulkwriteexception_error (ret.exc, &error)) { + test_error ("Expected top-level error but got:\n%s", test_bulkwriteexception_str (ret.exc)); + } + ASSERT_ERROR_CONTAINS (error, MONGOC_ERROR_COMMAND, MONGOC_ERROR_COMMAND_INVALID_ARG, "of size"); + + mongoc_bulkwriteexception_destroy (ret.exc); + mongoc_bulkwriteresult_destroy (ret.res); + mongoc_bulkwrite_destroy (bw); + } + + mongoc_bulkwriteopts_destroy (opts); + bson_destroy (&doc); + mongoc_write_concern_destroy (wc); + mongoc_client_destroy (client); +} + +static void +capture_all_bulkWrite_commands (const mongoc_apm_command_started_t *event) +{ + if (0 == strcmp (mongoc_apm_command_started_get_command_name (event), "bulkWrite")) { + mongoc_array_t *captured = mongoc_apm_command_started_get_context (event); + bson_t *cmd = bson_copy (mongoc_apm_command_started_get_command (event)); + _mongoc_array_append_val (captured, cmd); + } +} + +typedef struct { + mongoc_client_t *client; + int32_t maxMessageSizeBytes; + int32_t maxBsonObjectSize; + int32_t numModels; + mongoc_array_t captured; + mongoc_bulkwrite_t *bw; +} prose_test_11_fixture_t; + +static prose_test_11_fixture_t * +prose_test_11_fixture_new (void) +{ + bool ok; + bson_error_t error; + + prose_test_11_fixture_t *tf = bson_malloc0 (sizeof (*tf)); + tf->client = test_framework_new_default_client (); + // Get `maxMessageSizeBytes` and `maxBsonObjectSize` from the server. + server_limits_t sl = get_server_limits (tf->client); + tf->maxMessageSizeBytes = sl.maxMessageSizeBytes; + tf->maxBsonObjectSize = sl.maxBsonObjectSize; + + // See CRUD prose test 12 description for the calculation of these values. + const int32_t opsBytes = tf->maxMessageSizeBytes - 1122; + tf->numModels = opsBytes / tf->maxBsonObjectSize; + const int32_t remainderBytes = opsBytes % tf->maxBsonObjectSize; + + + _mongoc_array_init (&tf->captured, sizeof (bson_t *)); + // Set callback to capture all `bulkWrite` commands. + { + mongoc_apm_callbacks_t *cbs = mongoc_apm_callbacks_new (); + mongoc_apm_set_command_started_cb (cbs, capture_all_bulkWrite_commands); + mongoc_client_set_apm_callbacks (tf->client, cbs, &tf->captured); + mongoc_apm_callbacks_destroy (cbs); + } + + tf->bw = mongoc_client_bulkwrite_new (tf->client); + + + // Add initial list of documents. + { + // Create a document { 'a': 'b'.repeat(maxBsonObjectSize - 57) } + bson_t *doc; + { + char *large_str = repeat_char ('b', tf->maxBsonObjectSize - 57); + doc = BCON_NEW ("a", BCON_UTF8 (large_str)); + bson_free (large_str); + } + + for (size_t i = 0; i < tf->numModels; i++) { + ok = mongoc_bulkwrite_append_insertone (tf->bw, "db.coll", doc, NULL, &error); + ASSERT_OR_PRINT (ok, error); + } + + bson_destroy (doc); + } + + + if (remainderBytes >= 217) { + // Create a document { 'a': 'b'.repeat(remainderBytes - 57) } + bson_t *doc; + { + char *large_str = repeat_char ('b', remainderBytes - 57); + doc = BCON_NEW ("a", BCON_UTF8 (large_str)); + bson_free (large_str); + } + + ok = mongoc_bulkwrite_append_insertone (tf->bw, "db.coll", doc, NULL, &error); + ASSERT_OR_PRINT (ok, error); + tf->numModels++; + bson_destroy (doc); + } + return tf; +} + +static void +prose_test_11_fixture_destroy (prose_test_11_fixture_t *tf) +{ + if (!tf) { + return; + } + for (size_t i = 0; i < tf->captured.len; i++) { + bson_t *el = _mongoc_array_index (&tf->captured, bson_t *, i); + bson_destroy (el); + } + + _mongoc_array_destroy (&tf->captured); + + mongoc_bulkwrite_destroy (tf->bw); + mongoc_client_destroy (tf->client); + bson_free (tf); +} + +static void +prose_test_11 (void *ctx) +{ + /* + 11. `MongoClient.bulkWrite` batch splits when the addition of a new namespace exceeds the maximum message size + */ + BSON_UNUSED (ctx); + bool ok; + bson_error_t error; + + // Case 1: Does not split + { + prose_test_11_fixture_t *tf = prose_test_11_fixture_new (); + + // Add a document with the same namespace (expected to not result in a batch split). + { + bson_t *second_doc = BCON_NEW ("a", "b"); + ok = mongoc_bulkwrite_append_insertone (tf->bw, "db.coll", second_doc, NULL, &error); + ASSERT_OR_PRINT (ok, error); + bson_destroy (second_doc); + } + + // Execute. + { + mongoc_bulkwritereturn_t bwr = mongoc_bulkwrite_execute (tf->bw, NULL /* opts */); + ASSERT_NO_BULKWRITEEXCEPTION (bwr); + ASSERT (bson_in_range_int64_t_unsigned (tf->numModels)); + ASSERT_CMPINT64 (mongoc_bulkwriteresult_insertedcount (bwr.res), ==, (int64_t) tf->numModels + 1); + mongoc_bulkwriteresult_destroy (bwr.res); + mongoc_bulkwriteexception_destroy (bwr.exc); + } + + // Expect one `bulkWrite` command is sent. + ASSERT_CMPSIZE_T (tf->captured.len, ==, 1); + // Expect the event contains the namespace for `db.coll`. + bson_t *first = _mongoc_array_index (&tf->captured, bson_t *, 0); + { + bson_t *ops = bson_lookup_bson (first, "ops"); + ASSERT (bson_in_range_uint32_t_unsigned (tf->numModels)); + ASSERT_CMPUINT32 (bson_count_keys (ops), ==, (uint32_t) tf->numModels + 1); + bson_destroy (ops); + + bson_t *nsInfo = bson_lookup_bson (first, "nsInfo"); + ASSERT_CMPUINT32 (bson_count_keys (nsInfo), ==, 1); + bson_destroy (nsInfo); + + const char *ns = bson_lookup_utf8 (first, "nsInfo.0.ns"); + ASSERT_CMPSTR (ns, "db.coll"); + } + prose_test_11_fixture_destroy (tf); + } + + // Case 2: Splits with new namespace + { + prose_test_11_fixture_t *tf = prose_test_11_fixture_new (); + + // Create a large namespace. + char *large_ns; + { + char *coll = repeat_char ('c', 200); + large_ns = bson_strdup_printf ("db.%s", coll); + bson_free (coll); + } + + // Add a document that results in a batch split due to the namespace. + { + bson_t *second_doc = BCON_NEW ("a", "b"); + ok = mongoc_bulkwrite_append_insertone (tf->bw, large_ns, second_doc, NULL, &error); + ASSERT_OR_PRINT (ok, error); + bson_destroy (second_doc); + } + + // Execute. + { + mongoc_bulkwritereturn_t bwr = mongoc_bulkwrite_execute (tf->bw, NULL /* opts */); + ASSERT_NO_BULKWRITEEXCEPTION (bwr); + ASSERT (bson_in_range_int64_t_unsigned (tf->numModels)); + ASSERT_CMPINT64 (mongoc_bulkwriteresult_insertedcount (bwr.res), ==, (int64_t) tf->numModels + 1); + mongoc_bulkwriteresult_destroy (bwr.res); + mongoc_bulkwriteexception_destroy (bwr.exc); + } + + // Expect two `bulkWrite` commands were sent. + ASSERT_CMPSIZE_T (tf->captured.len, ==, 2); + // Expect the first only contains the namespace for `db.coll`. + bson_t *first = _mongoc_array_index (&tf->captured, bson_t *, 0); + { + bson_t *ops = bson_lookup_bson (first, "ops"); + ASSERT (bson_in_range_uint32_t_unsigned (tf->numModels)); + ASSERT_CMPUINT32 (bson_count_keys (ops), ==, (uint32_t) tf->numModels); + bson_destroy (ops); + + bson_t *nsInfo = bson_lookup_bson (first, "nsInfo"); + ASSERT_CMPUINT32 (bson_count_keys (nsInfo), ==, 1); + bson_destroy (nsInfo); + + const char *ns = bson_lookup_utf8 (first, "nsInfo.0.ns"); + ASSERT_CMPSTR (ns, "db.coll"); + } + + // Expect the second only contains the namespace for `large_ns`. + bson_t *second = _mongoc_array_index (&tf->captured, bson_t *, 1); + { + bson_t *ops = bson_lookup_bson (second, "ops"); + ASSERT_CMPUINT32 (bson_count_keys (ops), ==, 1); + bson_destroy (ops); + + bson_t *nsInfo = bson_lookup_bson (second, "nsInfo"); + ASSERT_CMPUINT32 (bson_count_keys (nsInfo), ==, 1); + bson_destroy (nsInfo); + + const char *ns = bson_lookup_utf8 (second, "nsInfo.0.ns"); + ASSERT_CMPSTR (ns, large_ns); + } + + bson_free (large_ns); + prose_test_11_fixture_destroy (tf); + } +} + +static void +prose_test_12 (void *ctx) +{ + /* + 12. `MongoClient.bulkWrite` returns an error if no operations can be added to `ops` + */ + mongoc_client_t *client; + BSON_UNUSED (ctx); + bool ok; + bson_error_t error; + + client = test_framework_new_default_client (); + + // Get `maxMessageSizeBytes` from the server. + server_limits_t sl = get_server_limits (client); + int32_t maxMessageSizeBytes = sl.maxMessageSizeBytes; + + // Create a large string. + char *large_str = repeat_char ('b', maxMessageSizeBytes); + + // Test too-big document. + { + mongoc_bulkwrite_t *bw = mongoc_client_bulkwrite_new (client); + + bson_t *large_doc = BCON_NEW ("a", BCON_UTF8 (large_str)); + + // Create bulk write. + { + ok = mongoc_bulkwrite_append_insertone (bw, "db.coll", large_doc, NULL, &error); + ASSERT_OR_PRINT (ok, error); + } + + // Execute. + { + mongoc_bulkwritereturn_t bwr = mongoc_bulkwrite_execute (bw, NULL); + ASSERT (bwr.exc); + if (!mongoc_bulkwriteexception_error (bwr.exc, &error)) { + test_error ("Expected top-level error but got:\n%s", test_bulkwriteexception_str (bwr.exc)); + } + ASSERT_ERROR_CONTAINS ( + error, MONGOC_ERROR_COMMAND, MONGOC_ERROR_COMMAND_INVALID_ARG, "unable to send document"); + mongoc_bulkwriteresult_destroy (bwr.res); + mongoc_bulkwriteexception_destroy (bwr.exc); + } + bson_destroy (large_doc); + mongoc_bulkwrite_destroy (bw); + } + + // Test too-big namespace. + { + mongoc_bulkwrite_t *bw = mongoc_client_bulkwrite_new (client); + + char *large_namespace = bson_strdup_printf ("db.%s", large_str); + + // Create bulk write. + { + ok = mongoc_bulkwrite_append_insertone (bw, large_namespace, tmp_bson ("{'a': 'b'}"), NULL, &error); + ASSERT_OR_PRINT (ok, error); + } + + // Execute. + { + mongoc_bulkwritereturn_t bwr = mongoc_bulkwrite_execute (bw, NULL); + ASSERT (bwr.exc); + if (!mongoc_bulkwriteexception_error (bwr.exc, &error)) { + test_error ("Expected top-level error but got:\n%s", test_bulkwriteexception_str (bwr.exc)); + } + ASSERT_ERROR_CONTAINS ( + error, MONGOC_ERROR_COMMAND, MONGOC_ERROR_COMMAND_INVALID_ARG, "unable to send document"); + mongoc_bulkwriteresult_destroy (bwr.res); + mongoc_bulkwriteexception_destroy (bwr.exc); + } + bson_free (large_namespace); + mongoc_bulkwrite_destroy (bw); + } + + bson_free (large_str); + mongoc_client_destroy (client); +} + +static void +prose_test_13 (void *ctx) +{ + /* + 13. `MongoClient.bulkWrite` errors if configured with automatic encryption. + */ + mongoc_client_t *client; + BSON_UNUSED (ctx); + bool ok; + bson_error_t error; + + client = test_framework_new_default_client (); + mongoc_auto_encryption_opts_t *aeo = mongoc_auto_encryption_opts_new (); + mongoc_auto_encryption_opts_set_keyvault_namespace (aeo, "db", "coll"); + mongoc_auto_encryption_opts_set_kms_providers ( + aeo, tmp_bson (BSON_STR ({"aws" : {"accessKeyId" : "foo", "secretAccessKey" : "bar"}}))); + ok = mongoc_client_enable_auto_encryption (client, aeo, &error); + ASSERT_OR_PRINT (ok, error); + + // Try to to a bulk write. + { + mongoc_bulkwrite_t *bw = mongoc_client_bulkwrite_new (client); + + // Create bulk write. + { + ok = mongoc_bulkwrite_append_insertone (bw, "db.coll", tmp_bson ("{'a': 'b'}"), NULL, &error); + ASSERT_OR_PRINT (ok, error); + } + + // Execute. + { + mongoc_bulkwritereturn_t bwr = mongoc_bulkwrite_execute (bw, NULL); + ASSERT (bwr.exc); + if (!mongoc_bulkwriteexception_error (bwr.exc, &error)) { + test_error ("Expected top-level error but got:\n%s", test_bulkwriteexception_str (bwr.exc)); + } + ASSERT_ERROR_CONTAINS (error, + MONGOC_ERROR_COMMAND, + MONGOC_ERROR_COMMAND_INVALID_ARG, + "bulkWrite does not currently support automatic encryption"); + mongoc_bulkwriteresult_destroy (bwr.res); + mongoc_bulkwriteexception_destroy (bwr.exc); + } + mongoc_bulkwrite_destroy (bw); + } + + mongoc_auto_encryption_opts_destroy (aeo); + mongoc_client_destroy (client); +} + + void test_crud_install (TestSuite *suite) { @@ -165,4 +1300,90 @@ test_crud_install (TestSuite *suite) NULL, /* dtor */ NULL, /* ctx */ test_framework_skip_if_max_wire_version_less_than_13); + + TestSuite_AddFull (suite, + "/crud/prose_test_3", + prose_test_3, + NULL, /* dtor */ + NULL, /* ctx */ + test_framework_skip_if_max_wire_version_less_than_25 /* require 8.0+ server */); + + TestSuite_AddFull (suite, + "/crud/prose_test_4", + prose_test_4, + NULL, /* dtor */ + NULL, /* ctx */ + test_framework_skip_if_max_wire_version_less_than_25 /* require 8.0+ server */); + + TestSuite_AddFull (suite, + "/crud/prose_test_5", + prose_test_5, + NULL, /* dtor */ + NULL, /* ctx */ + test_framework_skip_if_max_wire_version_less_than_25 /* require 8.0+ server */); + + TestSuite_AddFull (suite, + "/crud/prose_test_6", + prose_test_6, + NULL, /* dtor */ + NULL, /* ctx */ + test_framework_skip_if_max_wire_version_less_than_25 /* require 8.0+ server */ + ); + + + TestSuite_AddFull (suite, + "/crud/prose_test_7", + prose_test_7, + NULL, /* dtor */ + NULL, /* ctx */ + test_framework_skip_if_max_wire_version_less_than_25 /* require 8.0+ server */ + ); + + + TestSuite_AddFull (suite, + "/crud/prose_test_8", + prose_test_8, + NULL, /* dtor */ + NULL, /* ctx */ + test_framework_skip_if_max_wire_version_less_than_25, /* require 8.0+ server */ + test_framework_skip_if_no_txns); + + TestSuite_AddFull (suite, + "/crud/prose_test_9", + prose_test_9, + NULL, /* dtor */ + NULL, /* ctx */ + test_framework_skip_if_max_wire_version_less_than_25 /* require 8.0+ server */ + ); + + TestSuite_AddFull (suite, + "/crud/prose_test_10", + prose_test_10, + NULL, /* dtor */ + NULL, /* ctx */ + test_framework_skip_if_max_wire_version_less_than_25 /* require 8.0+ server */); + + TestSuite_AddFull (suite, + "/crud/prose_test_11", + prose_test_11, + NULL /* dtor */, + NULL /* ctx */, + test_framework_skip_if_max_wire_version_less_than_25 // require server 8.0 + ); + + TestSuite_AddFull (suite, + "/crud/prose_test_12", + prose_test_12, + NULL /* dtor */, + NULL /* ctx */, + test_framework_skip_if_max_wire_version_less_than_25 // require server 8.0 + ); + + TestSuite_AddFull (suite, + "/crud/prose_test_13", + prose_test_13, + NULL /* dtor */, + NULL /* ctx */, + test_framework_skip_if_max_wire_version_less_than_25, // require server 8.0 + test_framework_skip_if_no_client_side_encryption); } diff --git a/src/libmongoc/tests/test-mongoc-retryable-reads.c b/src/libmongoc/tests/test-mongoc-retryable-reads.c index 65b337cc5b5..3761db93e57 100644 --- a/src/libmongoc/tests/test-mongoc-retryable-reads.c +++ b/src/libmongoc/tests/test-mongoc-retryable-reads.c @@ -395,8 +395,8 @@ _test_retry_reads_sharded_on_same_mongos_cb (test_retry_reads_sharded_on_same_mo const mongoc_apm_command_succeeded_t *succeeded) { BSON_ASSERT_PARAM (ctx); - BSON_ASSERT (failed || true); - BSON_ASSERT (succeeded || true); + BSON_OPTIONAL_PARAM (failed); + BSON_OPTIONAL_PARAM (succeeded); ASSERT_WITH_MSG (ctx->failed_count + ctx->succeeded_count < 2, "expected at most two events, but observed %d failed and %d succeeded", diff --git a/src/libmongoc/tests/test-mongoc-retryable-writes.c b/src/libmongoc/tests/test-mongoc-retryable-writes.c index bcc6519a139..e8ac5650a86 100644 --- a/src/libmongoc/tests/test-mongoc-retryable-writes.c +++ b/src/libmongoc/tests/test-mongoc-retryable-writes.c @@ -742,6 +742,46 @@ retryable_writes_original_error_general_command (void *ctx) bson_destroy (cmd); } +static void +retryable_writes_original_error_bulkwrite (void *ctx) +{ + mongoc_client_t *client; + mongoc_collection_t *coll; + bson_error_t error = {0}; + mongoc_apm_callbacks_t *callbacks = {0}; + prose_test_3_apm_ctx_t apm_ctx = {0}; + + BSON_UNUSED (ctx); + + // setting up the client + client = test_framework_new_default_client (); + coll = get_test_collection (client, "retryable_writes"); + callbacks = mongoc_apm_callbacks_new (); + + // setup test + const uint32_t server_id = set_up_original_error_test (callbacks, &apm_ctx, "bulkWrite", client); + + mongoc_bulkwrite_t *bw = mongoc_client_bulkwrite_new (client); + bool ok = mongoc_bulkwrite_append_insertone (bw, "db.coll", tmp_bson ("{}"), NULL, &error); + ASSERT_OR_PRINT (ok, error); + + mongoc_bulkwritereturn_t bwr = mongoc_bulkwrite_execute (bw, NULL); + ASSERT (bwr.exc); + // Expect no top-level error (only a write concern error): + ASSERT_OR_PRINT (!mongoc_bulkwriteexception_error (bwr.exc, &error), error); + // Expect the original write concern error is returned: + const bson_t *reply = mongoc_bulkwriteexception_errorreply (bwr.exc); + ASSERT_MATCH (reply, + "{'writeConcernError' : { 'code' : { '$numberInt' : '91' } }, " + "'errorLabels' : [ 'RetryableWriteError' ], 'ok': { " + "'$numberDouble' : '1.0' }}"); + + cleanup_original_error_test (client, server_id, NULL, coll, callbacks); + mongoc_bulkwriteresult_destroy (bwr.res); + mongoc_bulkwriteexception_destroy (bwr.exc); + mongoc_bulkwrite_destroy (bw); +} + /* *----------------------------------------------------------------------- * @@ -866,13 +906,13 @@ _test_retry_writes_sharded_on_other_mongos_cb (const mongoc_apm_command_failed_t ctx->ports[ctx->count++] = host->port; } +typedef bool (*cmd_fn) (mongoc_client_t *client, bson_error_t *error); + // Test that in a sharded cluster writes are retried on a different mongos when // one is available. static void -retryable_writes_sharded_on_other_mongos (void *_ctx) +retryable_writes_sharded_on_other_mongos_impl (const char *cmd_name, cmd_fn cmd_func) { - BSON_UNUSED (_ctx); - bson_error_t error = {0}; // Create two clients `s0` and `s1` that each connect to a single mongos from @@ -895,11 +935,12 @@ retryable_writes_sharded_on_other_mongos (void *_ctx) " 'configureFailPoint': 'failCommand'," " 'mode': { 'times': 1 }," " 'data': {" - " 'failCommands': ['insert']," + " 'failCommands': ['%s']," " 'errorCode': 6," " 'errorLabels': ['RetryableWriteError']" " }" - "}"); + "}", + cmd_name); ASSERT_OR_PRINT (mongoc_client_command_simple (s0, "admin", command, NULL, NULL, &error), error); ASSERT_OR_PRINT (mongoc_client_command_simple (s1, "admin", command, NULL, NULL, &error), error); @@ -934,17 +975,9 @@ retryable_writes_sharded_on_other_mongos (void *_ctx) mongoc_apm_callbacks_destroy (callbacks); } - // Execute an `insert` command with `client`. Assert that the command + // Execute the target command with `client`. Assert that the command // failed. - { - mongoc_database_t *const db = mongoc_client_get_database (client, "db"); - mongoc_collection_t *const coll = mongoc_database_get_collection (db, "test"); - ASSERT_WITH_MSG (!mongoc_collection_insert_one (coll, tmp_bson ("{'x': 1}"), NULL, NULL, &error), - "expected insert command to fail"); - MONGOC_DEBUG ("insert error: %s", error.message); - mongoc_collection_destroy (coll); - mongoc_database_destroy (db); - } + ASSERT_WITH_MSG (!cmd_func (client, &error), "expected command '%s' to fail", cmd_name); // Assert that two failed command events occurred. ASSERT_WITH_MSG (ctx.count == 2, @@ -982,6 +1015,48 @@ retryable_writes_sharded_on_other_mongos (void *_ctx) _mongoc_array_destroy (&clients); } +static bool +cmd_insert (mongoc_client_t *client, bson_error_t *error) +{ + mongoc_database_t *const db = mongoc_client_get_database (client, "db"); + mongoc_collection_t *const coll = mongoc_database_get_collection (db, "test"); + bool ok = mongoc_collection_insert_one (coll, tmp_bson ("{'x': 1}"), NULL, NULL, error); + mongoc_collection_destroy (coll); + mongoc_database_destroy (db); + return ok; +} + +static void +retryable_writes_sharded_on_other_mongos_insert (void *_ctx) +{ + BSON_UNUSED (_ctx); + retryable_writes_sharded_on_other_mongos_impl ("insert", cmd_insert); +} + +static bool +cmd_bulkWrite (mongoc_client_t *client, bson_error_t *error) +{ + mongoc_bulkwrite_t *bw = mongoc_client_bulkwrite_new (client); + bool ok = mongoc_bulkwrite_append_insertone (bw, "db.coll", tmp_bson ("{}"), NULL, error); + ASSERT_OR_PRINT (ok, (*error)); + mongoc_bulkwritereturn_t bwr = mongoc_bulkwrite_execute (bw, NULL); + if (bwr.exc) { + ok = false; + mongoc_bulkwriteexception_error (bwr.exc, error); + } + mongoc_bulkwriteexception_destroy (bwr.exc); + mongoc_bulkwriteresult_destroy (bwr.res); + mongoc_bulkwrite_destroy (bw); + return ok; +} + +static void +retryable_writes_sharded_on_other_mongos_bulkWrite (void *_ctx) +{ + BSON_UNUSED (_ctx); + retryable_writes_sharded_on_other_mongos_impl ("bulkWrite", cmd_bulkWrite); +} + typedef struct _test_retry_writes_sharded_on_same_mongos_ctx { int failed_count; int succeeded_count; @@ -995,8 +1070,8 @@ _test_retry_writes_sharded_on_same_mongos_cb (test_retry_writes_sharded_on_same_ const mongoc_apm_command_succeeded_t *succeeded) { BSON_ASSERT_PARAM (ctx); - BSON_ASSERT (failed || true); - BSON_ASSERT (succeeded || true); + BSON_OPTIONAL_PARAM (failed); + BSON_OPTIONAL_PARAM (succeeded); ASSERT_WITH_MSG (ctx->failed_count + ctx->succeeded_count < 2, "expected at most two events, but observed %d failed and %d succeeded", @@ -1221,8 +1296,16 @@ test_retryable_writes_install (TestSuite *suite) test_framework_skip_if_max_wire_version_less_than_17, test_framework_skip_if_no_crypto); TestSuite_AddFull (suite, - "/retryable_writes/prose_test_4", - retryable_writes_sharded_on_other_mongos, + "/retryable_writes/prose_test_3/bulkwrite", + retryable_writes_original_error_bulkwrite, + NULL, + NULL, + test_framework_skip_if_not_replset, + test_framework_skip_if_max_wire_version_less_than_25, // require server 8.0 + test_framework_skip_if_no_crypto); + TestSuite_AddFull (suite, + "/retryable_writes/prose_test_4/insert", + retryable_writes_sharded_on_other_mongos_insert, NULL, NULL, test_framework_skip_if_not_mongos, @@ -1230,6 +1313,15 @@ test_retryable_writes_install (TestSuite *suite) // `errorLabels` is a 4.3.1+ feature. test_framework_skip_if_max_wire_version_less_than_9, test_framework_skip_if_no_crypto); + TestSuite_AddFull (suite, + "/retryable_writes/prose_test_4/bulkwrite", + retryable_writes_sharded_on_other_mongos_bulkWrite, + NULL, + NULL, + test_framework_skip_if_not_mongos, + test_framework_skip_if_no_failpoint, + test_framework_skip_if_max_wire_version_less_than_25, // require server 8.0 + test_framework_skip_if_no_crypto); TestSuite_AddFull (suite, "/retryable_writes/prose_test_5", retryable_writes_sharded_on_same_mongos, diff --git a/src/libmongoc/tests/unified/entity-map.c b/src/libmongoc/tests/unified/entity-map.c index 780c60c42cf..95be4c60c8e 100644 --- a/src/libmongoc/tests/unified/entity-map.c +++ b/src/libmongoc/tests/unified/entity-map.c @@ -87,8 +87,23 @@ uri_apply_options (mongoc_uri_t *uri, bson_t *opts, bson_error_t *error) mongoc_uri_set_read_concern (uri, rc); mongoc_read_concern_destroy (rc); } else if (0 == strcmp ("w", key)) { + if (BSON_ITER_HOLDS_UTF8 (&iter)) { + // Write concern may be string "majority". + const char *w = bson_iter_utf8 (&iter, NULL); + if (0 == strcmp (w, "majority")) { + mongoc_write_concern_set_w (wc, MONGOC_WRITE_CONCERN_W_MAJORITY); + } else { + test_set_error (error, "Unrecognized string value for 'w' URI option: %s", w); + } + } else if (BSON_ITER_HOLDS_INT32 (&iter)) { + mongoc_write_concern_set_w (wc, bson_iter_int32 (&iter)); + } else { + test_set_error (error, + "Expected int32 or string for 'w' URI option, got: %s", + _mongoc_bson_type_to_str (bson_iter_type (&iter))); + } + wcSet = true; - mongoc_write_concern_set_w (wc, bson_iter_int32 (&iter)); } else if (mongoc_uri_option_is_int32 (key)) { mongoc_uri_set_option_as_int32 (uri, key, bson_iter_int32 (&iter)); } else if (mongoc_uri_option_is_int64 (key)) { diff --git a/src/libmongoc/tests/unified/operation.c b/src/libmongoc/tests/unified/operation.c index 812a8477820..4f025e2161a 100644 --- a/src/libmongoc/tests/unified/operation.c +++ b/src/libmongoc/tests/unified/operation.c @@ -17,6 +17,7 @@ #include "operation.h" #include "mongoc-array-private.h" +#include "mongoc-bulkwrite.h" #include "mongoc-util-private.h" // hex_to_bin #include "result.h" #include "test-diagnostics.h" @@ -221,6 +222,285 @@ operation_list_database_names (test_t *test, operation_t *op, result_t *result, return ret; } +static bool +append_client_bulkwritemodel (mongoc_bulkwrite_t *bw, bson_t *model_wrapper, bson_error_t *error) +{ + bool ok = false; + // Example `model_wrapper`: + // { "insertOne": { "namespace": "db.coll", "document": { "_id": 1 } }} + char *namespace = NULL; + bson_t *document = NULL; + bson_t *filter = NULL; + bson_t *update = NULL; + bson_t *replacement = NULL; + bson_t *collation = NULL; + bson_val_t *hint = NULL; + bool *upsert = NULL; + bson_t *arrayFilters = NULL; + bson_parser_t *parser = bson_parser_new (); + + // Expect exactly one root key to identify the model (e.g. "insertOne"): + if (bson_count_keys (model_wrapper) != 1) { + test_set_error (error, + "expected exactly one key in model, got %" PRIu32 " : %s", + bson_count_keys (model_wrapper), + tmp_json (model_wrapper)); + goto done; + } + bson_iter_t model_wrapper_iter; + BSON_ASSERT (bson_iter_init (&model_wrapper_iter, model_wrapper)); + BSON_ASSERT (bson_iter_next (&model_wrapper_iter)); + const char *model_name = bson_iter_key (&model_wrapper_iter); + bson_t model_bson; + bson_iter_bson (&model_wrapper_iter, &model_bson); + + if (0 == strcmp ("insertOne", model_name)) { + // Parse an "insertOne". + bson_parser_utf8 (parser, "namespace", &namespace); + bson_parser_doc (parser, "document", &document); + if (!bson_parser_parse (parser, &model_bson, error)) { + goto done; + } + + if (!mongoc_bulkwrite_append_insertone (bw, namespace, document, NULL, error)) { + goto done; + } + } else if (0 == strcmp ("updateOne", model_name)) { + // Parse an "updateOne". + bson_parser_utf8 (parser, "namespace", &namespace); + bson_parser_doc (parser, "filter", &filter); + bson_parser_array_or_doc (parser, "update", &update); + bson_parser_array_optional (parser, "arrayFilters", &arrayFilters); + bson_parser_doc_optional (parser, "collation", &collation); + bson_parser_any_optional (parser, "hint", &hint); + bson_parser_bool_optional (parser, "upsert", &upsert); + if (!bson_parser_parse (parser, &model_bson, error)) { + goto done; + } + + mongoc_bulkwrite_updateoneopts_t *opts = mongoc_bulkwrite_updateoneopts_new (); + mongoc_bulkwrite_updateoneopts_set_arrayfilters (opts, arrayFilters); + mongoc_bulkwrite_updateoneopts_set_collation (opts, collation); + if (hint) { + mongoc_bulkwrite_updateoneopts_set_hint (opts, bson_val_to_value (hint)); + } + if (upsert) { + mongoc_bulkwrite_updateoneopts_set_upsert (opts, *upsert); + } + + if (!mongoc_bulkwrite_append_updateone (bw, namespace, filter, update, opts, error)) { + mongoc_bulkwrite_updateoneopts_destroy (opts); + goto done; + } + mongoc_bulkwrite_updateoneopts_destroy (opts); + } else if (0 == strcmp ("updateMany", model_name)) { + // Parse an "updateMany". + bson_parser_utf8 (parser, "namespace", &namespace); + bson_parser_doc (parser, "filter", &filter); + bson_parser_array_or_doc (parser, "update", &update); + bson_parser_array_optional (parser, "arrayFilters", &arrayFilters); + bson_parser_doc_optional (parser, "collation", &collation); + bson_parser_any_optional (parser, "hint", &hint); + bson_parser_bool_optional (parser, "upsert", &upsert); + if (!bson_parser_parse (parser, &model_bson, error)) { + goto done; + } + + mongoc_bulkwrite_updatemanyopts_t *opts = mongoc_bulkwrite_updatemanyopts_new (); + mongoc_bulkwrite_updatemanyopts_set_arrayfilters (opts, arrayFilters); + mongoc_bulkwrite_updatemanyopts_set_collation (opts, collation); + if (hint) { + mongoc_bulkwrite_updatemanyopts_set_hint (opts, bson_val_to_value (hint)); + } + if (upsert) { + mongoc_bulkwrite_updatemanyopts_set_upsert (opts, *upsert); + } + + if (!mongoc_bulkwrite_append_updatemany (bw, namespace, filter, update, opts, error)) { + mongoc_bulkwrite_updatemanyopts_destroy (opts); + goto done; + } + mongoc_bulkwrite_updatemanyopts_destroy (opts); + } else if (0 == strcmp ("deleteOne", model_name)) { + // Parse a "deleteOne". + bson_parser_utf8 (parser, "namespace", &namespace); + bson_parser_doc (parser, "filter", &filter); + bson_parser_doc_optional (parser, "collation", &collation); + bson_parser_any_optional (parser, "hint", &hint); + if (!bson_parser_parse (parser, &model_bson, error)) { + goto done; + } + + mongoc_bulkwrite_deleteoneopts_t *opts = mongoc_bulkwrite_deleteoneopts_new (); + mongoc_bulkwrite_deleteoneopts_set_collation (opts, collation); + if (hint) { + mongoc_bulkwrite_deleteoneopts_set_hint (opts, bson_val_to_value (hint)); + } + + if (!mongoc_bulkwrite_append_deleteone (bw, namespace, filter, opts, error)) { + mongoc_bulkwrite_deleteoneopts_destroy (opts); + goto done; + } + mongoc_bulkwrite_deleteoneopts_destroy (opts); + } else if (0 == strcmp ("deleteMany", model_name)) { + // Parse a "deleteMany". + bson_parser_utf8 (parser, "namespace", &namespace); + bson_parser_doc (parser, "filter", &filter); + bson_parser_doc_optional (parser, "collation", &collation); + bson_parser_any_optional (parser, "hint", &hint); + if (!bson_parser_parse (parser, &model_bson, error)) { + goto done; + } + + mongoc_bulkwrite_deletemanyopts_t *opts = mongoc_bulkwrite_deletemanyopts_new (); + mongoc_bulkwrite_deletemanyopts_set_collation (opts, collation); + if (hint) { + mongoc_bulkwrite_deletemanyopts_set_hint (opts, bson_val_to_value (hint)); + } + + if (!mongoc_bulkwrite_append_deletemany (bw, namespace, filter, opts, error)) { + mongoc_bulkwrite_deletemanyopts_destroy (opts); + goto done; + } + mongoc_bulkwrite_deletemanyopts_destroy (opts); + } else if (0 == strcmp ("replaceOne", model_name)) { + // Parse a "replaceOne". + bson_parser_utf8 (parser, "namespace", &namespace); + bson_parser_doc (parser, "filter", &filter); + bson_parser_doc (parser, "replacement", &replacement); + bson_parser_doc_optional (parser, "collation", &collation); + bson_parser_bool_optional (parser, "upsert", &upsert); + bson_parser_any_optional (parser, "hint", &hint); + if (!bson_parser_parse (parser, &model_bson, error)) { + goto done; + } + + mongoc_bulkwrite_replaceoneopts_t *opts = mongoc_bulkwrite_replaceoneopts_new (); + mongoc_bulkwrite_replaceoneopts_set_collation (opts, collation); + if (hint) { + mongoc_bulkwrite_replaceoneopts_set_hint (opts, bson_val_to_value (hint)); + } + if (upsert) { + mongoc_bulkwrite_replaceoneopts_set_upsert (opts, *upsert); + } + + if (!mongoc_bulkwrite_append_replaceone (bw, namespace, filter, replacement, opts, error)) { + mongoc_bulkwrite_replaceoneopts_destroy (opts); + goto done; + } + mongoc_bulkwrite_replaceoneopts_destroy (opts); + } else { + test_set_error (error, "unsupported model: %s", model_name); + goto done; + } + + ok = true; +done: + bson_parser_destroy_with_parsed_fields (parser); + return ok; +} + +static bool +operation_client_bulkwrite (test_t *test, operation_t *op, result_t *result, bson_error_t *error) +{ + bool ret = false; + mongoc_client_t *client = NULL; + mongoc_bulkwrite_t *bw = NULL; + mongoc_bulkwriteopts_t *opts = mongoc_bulkwriteopts_new (); + + client = entity_map_get_client (test->entity_map, op->object, error); + if (!client) { + goto done; + } + + int64_t nmodels = 0; + + // Parse arguments. + { + bool parse_ok = false; + bson_t *args_models = NULL; + bool *args_verboseResults = NULL; + bool *args_ordered = NULL; + bson_t *args_comment = NULL; + bool *args_bypassDocumentValidation = NULL; + bson_t *args_let = NULL; + mongoc_write_concern_t *args_wc = NULL; + bson_parser_t *parser = bson_parser_new (); + + bson_parser_array (parser, "models", &args_models); + bson_parser_bool_optional (parser, "verboseResults", &args_verboseResults); + bson_parser_bool_optional (parser, "ordered", &args_ordered); + bson_parser_doc_optional (parser, "comment", &args_comment); + bson_parser_bool_optional (parser, "bypassDocumentValidation", &args_bypassDocumentValidation); + bson_parser_doc_optional (parser, "let", &args_let); + bson_parser_write_concern_optional (parser, &args_wc); + if (!bson_parser_parse (parser, op->arguments, error)) { + goto parse_done; + } + if (args_verboseResults && *args_verboseResults) { + mongoc_bulkwriteopts_set_verboseresults (opts, true); + } + if (args_ordered) { + mongoc_bulkwriteopts_set_ordered (opts, *args_ordered); + } + if (args_comment) { + mongoc_bulkwriteopts_set_comment (opts, args_comment); + } + if (args_bypassDocumentValidation) { + mongoc_bulkwriteopts_set_bypassdocumentvalidation (opts, *args_bypassDocumentValidation); + } + if (args_let) { + mongoc_bulkwriteopts_set_let (opts, args_let); + } + if (args_wc) { + mongoc_bulkwriteopts_set_writeconcern (opts, args_wc); + } + + // Parse models. + bson_iter_t args_models_iter; + BSON_ASSERT (bson_iter_init (&args_models_iter, args_models)); + bw = mongoc_client_bulkwrite_new (client); + while (bson_iter_next (&args_models_iter)) { + nmodels++; + bson_t model_wrapper; + bson_iter_bson (&args_models_iter, &model_wrapper); + if (!append_client_bulkwritemodel (bw, &model_wrapper, error)) { + if (error->domain != TEST_ERROR_DOMAIN) { + // Propagate error as a test result. + result_from_val_and_reply (result, NULL, NULL, error); + // Return with a success (to not abort test runner) and propagate + // the error as a result. + ret = true; + *error = (bson_error_t){0}; + bson_parser_destroy_with_parsed_fields (parser); + goto done; + } + goto parse_done; + } + } + + parse_ok = true; + parse_done: + bson_parser_destroy_with_parsed_fields (parser); + if (!parse_ok) { + goto done; + } + } + + // Do client bulk write. + mongoc_bulkwrite_set_session (bw, op->session); + mongoc_bulkwritereturn_t bwr = mongoc_bulkwrite_execute (bw, opts); + + result_from_bulkwritereturn (result, bwr, nmodels); + mongoc_bulkwriteexception_destroy (bwr.exc); + mongoc_bulkwriteresult_destroy (bwr.res); + ret = true; +done: + mongoc_bulkwriteopts_destroy (opts); + mongoc_bulkwrite_destroy (bw); + return ret; +} + static bool operation_create_datakey (test_t *test, operation_t *op, result_t *result, bson_error_t *error) { @@ -2609,6 +2889,9 @@ operation_start_transaction (test_t *test, operation_t *op, result_t *result, bs bson_parser_read_concern_optional (bp, &rc); bson_parser_write_concern_optional (bp, &wc); bson_parser_read_prefs_optional (bp, &rp); + if (!bson_parser_parse (bp, op->arguments, error)) { + goto done; + } if (rc) { mongoc_transaction_opts_set_read_concern (opts, rc); } @@ -3006,8 +3289,8 @@ static bool create_loop_bson_array_entity (entity_map_t *em, const char *id, bson_error_t *error) { BSON_ASSERT_PARAM (em); - BSON_ASSERT (id || true); - BSON_ASSERT (error || true); + BSON_OPTIONAL_PARAM (id); + BSON_OPTIONAL_PARAM (error); if (!id) { // Nothing to do. @@ -3040,8 +3323,8 @@ static bool create_loop_size_t_entity (entity_map_t *em, const char *id, bson_error_t *error) { BSON_ASSERT_PARAM (em); - BSON_ASSERT (id || true); - BSON_ASSERT (error || true); + BSON_OPTIONAL_PARAM (id); + BSON_OPTIONAL_PARAM (error); if (!id) { return true; @@ -3063,7 +3346,7 @@ static void increment_loop_counter (entity_map_t *em, const char *id) { BSON_ASSERT_PARAM (em); - BSON_ASSERT (id || true); + BSON_OPTIONAL_PARAM (id); if (id) { bson_error_t error = {0}; @@ -3080,9 +3363,9 @@ static bool append_loop_error (entity_map_t *em, const char *id, bson_error_t *op_error, bson_error_t *error) { BSON_ASSERT_PARAM (em); - BSON_ASSERT (id || true); + BSON_OPTIONAL_PARAM (id); BSON_ASSERT_PARAM (op_error); - BSON_ASSERT (error || true); + BSON_OPTIONAL_PARAM (error); if (!id) { return true; @@ -3520,6 +3803,7 @@ operation_run (test_t *test, bson_t *op_bson, bson_error_t *error) {"createChangeStream", operation_create_change_stream}, {"listDatabases", operation_list_databases}, {"listDatabaseNames", operation_list_database_names}, + {"clientBulkWrite", operation_client_bulkwrite}, /* ClientEncryption operations */ {"createDataKey", operation_create_datakey}, diff --git a/src/libmongoc/tests/unified/result.c b/src/libmongoc/tests/unified/result.c index de8c5fbb067..14a226de03d 100644 --- a/src/libmongoc/tests/unified/result.c +++ b/src/libmongoc/tests/unified/result.c @@ -34,6 +34,10 @@ struct _result_t { bson_t *reply; char *str; bool array_of_root_docs; + // For a mongoc_client_bulkwrite operation, `write_errors` and + // `write_concern_errors` are optionally set. + bson_t *write_errors; + bson_t *write_concern_errors; }; result_t * @@ -66,6 +70,8 @@ _result_init (result_t *result, const bson_val_t *value, const bson_t *reply, co memcpy (&result->error, error, sizeof (bson_error_t)); result->ok = (error->code == 0); result->str = bson_string_free (str, false); + result->write_errors = bson_new (); + result->write_concern_errors = bson_new (); } void @@ -77,6 +83,8 @@ result_destroy (result_t *result) bson_val_destroy (result->value); bson_destroy (result->reply); bson_free (result->str); + bson_destroy (result->write_errors); + bson_destroy (result->write_concern_errors); bson_free (result); } @@ -329,6 +337,8 @@ result_check (result_t *result, entity_map_t *em, bson_val_t *expect_result, bso bson_t *error_labels_omit; bson_val_t *error_expect_result; bson_val_t *error_response; + bson_t *write_errors = NULL; + bson_t *write_concern_errors = NULL; if (!expect_result && !expect_error) { if (!result->ok) { @@ -342,11 +352,23 @@ result_check (result_t *result, entity_map_t *em, bson_val_t *expect_result, bso /* check result. */ if (expect_result) { if (!result->ok) { + if (!bson_empty (result->write_errors)) { + char *as_json = bson_as_canonical_extended_json (result->write_errors, NULL); + test_diagnostics_error_info ("Write errors: %s", as_json); + bson_free (as_json); + } + if (!bson_empty (result->write_concern_errors)) { + char *as_json = bson_as_canonical_extended_json (result->write_concern_errors, NULL); + test_diagnostics_error_info ("Write concern errors: %s", as_json); + bson_free (as_json); + } test_set_error (error, "expected result, but got error: %s", result->error.message); goto done; } if (!entity_map_match (em, expect_result, result->value, result->array_of_root_docs, error)) { - test_diagnostics_error_info ("expectResult mismatch:\nExpected: %s\nActual: %s\n", + test_diagnostics_error_info ("Result mismatch:\n" + "Expected: %s\n" + "Actual: %s\n", bson_val_to_json (expect_result), bson_val_to_json (result->value)); goto done; @@ -364,6 +386,8 @@ result_check (result_t *result, entity_map_t *em, bson_val_t *expect_result, bso bson_parser_array_optional (parser, "errorLabelsOmit", &error_labels_omit); bson_parser_any_optional (parser, "expectResult", &error_expect_result); bson_parser_any_optional (parser, "errorResponse", &error_response); + bson_parser_doc_optional (parser, "writeErrors", &write_errors); + bson_parser_array_optional (parser, "writeConcernErrors", &write_concern_errors); if (!bson_parser_parse (parser, expect_error, error)) { goto done; } @@ -544,6 +568,91 @@ result_check (result_t *result, entity_map_t *em, bson_val_t *expect_result, bso bson_val_destroy (val_to_match); bson_destroy (&doc_to_match); } + + if (write_errors) { + if (!result->write_errors) { + test_set_error (error, "Expected writeErrors but got none"); + goto done; + } + + // Ensure the numeric keys of the expected `writeErrors` exactly match + // the actual `writeErrors`. + { + bson_t *expected = write_errors; + bson_t *actual = result->write_errors; + + bson_iter_t expected_iter; + bson_iter_init (&expected_iter, expected); + while (bson_iter_next (&expected_iter)) { + bson_val_t *expected_val = bson_val_from_iter (&expected_iter); + bson_val_t *actual_val = NULL; + const char *key = bson_iter_key (&expected_iter); + bson_iter_t actual_iter; + if (!bson_iter_init_find (&actual_iter, actual, key)) { + test_set_error (error, + "error.writeErrors[%s] not found.\n" + "Expected: %s\n" + "Actual : (not found)\n", + key, + bson_val_to_json (expected_val)); + bson_val_destroy (actual_val); + bson_val_destroy (expected_val); + goto done; + } + actual_val = bson_val_from_iter (&actual_iter); + + if (!bson_match (expected_val, actual_val, false, error)) { + test_diagnostics_error_info ("error.writeErrors[%s] mismatch:\n" + "Expected: %s\n" + "Actual : %s\n", + key, + bson_val_to_json (expected_val), + bson_val_to_json (actual_val)); + bson_val_destroy (actual_val); + bson_val_destroy (expected_val); + goto done; + } + bson_val_destroy (actual_val); + bson_val_destroy (expected_val); + } + + // Ensure no extra reported errors. + bson_iter_t actual_iter; + bson_iter_init (&actual_iter, actual); + if (bson_iter_next (&actual_iter)) { + bson_val_t *actual_val = bson_val_from_iter (&actual_iter); + const char *key = bson_iter_key (&actual_iter); + if (!bson_has_field (expected, key)) { + test_set_error (error, + "error.writeErrors[%s] mismatch:\n" + "Expected: (not found)\n" + "Actual : %s\n", + key, + bson_val_to_json (actual_val)); + bson_val_destroy (actual_val); + goto done; + } + bson_val_destroy (actual_val); + } + } + } + + if (write_concern_errors) { + bson_val_t *expected_val = bson_val_from_array (write_concern_errors); + bson_val_t *actual_val = bson_val_from_array (result->write_concern_errors); + if (!bson_match (expected_val, actual_val, true /* array of root documents */, error)) { + test_diagnostics_error_info ("error.writeConcernErrors mismatch:\n" + "Expected: %s\n" + "Actual : %s\n", + bson_val_to_json (expected_val), + bson_val_to_json (actual_val)); + bson_val_destroy (actual_val); + bson_val_destroy (expected_val); + goto done; + } + bson_val_destroy (actual_val); + bson_val_destroy (expected_val); + } } ret = true; @@ -552,6 +661,57 @@ result_check (result_t *result, entity_map_t *em, bson_val_t *expect_result, bso return ret; } +void +result_from_bulkwritereturn (result_t *result, mongoc_bulkwritereturn_t bwr, size_t nmodels) +{ + // Build up the result value as a BSON document. + bson_t bwr_bson = BSON_INITIALIZER; + if (bwr.res) { + BSON_APPEND_INT32 (&bwr_bson, "insertedCount", mongoc_bulkwriteresult_insertedcount (bwr.res)); + BSON_APPEND_INT32 (&bwr_bson, "upsertedCount", mongoc_bulkwriteresult_upsertedcount (bwr.res)); + BSON_APPEND_INT32 (&bwr_bson, "matchedCount", mongoc_bulkwriteresult_matchedcount (bwr.res)); + BSON_APPEND_INT32 (&bwr_bson, "modifiedCount", mongoc_bulkwriteresult_modifiedcount (bwr.res)); + BSON_APPEND_INT32 (&bwr_bson, "deletedCount", mongoc_bulkwriteresult_deletedcount (bwr.res)); + const bson_t *ir = mongoc_bulkwriteresult_insertresults (bwr.res); + if (ir) { + BSON_APPEND_DOCUMENT (&bwr_bson, "insertResults", ir); + } + const bson_t *ur = mongoc_bulkwriteresult_updateresults (bwr.res); + if (ur) { + BSON_APPEND_DOCUMENT (&bwr_bson, "updateResults", ur); + } + const bson_t *dr = mongoc_bulkwriteresult_deleteresults (bwr.res); + if (dr) { + BSON_APPEND_DOCUMENT (&bwr_bson, "deleteResults", dr); + } + } + + bson_error_t error = {0}; + const bson_t *errorReply = NULL; + // Include `errorLabels` and `errorReply` when initializing `result`. + if (bwr.exc) { + mongoc_bulkwriteexception_error (bwr.exc, &error); + errorReply = mongoc_bulkwriteexception_errorreply (bwr.exc); + } + + bson_val_t *bwr_val = bson_val_from_bson (&bwr_bson); + result_from_val_and_reply (result, bwr_val, errorReply, &error); + + // Add `writeErrors` and `writeConcernErrors` after initializing. + if (bwr.exc) { + result->ok = false; // An error occurred. + const bson_t *writeErrors = mongoc_bulkwriteexception_writeerrors (bwr.exc); + bson_destroy (result->write_errors); + result->write_errors = bson_copy (writeErrors); + const bson_t *writeConcernErrors = mongoc_bulkwriteexception_writeconcernerrors (bwr.exc); + bson_destroy (result->write_concern_errors); + result->write_concern_errors = bson_copy (writeConcernErrors); + } + + bson_destroy (&bwr_bson); + bson_val_destroy (bwr_val); +} + static void test_resultfrombulkwrite (void) { diff --git a/src/libmongoc/tests/unified/result.h b/src/libmongoc/tests/unified/result.h index b0cf1b75f86..c723a9d576a 100644 --- a/src/libmongoc/tests/unified/result.h +++ b/src/libmongoc/tests/unified/result.h @@ -21,6 +21,7 @@ #include "bsonutil/bson-val.h" #include "entity-map.h" #include "mongoc-cursor.h" +#include "mongoc-bulkwrite.h" typedef struct _result_t result_t; @@ -33,6 +34,9 @@ result_destroy (result_t *result); void result_from_bulk_write (result_t *result, const bson_t *reply, const bson_error_t *error); +void +result_from_bulkwritereturn (result_t *result, mongoc_bulkwritereturn_t bwr, size_t nmodels); + void result_from_insert_one (result_t *result, const bson_t *reply, const bson_error_t *error); diff --git a/src/libmongoc/tests/unified/runner.c b/src/libmongoc/tests/unified/runner.c index c43a6d4248c..6b2d0c92e65 100644 --- a/src/libmongoc/tests/unified/runner.c +++ b/src/libmongoc/tests/unified/runner.c @@ -571,27 +571,28 @@ get_topology_type (mongoc_client_t *client) static void check_schema_version (test_file_t *test_file) { - const char *supported_version_strs[] = {"1.8", /* fully supported through this version */ - "1.12", /* partially supported (expectedError.errorResponse assertions) */ - "1.18" /* partially supported (additional properties in kmsProviders) */}; - int i; + // `schema_version` is the latest schema version the test runner will try to run. + // 1.8 is fully supported. Later minor versions are partially supported. + // 1.12 is partially supported (expectedError.errorResponse assertions) + // 1.18 is partially supported (additional properties in kmsProviders) + // 1.20 is partially supported (expectedError.writeErrors and expectedError.writeConcernErrors) + semver_t schema_version; + semver_parse ("1.20", &schema_version); - for (i = 0; i < sizeof (supported_version_strs) / sizeof (supported_version_strs[0]); i++) { - semver_t supported_version; + if (schema_version.major != test_file->schema_version.major) { + goto fail; + } - semver_parse (supported_version_strs[i], &supported_version); - if (supported_version.major != test_file->schema_version.major) { - continue; - } - if (!supported_version.has_minor) { - /* All minor versions for this major version are supported. */ - return; - } - if (supported_version.minor >= test_file->schema_version.minor) { - return; - } + if (!schema_version.has_minor) { + /* All minor versions for this major version are supported. */ + return; } + if (schema_version.minor >= test_file->schema_version.minor) { + return; + } + +fail: test_error ("Unsupported schema version: %s", semver_to_string (&test_file->schema_version)); } @@ -1338,7 +1339,7 @@ static void append_bson_array (bson_t *doc, const char *key, const mongoc_array_t *array) { BSON_ASSERT_PARAM (key); - BSON_ASSERT (array || true); + BSON_OPTIONAL_PARAM (array); if (!array) { bson_t empty = BSON_INITIALIZER; @@ -1718,4 +1719,6 @@ test_install_unified (TestSuite *suite) run_unified_tests (suite, JSON_DIR, "retryable_writes/unified"); run_unified_tests (suite, JSON_DIR, "index-management"); + + run_unified_tests (suite, JSON_DIR, "command-logging-and-monitoring/monitoring"); }