Skip to content

Conversation

@eramongodb
Copy link
Contributor

@eramongodb eramongodb commented Oct 9, 2025

Summary

Resolves CXX-3236. Followup to #1462 and companion PR to #1469 and #1470. Contains cherry-pick of proposed changes in #1469 (the "cpx: cxx-abi-v1-instance" commit).

This is 5 out of an estimated 7 major PRs which in total are expected to resolve CXX-2745 and CXX-3320.

Related tickets affected by the changes in this PR (applicable only to the v1 API) include (not exhaustive, but tried my best):

  • CXX-849: "Audit API usage of int32_t"
  • CXX-1046: "Hide members of options classes behind private implementation"
  • CXX-1203: "Audit methods for noexcept and constexpr" (no constexpr)
  • CXX-1229: "Fix incorrect constness in several places"
  • CXX-1245 "Guarantee thread safety for const methods in mongocxx API"
  • CXX-1524: "Audit CXX usage of libmongoc APIs"
  • CXX-1546: "Return document::view instead of document::view_or_value from options getters"
  • CXX-1617: "Permit options::client::ssl_opts without "ssl=true" in URI"
  • CXX-1801: "stop deleting the rvalue overload of pool::entry::operator->()"
  • CXX-1826: "Reduce namespace nesting"
  • CXX-1836: "Nest options inside of their associated classes"
  • CXX-1827: "Reduce friction between view and value types"
  • CXX-1828: "Add constraints for templated functions"
  • CXX-1844: "Error hierarchy uncorrelated with error domains"
  • CXX-2038: "Change URI option getters to return optional views where possible"
  • CXX-2266: "Use bsoncxx::string::view_or_value in gridfs::bucket"
  • CXX-2270: "Audit string arguments"
  • CXX-2367: "Review #include hygiene in header files"
  • CXX-2376: "Revisit C++ client exceptions"
  • CXX-3257: "Remove deprecated mongocxx::v_noabi::options::index::storage_options"

Relative Changelog (v_noabi -> v1)

  • Added
    • is_open() to v1::gridfs::downloader and v1::gridfs::uploader to extend behavior of operator bool().
    • flush() to v1::gridfs::uploader (exposing v_noabi::gridfs::uploader::flush_chunks()) + documenting the existence of underlying buffers for better control over its behavior.
    • chunks_written() to v1::gridfs::uploader (useful information).
    • Add .upserted_ids() for v1::update_many_result for consistency with v1::bulk_write::result (vs. v1::update_one_result which uses the old .upserted_id()).
    • Add k_unknown to v1::write_concern for symmetry with v1::read_concern.
    • Add copyability to v1::indexes, v1::pipeline, v1::bulk_write::single, etc.
    • v1::bulk_write::result::raw() to allow access to the raw server response.
    • v1::bulk_write::insert_one::value is made public.
    • Allow rvalue invocation of v1::pool::entry::operator->, v1::client::database(), etc. (by removing deleted overloads and ref-qualifiers; premature pessimization).
  • Changed
    • v_noabi::*::view_or_value is replaced with either view or value (std::string for v_noabi::string::view_or_value).
      • For parameters:
        • If the value is (eventually) owned by mongocxx, value.
        • If the value is (eventually) passed to or owned by mongoc, view.
        • Some string parameters known to require null-terminated strings are defined in terms of a char const* to allow avoiding forced allocations. (We could really use a cstring_view...)
      • For return values:
        • Always view, with a few exceptions for "simple" classes wrapping the underlying value with no additional invariants (e.g. v1::bulk_write::insert_one).
        • Always replace Optional<view_or_value> -> Optional<View> (view_or_value implies internal ownership).
    • Do not return T const& or Optional<T> const&: return T or Optional<T> instead.
      • Avoid returning references to internal representation (e.g. Optional<T> const&) which severely restrict implementation freedom.
      • The only functions which return a reference are special member functions, error category functions, and setters (method chaining), plus the following exceptions:
        • v1::client_session::client(): return the associated client (must be a mongocxx reference).
        • v1::bulk_write::single::get_*(): straightforward union-like API.
    • Remove const qualifier from member functions and parameters which are not logically const.
      • v1::collection::list_indexes() uses mongoc_collection_t*.
      • v1::database parameters in v1::client_encryption use mongoc_database_t*.
      • v1::client database operations use mongoc_client_t*.
      • Several stray top-level const in return types are removed.
    • Consistently return T& for setters (method-chaining) where various functions used to return void.
    • v1::server_error
      • Invariant: a raw server error document is always available (not optional).
      • When a server error code is not accompanied by a raw server document (rare), throw as mongocxx::v1::exception with .code() == v1::source_errc::server.
      • Fix documentation for server vs. client error codes (.code() is always the server error code, .client_code() is the optional client error code, for consistency with the .raw() invariant).
    • v1::event::*
      • Use Ro0 instead of Ro5 for "view-only" trivially-copyable classes.
        • All void* are labeled with a comment indicating the underlying type.
        • v1::events::server_description is the sole exception due to v1::events::topology_description::servers(), see "Removed" entry below.
        • The "impl" constructors are made private and included in this PR to indicate the default constructor(s) are deliberately disabled.
      • server_heartbeat_failed::message() return type: string -> string_view.
    • v1::read_concern: ignore unsupported values instead of throwing an exception.
    • v1::read_preference: treat invalid read modes as k_primary instead of an error (same behavior as mongoc_read_prefs_get_mode()).
    • v1::write_concern
      • Treat .tag(nullptr) as k_default instead of an error.
      • Avoid .acknowledge_level() errors by converting unsupported values to k_unknown and document "not k_unknown" as a precondition for .to_document() (treat as if unset).
    • Ensure and document v1::gridfs::uploader flushes its buffer on destruction (it didn't already do this?!).
    • Unconditionally not-null T* parameters are made T& instead for input/output streams to v1::gridfs::bucket.
    • Return T* instead of Optional<T*> for .key_vault_*() in v1::auto_encryption and v1::client_encryption.
    • Change v1::search_indexes::create_index() return type to be consistent with v1::indexes::create_one().
    • Replace the deprecated .comment() with .comment_option() in v1::find_options for consistency with other option classes.
    • Basic template parameter constraints for v1::collection's bulk write API.
    • Int32 -> Int64 for bulk write API result values and some v1::pipeline fields.
    • id_map for v1::bulk_write::result and related API:
      • Change key type from Size -> Int64 for keys of id_map (e.g. v1::bulk_write::result) for better spec accuracy.
      • Change value type from document::element to types::view to avoid unnecessary proxy type (element key is always "_id"; useless info) + consistency with other "result" API.
    • Make v1::bulk_write::* operation single-argument constructors explicit.
    • Rename v1::client_session::options() -> .opts() to avoid conflict with ::options.
    • Change with_transaction_cb parameter type from T* (unconditional not-null) to T&.
  • Removed
    • v_noabi::events::topology_description::server_descriptions: move ownership of underlying mongoc_server_description_t objects to v1::events::server_description and avoid the awkward intermediate class. The array itself (not its elements) is deallocated after the std::vector<v1::events::server_description> is fully constructed.
    • v_noabi::gridfs::chunks_and_bytes_offset: implementation detail not used by any public API.
    • v_noabi::result::gridfs::upload ctor is made internal for v1::gridfs::upload_result.
    • ordered() for v1::insert_one_options (specific to v1::insert_many_options).
    • Removed deprecated v_noabi::options::index::base_storage_options and related API.
    • Removed deprecated v_noabi::read_preference::hedge().
    • Deprecated concern+preference accessors for v1::client.
    • Deprecated .ssl() API.
    • Remove deprecated v_noabi::options::index::haystack*() API.
    • Remove unused v_noabi::options::index::version(). (Was this meant to be "textIndexVersion"?)
  • Deferred/Rejected/Todo
    • API consistency across various "convertible to a BSON document" classes.
      • Required changes are far beyond the scope of this PR.
      • Conflicting design philosophies and goals over the years, e.g. CXX-763 vs. CXX-1359 vs. CXX-1833 (non-exhaustive list).
      • Inconsistent presence vs. absence of equality comparison operators is left unchanged.
    • Specification conformance for exception class types that should derive from v1::server_error (e.g. WriteConcernError, BulkWriteError, etc.).
      • Prior exception class types were not conforming either (did not provide necessary fields with error-specific information).
      • v1::server_error::raw() should continue to suffice as a workaround for obtaining error-specific fields manually.
    • Improving constraint validation of APM event handlers (e.g. noexcept) via template parameter constraints (e.g. like bsoncxx::v1::document::value's deleter API).
    • v1::events::*: .duration() and .round_trip_time(): changing the return type from std::int64_t to std::chrono::microseconds.
    • v1::events::server_heartbeat_failed: support returning or throwing a mongocxx::v1::exception for the bson_error_t?
    • v1::events::server_description does not conform to specification for ServerDescription.
    • v1::tls underspecification and lack of support for all mongoc_ssl_opt_t fields.
    • Many issues with v1::collection: missing access to raw server response (e.g. .rename()), missing support for additional command options (e.g. .rename()), missing session overloads (e.g. .estimated_document_count()), awkward range templates, use of bulk write operations instead of dedicated database commands (e.g. .insert_one() using bulk write "insertMany" instead of the insert database command?).
    • Many issues with indexes vs. search_indexes API symmetry and specification compliance (exacerbated by the model vs. command-specific options vs. common index options situation; v1::search_indexes::model should probably be split into SearchIndexModel and SearchIndexOptions).
    • Missing support for various fields, options, and commands.
    • Validation and error handling that should be handled by mongoc, but is not yet being done (e.g. the collation && !is_acknowledged condition for .find_and_modify() and etc.).
    • Fields which may be specified in multiple ways and override one another (underspecified).

General Notes

  • Minimize noisy, redundant, and superfluous documentation as much as possible.
    • Avoid silly \param or \returns when behavior is plainly obvious from the declaration (see: Slack). Only include when at least one parameter or the return value requires documentation of special values and behaviors that is not apparent from the types alone. Prefer \par Preconditions for partial parameter documentation.
      • e.g. Optional<T> return type for a "field" is "obviously" empty when "unset".
      • e.g. an argument of 0 being equivalent to "unset" for an Int field is not "obvious", thus it is documented.
      • e.g. v1::client::start_session documents: "The client session object MUST be passed as the first argument to all operations that are intended to be executed in the context of a session." -> avoid needing countless repetition of \param session paragraphs. The overload complexity should be revisited and addressed at a later time... (CXX-1835)
    • Use "Equivalent to ..." whenever able to avoid repetitive documention when \copydoc is not applicable.
      • Note: () is omitted given \ref func_name when more than one overload may apply.
      • Use \{ ... \} Doxygen groups when able (helped by absence of \param).
      • Take advantage of Markdown code blocks: "a code block is worth a thousand words".
    • Avoid documenting error codes coming from external libraries (e.g. mongoc) in detail.
      • e.g. "\throws mongocxx::v1::exception for all other runtime errors".
      • Most users only use ex.what(); encourage programmatic inspection via v1::source_errc and v1::type_errc instead.
      • Errors specific to mongocxx library behavior are defined using component-specific errc (as in bsoncxx), of which there are currently only 9 in total (all other errors are deferred to mongoc):
        • instance (multiple object detection: CXX-3320 migrate instance and logger to mongocxx::v1 #1469)
        • server_api (translate false returned by mongoc API for invalid versions into an exception)
        • uri (server_selection_try_once() setter throws instead of returning a boolean)
        • pool (wait queue timeout for .acquire())
        • collection (narrowing conversion from std::chrono::duration Int64 into a UInt32 for mongoc API compatibility)
        • indexes (index name "*" is not permitted for .drop_one(), no relevant mongoc API to defer to)
        • v1::gridfs::bucket (GridFS file metadata validation + database commands)
        • v1::gridfs::downloader (runtime GridFS file metadata validation and underlying stream state)
        • v1::gridfs::uploader (runtime GridFS file metadata validation and underlying stream state)
    • Define and document "supported fields" instead of "data members" with explicit reference to BSON document field names when applicable for reference and searchability.
      • Ensure parameter names and/or documented "supported fields" match external documentation and specification.
  • Defer validation and error handling to mongoc whenever possible.
    • Reduce mongocxx specific behaviors and redundant (re)implementation of behaviors handled by mongoc.
    • Avoid the need for many exception cases and isolate "may throw an exception" points to the actual point-of-use (i.e. executing a database command).
  • Aggressively use forward headers to minimize transitive dependencies as much as possible. (CXX-2367)
    • e.g. v1/database.hpp no longer includes v1/collection.hpp and all of its transitive dependencies. 🎉
    • Note: void fn(T) and T fn() do not require T to be a complete type (even when T = Optional<U>!).
    • Unable when default arguments are used. 😢 An effort is made to avoid default arguments, but not feasible at scale for some API (i.e. collection member functions)...
    • Somewhat forces Include What You Use (IWYU) in downstream code by providing the most minimal declarations necessary per component.
  • Clarify and fixup API for iterator types (similar to bsoncxx::v1::document::view).
    • Make end iterators equivalent to default-constructed iterator + iterator end() const even when iterator begin() (not-const).
    • Avoid confusing "exhausted" terminology for cursor iterators (not to be confused with "exhaust cursors").
  • Clarify and document discrepancies between specification and mongoc-specific behavior (e.g. bulk write result documents).
  • Identify and avoid unnecessary pointer semantics by changing T* -> T& when unconditionally not-null, or by changing Optional<T*> -> T* (double-null).

Review Focus Items

  • All classes and member functions should have a reference to documentation or specification describing the behavior and/or purpose of the given class or function.
    • This includes supported "fields" as parameters or data members (e.g. filter).
    • Documentation for member functions defer to the parent class (e.g. client_encryption) or the parameter (class) type (e.g. write_concern).
  • GridFS download/upload buffer behavior was previously undocumented/underspecified. Check that the new documentation is accurate.
  • Check that documented preconditions and postconditions are correct and consistent.

Questions for Review

  • Should std::unique_ptr<impl> _impl be made void* _impl instead (even when a class impl is used) to leave open the possibility of replacing impl with a mongoc equivalent or vise versa as an ABI compatible change? Or is this "premature optimization" and using std::unique_ptr<T> is sufficient? Applied: now using void* _impl; for all mongocxx classes.
  • Regardless of the question above, should classes which can currently be implemented entirely in terms of a mongoc_*_t type use void* _impl; // mongoc_*_t, e.g. to avoid double-indirection? (Initial changes propose that only the view-like v1::events::* classes do this.) Applied: annotated with underlying type when applicable.
  • Is providing char const* + std::string + string_view set of overloads premature optimization? Should they be simplified to just a single string_view overload with an internal std::string allocation? Removed extra overloads: simple string_view only.
  • Is removing the deprecated v1::read_preference::hedge() premature? (CXX-3241) Reverted: removal is premature.
  • Is removing the deprecated readConcern + writeConcern + readPreference accessors in v1::client premature? (CXX-1939) Applied: removal is acceptable.
  • Is removing the deprecated geoHaystack API for v1::indexes premature? (CXX-1978) Applied: removal is acceptable.
  • Should mongoc_cursor_clone() be implemented by mongocxx by making cursors copyable? Or should mongoc_cursor_clone() be deprecated given its confusing behavior (re-execute the underlying query)? Applied: may add later upon request.
  • Should v1::change_stream::batch_size() support std::uint32_t for consistency with mongoc_cursor_set_batch_size()? Or does mongoc_cursor_set_batch_size() need to be updated to support std::int64_t instead? Neither: spec expects Int32.

@eramongodb eramongodb self-assigned this Oct 9, 2025
@eramongodb eramongodb requested a review from a team as a code owner October 9, 2025 16:24
Copy link
Collaborator

@kevinAlbs kevinAlbs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posting initial comments.

  • Should std::unique_ptr<impl> _impl be made void* _impl instead (even when a class impl is used) to leave open the possibility of replacing impl with a mongoc equivalent or vise versa as an ABI compatible change? Or is this "premature optimization" and using std::unique_ptr<T> is sufficient?

I am slightly in favor of switching to void *, but also OK with the current proposal. I expect saving one pointer dereference may not make an observable performance difference in most cases (especially when much of mongocxx API includes network calls), but this may help to future-proof the ABI.

  • Regardless of the question above, should classes which can currently be implemented entirely in terms of a mongoc_*_t type use void* _impl; // mongoc_*_t, e.g. to avoid double-indirection? (Initial changes propose that only the view-like v1::events::* classes do this.)

Similar: Slightly in favor, but OK with current proposal.

  • Is providing char const* + std::string + string_view set of overloads premature optimization? Should they be simplified to just a single string_view overload with an internal std::string allocation?

IIUC this only appears to impact four methods: read_concern::acknowledge_string(), server_error::has_error_label(), uri::uri(), and write_concern::tag.

Some libbson API was added accept a length argument (e.g. bson_iter_init_find_w_len). If libmongoc adds functions accepting a string length (suggested in comment), that may avoid a need for the char const* / std::string overloads. Filed CDRIVER-6128 to propose this API in the C driver.

IMO: I am slightly in favor of reducing the overloads since this could be eventually improved by libmongoc additions.

  • Is removing the deprecated v1::read_preference::hedge() premature? (CXX-3241)

I am in favor of adding to v1. Hedged reads was only deprecated in server 8.0. And only deprecated in the C++ driver v4.1.

  • Is removing the deprecated readConcern + writeConcern + readPreference accessors in v1::client premature? (CXX-1939)
  • Is removing the deprecated geoHaystack API for v1::indexes premature? (CXX-1978)

I am in favor of keepint these out. CXX-1939 deprecated 5 years ago, and CXX-1978 deprecated 8 years ago, I expect those have low usage. If that assumption is wrong, they can be added to v1 on request.

  • Should mongoc_cursor_clone() be implemented by mongocxx by making cursors copyable? Or should mongoc_cursor_clone() be deprecated given its confusing behavior (re-execute the underlying query)?

I also think mongoc_cursor_clone behavior is surprising (I would expect "clone" means to clone the current state). mongoc_cursor_clone documents the behavior. And this appears to be the requested behavior in CDRIVER-204 for a language binding. I would rather not continue the pattern in C++ driver unless there is a known need. But if there are users wanting the C API, I think it is fine as-is.

  • Should v1::change_stream::batch_size() support std::uint32_t for consistency with mongoc_cursor_set_batch_size()? Or does mongoc_cursor_set_batch_size() need to be updated to support std::int64_t instead?

The spec suggests both should be std::int32_t. I expect mongoc_cursor_set_batch_size should instead be int32_t to match the spec.

Copy link
Contributor Author

@eramongodb eramongodb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am slightly in favor of reducing the [NTBS] overloads since this could be eventually improved by libmongoc additions.

Done. This means we are accepting a performance penalty (due to unconditional std::string allocations) until mongoc improves support for length-based string API.

I am in favor of adding [.hedge()] to v1. Hedged reads was only deprecated in server 8.0.

Done. v1::read_preference also has deprecated .hedge() accessors.

I would rather not continue the [mongoc_cursor_clone()] pattern in C++ driver unless there is a known need.

Will leave v1::cursor as-is then. Both v1::cursor and v_noabi::cursor will remain non-copyable.

I am slightly in favor of switching to void * ... this may help to future-proof the ABI.

Done. All _impl data members are now annotated with the underlying type (class impl; is assumed and implied by the presence of the declaration).

The following classes are defined entirely in terms of the mongoc API:

  • v1::events::* (all event types are fully backed by mongoc equivalents)
  • v1::client_encryption -> mongoc_client_encryption_t
  • v1::client_session::options -> mongoc_session_opt_t
  • v1::read_concern -> mongoc_read_concern_t
  • v1::read_preference -> mongoc_read_prefs_t
  • v1::transaction -> mongoc_transaction_opt_t
  • v1::uri -> mongoc_uri_t
  • v1::write_concern -> mongoc_write_concern_t

The following classes are defined entirely in terms of other mongocxx API:

  • v1::change_stream::iterator -> v1::change_stream (shared state iterators)
  • v1::cursor::iterator -> v1::cursor (shared state iterators)

The following classes still use class impl; despite an apparent mongoc equivalent being available:

  • v1::apm (mongoc_apm_callbacks_t)
    • Needs to support std::function<T> getters.
  • v1::auto_encryption (mongoc_auto_encryption_opts_t)
    • Needs to support .key_vault_client() -> v1::client* and .key_vault_pool() -> v1::pool*.
    • Missing mongoc_auto_encryption_opts_get_*() for getters.
  • v1::change_stream (mongoc_change_stream_t)
    • Needs to support shared state for iterators (i.e. the event document).
  • v1::client_encryption::options (mongoc_client_encryption_opts_t)
    • Needs to support mongocxx getters (no mongoc_client_encryption_opts_get_*()).
  • v1::client_session (mongoc_client_session_t)
    • Needs to support .client() -> v1::client const& and .options() -> v1::client_session::options const&.
  • v1::client (mongoc_client_t)
    • Needs to extend the lifetime of v1::apm.
  • v1::collection (mongoc_collection_t)
    • Needs to store a reference to the associated v1::client object (possibly avoidable if we use mongoc_collection_create_indexes_with_opts() in v1::indexes::create_many()?).
  • v1::cursor (mongoc_cursor_t)
    • Needs to support shared state for iterators (i.e. the result document).
  • v1::database (mongoc_database_t)
    • Needs to store a reference to the associated v1::client object.
    • Missing mongoc_database_command_simple_with_server_id().
  • v1::encrypt (mongoc_client_encryption_encrypt_opts_t)
    • Missing mongoc_client_encryption_encrypt_opts_get_*() for getters.
  • v1::indexes::model (mongoc_index_model_t)
    • Missing mongoc_index_model_get_*() for getters.
  • v1::range (mongoc_client_encryption_encrypt_range_opts_t)
    • Missing mongoc_client_encryption_encrypt_range_opts_get_*() for getters.
  • v1::pool (mongoc_client_pool_t)
    • Needs to extend the lifetime of v1::apm.

@eramongodb eramongodb requested a review from kevinAlbs October 21, 2025 21:50
@eramongodb
Copy link
Contributor Author

Applied the decision made in #1487 to v1::server_error (Ro5 is still needed due to the _impl data member).

Copy link
Collaborator

@kevinAlbs kevinAlbs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work. The efforts towards API consistency and removal of view_or_value types is much appreciated. Most of comments are documentation fix-ups and questions.

/// - `upserted_count` ("nUpserted" or "upsertedCount")
/// - `upserted_ids` ("upserted" or "upsertedIds")
///
/// @important The raw server response is translated by mongoc into the Bulk Write API specification format even when
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider moving this note to v1::bulk_write::raw since I expect that is the method directly exposing the BSON result of mongoc_bulk_operation_execute.

Does "even when the CRUD API specification is used" mean when v1::bulk_write is used as the underlying operation for a different CRUD method (like v1::collection::insert_many?).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The note is class-level because the entire class is implemented in terms of mongoc's translated Bulk Write API fields:

std::int32_t bulk_write::inserted_count() const {
return view()["nInserted"].get_int32();
}
std::int32_t bulk_write::matched_count() const {
return view()["nMatched"].get_int32();
}
std::int32_t bulk_write::modified_count() const {
return view()["nModified"].get_int32();
}
std::int32_t bulk_write::deleted_count() const {
return view()["nRemoved"].get_int32();
}
std::int32_t bulk_write::upserted_count() const {
return view()["nUpserted"].get_int32();
}

The v1 implementation does not make any attempt to "revert" this field translation by mongoc, hence the reference to MONGOC_WRITE_RESULT_COMPLETE in case any particulars of this behavior need to be examined.

"Even when the CRUD API specification is used" is referring to use of API that conforms to the CRUD API specification in (nearly) all respects but the fields of the server reply. e.g. collection::bulk_write() -> Optional<BulkWriteResult> suggests the server reply document should contain a field "insertedIds", but instead it contains the field "nInserted".

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggests the server reply document

I was thinking it was unnecessary to document the server reply if users would not see it. This seems to be documenting more of the internal implementation than the exposed API.

On further thought: I expect users may see the server reply in the v1::server_error exception. Is that the motivation for documenting the server reply?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking it was unnecessary to document the server reply if users would not see it. This seems to be documenting more of the internal implementation than the exposed API.

You're correct. This class doesn't have a .view() or .raw() exposing the underlying document. I am fine with demoting these comments into developer-only internal comments instead of including them in the public API documentation. (This was due to the concern that MONGOC_WRITE_RESULT_COMPLETE could possibly introduce unique behavior that is observable in the value of the resulting fields, but perhaps this was paranoia.)

Is that the motivation for documenting the server reply?

No, it is not. We deliberately do not specify any details about what may be in the server error document.

/// - Use `this->code()` to obtain the primary error code (which may equal `this->server_code()`).
/// - Use `this->server_code()` to obtain the server error code (which may equal `zero`).
/// Use @ref mongocxx::v1::source_errc to determine the origin of `this->code()`.
/// @important `this->code()` always returns the raw server error code. Use `this->client_code()` to query the
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I neglected to ask before. What motivated the separate client error code? If a server error code is always available and can be compared against error conditions (like v1::source_errc::server), I do not see when the client error code would be used.

Copy link
Contributor Author

@eramongodb eramongodb Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to support the situation (hopefully rare, difficult to verify) where bson_error_t::reserved (category) may indicate a client-side error code, but a server error document is also provided. In such a situation, this PR proposes saving that information alongside the server error code rather than ignoring/dropping it.

In full summary, given:

if (!mongoc_generic_server_command(..., reply.out_ptr(), &error)) {
  v1::exception::internal::throw_exception(error, reply.view()); // CXX-3237
}

there are four possible outcomes (pseudocode for brevity):

reply is server error code client error code
not empty v1::server_error
.code() == error.code (server)
.client_code() == {}
v1::server_error
.code() == .raw()["code"] (server)
.client_code() == error.code (client)
empty v1::exception
.code() == error.code (server)
v1::exception
.code() == error.code (client)

The separate client code is to support the top-right cell of this table.

Copy link
Collaborator

@kevinAlbs kevinAlbs Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indicate a client-side error code, but a server error document is also provided

One possible scenario: a reply might contain only error labels in some client-side error scenarios (e.g. network error). For example:

{"errorLabels" : [ "UnknownTransactionCommitResult", "RetryableWriteError" ]}

Reporting the error label appears required in some specs. For example in transactions:

Drivers MUST add error labels to certain errors when commitTransaction fails.

In this scenario, there is no .raw()["code"] to use as the server error code. I am not sure how to best address. Maybe this motivates adding raw / has_error_label methods to v1::exception?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this motivates adding raw / has_error_label methods to v1::exception?

Possibly. The Transactions spec quote is followed by a reference to error reporting changes:

Any error reported by the driver in response to a server error, server selection error, or network error MUST have an API for determining whether it has a given label.

Examples of such an API include exception classes such as OperationFailure and ConnectionFailure which are not defined anywhere else in the specs.

We have v1::server_error for server errors; we do not have any exception class dedicated to server selection errors or network errors, in mongocxx or in mongoc for that matter (since mongoc blobs all errors into bson_t reply parameters). It looks like this issue was known and acknowledged in the past, in reference to CXX-834:

// TODO CXX-834: client-side errors may contain error labels. However, only
// mongocxx::operation_exception keeps track of the raw_sever_error (and consequently the
// error label) and, as a result, is the only exception type with the required
// `has_error_label` method. Until we fix CXX-834, there's no way to check the error label of a
// mongocxx::exception.
REQUIRE_FALSE(expect_error["errorLabelsContain"]);
REQUIRE_FALSE(expect_error["errorLabelsOmit"]);
REQUIRE_FALSE(/* TODO */ expect_error["errorContains"]);
REQUIRE_FALSE(/* TODO */ expect_error["errorCode"]);
REQUIRE_FALSE(/* TODO */ expect_error["errorCodeName"]);

and:

/*
// This has no data to act on until CXX-834 as been implemented; see notes
if (auto expected_error = expect_error["errorContains"]) {
// in assert_error():
// See
//
"https://github.com/mongodb/specifications/blob/master/source/unified-test-format/unified-test-format.md#expectederror":
// A substring of the expected error message (e.g. "errmsg" field in a server error
// document). The test runner MUST assert that the error message contains this string using
// a case-insensitive match.
std::string expected_error_str(expected_error.get_string().value);
std::string actual_str(reinterpret_cast<const std::string::value_type*>(actual.data()),
actual.length());
transform(begin(expected_error_str),
end(expected_error_str),
begin(expected_error_str),
&toupper);
REQUIRE(actual_str.substr(expected_error_str.size()) == expected_error_str);
}
*/

The current spec tests runners only support validating error labels for errors thrown as v_noabi::operation_exception, e.g. here:

try {
auto op_runner = make_op_runner();
actual_result = op_runner.run(op);
} catch (operation_exception const& e) {
error_msg = e.what();
server_error = e.raw_server_error();
op_exception = e;
exception = e;
} catch (std::exception& e) {
error_msg = e.what();
exception = e;
}

and here:

try {
session->with_transaction(with_txn_test_cb);
} catch (operation_exception const& e) {
error_msg = e.what();
server_error = e.raw_server_error();
exception = e;
ec = e.code();
} catch (mongocxx::logic_error const& e) {
// CXX-1679: some tests trigger client errors that are thrown as logic_error rather
// than operation_exception (i.e. update without $ operator).
error_msg = e.what();
exception.emplace(make_error_code(mongocxx::error_code(0)));
ec = e.code();
}

Since we now have a proper mechanism to address CXX-834 following CDRIVER-5854, this may be an opportunity to add proper support for extending the error/exception API to support additional fields such as errorLabels regardless whether the source of the error is client-side or server-side.

Copy link
Collaborator

@kevinAlbs kevinAlbs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great work. The efforts towards API consistency and removal of view_or_value types is much appreciated. Most of comments are documentation fix-ups and questions.

Copy link
Contributor Author

@eramongodb eramongodb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Why is GitHub not allowing the submission of a comment review without a top-level comment?)

/// - `upserted_count` ("nUpserted" or "upsertedCount")
/// - `upserted_ids` ("upserted" or "upsertedIds")
///
/// @important The raw server response is translated by mongoc into the Bulk Write API specification format even when
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The note is class-level because the entire class is implemented in terms of mongoc's translated Bulk Write API fields:

std::int32_t bulk_write::inserted_count() const {
return view()["nInserted"].get_int32();
}
std::int32_t bulk_write::matched_count() const {
return view()["nMatched"].get_int32();
}
std::int32_t bulk_write::modified_count() const {
return view()["nModified"].get_int32();
}
std::int32_t bulk_write::deleted_count() const {
return view()["nRemoved"].get_int32();
}
std::int32_t bulk_write::upserted_count() const {
return view()["nUpserted"].get_int32();
}

The v1 implementation does not make any attempt to "revert" this field translation by mongoc, hence the reference to MONGOC_WRITE_RESULT_COMPLETE in case any particulars of this behavior need to be examined.

"Even when the CRUD API specification is used" is referring to use of API that conforms to the CRUD API specification in (nearly) all respects but the fields of the server reply. e.g. collection::bulk_write() -> Optional<BulkWriteResult> suggests the server reply document should contain a field "insertedIds", but instead it contains the field "nInserted".

/// - Use `this->code()` to obtain the primary error code (which may equal `this->server_code()`).
/// - Use `this->server_code()` to obtain the server error code (which may equal `zero`).
/// Use @ref mongocxx::v1::source_errc to determine the origin of `this->code()`.
/// @important `this->code()` always returns the raw server error code. Use `this->client_code()` to query the
Copy link
Contributor Author

@eramongodb eramongodb Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to support the situation (hopefully rare, difficult to verify) where bson_error_t::reserved (category) may indicate a client-side error code, but a server error document is also provided. In such a situation, this PR proposes saving that information alongside the server error code rather than ignoring/dropping it.

In full summary, given:

if (!mongoc_generic_server_command(..., reply.out_ptr(), &error)) {
  v1::exception::internal::throw_exception(error, reply.view()); // CXX-3237
}

there are four possible outcomes (pseudocode for brevity):

reply is server error code client error code
not empty v1::server_error
.code() == error.code (server)
.client_code() == {}
v1::server_error
.code() == .raw()["code"] (server)
.client_code() == error.code (client)
empty v1::exception
.code() == error.code (server)
v1::exception
.code() == error.code (client)

The separate client code is to support the top-right cell of this table.

@eramongodb eramongodb requested a review from kevinAlbs October 28, 2025 19:12
Copy link
Contributor

@vector-of-bool vector-of-bool left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with only very minor comments

Comment on lines +46 to +49
private:
void const* _impl; // mongoc_apm_command_succeeded_t const

public:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have precedent for the placement of data members? e.g. in this case, the private imply is the first thing a user will see when they look at the class, rather than the public API functions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have precedent for the placement of data members?

Per the current (and still not yet updated) coding guidelines, precedent is the traditional "for users reading the header file" layout. Instead, bsoncxx::v1 and mongocxx::v1 API follows the advice of Howard Hinnant:

But I've since come to the conclusion that this is exactly wrong. When I'm reading a class declaration, the very first things I want to know are:

  • What resources does this class own?
  • Can the class be default constructed?
  • Can the class be copied or moved?
  • How can the class be constructed (other than by default, copy or move)?
  • What else can one do with the class?

Note that this is an ordered list: top priority is listed first. [...] It provides the most important characteristics of a class as soon as possible to the reader. It prevents the reader from having to search the entire class declaration for compiler-declared special members which are otherwise invisible to the reader.

Hence why bsoncxx::v1 and mongocxx::v1 instead adopt the following order of class member declarations:

class C : /* base classes */ {
  private:
    // Data members: informs what SMFs are required.

  public:
    // Dtor: clearest indication of resource ownership (or the lack/absence of it).

    // Move + Copy: informed by the data members and dtor (Ro3/Ro5/Ro0).

    // (Default) ctors: how to create this class?

    // The rest of the class API: public, then private (usual ordering).
};

Furthermore, given we provide official API documentation pages (whose contents are also made immediately-visible via Doxygen-aware IDE tooling), I do not believe we should (need to) be accomodating traditional "the header file is the API documentation" practices.

If this HH-style is too confusing/disruptive, I can make the necessary changes to return to the traditional public-then-private ordering. (bsoncxx::v1 will also be updated for consistency in a separate PR.)

/// Equivalent to @ref open_upload_stream_with_id with a file ID generated using @ref bsoncxx::v1::oid.
///
/// @{
v1::gridfs::uploader open_upload_stream(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this one use the export macro as well?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Yes, it should be exported.

/// @throws mongocxx::v1::exception when a client-side error is encountered.
/// @throws mongocxx::v1::server_error when a server-side error is encountered and all retry attempts have failed.
///
MONGOCXX_ABI_EXPORT_CDECL(void) with_transaction(with_transaction_cb const& fn, v1::transaction const& opts);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: Suggest swapping these two parameters only so that a potential user's lambda expression will be the final argument and formatted more nicely:

foo.with_transaction(some_options, [&](client_session& sess) {
    // do stuff
});

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ordering is for consistency with v_noabi API:

MONGOCXX_ABI_EXPORT_CDECL(void)
with_transaction(with_transaction_cb cb, options::transaction opts = {});

Given this would also be a departure from the rest of the API's preference of appending option arguments to the parameter list (probably mostly motivated by the = {} pattern), I think it is better to leave the ordering as-is for now.

/// @{
template <typename InputIt, bsoncxx::detail::enable_if_t<is_write_iter<InputIt>::value>* = nullptr>
bsoncxx::v1::stdx::optional<v1::bulk_write::result>
bulk_write(InputIt begin, InputIt end, v1::bulk_write::options const& opts = {}) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommend deducing end type separately to support uncommon ranges with distinct sentinel types.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suspect this is probably overkill + personally I'd rather these range-based overload templates be removed from the API in favor of a std::span<T>-based non-template API (so that the private-but-exported *_insert_many helper functions can also be avoided), but nevertheless, updated InputIt end -> Sentinel end and added a simple is_sentinel_for<Sentinel, InputIt> constraint. The constraint only tests support for weak equality comparison, not the full std::sentinel_for concept.

Copy link
Collaborator

@kevinAlbs kevinAlbs left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Additional questions about error reporting. Otherwise LGTM.

/// - `upserted_count` ("nUpserted" or "upsertedCount")
/// - `upserted_ids` ("upserted" or "upsertedIds")
///
/// @important The raw server response is translated by mongoc into the Bulk Write API specification format even when
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggests the server reply document

I was thinking it was unnecessary to document the server reply if users would not see it. This seems to be documenting more of the internal implementation than the exposed API.

On further thought: I expect users may see the server reply in the v1::server_error exception. Is that the motivation for documenting the server reply?

/// - Use `this->code()` to obtain the primary error code (which may equal `this->server_code()`).
/// - Use `this->server_code()` to obtain the server error code (which may equal `zero`).
/// Use @ref mongocxx::v1::source_errc to determine the origin of `this->code()`.
/// @important `this->code()` always returns the raw server error code. Use `this->client_code()` to query the
Copy link
Collaborator

@kevinAlbs kevinAlbs Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

indicate a client-side error code, but a server error document is also provided

One possible scenario: a reply might contain only error labels in some client-side error scenarios (e.g. network error). For example:

{"errorLabels" : [ "UnknownTransactionCommitResult", "RetryableWriteError" ]}

Reporting the error label appears required in some specs. For example in transactions:

Drivers MUST add error labels to certain errors when commitTransaction fails.

In this scenario, there is no .raw()["code"] to use as the server error code. I am not sure how to best address. Maybe this motivates adding raw / has_error_label methods to v1::exception?

@eramongodb
Copy link
Contributor Author

eramongodb commented Oct 30, 2025

In light of the realization that the C++ Driver has not been, yet should be, supporting an API to query the error labels for a given error regardless whether it is associated with a server-side error or a client-side error, per Transactions Specification:

Which labels are applied to an error may be communicated from the server to the client, or determined client-side. Any error reported by the driver in response to a server error, server selection error, or network error MUST have an API for determining whether it has a given label.

refactored the proposed API such that .has_error_label() is moved from mongocxx::v1::server_error to mongocxx::v1::exception. The API does not expose the list of all error labels at this time, as permitted by spec:

Drivers MAY expose the list of all error labels for an exception object.

This necessarily makes v1::exception stateful. I believe this is acceptable, given error labels are not limited to transactions, e.g. there is the RetryableWriteError label from the Retryable Writes Specification which may be applied by network errors before an initial connection to the server is established, thus making it unconditionally a client-side error. As noted here, spec test runners currently do not validate the "errorLabels" field for any non-server-side errors (that is, not thrown as a v_noabi::operation_exception) due to v_noabi::exception being stateless. However, the v1 API is now be able to take advantage of CDRIVER-5854 to address these workarounds by distinguishing server-side vs. client-side errors using ex.code() == v1::source_errc::server (even when ex: v1::exception).

In short, the table described here no longer excludes the ability to query .has_error_label() for non-server-side errors due to lack of a .raw() document. Previously, the "errorLabels" field was only stored as part of the raw server error document. Now, the "errorLabels" field is a property shared by all mongocxx v1 exceptions, regardless whether the error is client-side or server-side.


These changes motivated an additional fix/improvement, as indicated by clangd diagnostics for stateful exception classes to support nothrow copyability, per ERR60-CPP:

If the copy constructor for the exception object type throws during the copy initialization, std::terminate() is called, which can result in possibly unexpected implementation-defined behavior. [...] The copy constructor for an object thrown as an exception must be declared noexcept, including any implicitly-defined copy constructors. [...] The C++ Standard allows the copy constructor to be elided when initializing the exception object to perform the initialization if a temporary is thrown. Many modern compiler implementations make use of both optimization techniques. However, the copy constructor for an exception object still must not throw an exception because compilers are not required to elide the copy constructor call in all situations, and common implementations of std::exception_ptr will call a copy constructor even if it can be elided from a throw expression.

and per CppCoreGuidelines E.16:

  • If writing a type intended to be used as an exception type, ensure its copy constructor is noexcept.
  • Try not to throw a type whose copy constructor is not noexcept.

A std::shared_ptr<impl> is used instead of std::unique_ptr<impl>, as suggested in the PR discussions which introduced E.16:

A std::shared_ptr<const char[]> or a std::shared_ptr<const std::string> inside the exception object is one way of achieving this.

This has the additional benefit of removing the need for Ro5 boilerplate and the assign-or-destroy-only moved-from state (copy semantics only).

This change also highlighted the unnecessary (and incorrect) public declaration of inherited constructors for v1::server_error, given the opaque impl class forbids construction of the class object in contexts where impl is not defined (that is, in user code):

class v1::exception : std::system_error {
  private:
    class impl;
    std::unique_ptr<impl> _impl;

  public:
    using std::system_error::system_error;
};

void example() {
  (void)(mongocxx::v1::exception{std::error_code{}}); // error: incomplete type
}
error: invalid application of 'sizeof' to an incomplete type 'mongocxx::v1::exception::impl'
   75 |     static_assert(sizeof(_Tp) >= 0, "cannot delete an incomplete type");
      |                   ^~~~~~~~~~~
note: in instantiation of member function 'std::unique_ptr<mongocxx::v1::exception::impl>::~unique_ptr' requested here
      |     using std::system_error::system_error;
      |                              ^
note: forward declaration of 'mongocxx::v1::exception::impl'
      |     class impl;
      |           ^

Taking this into account, v1::exception and v1::server_error are no longer constructible by users. This is demonstrated by the required use of v1::exception::internal::make() in v1::instance::impl::init() as a result of this refactor. We do not expect users to be extending our exception classes or throwing these exception classes themselves, so I believe dropping support for their creation by users is acceptable (only ever initially-thrown by the mongocxx library).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants