# C++ driver woes **Key** * :skull: - Fundamental issue (requires non-trivial API break or massive impl change) * :sleeping: - Sign of neglect * :construction: - Fixable in code without "too much" disruption * :books: - Fixable with documentation * :question: - I don't know Combinations have aspects of each. Horizontal lines separate coments from different people --- * :sleeping: It doesn't compile with C++17 (fix in master but not released) * :construction: It doesn't use CMake in a way that supports directly consuming the library without installing it first * This affects everyday usability even after you wrangle dependencies because it breaks useful navigation tools like GoToDefinition. * :construction: BSON values not being views (or convertable to views) requires annoying boilerplate * :skull: The split between bson views vs values introduces friction * :skull: Everything is nested way too deeply in namespaces, but requires those namespaces to disambiguate radically different types, eg `bsoncxx::builder::basic::document` and `bsoncxx::builder::stream::document`. * :construction: Many functions are templated with no constraints so it isn't clear what arguments they take. In some cases it requires looking through several layers of implementation. * :construction: The `bsoncxx::builder::basic::kvp` function makes constructing bson ridiculously verbose unless you use macros that the library doesn't supply: ```cpp bsoncxx::builder::basic::document builder; builder.append(bsoncxx::builder::basic::kvp("every", "field")); builder.append(bsoncxx::builder::basic::kvp("looks", "like this")); bsoncxx::document::value obj = builder.extract() ``` * :construction: :sleeping: :books: There is [another bson building API](http://mongocxx.org/mongocxx-v3/working-with-bson/#stream-builder), but the documentation says "Because doing this properly is difficult and the compiler error messages can be confusing, using the stream builder is discouraged." In spite of this, most of the examples in the documentation use this API. The recommended API is barely documented. * :construction: Consuming a BSON object is also painful. Real example from code: `op["u"].get_document().view()["$set"].get_document().view()["i"].get_int64()` Reasonable alternative: `op["u"]["$set"]["i"].as<int64_t>()` * :construction: :skull: The way that it pools clients but not sessions even though sessions are bound to specific clients, means that if you want to reuse sessions, you also need to keep track of the clients they are bound to. This basically requires implementing your own connection+session pool on top of the one provided by the driver. * :construction: It doesn't have a way to specify the raw readConcern object, so you can't access some (admittedly undocumented) features of the server. * :skull: It uses `std::system_error` for its exceptions which is rather convoluted to use. * :skull: Because it uses overlapping code numbers, users need to also check the error domain every time, which is easy to forget. * :question: These domains seem to be uncorrelated with the exception type hierarchy that also exists * :construction: :sleeping: Even if users remember to, due to [CXX-1679](https://jira.mongodb.org/browse/CXX-1679) errors are sometimes attached to the wrong domain. That has been left unfixed for nearly a year because "we'll try to handle this as part of a general overhaul of the C++ Driver's error handling, scheduled for the near future." * :construction: When you try to stringify the error, every server error just says ["generic server error"](https://github.com/mongodb/mongo-cxx-driver/blob/6c692da262de98e87b2bcd1a1fc70cbaafed2331/src/mongocxx/exception/server_error_code.cpp#L36) * :construction: Some types, such as [`mongocxx::pool::entry`](https://github.com/mongodb/mongo-cxx-driver/blob/6c692da262de98e87b2bcd1a1fc70cbaafed2331/src/mongocxx/pool.hpp#L82) go out of their way to be harder to use in a way that prevents valid usage patterns. While this is ostensibly for safety, [this post](https://quuxplusone.github.io/blog/2019/03/11/value-category-is-not-lifetime/) describes why this is a bad idea. * :construction: There is an artificial distinction between a `bsoncxx::document::element` and a `bsoncxx::array::element` with the latter missing some features of the former. This also makes it harder for code to just work with any bson element, regardless of what it is contained in. * :question: The use of `bsoncxx::types::b_bool` rather than just the native `bool` type (ditto many similar types) is another needless bit of verbosity. * :construction: Binary using only `uint8_t*` makes it harder to work with in codebases that use a different type for binary data. It should make it easy to use any of `char*`, `unsigned char*`, `uint8_t*` (which is basically always one of the prior two), `void*`, and `std::byte*` (>=17) * :construction: `b_timestamp` should expose a `uint64_t` in addition to the 32-bit parts. * :skull: There is no support for aync networking. * :construction: Should have umbrella headers to be easier to consume. * :construction: Option types, such as `mongocxx::options::find` should be aggregate structs. This supports Aggregate initialization, enhanced by C++20 designated initializers which are supported as en extension in many compilers in earlier versions. --- * :question: Specifically on the types, I found that I was forced to unique_ptr wrap often...even though the types I was unique_ptr'ing were thin abstractions around unique_ptrs. * :question: Options that do not fit in a MongoURI are arbitrarily tacked on via client_ops which are intensely frustrating to compose compared to the original c driver options. There is no way to chose to use the more reasonable c driver uri structs that incorporate these options with the cxx driver. --- * :construction: The well-typed `mongocxx::pipeline` class prevents using newer aggregation stages that the driver isn't aware of. For example, ones being developed in the upcoming server release. There's a similar lack of an escape hatch for read concern options. Requiring the user to `run_command()` themselves would be less painful if `mongocxx::cursor` instances could be constructed manually. * :construction: The driver isn't aware of any named error codes from the server. For example, duplicate key errors are a special subclass of the exceptions raised by PyMongo. --- * :books: :sleeping: The lack of documentation is problematic when learning how to use MongoDB and the driver. I found myself reading the source code instead of the documentation to figure out crucial things such as: * Which methods are safe to call across different threads? * What objects can be passed between threads? * Which methods throw exceptions, and why? * Which objects represent ownership? The distinction between value/view on the bsoncxx side suggests awareness of this, but was hard to figure out for other objects, such as client, connection, pool, transaction, etc. * :books: I recommend not using doxygen as the primary user-facing documentation. Listing all classes and methods is the least useful thing when trying to figure out how to tie it all together. * :construction: :skull: The library seems to go out of its way to avoid allocating memory. While that is admirable, it does not make it easy when you are building essentially tree-like structures (such as BSON), and it hardly matters when it must eventually be sent over the network anyway. --- - :construction: the pool API is confusing/awkward `(*client)[db]`? Idk. - :construction: :skull: bson library is super hard to use correctly (echoing statements from above) - consider an API inspired by YamlCpp - in particular `bsonNode.as<T>` and/or `BsonNode::from<T>(T)` for conversions to/from native types with built-in specializations for strings/ints/bools/STL containers so you could have `bson::array vals = bson::from(vectorOfMapsFromStringToInt)`. - make the ownership model explicit - and either kill the builder APIs or make them have regular syntax. See extended example below. - This will mean potentially additional allocations but it seems like building bson is already not super performant with the builders (anecdotally); let perf-critical things use a lower-level API perhaps just a ver, very thin wrapper atop the libbson C api - :construction: many places I wish would use a `st::optional<T>` instead are overloaded so there's `x->foo(7)` and `x->foo(7, obj)` when really I'd like `x->foo(7, optional)` - This is especially true for crud operations that take a session. fact that it's an overload rather than taking by optional makes writing generic code that maybe or maybe does not want to use a session [very tedious](https://github.com/mongodb/genny/blob/71d1eea6198c10812c1f836d4672c53d51fd577b/src/cast_core/src/CrudActor.cpp#L925-L928) - :construction: the types in the `mongocxx::options` namespace are confusingly separated from the types they are options for. Why `mongocxx::options::transaction` and not `mongocxx::transaction::options`? - :question: :books: it's not clear what parts of the API could throw an exception at query/execution time vs at cursor-iteration time; other places will return a document with an error-code or will return an optional thing. (May be shared with other drivers? idk) - :question: some things can return an empty optional others can return a cursor that may have zero results or may throw an exception but only when first attempting to iterate the cursor [e.g. here](https://github.com/mongodb/genny/blob/0b30c1585fd4b31dc5232e2bfa8e79473dbc3b7a/src/cast_core/src/CollectionScanner.cpp#L107-L114) - :question: exceptions are hard to trace down what happened - e.g. `operation_exception` could be the result of bad data or the result of a network error and you have to inspect the `what()` to know - :construction: :books: should I have and pass `mongocxx::collection` by value or ref? Similar for most other types. Intended ownership model is hard. Maybe it's safest to always just use value but it's not clear what the "cost" is to copy and the instinct is to pass by ref wherever possible This is what I'd really *like* the bson syntax to look like: ```cpp void example() { auto doc = bson::document({ {"k1", "v1"}, // simple kvp {"k2", 2}, // simple kvp with other types {"k2-other-style", bson::value{2}}, // native types as values probably just get wrapped in bson::value? {"k3", bson::array{1,2,"3"}}, // val can be another bson type {"k3-other-style", std::vector<bson::value>{1, 2, "3"}} // or an stl type }); doc["k4"] = std::vector<int>{5,6,7}; // operator= for assignment to previously-non-existant key doc["k5"] = std::map<std::string, std::vector<std::string>>{ {"nested", {"strings", "are", "nice"}}, }; // conversions can go deep // no explicit "builder" needed just support conventional operations on the bson types doc["k6"] = bson::array{}; std::vector<int> vals = {5,6,7}; for(auto&& v : vals) { doc["k6"].push_back(v); } // traversal with operator[] assert(doc["k5"]["nested"][1].to<std::string>() == "are"); // support iterating over possibly-missing fields - operator bool to see if there or not? assert(doc["k100"]["does"][100]["not"]["exist"] == false) } ``` --- ---