Stroika Library 3.0d23
 
Loading...
Searching...
No Matches
MongoDBClient.cpp
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2026. All rights reserved
3 */
4#include "Stroika/Foundation/StroikaPreComp.h"
5
6#if qStroika_HasComponent_mongocxxdriver
7DISABLE_COMPILER_MSC_WARNING_START (4166) // avoid warning of buggy usage cdecl on CTOR/DTOR- only shows up on x86 MSVC compilers
8#if defined(__clang_major__) and (16 < __clang_major__ && __clang_major__ < 21)
9DISABLE_COMPILER_CLANG_WARNING_START ("clang diagnostic ignored \"-Wdeprecated-literal-operator\""); // ...bsoncxx/v_noabi/bsoncxx/json.hpp:85:54: warning: identifier '_bson' preceded by whitespace in a literal operator declaration is deprecated [-Wdeprecated-literal-operator]
10#endif
11#include <bsoncxx/builder/basic/document.hpp>
12#include <bsoncxx/document/value.hpp>
13#include <bsoncxx/json.hpp>
14#include <bsoncxx/string/to_string.hpp>
15#include <bsoncxx/types.hpp>
16#include <bsoncxx/types/bson_value/value.hpp>
17#include <bsoncxx/types/bson_value/view.hpp>
18#include <bsoncxx/view_or_value.hpp>
19#include <mongocxx/client.hpp>
20#include <mongocxx/collection.hpp>
21#include <mongocxx/exception/exception.hpp>
22#include <mongocxx/exception/operation_exception.hpp>
23#include <mongocxx/instance.hpp>
24#include <mongocxx/pool.hpp>
25#include <mongocxx/uri.hpp>
26#if defined(__clang_major__) and (16 < __clang_major__ && __clang_major__ < 21)
27DISABLE_COMPILER_CLANG_WARNING_END ("clang diagnostic ignored \"-Wdeprecated-literal-operator\"");
28#endif
29DISABLE_COMPILER_MSC_WARNING_END (4166)
30#endif
31
34#include "Stroika/Foundation/Containers/Concrete/Mapping_HashTable.h"
38#include "Stroika/Foundation/Database/Exception.h"
39#include "Stroika/Foundation/Debug/Main.h"
41#include "Stroika/Foundation/Memory/Common.h"
42
43#include "MongoDBClient.h"
44
45using namespace Stroika::Foundation;
48using namespace Stroika::Foundation::Database;
49using namespace Stroika::Foundation::Database::Document::MongoDBClient;
51using namespace Stroika::Foundation::Debug;
52using namespace Stroika::Foundation::Execution;
53using namespace Stroika::Foundation::Memory;
54
57
62
63// Comment this in to turn on aggressive noisy DbgTrace in this module
64// #define USE_NOISY_TRACE_IN_THIS_MODULE_ 1
65
66/**
67 * Character set Docx
68 * Haven't found clear docs about characterset (apis all take 'string') - but it appears (search through docs for
69 * characterset comes up with nothing but UTF-8) that they use UTF-8.
70 */
71
72#if qStroika_HasComponent_mongocxxdriver
73
74using bsoncxx::builder::basic::kvp;
75using bsoncxx::builder::basic::make_array;
76using bsoncxx::builder::basic::make_document;
77using bsoncxx::builder::basic::sub_array;
78using bsoncxx::builder::basic::sub_document;
79
80namespace {
81 const String kMongoID_ = "_id"sv;
82}
83
84namespace {
85 atomic<unsigned int> sActivatorLiveCnt_{0};
86}
87
88namespace {
89 auto ConnectionString2MongoURI_ (const String& connectionString)
90 {
91 return mongocxx::uri{connectionString.AsUTF8<string> ()};
92 }
93}
94
95namespace {
96 bsoncxx::types::bson_value::value ToBSONId_ (const string_view& s)
97 {
98 if (s.length () == 24) {
99 // check just hex digits too?
100 return bsoncxx::types::b_oid{bsoncxx::oid{s}};
101 }
102 else {
103 return bsoncxx::types::bson_value::value{s};
104 }
105 }
106}
107
108namespace {
109 String cvt2String_ (const bsoncxx::stdx::string_view& bs)
110 {
111 return String::FromUTF8 (SpanBytesCast<span<const char8_t>> (span{bs}));
112 }
113 String cvt2String_ (const bsoncxx::types::b_string& bs)
114 {
115 return String::FromUTF8 (SpanBytesCast<span<const char8_t>> (span{bs.value}));
116 }
117}
118
119namespace {
120 [[noreturn]] void DoReThrow_ ()
121 {
122 // @todo ALSO - should check if current_exception() already a Stroika exception - and only bother wrapping native mongo ones, but this caputres
123 // most such cases...
124 if (AnyCurrentActivities ()) {
125 // capture current activities in message
126 Throw (NestedException{current_exception ()});
127 }
128 else {
129 ReThrow ();
130 }
131 }
132 String ID_2_string_ (const bsoncxx::types::bson_value::view& value)
133 {
134 switch (value.type ()) {
135 case bsoncxx::type::k_oid:
136 return String{value.get_oid ().value.to_string ()};
137 case bsoncxx::type::k_string:
138 return cvt2String_ (value.get_string ());
139 default:
141 return String{};
142 }
143 }
144 template <Common::IAnyOf<bsoncxx::types::bson_value::view, bsoncxx::document::element, bsoncxx::document::value, bsoncxx::array::element> T>
145 VariantValue BSON2VV_ (const T& value)
146 {
147 switch (value.type ()) {
148 case bsoncxx::type::k_double:
149 return value.get_double ().value;
150 case bsoncxx::type::k_string:
151 return cvt2String_ (value.get_string ());
152 case bsoncxx::type::k_document: {
153 Mapping_HashTable<String, VariantValue>::DEFAULT_HASHTABLE<> vvResult; // performance tweak, add in STL, avoiding virtual calls for each add, and then move to Stroika mapping
154 const bsoncxx::types::b_document& thisDoc = value.get_document ();
155 for (auto di : thisDoc.value) {
156 vvResult.Add (String::FromUTF8 (span{di.key ()}), BSON2VV_ (di));
157 }
159 }
160 case bsoncxx::type::k_array: {
161 vector<VariantValue> vvResult;
162 const bsoncxx::types::b_array& thisArray = value.get_array ();
163 vvResult.reserve (distance (thisArray.value.begin (), thisArray.value.end ()));
164 for (auto ai : thisArray.value) {
165 vvResult.push_back (BSON2VV_ (ai));
166 }
167 return VariantValue{Sequence_stdvector<VariantValue>{move (vvResult)}};
168 }
169 case bsoncxx::type::k_binary:
170 return Memory::BLOB{span{value.get_binary ().bytes, static_cast<size_t> (value.get_binary ().size)}};
171 case bsoncxx::type::k_undefined:
172 return VariantValue{nullptr}; // Stroika VariantValue doesn't distinguish between null and undefined
173 case bsoncxx::type::k_oid:
174 return String{value.get_oid ().value.to_string ()};
175 case bsoncxx::type::k_bool:
176 return static_cast<bool> (value.get_bool ());
177 case bsoncxx::type::k_date:
178 // Note - STROIKA doesn't usually (ever) generate these, because of problems documented in VV2BSONV_
179 return Time::DateTime{chrono::time_point<chrono::system_clock>{value.get_date ().value}}; // UTC datetime.
180 case bsoncxx::type::k_null:
181 return VariantValue{nullptr};
182 case bsoncxx::type::k_regex:
183 return cvt2String_ (value.get_string ());
184 case bsoncxx::type::k_dbpointer:
185 WeakAssertNotReached (); ///< DBPointer. @deprecated
186 return VariantValue{};
187 case bsoncxx::type::k_code:
189 return VariantValue{};
190 case bsoncxx::type::k_symbol:
191 WeakAssertNotReached (); ///< Symbol. @deprecated
192 return VariantValue{};
193 case bsoncxx::type::k_codewscope:
195 return VariantValue{};
196 case bsoncxx::type::k_int32:
197 return value.get_int32 ().value;
198 case bsoncxx::type::k_timestamp:
199 WeakAssertNotReached (); // not sure how to translate/interpret
200 return VariantValue{};
201 case bsoncxx::type::k_int64:
202 return value.get_int64 ().value;
203 case bsoncxx::type::k_decimal128:
204 WeakAssertNotReached (); // ///< 128-bit decimal floating point. == not sure what todo
205 return VariantValue{};
206 case bsoncxx::type::k_maxkey:
207 case bsoncxx::type::k_minkey:
208 WeakAssertNotReached (); // not sure what todo
209 return VariantValue{};
210 default:
211 AssertNotReached (); // all cases covered
212 return VariantValue{};
213 }
214 }
215 bsoncxx::types::bson_value::value VV2BSONV_ (const VariantValue& vv)
216 {
217 using namespace std::chrono;
218 // @todo adequate first draft, but not 100% right conversions --LGP 2025-03-24
219 switch (vv.GetType ()) {
220 case VariantValue::Type::eNull:
221 return bsoncxx::types::bson_value::value{nullptr};
222 case VariantValue::Type::eBLOB:
223 return bsoncxx::types::bson_value::value{vv.As<Memory::BLOB> ().As<vector<uint8_t>> ()};
224 case VariantValue::Type::eBoolean:
225 return bsoncxx::types::bson_value::value{vv.As<bool> ()};
226 case VariantValue::Type::eInteger:
227 return bsoncxx::types::bson_value::value{vv.As<int64_t> ()};
228 case VariantValue::Type::eUnsignedInteger:
229 return bsoncxx::types::bson_value::value{static_cast<int64_t> (vv.As<uint64_t> ())}; // @todo tweak - not quite right
230 case VariantValue::Type::eFloat:
231 return bsoncxx::types::bson_value::value{vv.As<double> ()};
232 case VariantValue::Type::eDate:
233 // MongoDB doesn't support dates before 1970, so store as string
234 // Also, because mongocxx api uses system_clock, which is neither a steady_clock nor monotonic_clock, its really not suitable for roundtripping.
235 // So just store dates as strings in the database (and our read code will convert them back to DateTime as appropriate - really VariantValue)
236 // if (Date dt = vv.As<Date> (); dt > Date{1970y / January / 1d}) {
237 // return bsoncxx::types::bson_value::value{bsoncxx::types::b_date{dt.As<std::chrono::system_clock::time_point> ()}};
238 // }
239 return vv.As<Date> ().Format (Date::kISO8601Format).AsUTF8<string> ();
240 case VariantValue::Type::eDateTime:
241 // MongoDB doesn't support dates before 1970, so store as string
242 // only makes sense to store UTC dates in database (so convert to UTC before ISO8601)
243 // Also, because mongocxx api uses system_clock, which is neither a steady_clock nor monotonic_clock, its really not suitable for roundtripping.
244 // So just store dates as strings in the database (and our read code will convert them back to DateTime as appropriate - really VariantValue)
245 // if (DateTime dt = vv.As<DateTime> ().AsUTC (); dt.GetDate () > Date{1970y / January / 1d}) {
246 // return bsoncxx::types::bson_value::value{bsoncxx::types::b_date{dt.As<std::chrono::system_clock::time_point> ()}};
247 // }
248 return vv.As<DateTime> ().AsUTC ().Format (DateTime::kISO8601Format).AsUTF8<string> ();
249 case VariantValue::Type::eString:
250 return bsoncxx::types::bson_value::value{vv.As<String> ().AsUTF8<string> ()};
251 case VariantValue::Type::eArray: {
252 bsoncxx::builder::basic::array bsonArr;
253 for (const auto& ai : vv.As<Sequence<VariantValue>> ()) {
254 bsonArr.append (VV2BSONV_ (ai));
255 }
256 return bsoncxx::types::bson_value::value{bsonArr};
257 }
258 case VariantValue::Type::eMap: {
259 bsoncxx::builder::basic::document bsonDoc;
260 for (const KeyValuePair<String, VariantValue>& ai : vv.As<Mapping<String, VariantValue>> ()) {
261 bsonDoc.append (kvp (ai.fKey.AsUTF8<string> (), VV2BSONV_ (ai.fValue)));
262 }
263 return bsoncxx::types::bson_value::value{bsonDoc};
264 }
265 default:
266 AssertNotReached (); // all cases covered
267 return bsoncxx::types::bson_value::value{nullptr};
268 }
269 }
270 Document::Document FromBSON_ (const bsoncxx::document::view_or_value& b)
271 {
273 for (const bsoncxx::document::element& di : b.view ()) {
274 result.Add (String::FromUTF8 (span{di.key ()}), BSON2VV_ (di));
275 }
276 if (result.ContainsKey (kMongoID_)) {
277 // patch '_id':oid => 'id':string
278 VariantValue idValue = result[kMongoID_]; // {id: {$oid -> 67da17b30c4265ac0302f483}}
279 result.Remove (kMongoID_);
280 result.Add (Database::Document::kID, idValue);
281 }
282 return result;
283 }
284 bsoncxx::document::value ToBSON_ (const Document::Document& vv)
285 {
286 // more complex, but more performant version of
287 // bsoncxx::from_json (Variant::JSON::Writer{}.WriteAsString (VariantValue{vvv}).AsUTF8<string> ());
288 Document::Document newDoc = vv;
289 optional<string> id; // patch 'id':string => '_id':oid
290 if (vv.ContainsKey (Database::Document::kID)) {
291 auto idValue = vv[Database::Document::kID];
292 newDoc.Remove (Database::Document::kID);
293 id = idValue.As<String> ().AsUTF8<string> ();
294 }
295 bsoncxx::builder::basic::document bsonDoc;
296 for (const KeyValuePair<String, VariantValue>& ai : newDoc) {
297 bsonDoc.append (kvp (ai.fKey.AsUTF8<string> (), VV2BSONV_ (ai.fValue)));
298 }
299 if (id) {
300 bsonDoc.append (kvp ("_id", ToBSONId_ (*id)));
301 }
302 return bsonDoc.extract ();
303 }
304}
305
306namespace {
307 /**
308 * Break the given Stroika filter into parts that can be remoted to MongoDB, and parts that must be handled locally
309 *
310 * @param filter
311 * @return tuple<bsoncxx::document,Filter>
312 */
313 tuple<optional<bsoncxx::document::value>, optional<Filter>> Partition_ (const optional<Filter>& filter)
314 {
315 if (filter) {
316 /*
317 * For now just look for FIELD EQUALS VALUE expressions in the top level conjunction. These can be done
318 * server or client side transparently, and moving them server side is more efficient.
319 *
320 * Much more could be done, but this is a good cost/benefit start.
321 */
323 bsoncxx::builder::basic::document filterDoc;
324 bool anyTransfers = false;
325 for (Document::FilterElements::Operation op : filter->GetConjunctionOperations ()) {
326 bool transferred = false;
327 if (const Document::FilterElements::Equals* eqOp = get_if<Document::FilterElements::Equals> (&op)) {
328 String useFieldName = eqOp->fLHS == Database::Document::kID ? kMongoID_ : eqOp->fLHS;
329 if (const Document::FilterElements::Value* rhsValue = get_if<Document::FilterElements::Value> (&eqOp->fRHS)) {
330 // move to server side
331 if (useFieldName == kMongoID_) {
332 // @todo find cleaner way todo this cuz other thinks could be of this type?
333 filterDoc.append (kvp ("_id", ToBSONId_ (rhsValue->As<String> ().AsUTF8<string> ()))); //kMongoID
334 }
335 else {
336 filterDoc.append (kvp (useFieldName.AsUTF8<string> (), VV2BSONV_ (*rhsValue)));
337 }
338 transferred = true;
339 anyTransfers = true;
340 }
341 }
342 if (not transferred) {
343 clientSideOps += op; // keep for client side
344 }
345 }
346 if (anyTransfers) {
347 // if we moved any to server side, then return the filterDoc and the client side ops
348 return make_tuple (filterDoc.extract (), clientSideOps.empty () ? optional<Filter>{} : make_optional (Filter{clientSideOps}));
349 }
350 // else no change
351 return make_tuple (nullopt, filter);
352 }
353 return make_tuple (nullopt, nullopt);
354 }
355}
356
357namespace {
358 /**
359 * Break the given Stroika filter into parts that can be remoted to MongoDB, and parts that must be handled locally
360 *
361 * @param filter
362 * @return tuple<optional<bsoncxx::document::value>, optional<Projection>>
363 *
364 * SEE https://stackoverflow.com/questions/62704615/mongodb-projection-on-c
365 */
366 tuple<optional<bsoncxx::document::value>, optional<Projection>> Partition_ (const optional<Projection>& p)
367 {
368 if (p) {
369 /*
370 * support mongoProjection - e.g. {{a: 1, b:0}} etc...
371 */
372 tuple<Document::Projection::Flag, Set<String>> fields = p->GetFields ();
373 Require (get<1> (fields).size () >= 1); // cannot (usefully) project to null-space
374 bsoncxx::builder::basic::document projectionDoc;
375 for (String f : get<1> (fields)) {
376 String mongoFieldName = f;
377 if (mongoFieldName == Database::Document::kID) {
378 mongoFieldName = kMongoID_;
379 }
380 projectionDoc.append (kvp (mongoFieldName.AsUTF8<string> (), get<0> (fields) == Document::Projection::Flag::eInclude ? 1 : 0));
381 }
382 return make_tuple (projectionDoc.extract (), nullopt);
383 }
384 return make_tuple (nullopt, nullopt);
385 }
386}
387
388namespace {
389 template <InternallySynchronized SYNC_STYLE>
390 using MyMaybeLock_ =
391 conditional_t<SYNC_STYLE == InternallySynchronized::eNotKnownInternallySynchronized, Debug::AssertExternallySynchronizedMutex, recursive_mutex>;
392 static_assert (Common::StdCompat::BasicLockable<MyMaybeLock_<InternallySynchronized::eNotKnownInternallySynchronized>>);
393 static_assert (Common::StdCompat::BasicLockable<MyMaybeLock_<InternallySynchronized::eInternallySynchronized>>);
394}
395
396namespace {
397 template <Execution::InternallySynchronized SYNC_STYLE>
398 struct AdminRep_ final : Stroika::Foundation::Database::Document::MongoDBClient::AdminConnection::IRep {
399 // qStroika_ATTRIBUTE_NO_UNIQUE_ADDRESS_VCBUGGY because ./Builds/Release/Tests/Test36.exe
400 //...[ RUN ] Foundation_Database.SimpleMongoDBClientTest_
401 // unknown file: error: SEH exception with code 0xc0000005 thrown in the test body.
402 qStroika_ATTRIBUTE_NO_UNIQUE_ADDRESS_VCBUGGY mutable MyMaybeLock_<SYNC_STYLE> fMaybeLock_; // mutable cuz this is what we lock to assure internal sync for const/non-const methods
403 variant<mongocxx::client, mongocxx::pool::entry> fClientStorage_;
404 mongocxx::client* fClientPtr_;
405
406 AdminRep_ (const AdminRep_&) = delete;
407 AdminRep_ (const AdminConnection::Options& options)
408 {
409 TraceContextBumper ctx{"MongoDBClient:::AdminConnection::Rep_::CTOR"};
410 if (auto os = get_if<String> (&options.fConnectionTarget)) {
411 fClientStorage_ = mongocxx::client{ConnectionString2MongoURI_ (*os)};
412 fClientPtr_ = get_if<mongocxx::client> (&fClientStorage_);
413 }
414 else if (auto op = get_if<ConnectionPool> (&options.fConnectionTarget)) {
415 fClientStorage_ = op->PeekPool ().acquire ();
416 fClientPtr_ = get<mongocxx::pool::entry> (fClientStorage_).operator->();
417 }
418 EnsureNotNull (fClientPtr_);
419 }
420 ~AdminRep_ () = default;
421
422 // MongoDBClient::Connection::IRep overrides
423 public:
424 virtual mongocxx::client& GetClientRef () override
425 {
426 return *fClientPtr_;
427 }
428 virtual Document::Document run_command (const Document::Document& v) override
429 {
430#if USE_NOISY_TRACE_IN_THIS_MODULE_
431 TraceContextBumper ctx{"MongoDBClient::AdminRep_::run_command"};
432#endif
433 scoped_lock critSec{fMaybeLock_};
434 try {
435 return FromBSON_ (fClientPtr_->database ("admin").run_command (ToBSON_ (v)));
436 }
437 catch (...) {
438 DoReThrow_ ();
439 }
440 }
441 virtual Set<String> GetDatabases () override
442 {
443 scoped_lock critSec{fMaybeLock_};
444 try {
445 vector<string> n = fClientPtr_->list_database_names ();
446 return Iterable<string>{n}.Map<Set<String>> ([] (string i) { return String::FromUTF8 (i); });
447 }
448 catch (...) {
449 DoReThrow_ ();
450 }
451 }
452 virtual void DropDatabase (const String& dbName) override
453 {
454 scoped_lock critSec{fMaybeLock_};
455 try {
456 mongocxx::database{fClientPtr_->database (dbName.AsUTF8<string> ())}.drop ();
457 }
458 catch (...) {
459 DoReThrow_ ();
460 }
461 }
462 virtual void CreateDatabase (const String& dbName) override
463 {
464 scoped_lock critSec{fMaybeLock_};
465 try {
466 // doesn't appear to be anything todo to create the database except maybe writing to it
467 mongocxx::database d{fClientPtr_->database (dbName.AsUTF8<string> ())};
468 d.create_collection ("_junk_");
469 d.collection ("_junk_").drop ();
470 }
471 catch (...) {
472 DoReThrow_ ();
473 }
474 }
475 };
476}
477
478namespace {
479 template <Execution::InternallySynchronized SYNC_STYLE>
480 struct ConnectionRep_ final : Stroika::Foundation::Database::Document::MongoDBClient::Connection::IRep {
482 shared_ptr<ConnectionRep_> fConnectionRep_; // save to bump reference count
483 mongocxx::collection fCollection_;
484
485 CollectionRep_ (const shared_ptr<ConnectionRep_>& connectionRep, const String& collectionName)
486 : fConnectionRep_{connectionRep}
487 , fCollection_{connectionRep->fDatabase_.collection (collectionName.AsUTF8<string> ())}
488 {
489 }
490 virtual String GetName () const override
491 {
492 return cvt2String_ (fCollection_.name ());
493 }
494 virtual IDType Add (const Document::Document& v) override
495 {
496#if USE_NOISY_TRACE_IN_THIS_MODULE_
497 TraceContextBumper ctx{"MongoDBClient::CollectionRep_::Add"};
498#endif
499 Require (not v.ContainsKey (Database::Document::kID) or fConnectionRep_->fOptions_.fAddAllowsExternallySpecifiedIDs);
500 scoped_lock declareContext{fConnectionRep_->fMaybeLock_};
501 return fConnectionRep_->WrapExecute_ (
502 [&] () {
503 try {
504 // auto insert_one_result = fCollection_.insert_one(make_document(kvp("i", 0)));
505 if (auto insert_one_result = fCollection_.insert_one (ToBSON_ (v))) {
506 return ID_2_string_ (insert_one_result->inserted_id ());
507 }
508 }
509 catch (...) {
510 DoReThrow_ ();
511 }
512 Throw (RuntimeErrorException{"failed to add document"sv});
513 },
514 cvt2String_ (fCollection_.name ()), true);
515 }
516 virtual optional<Document::Document> Get (const IDType& id, const optional<Projection>& projection) override
517 {
518#if USE_NOISY_TRACE_IN_THIS_MODULE_
519 TraceContextBumper ctx{"MongoDBClient::CollectionRep_::Get"};
520#endif
521 scoped_lock declareContext{fConnectionRep_->fMaybeLock_};
522 return fConnectionRep_->WrapExecute_ (
523 [&] () -> optional<Document::Document> {
524 try {
525 bsoncxx::builder::basic::document filterDoc;
526 filterDoc.append (kvp ("_id", ToBSONId_ (id.AsUTF8<string> ()))); //kMongoID
527 auto [mongoProjection, myProjection] = Partition_ (projection);
528 mongocxx::options::find o;
529 if (mongoProjection) {
530 o.projection (mongoProjection->view ());
531 }
532 auto result = fCollection_.find_one (filterDoc.view (), o);
533 if (result) {
534 auto rr = FromBSON_ (bsoncxx::document::view_or_value{*result});
535 if (myProjection) {
536 rr = myProjection->Apply (rr);
537 }
538 return rr;
539 }
540 return nullopt;
541 }
542 catch (...) {
543 DoReThrow_ ();
544 }
545 },
546 cvt2String_ (fCollection_.name ()), false);
547 }
548 virtual Sequence<Document::Document> GetAll (const optional<Filter>& filter, const optional<Projection>& projection) override
549 {
550#if USE_NOISY_TRACE_IN_THIS_MODULE_
551 TraceContextBumper ctx{"MongoDBClient::CollectionRep_::GetAll", "filter={}, projection={}"_f, filter, projection};
552#endif
553 scoped_lock declareContext{fConnectionRep_->fMaybeLock_};
554 return fConnectionRep_->WrapExecute_ (
555 [&] () {
556 auto [mongoFilter, myFilter] = Partition_ (filter);
557 // must be careful if myFilter != nullptr, may not be able to do projection server-side!
558 // cuz data needed client side to do the filtering...
559 auto [mongoProjection, myProjection] = myFilter == nullopt ? Partition_ (projection) : make_tuple (nullopt, projection);
560#if USE_NOISY_TRACE_IN_THIS_MODULE_
561 DbgTrace ("myFilter={}"_f, myFilter);
562 if (mongoFilter) {
563 // DbgTrace ("mongoFilter={}"_f, BSON2VV_ (*mongoFilter));
564 }
565#endif
567 mongocxx::options::find o;
568 if (mongoProjection) {
569 o.projection (mongoProjection->view ());
570 }
571 try {
572 auto cursor = fCollection_.find (mongoFilter ? mongoFilter->view () : bsoncxx::builder::basic::document{}.view (), o);
573 for (auto&& doc : cursor) {
574 auto rr = FromBSON_ (doc);
575 if (myProjection) {
576 rr = myProjection->Apply (rr);
577 }
578 if (not myFilter or myFilter->Matches (rr)) {
579 result += rr;
580 }
581 }
582 return result;
583 }
584 catch (...) {
585 DoReThrow_ ();
586 }
587 },
588 cvt2String_ (fCollection_.name ()), false);
589 }
590 virtual void Update (const IDType& id, const Document::Document& newV, const optional<Set<String>>& onlyTheseFields) override
591 {
592#if USE_NOISY_TRACE_IN_THIS_MODULE_
593 TraceContextBumper ctx{"MongoDBClient::CollectionRep_::Update"};
594#endif
595 scoped_lock declareContext{fConnectionRep_->fMaybeLock_};
596 fConnectionRep_->WrapExecute_ (
597 [&] () {
598 // incomplete... - not sure how to handle partial update vs full update - must read mongo docs more carefully
599 Document::Document uploadDoc = newV;
600 if (onlyTheseFields) {
601 uploadDoc.RetainAll (*onlyTheseFields);
602 }
603 uploadDoc.RemoveIf (Database::Document::kID);
604 try {
605 bsoncxx::document::value bsonDoc = ToBSON_ (uploadDoc);
606 if (onlyTheseFields) {
607 if (auto o = fCollection_.update_one (make_document (kvp ("_id", ToBSONId_ (id.AsUTF8<string> ()))),
608 make_document (kvp ("$set", bsonDoc.view ())))) {
609 if (o->modified_count () == 0) {
610 static const auto kExcept_ = RuntimeErrorException{"failed to update doc - not modified"sv};
611 Throw (kExcept_);
612 }
613 }
614 else {
615 static const auto kExcept_ = RuntimeErrorException{"failed to update doc"sv};
616 Throw (kExcept_);
617 }
618 }
619 else {
620 if (auto o = fCollection_.replace_one (make_document (kvp ("_id", ToBSONId_ (id.AsUTF8<string> ()))),
621 bsonDoc.view ())) {
622 if (o->modified_count () == 0) {
623 static const auto kExcept_ = RuntimeErrorException{"failed to replace doc - not modified"sv};
624 Throw (kExcept_);
625 }
626 }
627 else {
628 static const auto kExcept_ = RuntimeErrorException{"failed to replace doc"sv};
629 Throw (kExcept_);
630 }
631 }
632 }
633 catch (...) {
634 DoReThrow_ ();
635 }
636 },
637 cvt2String_ (fCollection_.name ()), true);
638 }
639 virtual void Remove (const IDType& id) override
640 {
641#if USE_NOISY_TRACE_IN_THIS_MODULE_
642 TraceContextBumper ctx{"MongoDBClient::CollectionRep_::Remove"};
643#endif
644 scoped_lock declareContext{fConnectionRep_->fMaybeLock_};
645 fConnectionRep_->WrapExecute_ (
646 [&] () {
647 try {
648 bsoncxx::builder::basic::document filterDoc;
649 filterDoc.append (kvp ("_id", ToBSONId_ (id.AsUTF8<string> ()))); // kMongoID_
650 auto result = fCollection_.delete_one (filterDoc.view ());
651 if (result && result->deleted_count () == 0) {
652 Throw (RuntimeErrorException{"failed to delete doc"sv});
653 }
654 }
655 catch (...) {
656 DoReThrow_ ();
657 }
658 },
659 cvt2String_ (fCollection_.name ()), true);
660 }
661 };
662
663 //qStroika_ATTRIBUTE_NO_UNIQUE_ADDRESS_VCBUGGY because: ./Builds/Release/Tests/Test36.exe
664 //..[ RUN ] Foundation_Database.SimpleMongoDBClientTest_
665 //unknown file: error: SEH exception with code 0xc0000005 thrown in the test body.
666 qStroika_ATTRIBUTE_NO_UNIQUE_ADDRESS_VCBUGGY mutable MyMaybeLock_<SYNC_STYLE> fMaybeLock_; // mutable cuz this is what we lock to assure internal sync for const/non-const methods
667 variant<mongocxx::client, mongocxx::pool::entry> fClientStorage_; // not directly used, but needed to free the resource when this connection obj goes away - and implicitly stored in database
668 mongocxx::database fDatabase_;
669 const Connection::Options fOptions_;
670
671 ConnectionRep_ (const Connection::Options& options)
672 : fOptions_{options}
673 {
674 TraceContextBumper ctx{"MongoDBClient::ConnectionRep_::CTOR"};
675 try {
676 if (auto os = get_if<String> (&options.fConnectionTarget)) {
677 fClientStorage_ = mongocxx::client{ConnectionString2MongoURI_ (*os)};
678 fDatabase_ = get<mongocxx::client> (fClientStorage_).database (options.fDatabase.AsUTF8<string> ());
679 }
680 else if (auto op = get_if<ConnectionPool> (&options.fConnectionTarget)) {
681 fClientStorage_ = op->PeekPool ().acquire ();
682 fDatabase_ = get<mongocxx::pool::entry> (fClientStorage_)->database (options.fDatabase.AsUTF8<string> ());
683 }
684 Ensure (fDatabase_); // properly constructed
685 }
686 catch (...) {
687 DoReThrow_ ();
688 }
689 }
690 ~ConnectionRep_ () = default;
691
692 // Document::Connection::IRep overrides
693 public:
694 virtual shared_ptr<const EngineProperties> GetEngineProperties () const override
695 {
696 struct MyEngineProperties_ final : EngineProperties {
697 virtual String GetEngineName () const override
698 {
699 return "mongo-cxx-driver"sv; // must indirect to connection to get more info (from dns at least? not clear)
700 }
701 };
702 return Memory::MakeSharedPtr<const MyEngineProperties_> (); // dynamic info based on connection/dsn
703 }
704 virtual Document::Connection::Options GetOptions () const override
705 {
706 return fOptions_;
707 }
708 virtual uintmax_t GetSpaceConsumed () const override
709 {
710 bsoncxx::builder::basic::document db_stats_cmd_builder;
711 db_stats_cmd_builder.append (bsoncxx::builder::basic::kvp ("dbStats", 1));
712 bsoncxx::document::value command_result = mongocxx::database{fDatabase_}.run_command (db_stats_cmd_builder.view ());
713 bsoncxx::document::view result_view = command_result.view ();
714 // DbgTrace ("dbStats result={}"_f, FromBSON_ (result_view));
715 if (auto oStorageSize = result_view["storageSize"]) {
716 if (oStorageSize.type () == bsoncxx::types::b_int64::type_id) {
717 return oStorageSize.get_int64 ();
718 }
719 else if (oStorageSize.type () == bsoncxx::types::b_double::type_id) {
720 return static_cast<uintmax_t> (oStorageSize.get_double ());
721 }
722 else {
723 DbgTrace ("dbStats result_view[storageSize].type ()={}"_f, oStorageSize.type ());
724 }
725 }
727 return 0;
728 }
729 virtual Set<String> GetCollections () override
730 {
731 scoped_lock declareContext{fMaybeLock_};
732 return WrapExecute_ (
733 [&] () -> Set<String> {
734 try {
735 vector<string> n = fDatabase_.list_collection_names ();
736 return Iterable<string>{n}.Map<Set<String>> ([] (string i) { return String{i}; }); // @todo fix wrong codepage map (not sure right one)
737 }
738 catch (const mongocxx::v_noabi::operation_exception& e) {
739 DbgTrace ("e={}"_f, e);
740 if (e.raw_server_error ()) {
741 DbgTrace ("e.raw={}"_f, FromBSON_ (e.raw_server_error ()->view ()));
742 }
743 // a specific error here - check for that - and throw others... no such database gets mapped to empty collection list
744 return {};
745 }
746 catch (...) {
747 DoReThrow_ ();
748 }
749 },
750 nullopt, false);
751 }
752 virtual Document::Collection::Ptr CreateCollection (const String& name) override
753 {
754#if USE_NOISY_TRACE_IN_THIS_MODULE_
755 TraceContextBumper ctx{"mongocxx::CreateCollection", "name={}"_f, name};
756#endif
757 scoped_lock declareContext{fMaybeLock_};
758 return WrapExecute_ (
759 [&] () {
760 fDatabase_.create_collection (name.AsUTF8<string> ());
761 return GetCollection (name);
762 },
763 name, true);
764 }
765 virtual void DropCollection (const String& name) override
766 {
767#if USE_NOISY_TRACE_IN_THIS_MODULE_
768 TraceContextBumper ctx{"mongocxx::DropCollection", "name={}"_f, name};
769#endif
770 scoped_lock declareContext{fMaybeLock_};
771 WrapExecute_ (
772 [&] () {
773 try {
774 fDatabase_.collection (name.AsUTF8<string> ()).drop ();
775 }
776 catch (...) {
777 DoReThrow_ ();
778 }
779 },
780 name, true);
781 }
782 virtual Document::Collection::Ptr GetCollection (const String& name) override
783 {
784 scoped_lock declareContext{fMaybeLock_};
785 Require (GetCollections ().Contains (name));
786 try {
788 Memory::MakeSharedPtr<CollectionRep_> (Debug::UncheckedDynamicPointerCast<ConnectionRep_> (shared_from_this ()), name)};
789 }
790 catch (...) {
791 DoReThrow_ ();
792 }
793 }
794 virtual Document::Transaction mkTransaction () override
795 {
796 scoped_lock declareContext{fMaybeLock_};
797 try {
798 Connection::Ptr conn = Connection::Ptr{Debug::UncheckedDynamicPointerCast<Connection::IRep> (shared_from_this ())};
799 return Transaction{conn};
800 }
801 catch (...) {
802 DoReThrow_ ();
803 }
804 }
805
806 // MongoDBClient::Connection::IRep overrides
807 public:
808 virtual mongocxx::client& GetClientRef () override
809 {
810 if (auto oc = get_if<mongocxx::client> (&fClientStorage_)) {
811 return *oc;
812 }
813 else if (auto oe = get_if<mongocxx::pool::entry> (&fClientStorage_)) {
814 return *oe->operator->();
815 }
816 else {
818 static mongocxx::client x;
819 return x;
820 }
821 }
822
823 public:
824 template <typename FUN>
825 inline auto WrapExecute_ (FUN&& f, const optional<String>& collectionName, bool write) -> invoke_result_t<FUN>
826 {
827 return Document::Connection::Private_::WrapLoggingExecuteHelper_ (forward<FUN> (f), this, fOptions_, collectionName, write);
828 }
829 };
830}
831
832/*
833 ********************************************************************************
834 ********************* Document::MongoDBClient::Activator ***********************
835 ********************************************************************************
836 */
837unique_ptr<mongocxx::instance> Document::MongoDBClient::Activator::sMongoInstance_;
838
839Document::MongoDBClient::Activator::Activator ()
840 : fAllowReactivation_{false}
841{
842 Require (Debug::AppearsDuringMainLifetime ());
843 if (sActivatorLiveCnt_.fetch_add (1) == 0) {
844 if (not sMongoInstance_) {
845 sMongoInstance_ = make_unique<mongocxx::instance> ();
846 }
847 }
848}
849
850Document::MongoDBClient::Activator::Activator (AllowReactivateFlag)
851 : Activator{}
852{
853 fAllowReactivation_ = true;
854}
855
856Document::MongoDBClient::Activator::~Activator ()
857{
858 Require (Debug::AppearsDuringMainLifetime ());
859 Require (sActivatorLiveCnt_ > 0);
860 if (sActivatorLiveCnt_.fetch_sub (1) == 0 and not fAllowReactivation_) {
861 sMongoInstance_.reset ();
862 }
863}
864
865/*
866 ********************************************************************************
867 ******************* Document::MongoDBClient::ConnectionPool ********************
868 ********************************************************************************
869 */
870ConnectionPool::ConnectionPool (shared_ptr<mongocxx::pool>&& poolRep)
871 : fPool_{move (poolRep)}
872{
873}
874ConnectionPool::ConnectionPool (const String& connectionString)
875 : ConnectionPool{Memory::MakeSharedPtr<mongocxx::pool> (ConnectionString2MongoURI_ (connectionString))}
876{
877}
878
879mongocxx::pool& ConnectionPool::PeekPool () const
880{
881 AssertNotNull (fPool_);
882 return *fPool_;
883}
884
885/*
886 ********************************************************************************
887 ****************** Document::MongoDBClient::Connection::Ptr ********************
888 ********************************************************************************
889 */
890Document::MongoDBClient::Connection::Ptr::Ptr (const shared_ptr<IRep>& src)
891 : inherited{src}
892{
893}
894
895/*
896 ********************************************************************************
897 **************** Document::MongoDBClient::AdminConnection::New *****************
898 ********************************************************************************
899 */
900auto Document::MongoDBClient::AdminConnection::New (const AdminConnection::Options& options) -> Ptr
901{
902#if qStroika_Foundation_Debug_AssertionsChecked
903 Require (sActivatorLiveCnt_ > 0);
904#endif
905 switch (options.fInternallySynchronizedLetter) {
906 case Execution::eInternallySynchronized:
907 return Ptr{Memory::MakeSharedPtr<AdminRep_<Execution::eInternallySynchronized>> (options)};
908 case Execution::eNotKnownInternallySynchronized:
909 return Ptr{Memory::MakeSharedPtr<AdminRep_<Execution::eNotKnownInternallySynchronized>> (options)};
910 default:
912 return nullptr;
913 }
914}
915
916/*
917 ********************************************************************************
918 ******************* Document::MongoDBClient::Connection::New *******************
919 ********************************************************************************
920 */
921auto Document::MongoDBClient::Connection::New (const Connection::Options& options) -> Ptr
922{
923#if qStroika_Foundation_Debug_AssertionsChecked
924 Require (sActivatorLiveCnt_ > 0);
925#endif
926 switch (options.fInternallySynchronizedLetter) {
927 case Execution::eInternallySynchronized:
928 return Ptr{Memory::MakeSharedPtr<ConnectionRep_<Execution::eInternallySynchronized>> (options)};
929 case Execution::eNotKnownInternallySynchronized:
930 return Ptr{Memory::MakeSharedPtr<ConnectionRep_<Execution::eNotKnownInternallySynchronized>> (options)};
931 default:
933 return nullptr;
934 }
935}
936
937/*
938 ********************************************************************************
939 ******************************* SQLite::Transaction ****************************
940 ********************************************************************************
941 */
942struct Transaction::MyRep_ : public MongoDBClient::Transaction::IRep {
943 MyRep_ (const Connection::Ptr& db)
944 : fConnectionPtr_{db}
945 {
947 }
948 virtual void Commit () override
949 {
950 Require (not fCompleted_);
951 fCompleted_ = true;
952 // fConnectionPtr_->Exec ("COMMIT;"sv);
953 }
954 virtual void Rollback () override
955 {
956 Require (not fCompleted_);
957 fCompleted_ = true;
958 // fConnectionPtr_->Exec ("ROLLBACK;"sv);
959 }
960 virtual Disposition GetDisposition () const override
961 {
962 // @todo record more info so we can report finer grained status ; try/catch in rollback/commit and dbgtraces
963 return fCompleted_ ? Disposition::eCompleted : Disposition::eNone;
964 }
965 Connection::Ptr fConnectionPtr_;
966 bool fCompleted_{false};
967};
968Transaction::Transaction (const Connection::Ptr& db)
969 : inherited{make_unique<MyRep_> (db)}
970{
971}
972#endif
#define AssertNotNull(p)
Definition Assertions.h:334
#define EnsureNotNull(p)
Definition Assertions.h:341
#define WeakAssertNotReached()
Definition Assertions.h:468
#define AssertNotImplemented()
Definition Assertions.h:402
#define RequireNotReached()
Definition Assertions.h:386
#define AssertNotReached()
Definition Assertions.h:356
auto MakeSharedPtr(ARGS_TYPE &&... args) -> shared_ptr< T >
same as make_shared, but if type T has block allocation, then use block allocation for the 'shared pa...
#define qStroika_ATTRIBUTE_NO_UNIQUE_ADDRESS_VCBUGGY
qStroika_ATTRIBUTE_NO_UNIQUE_ADDRESS_VCBUGGY same as qStroika_ATTRIBUTE_NO_UNIQUE_ADDRESS
Definition StdCompat.h:454
#define DbgTrace
Definition Trace.h:317
String is like std::u32string, except it is much easier to use, often much more space efficient,...
Definition String.h:201
Mapping_HashTable<KEY_TYPE, MAPPED_VALUE_TYPE, TRAITS> is a HashTable based concrete implementation o...
Sequence_stdvector<T> is an std::vector-based concrete implementation of the Sequence<T> container pa...
nonvirtual bool Add(ArgByValueType< key_type > key, ArgByValueType< mapped_type > newElt, AddReplaceMode addReplaceMode=AddReplaceMode::eAddReplaces)
Definition Mapping.inl:188
nonvirtual bool ContainsKey(ArgByValueType< key_type > key) const
Definition Mapping.inl:177
nonvirtual bool RemoveIf(ArgByValueType< key_type > key)
Remove the given item, if it exists. Return true if found and removed.
Definition Mapping.inl:235
nonvirtual void Remove(ArgByValueType< key_type > key)
Remove the given item (which must exist).
Definition Mapping.inl:223
nonvirtual void RetainAll(const ITERABLE_OF_KEY_TYPE &items)
A generalization of a vector: a container whose elements are keyed by the natural numbers.
Set<T> is a container of T, where once an item is added, additionally adds () do nothing.
Simple variant-value (case variant union) object, with (variant) basic types analogous to a value in ...
EngineProperties captures the features associated with a given database engine (being talked to throu...
define a (simple) projection on a document, subsetting the fields of that document.
Definition Projection.h:32
NestedException contains a new higher level error message (typically based on argument basedOnExcepti...
Definition Exceptions.h:212
static constexpr string_view kISO8601Format
Y-M-D format - locale independent, and ISO-8601 date format standard.
Definition Date.h:467
Iterable<T> is a base class for containers which easily produce an Iterator<T> to traverse them.
Definition Iterable.h:237
nonvirtual void Apply(const function< void(ArgByValueType< T > item)> &doToElement, Execution::SequencePolicy seq=Execution::SequencePolicy::eDEFAULT) const
Run the argument function (or lambda) on each element of the container.
nonvirtual RESULT_CONTAINER Map(ELEMENT_MAPPER &&elementMapper) const
functional API which iterates over all members of an Iterable, applies a map function to each element...
nonvirtual bool empty() const
Returns true iff size() == 0.
Definition Iterable.inl:309
void Throw(T &&e2Throw)
identical to builtin C++ 'throw' except that it does helpful, type dependent DbgTrace() messages firs...
Definition Throw.inl:43
bool AnyCurrentActivities()
Checks if CaptureCurrentActivities() would produce a non-empty stack (but faster)
Definition Activity.cpp:39