Stroika Library 3.0d23
 
Loading...
Searching...
No Matches
LocalDocumentDB.cpp
1/*
2 * Copyright(c) Sophist Solutions, Inc. 1990-2026. All rights reserved
3 */
4#include "Stroika/Foundation/StroikaPreComp.h"
5
6#include <system_error>
7
10#include "Stroika/Foundation/Common/GUID.h"
19
20#include "LocalDocumentDB.h"
21
22using namespace Stroika::Foundation;
23
24using namespace Characters;
25using namespace Containers;
26using namespace Debug;
27using namespace DataExchange;
28using namespace Database;
29using namespace Database::Document::LocalDocumentDB;
30using namespace Execution;
31using namespace Time;
32
37
38// Comment this in to turn on aggressive noisy DbgTrace in this module
39// #define USE_NOISY_TRACE_IN_THIS_MODULE_ 1
40
41using Common::GUID;
43
44namespace {
45
46 template <InternallySynchronized SYNC_STYLE>
47 using MyMaybeLock_ =
48 conditional_t<SYNC_STYLE == InternallySynchronized::eNotKnownInternallySynchronized, Debug::AssertExternallySynchronizedMutex, recursive_mutex>;
49 static_assert (Common::StdCompat::BasicLockable<MyMaybeLock_<InternallySynchronized::eNotKnownInternallySynchronized>>);
50 static_assert (Common::StdCompat::BasicLockable<MyMaybeLock_<InternallySynchronized::eInternallySynchronized>>);
51
52 /*
53 * Store collections entirely in RAM.
54 * \note \em Thread-Safety depends on InternallySynchronized
55 */
56 template <InternallySynchronized SYNC_STYLE>
57 struct MemoryDatabaseRep_ final : Database::Document::LocalDocumentDB::IRep {
58
59 using CollectionRep_ = Mapping<GUID, Document::Document>;
60
62 qStroika_ATTRIBUTE_NO_UNIQUE_ADDRESS_VCFORCE mutable MyMaybeLock_<SYNC_STYLE> fMaybeLock_; // mutable cuz this is what we lock to assure internal sync for const/non-const methods
64
65 struct MyCollectionRep_ final : Document::Collection::IRep {
66 const shared_ptr<MemoryDatabaseRep_> fConnectionRep_; // save to bump reference count (so lifetime of collection always >= lifetime of documentDB)
67 const String fTableName_;
68
69 MyCollectionRep_ (const shared_ptr<MemoryDatabaseRep_>& connectionRep, const String& collectionName)
70 : fConnectionRep_{connectionRep}
71 , fTableName_{collectionName}
72 {
73 }
74 virtual String GetName () const override
75 {
76 scoped_lock critSec{fConnectionRep_->fMaybeLock_};
77 return fTableName_;
78 }
79 virtual IDType Add (const Document::Document& v) override
80 {
81#if USE_NOISY_TRACE_IN_THIS_MODULE_
82 TraceContextBumper ctx{"LocalDocumentDB::MemoryDatabaseRep_::MyCollectionRep_::Add"};
83#endif
84 scoped_lock critSec{fConnectionRep_->fMaybeLock_};
85 return fConnectionRep_->WrapExecute_ (
86 [&] () {
87 optional<VariantValue> vID = v.Lookup (Document::kID);
88 Require (not vID.has_value () or fConnectionRep_->fOptions_.fAddAllowsExternallySpecifiedIDs);
89 GUID id = vID.has_value () ? GUID{vID->As<String> ()} : GUID::GenerateNew ();
90 CollectionRep_ collection = fConnectionRep_->fCollections_.LookupValue (fTableName_);
91 Document::Document doc2Add = v;
92 if (vID) {
93 doc2Add.Remove (Document::kID); // already in parent KEY so don't store redundantly
94 }
95 collection.Add (id, doc2Add);
96 fConnectionRep_->fCollections_.Add (fTableName_, collection);
97 return id.ToString ();
98 },
99 fTableName_, true);
100 }
101 virtual optional<Document::Document> Get (const IDType& id, const optional<Projection>& projection) override
102 {
103#if USE_NOISY_TRACE_IN_THIS_MODULE_
104 TraceContextBumper ctx{"LocalDocumentDB::MemoryDatabaseRep_::MyCollectionRep_::Get"};
105#endif
106 scoped_lock critSec{fConnectionRep_->fMaybeLock_};
107 return fConnectionRep_->WrapExecute_ (
108 [&] () {
109 optional<Document::Document> r = fConnectionRep_->fCollections_.LookupValue (fTableName_).Lookup (GUID{id});
110 if (r) {
111 r->Add (Document::kID, id);
112 }
113 if (projection and r) {
114 r = projection->Apply (*r);
115 }
116 return r;
117 },
118 fTableName_, false);
119 }
120 virtual Sequence<Document::Document> GetAll (const optional<Filter>& filter, const optional<Projection>& projection) override
121 {
122#if USE_NOISY_TRACE_IN_THIS_MODULE_
123 TraceContextBumper ctx{"LocalDocumentDB::MemoryDatabaseRep_::MyCollectionRep_::GetAll", "filter={}, projection={}"_f, filter, projection};
124#endif
125 scoped_lock critSec{fConnectionRep_->fMaybeLock_};
126 return fConnectionRep_->WrapExecute_ (
127 [&] () {
128 return fConnectionRep_->fCollections_.LookupValue (fTableName_)
129 .template Map<Sequence<Document::Document>> (
130 [&] (const KeyValuePair<GUID, Document::Document>& kvp) -> optional<Document::Document> {
131 Document::Document d = kvp.fValue;
132 d.Add (Document::kID, kvp.fKey.ToString ());
133 if (filter and not filter->Matches (d)) {
134 return nullopt; // skip cuz didn't match filter
135 }
136 else {
137 if (projection) {
138 d = projection->Apply (d);
139 }
140 return d;
141 }
142 });
143 },
144 fTableName_, false);
145 }
146 virtual void Update (const IDType& id, const Document::Document& newV, const optional<Set<String>>& onlyTheseFields) override
147 {
148#if USE_NOISY_TRACE_IN_THIS_MODULE_
149 TraceContextBumper ctx{"LocalDocumentDB::MemoryDatabaseRep_::MyCollectionRep_::Update"};
150#endif
151 scoped_lock critSec{fConnectionRep_->fMaybeLock_};
152 fConnectionRep_->WrapExecute_ (
153 [&] () {
154 Document::Document uploadDoc = newV;
155 if (onlyTheseFields) {
156 uploadDoc.RetainAll (*onlyTheseFields);
157 }
158 static const auto kExcept1_ = RuntimeErrorException{"no such table"sv};
159 static const auto kNoSuchIDException_ = RuntimeErrorException{"no such id"sv};
160 CollectionRep_ collection = fConnectionRep_->fCollections_.LookupChecked (fTableName_, kExcept1_);
161 Document::Document d2Update = onlyTheseFields ? collection.LookupChecked (id, kNoSuchIDException_) : uploadDoc;
162 // any fields listed in onlyTheseFields, but not present in newV need to be removed
163 if (onlyTheseFields) {
164 d2Update.AddAll (uploadDoc);
165 Set<String> removeMe = *onlyTheseFields - newV.Keys ();
166 d2Update.RemoveAll (removeMe);
167 }
168 d2Update.RemoveIf (Document::kID);
169 collection.Add (id, d2Update);
170 fConnectionRep_->fCollections_.Add (fTableName_, collection); // replace the actual collection in our master database of collections
171 },
172 fTableName_, true);
173 }
174 virtual void Remove (const IDType& id) override
175 {
176#if USE_NOISY_TRACE_IN_THIS_MODULE_
177 TraceContextBumper ctx{"LocalDocumentDB::MemoryDatabaseRep_::MyCollectionRep_::Remove"};
178#endif
179 scoped_lock critSec{fConnectionRep_->fMaybeLock_};
180 fConnectionRep_->WrapExecute_ (
181 [&] () {
182 if (optional<CollectionRep_> oc = fConnectionRep_->fCollections_.Lookup (fTableName_)) {
183 CollectionRep_ c = *oc;
184 if (c.RemoveIf (id)) {
185 fConnectionRep_->fCollections_.Add (fTableName_, c); // replace the actual collection in our master database of collections
186 }
187 }
188 },
189 fTableName_, true);
190 }
191 };
192
193 struct MyTransactionRep_ final : Database::Document::Transaction::IRep {
194 virtual void Commit () override
195 {
196 // nothing todo
197 }
198 virtual void Rollback () override
199 {
201 }
202 virtual Disposition GetDisposition () const override
203 {
204 return Disposition::eCompleted;
205 }
206 };
207
208 MemoryDatabaseRep_ () = delete;
209 MemoryDatabaseRep_ (const MemoryDatabaseRep_&) = delete;
210 MemoryDatabaseRep_ ([[maybe_unused]] const Document::LocalDocumentDB::Options& options)
211 : fOptions_{options}
212 {
213 TraceContextBumper ctx{"LocalDocumentDB::MemoryDatabaseRep_::MemoryDatabaseRep_"};
214 //Assert (shared_from_this ().get () == this); // only support allocating with make_shared - cannot check here cuz object not yet fully constructed
215 }
216 virtual shared_ptr<const EngineProperties> GetEngineProperties () const override
217 {
218 struct MyEngineProperties_ final : EngineProperties {
219 virtual String GetEngineName () const override
220 {
221 return "LocalDocumentDB.MemoryDB"sv;
222 }
223 };
224 static const shared_ptr<const EngineProperties> kProps_ = Memory::MakeSharedPtr<const MyEngineProperties_> ();
225 return kProps_;
226 }
227 virtual Database::Document::Connection::Options GetOptions () const override
228 {
229 return fOptions_;
230 }
231 static uintmax_t EstimateSize_ (const VariantValue& vv)
232 {
233 switch (vv.GetType ()) {
234 case VariantValue::Type::eBLOB:
235 return vv.As<Memory::BLOB> ().size ();
236 case VariantValue::Type::eString:
237 return vv.As<String> ().size ();
238 case VariantValue::Type::eFloat:
239 return sizeof (long double);
240 default:
242 return 1000; //tmphack
243 }
244 return 0;
245 }
246 virtual uintmax_t GetSpaceConsumed () const override
247 {
248 uintmax_t totalSize{};
249 // WAG/Weak but adequate Estimate
250 for (const KeyValuePair<String, CollectionRep_>& ci : fCollections_) {
251 totalSize += ci.fKey.size () + 3;
252 for (const KeyValuePair<GUID, Document::Document>& di : ci.fValue) {
253 totalSize += 20; // for GUID
254 for (const KeyValuePair<String, VariantValue>& xi : di.fValue) {
255 totalSize += xi.fKey.size () + 3;
256 totalSize += EstimateSize_ (xi.fValue) + 4;
257 }
258 }
259 }
260 return totalSize;
261 }
262 virtual Set<String> GetCollections () override
263 {
264 scoped_lock declareContext{fMaybeLock_};
265 return Set<String>{fCollections_.Keys ()};
266 }
267 virtual Document::Collection::Ptr CreateCollection (const String& name) override
268 {
269 scoped_lock declareContext{fMaybeLock_};
270 if (not fCollections_.Lookup (name)) {
271 fCollections_.Add (name, {});
272 }
273 return GetCollection (name);
274 }
275 virtual void DropCollection (const String& name) override
276 {
277 scoped_lock declareContext{fMaybeLock_};
278 fCollections_.RemoveIf (name);
279 }
280 virtual Document::Collection::Ptr GetCollection (const String& name) override
281 {
282 scoped_lock declareContext{fMaybeLock_};
283 Require (fCollections_.ContainsKey (name));
285 Memory::MakeSharedPtr<MyCollectionRep_> (Debug::UncheckedDynamicPointerCast<MemoryDatabaseRep_> (shared_from_this ()), name)};
286 }
287 virtual Document::Transaction mkTransaction () override
288 {
289 return Document::Transaction{make_unique<MyTransactionRep_> ()};
290 }
291 virtual void Flush () override
292 {
293 // nothing todo - all in memory
294 }
295 template <typename FUN>
296 inline auto WrapExecute_ (FUN&& f, const optional<String>& collectionName, bool write) -> invoke_result_t<FUN>
297 {
298 return Document::Connection::Private_::WrapLoggingExecuteHelper_ (forward<FUN> (f), this, fOptions_, collectionName, write);
299 }
300 };
301
302 /*
303 * Store collections in json file (leveraging MemoryDatabaseRep_ internally).
304 * \note \em Thread-Safety <a href='#Internally-Synchronized-Thread-Safety'>Internally-Synchronized-Thread-Safety</a>
305 */
306 template <InternallySynchronized SYNC_STYLE>
307 struct SingleFileDatabaseRep_ final : Database::Document::LocalDocumentDB::IRep {
308
309 const filesystem::path fExternalFile_;
310 shared_ptr<MemoryDatabaseRep_<SYNC_STYLE>> fMemoryDB_; // already internally synrchonized, must be shared_ptr cuz it uses shared_from_this
311 const DataExchange::Variant::Reader fReader_;
312 const DataExchange::Variant::Writer fWriter_;
313 const bool fFlushOnEachWrite_;
314 bool fDirty_{true}; // if true, we have changes that haven't yet been flushed to disk
315 const bool fReadOnly_{false};
316 const OpertionCallbackPtr fOperationLoggingCallback_{nullptr};
317#if qStroika_Foundation_Common_Platform_Windows
318 const optional<Time::DurationSeconds> fRetryOnSharingViolationFor_;
319#endif
320
321 struct MyCollectionRep_ final : Document::Collection::IRep {
322 const shared_ptr<SingleFileDatabaseRep_> fDBRep_; // save to bump reference count (lifetime safety), and to force write
323 const String fName_;
324 shared_ptr<Database::Document::Collection::IRep> fDelegateToInMemoryDB_; // inside memorydb
325
326 MyCollectionRep_ (const shared_ptr<SingleFileDatabaseRep_>& dbRep, const String& name,
327 const shared_ptr<Database::Document::Collection::IRep>& delgateImplTo)
328 : fDBRep_{dbRep}
329 , fName_{name}
330 , fDelegateToInMemoryDB_{delgateImplTo}
331 {
332 }
333 virtual String GetName () const override
334 {
335 return fName_;
336 }
337 virtual IDType Add (const Document::Document& v) override
338 {
339#if USE_NOISY_TRACE_IN_THIS_MODULE_
340 TraceContextBumper ctx{"LocalDocumentDB::SingleFileDatabaseRep_::MyCollectionRep_::Add"};
341#endif
342 scoped_lock critSec{fDBRep_->fMemoryDB_->fMaybeLock_};
343 return fDBRep_->WrapExecute_ (
344 [&] () {
345 auto id = fDelegateToInMemoryDB_->Add (v);
346 fDBRep_->DataChangedSoMaybeWrite2Disk ();
347 return id;
348 },
349 fName_, true);
350 }
351 virtual optional<Document::Document> Get (const IDType& id, const optional<Projection>& projection) override
352 {
353#if USE_NOISY_TRACE_IN_THIS_MODULE_
354 TraceContextBumper ctx{"LocalDocumentDB::SingleFileDatabaseRep_::MyCollectionRep_::Get"};
355#endif
356 scoped_lock critSec{fDBRep_->fMemoryDB_->fMaybeLock_};
357 return fDBRep_->WrapExecute_ ([&] () { return fDelegateToInMemoryDB_->Get (id, projection); }, fName_, false);
358 }
359 virtual Sequence<Document::Document> GetAll (const optional<Filter>& filter, const optional<Projection>& projection) override
360 {
361#if USE_NOISY_TRACE_IN_THIS_MODULE_
362 TraceContextBumper ctx{"LocalDocumentDB::SingleFileDatabaseRep_::MyCollectionRep_::GetAll", "filter={}, projection={}"_f,
363 filter, projection};
364#endif
365 return fDBRep_->WrapExecute_ ([&] () { return fDelegateToInMemoryDB_->GetAll (filter, projection); }, fName_, false);
366 }
367 virtual void Update (const IDType& id, const Document::Document& newV, const optional<Set<String>>& onlyTheseFields) override
368 {
369#if USE_NOISY_TRACE_IN_THIS_MODULE_
370 TraceContextBumper ctx{"LocalDocumentDB::SingleFileDatabaseRep_::MyCollectionRep_::Update"};
371#endif
372 scoped_lock critSec{fDBRep_->fMemoryDB_->fMaybeLock_};
373 fDBRep_->WrapExecute_ (
374 [&] () {
375 fDelegateToInMemoryDB_->Update (id, newV, onlyTheseFields);
376 fDBRep_->DataChangedSoMaybeWrite2Disk ();
377 },
378 fName_, true);
379 }
380 virtual void Remove (const IDType& id) override
381 {
382#if USE_NOISY_TRACE_IN_THIS_MODULE_
383 TraceContextBumper ctx{"LocalDocumentDB::SingleFileDatabaseRep_::MyCollectionRep_::Remove"};
384#endif
385 scoped_lock critSec{fDBRep_->fMemoryDB_->fMaybeLock_};
386 fDBRep_->WrapExecute_ (
387 [&] () {
388 fDelegateToInMemoryDB_->Remove (id);
389 fDBRep_->DataChangedSoMaybeWrite2Disk ();
390 },
391 fName_, true);
392 }
393 };
394
395 struct MyTransactionRep_ final : Database::Document::Transaction::IRep {
396 virtual void Commit () override
397 {
398 // nothing todo
399 }
400 virtual void Rollback () override
401 {
403 }
404 virtual Disposition GetDisposition () const override
405 {
406 return Disposition::eCompleted;
407 }
408 };
409
411 {
412 o.fOperationLoggingCallback = nullptr;
413 return o;
414 }
415
416 SingleFileDatabaseRep_ () = delete;
417 SingleFileDatabaseRep_ (const SingleFileDatabaseRep_&) = delete;
418 SingleFileDatabaseRep_ ([[maybe_unused]] const Document::LocalDocumentDB::Options& options,
419 const Document::LocalDocumentDB::Options::SingleFileStorage& sfOptions)
420 : fExternalFile_{sfOptions.fFile}
421 , fMemoryDB_{make_shared<MemoryDatabaseRep_<SYNC_STYLE>> (stripOptionsForMemDB_ (options))}
422 , fReader_{get<DataExchange::Variant::Reader> (sfOptions.fSerialization)}
423 , fWriter_{get<DataExchange::Variant::Writer> (sfOptions.fSerialization)}
424 , fFlushOnEachWrite_{sfOptions.fFlushOnEachWrite}
425 , fDirty_{not fFlushOnEachWrite_}
426 , fReadOnly_{sfOptions.fReadOnly}
427 , fOperationLoggingCallback_{options.fOperationLoggingCallback}
428#if qStroika_Foundation_Common_Platform_Windows
429 , fRetryOnSharingViolationFor_{sfOptions.fRetryOnSharingViolationFor}
430#endif
431 {
432 TraceContextBumper ctx{"LocalDocumentDB::SingleFileDatabaseRep_::SingleFileDatabaseRep_"};
433 if (not sfOptions.fForceCreateNew) {
434 DoReadFromFS ();
435 }
436 }
437 virtual ~SingleFileDatabaseRep_ () override
438 {
439 if (fDirty_) {
440 DoWriteToFS ();
441 }
442 }
443 virtual shared_ptr<const EngineProperties> GetEngineProperties () const override
444 {
445 struct MyEngineProperties_ final : EngineProperties {
446 virtual String GetEngineName () const override
447 {
448 return "LocalDocumentDB.SingleFile"sv;
449 }
450 };
451 static const shared_ptr<const EngineProperties> kProps_ = Memory::MakeSharedPtr<const MyEngineProperties_> ();
452 return kProps_;
453 }
454 virtual Database::Document::Connection::Options GetOptions () const override
455 {
456 scoped_lock declareContext{fMemoryDB_->fMaybeLock_};
457 Database::Document::Connection::Options o = fMemoryDB_->GetOptions ();
458 o.fOperationLoggingCallback = fOperationLoggingCallback_;
459 return o;
460 }
461 virtual uintmax_t GetSpaceConsumed () const override
462 {
463 if (fDirty_) {
464 IgnoreExceptionsExceptThreadAbortForCall (const_cast<SingleFileDatabaseRep_*> (this)->DoWriteToFS ()); // cannot get size otherwise
465 }
466 error_code ignoredEC;
467 return filesystem::file_size (fExternalFile_, ignoredEC);
468 }
469 virtual Set<String> GetCollections () override
470 {
471 return fMemoryDB_->GetCollections ();
472 }
473 virtual Document::Collection::Ptr CreateCollection (const String& name) override
474 {
475 scoped_lock declareContext{fMemoryDB_->fMaybeLock_};
476 return WrapExecute_ (
477 [&] () {
478 fMemoryDB_->CreateCollection (name);
479 DataChangedSoMaybeWrite2Disk ();
480 return GetCollection (name);
481 },
482 name, true);
483 }
484 virtual void DropCollection (const String& name) override
485 {
486 scoped_lock declareContext{fMemoryDB_->fMaybeLock_};
487 WrapExecute_ (
488 [&] () {
489 fMemoryDB_->DropCollection (name);
490 DataChangedSoMaybeWrite2Disk ();
491 },
492 name, true);
493 }
494 virtual Document::Collection::Ptr GetCollection (const String& name) override
495 {
496 scoped_lock declareContext{fMemoryDB_->fMaybeLock_};
497 Document::Collection::Ptr memDBCollection = fMemoryDB_->GetCollection (name);
498 return Document::Collection::Ptr{Memory::MakeSharedPtr<MyCollectionRep_> (
499 Debug::UncheckedDynamicPointerCast<SingleFileDatabaseRep_> (shared_from_this ()), name, memDBCollection)};
500 }
501 virtual Document::Transaction mkTransaction () override
502 {
503 return Document::Transaction{make_unique<MyTransactionRep_> ()};
504 }
505 virtual void Flush () override
506 {
507 DoWriteToFS ();
508 }
509 void DoReadFromFS ()
510 {
511 using namespace IO::FileSystem;
512#if USE_NOISY_TRACE_IN_THIS_MODULE_
513 TraceContextBumper ctx{"LocalDocumentDB::SingleFileDatabaseRep_::DoReadFromFS", "path={}"_f, fExternalFile_};
514#endif
515 scoped_lock declareContext{fMemoryDB_->fMaybeLock_};
516 if (filesystem::exists (fExternalFile_)) {
517 fMemoryDB_->fCollections_.clear ();
518 for (KeyValuePair<String, VariantValue> collectionAndDocument :
519 fReader_.Read (FileInputStream::New (fExternalFile_)).template As<Mapping<String, VariantValue>> ()) {
520 fMemoryDB_->fCollections_.Add (
521 collectionAndDocument.fKey,
522 collectionAndDocument.fValue.As<Mapping<String, VariantValue>> ().template Map<Mapping<GUID, Document::Document>> (
524 return {GUID{kvp.fKey}, kvp.fValue.As<Document::Document> ()};
525 }));
526 }
527 }
528 }
529 void DataChangedSoMaybeWrite2Disk ()
530 {
531 if (fFlushOnEachWrite_) {
532 DoWriteToFS ();
533 }
534 else {
535 fDirty_ = true;
536 }
537 }
538 void DoWriteToFS ()
539 {
540#if USE_NOISY_TRACE_IN_THIS_MODULE_
541 TraceContextBumper ctx{"LocalDocumentDB::SingleFileDatabaseRep_::DoWriteToFS", "path={}"_f, fExternalFile_};
542#endif
543 if (not this->fReadOnly_) {
544 scoped_lock declareContext{fMemoryDB_->fMaybeLock_};
545 using namespace IO::FileSystem;
546 ThroughTmpFileWriter tmpFile{fExternalFile_};
547 IO::FileSystem::FileOutputStream::Ptr outStream = IO::FileSystem::FileOutputStream::New (tmpFile.GetFilePath ());
548 Mapping<String, VariantValue> collectionsAsVV;
549 for (const KeyValuePair<String, Mapping<GUID, Document::Document>>& collection : fMemoryDB_->fCollections_) {
550 Mapping<GUID, Document::Document> collectionValue = collection.fValue;
551 Mapping<String, VariantValue> collWithStringKey;
552 for (const KeyValuePair<GUID, Document::Document>& kvp : collectionValue) {
553 collWithStringKey.Add (kvp.fKey.ToString (), VariantValue{kvp.fValue});
554 }
555 collectionsAsVV.Add (collection.fKey, VariantValue{collWithStringKey});
556 }
557 this->fWriter_.Write (VariantValue{collectionsAsVV}, outStream);
558 outStream.Close (); // close like this so we can throw exception - cannot throw if we count on DTOR
559#if qStroika_Foundation_Common_Platform_Windows
560 tmpFile.fRetryOnSharingViolationFor = fRetryOnSharingViolationFor_;
561#endif
562 tmpFile.Commit (); // any exceptions cause the tmp file to be automatically cleaned up
563 }
564 fDirty_ = false;
565 }
566 template <typename FUN>
567 inline auto WrapExecute_ (FUN&& f, const optional<String>& collectionName, bool write) -> invoke_result_t<FUN>
568 {
569 if (fOperationLoggingCallback_) {
570 Database::Document::Connection::Options o = fMemoryDB_->GetOptions ();
571 o.fOperationLoggingCallback = fOperationLoggingCallback_;
572 return Document::Connection::Private_::WrapLoggingExecuteHelper_ (forward<FUN> (f), this, o, collectionName, write);
573 }
574 else {
575 return f ();
576 }
577 }
578 };
579
580 // Store each collection in a folder under the root folder
581 // Mostly intrinsically internally synchronized, but maybe could use locks here. Corner cases?
582 // like update, while adds happening. Won't cause CORRUPTION, but unclear what guarantees we want
583 // to offer about what completes before what?
584 template <InternallySynchronized SYNC_STYLE>
585 struct DirectoryFilesystemDatabaseRep_ final : Database::Document::LocalDocumentDB::IRep {
587 const filesystem::path fRoot_;
588 const DataExchange::Variant::Reader fReader_;
589 const DataExchange::Variant::Writer fWriter_;
590#if qStroika_Foundation_Common_Platform_Windows
591 const optional<Time::DurationSeconds> fRetryOnSharingViolationFor_;
592#endif
593
594 struct MyCollectionRep_ final : Document::Collection::IRep {
595 const shared_ptr<DirectoryFilesystemDatabaseRep_> fDBRep_; // save to bump reference count (lifetime safety)
596 const String fName_;
597 const filesystem::path fCollectionRoot_;
598
599 MyCollectionRep_ (const shared_ptr<DirectoryFilesystemDatabaseRep_>& dbRep, const String& name)
600 : fDBRep_{dbRep}
601 , fName_{name}
602 , fCollectionRoot_{dbRep->GetCollectionFilePath_ (name)}
603 {
604 }
605 virtual String GetName () const override
606 {
607 return fName_;
608 }
609 virtual IDType Add (const Document::Document& v) override
610 {
611#if USE_NOISY_TRACE_IN_THIS_MODULE_
612 TraceContextBumper ctx{"LocalDocumentDB::DirectoryFilesystemDatabaseRep_::MyCollectionRep_::Add"};
613#endif
614 optional<VariantValue> vID = v.Lookup (Document::kID);
615 Require (not vID.has_value () or fDBRep_->fOptions_.fAddAllowsExternallySpecifiedIDs);
616 GUID id = vID.has_value () ? GUID{vID->As<String> ()} : GUID::GenerateNew ();
617 fDBRep_->WrapExecute_ (
618 [&] () {
619 Document::Document doc2Add = v;
620 if (vID) {
621 doc2Add.Remove (Document::kID); // already in parent KEY so don't store redundantly
622 }
623 DoWriteToFS_ (id, VariantValue{doc2Add});
624 },
625 fName_, true);
626 return id.As<IDType> ();
627 }
628 virtual optional<Document::Document> Get (const IDType& id, const optional<Projection>& projection) override
629 {
630#if USE_NOISY_TRACE_IN_THIS_MODULE_
631 TraceContextBumper ctx{"LocalDocumentDB::DirectoryFilesystemDatabaseRep_::MyCollectionRep_::Get"};
632#endif
633 return fDBRep_->WrapExecute_ (
634 [&] () -> optional<Document::Document> {
635 if (auto od = DoReadFromFS_ (GUID{id})) {
636 Document::Document d = *od;
637 d.Add (Document::kID, id);
638 if (projection) {
639 d = projection->Apply (d);
640 }
641 return d;
642 }
643 return nullopt;
644 },
645 fName_, false);
646 }
647 virtual Sequence<Document::Document> GetAll (const optional<Filter>& filter, const optional<Projection>& projection) override
648 {
649#if USE_NOISY_TRACE_IN_THIS_MODULE_
650 TraceContextBumper ctx{"LocalDocumentDB::DirectoryFilesystemDatabaseRep_::MyCollectionRep_::GetAll",
651 "filter={}, projection={}"_f, filter, projection};
652#endif
653 return fDBRep_->WrapExecute_ (
654 [&] () {
656 for (const auto& entry : filesystem::directory_iterator{fCollectionRoot_}) {
657 if (entry.path ().extension () == ".json"sv) { // Check if the entry is a JSON file
659 fDBRep_->fReader_.Read (IO::FileSystem::FileInputStream::New (entry.path ())).template As<Document::Document> ();
660 d.Add (Document::kID, entry.path ().stem ().string ());
661 if (not filter or filter->Matches (d)) {
662 if (projection) {
663 d = projection->Apply (d);
664 }
665 result += d;
666 }
667 }
668 }
669 return result;
670 },
671 fName_, false);
672 }
673 virtual void Update (const IDType& id, const Document::Document& newV, const optional<Set<String>>& onlyTheseFields) override
674 {
675#if USE_NOISY_TRACE_IN_THIS_MODULE_
676 TraceContextBumper ctx{"LocalDocumentDB::DirectoryFilesystemDatabaseRep_::MyCollectionRep_::Update",
677 "id={},newV={}, onlyTheseFields={}"_f, id, newV, onlyTheseFields};
678#endif
679 fDBRep_->WrapExecute_ (
680 [&] () {
681 Document::Document updatedDoc =
682 onlyTheseFields ? Memory::ValueOfOrThrow (DoReadFromFS_ (id), RuntimeErrorException{"no such id"sv}) : newV;
683 Document::Document updateWithDoc = newV;
684 if (onlyTheseFields) {
685 updateWithDoc.RetainAll (*onlyTheseFields);
686 }
687 updatedDoc.AddAll (updateWithDoc);
688 if (onlyTheseFields) {
689 // any fields listed in onlyTheseFields, but not present in newV need to be removed
690 Set<String> removeMe = *onlyTheseFields - newV.Keys ();
691 updatedDoc.RemoveAll (removeMe);
692 }
693 DoWriteToFS_ (GUID{id}, VariantValue{updatedDoc});
694 },
695 fName_, true);
696 }
697 virtual void Remove (const IDType& id) override
698 {
699#if USE_NOISY_TRACE_IN_THIS_MODULE_
700 TraceContextBumper ctx{"LocalDocumentDB::DirectoryFilesystemDatabaseRep_::MyCollectionRep_::Remove", "id={}"_f, id};
701#endif
702 fDBRep_->WrapExecute_ ([&] () { (void)filesystem::remove (GetDocumentFilePath_ (GUID{id})); }, fName_, true);
703 }
704 filesystem::path GetDocumentFilePath_ (const GUID& id) const
705 {
706 return fCollectionRoot_ / (id.As<String> () + ".json"sv).As<filesystem::path> ();
707 }
708 optional<Document::Document> DoReadFromFS_ (const GUID& id)
709 {
710 using namespace IO::FileSystem;
711 filesystem::path docFilePath = GetDocumentFilePath_ (id);
712#if USE_NOISY_TRACE_IN_THIS_MODULE_
713 TraceContextBumper ctx{"LocalDocumentDB::DirectoryFilesystemDatabaseRep_::DoReadFromFS", "path={}"_f, docFilePath};
714#endif
715 if (filesystem::exists (docFilePath)) {
716 return fDBRep_->fReader_.Read (FileInputStream::New (docFilePath)).template As<Document::Document> ();
717 }
718 return nullopt;
719 }
720 void DoWriteToFS_ (const GUID& id, const VariantValue& vv)
721 {
722 filesystem::path docFilePath = GetDocumentFilePath_ (id);
723#if USE_NOISY_TRACE_IN_THIS_MODULE_
724 TraceContextBumper ctx{"LocalDocumentDB::DirectoryFilesystemDatabaseRep_::DoWriteToFS", "path={}"_f, docFilePath};
725#endif
726 using namespace IO::FileSystem;
727 ThroughTmpFileWriter tmpFile{docFilePath};
728 IO::FileSystem::FileOutputStream::Ptr outStream = IO::FileSystem::FileOutputStream::New (tmpFile.GetFilePath ());
729 fDBRep_->fWriter_.Write (vv, outStream);
730 outStream.Close (); // close like this so we can throw exception - cannot throw if we count on DTOR
731#if qStroika_Foundation_Common_Platform_Windows
732 tmpFile.fRetryOnSharingViolationFor = fDBRep_->fRetryOnSharingViolationFor_;
733#endif
734 tmpFile.Commit (); // any exceptions cause the tmp file to be automatically cleaned up
735 }
736 };
737
738 struct MyTransactionRep_ final : Database::Document::Transaction::IRep {
739 virtual void Commit () override
740 {
741 // nothing todo
742 }
743 virtual void Rollback () override
744 {
746 }
747 virtual Disposition GetDisposition () const override
748 {
749 return Disposition::eCompleted;
750 }
751 };
752
753 filesystem::path GetCollectionFilePath_ (const String& collectionName) const
754 {
755 // @todo - in future - consider mapping name to URL-safe name
756 // @todo use PCTEncode2String
757 return fRoot_ / (collectionName.As<filesystem::path> ());
758 }
759
760 DirectoryFilesystemDatabaseRep_ () = delete;
761 DirectoryFilesystemDatabaseRep_ (const DirectoryFilesystemDatabaseRep_&) = delete;
762 DirectoryFilesystemDatabaseRep_ (const Document::LocalDocumentDB::Options& options,
763 const Document::LocalDocumentDB::Options::DirectoryFileStorage& dfOptions)
764 : fOptions_{options}
765 , fRoot_{dfOptions.fRoot}
766 , fReader_{get<DataExchange::Variant::Reader> (dfOptions.fSerialization)}
767 , fWriter_{get<DataExchange::Variant::Writer> (dfOptions.fSerialization)}
768#if qStroika_Foundation_Common_Platform_Windows
769 , fRetryOnSharingViolationFor_{dfOptions.fRetryOnSharingViolationFor}
770#endif
771 {
772 TraceContextBumper ctx{"LocalDocumentDB::DirectoryFilesystemDatabaseRep_::DirectoryFilesystemDatabaseRep_"};
773 filesystem::create_directories (fRoot_);
774 if (dfOptions.fForceCreateNew) {
775 for (const auto& entry : filesystem::directory_iterator{fRoot_}) {
776 filesystem::remove_all (entry.path ());
777 }
778 }
779 }
780 virtual shared_ptr<const EngineProperties> GetEngineProperties () const override
781 {
782 struct MyEngineProperties_ final : EngineProperties {
783 virtual String GetEngineName () const override
784 {
785 return "LocalDocumentDB.Folder"sv;
786 }
787 };
788 static const shared_ptr<const EngineProperties> kProps_ = Memory::MakeSharedPtr<const MyEngineProperties_> ();
789 return kProps_;
790 }
791 virtual Database::Document::Connection::Options GetOptions () const override
792 {
793 return fOptions_;
794 }
795 virtual uintmax_t GetSpaceConsumed () const override
796 {
797 uintmax_t totalSize{};
798 try {
799 for (const auto& entry : filesystem::recursive_directory_iterator (fRoot_, filesystem::directory_options::skip_permission_denied)) {
800 // Check if the current entry is a regular file before getting its size
801 if (filesystem::is_regular_file (entry.status ())) {
802 // file_size() throws an exception or returns (uintmax_t)-1 on error
803 std::uintmax_t file_size = entry.file_size ();
804 totalSize += file_size;
805 }
806 }
807 }
808 catch (const filesystem::filesystem_error& e) {
809 DbgTrace ("suppressing error in GetSpaceConsumed () = returning zero: {}"_f, e);
810 return 0;
811 }
812 return totalSize;
813 }
814 virtual Set<String> GetCollections () override
815 {
816 Set<String> result;
817 for (const auto& entry : filesystem::directory_iterator{fRoot_}) {
818 if (filesystem::is_directory (entry.path ())) { // Check if the entry is a directory
819 result += String{entry.path ().filename ()};
820 }
821 }
822 return result;
823 }
824 virtual Document::Collection::Ptr CreateCollection (const String& name) override
825 {
826 return WrapExecute_ (
827 [&] () {
828 filesystem::create_directories (GetCollectionFilePath_ (name));
829 return Document::Collection::Ptr{Memory::MakeSharedPtr<MyCollectionRep_> (
830 Debug::UncheckedDynamicPointerCast<DirectoryFilesystemDatabaseRep_> (shared_from_this ()), name)};
831 },
832 name, true);
833 }
834 virtual void DropCollection (const String& name) override
835 {
836 WrapExecute_ ([&] () { filesystem::remove_all (GetCollectionFilePath_ (name)); }, name, true);
837 }
838 virtual Document::Collection::Ptr GetCollection (const String& name) override
839 {
840 Require (GetCollections ().Contains (name));
841 return Document::Collection::Ptr{Memory::MakeSharedPtr<MyCollectionRep_> (
842 Debug::UncheckedDynamicPointerCast<DirectoryFilesystemDatabaseRep_> (shared_from_this ()), name)};
843 }
844 virtual Document::Transaction mkTransaction () override
845 {
846 return Document::Transaction{make_unique<MyTransactionRep_> ()};
847 }
848 virtual void Flush () override
849 {
850 // nothing todo - already flushed to FS on each operation
851 }
852 template <typename FUN>
853 inline auto WrapExecute_ (FUN&& f, const optional<String>& collectionName, bool write) -> invoke_result_t<FUN>
854 {
855 return Document::Connection::Private_::WrapLoggingExecuteHelper_ (forward<FUN> (f), this, fOptions_, collectionName, write);
856 }
857 };
858}
859
860/*
861 ********************************************************************************
862 *********************** SQL::LocalDocumentDB::New ******************************
863 ********************************************************************************
864 */
865auto Document::LocalDocumentDB::New (const Options& options) -> Ptr
866{
867 switch (options.fInternallySynchronizedLetter) {
868 case eInternallySynchronized:
869 if (get_if<Options::MemoryStorage> (&options.fStorage)) {
870 return Ptr{Memory::MakeSharedPtr<MemoryDatabaseRep_<eInternallySynchronized>> (options)};
871 }
872 else if (auto fop = get_if<Options::SingleFileStorage> (&options.fStorage)) {
873 return Ptr{Memory::MakeSharedPtr<SingleFileDatabaseRep_<eInternallySynchronized>> (options, *fop)};
874 }
875 else if (auto dop = get_if<Options::DirectoryFileStorage> (&options.fStorage)) {
876 return Ptr{Memory::MakeSharedPtr<DirectoryFilesystemDatabaseRep_<eInternallySynchronized>> (options, *dop)};
877 }
879 return nullptr;
880 case eNotKnownInternallySynchronized:
881 if (get_if<Options::MemoryStorage> (&options.fStorage)) {
882 return Ptr{Memory::MakeSharedPtr<MemoryDatabaseRep_<Execution::eNotKnownInternallySynchronized>> (options)};
883 }
884 else if (auto fop = get_if<Options::SingleFileStorage> (&options.fStorage)) {
885 return Ptr{Memory::MakeSharedPtr<SingleFileDatabaseRep_<eNotKnownInternallySynchronized>> (options, *fop)};
886 }
887 else if (auto dop = get_if<Options::DirectoryFileStorage> (&options.fStorage)) {
888 return Ptr{Memory::MakeSharedPtr<DirectoryFilesystemDatabaseRep_<eNotKnownInternallySynchronized>> (options, *dop)};
889 }
891 return nullptr;
892 default:
894 return nullptr;
895 }
896
898 return nullptr;
899}
#define AssertNotImplemented()
Definition Assertions.h:402
#define RequireNotReached()
Definition Assertions.h:386
function< void(Operation op, const Ptr &documentDBConnection, const optional< String > &collectionName, const exception_ptr &e)> OpertionCallbackPtr
#define qStroika_ATTRIBUTE_NO_UNIQUE_ADDRESS_VCFORCE
[[msvc::no_unique_address]] isn't always broken in MSVC. Annotate with this on things where its not b...
Definition StdCompat.h:445
#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
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 optional< mapped_type > Lookup(ArgByValueType< key_type > key) const
Definition Mapping.inl:142
nonvirtual mapped_type LookupChecked(ArgByValueType< key_type > key, const THROW_IF_MISSING &throwIfMissing) const
nonvirtual unsigned int AddAll(ITERABLE_OF_ADDABLE &&items, AddReplaceMode addReplaceMode=AddReplaceMode::eAddReplaces)
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 RemoveAll()
RemoveAll removes all, or all matching (predicate, iterator range, equals comparer or whatever) items...
Definition Mapping.inl:240
nonvirtual void Remove(ArgByValueType< key_type > key)
Remove the given item (which must exist).
Definition Mapping.inl:223
nonvirtual Iterable< key_type > Keys() const
Definition Mapping.inl:111
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.
abstract class specifying interface for readers that map a source like XML or JSON to a VariantValue ...
abstract class specifying interface for writers VariantValue objects to serialized formats like JSON,...
nonvirtual void Write(const VariantValue &v, const Streams::OutputStream::Ptr< byte > &out) const
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
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.
static GUID GenerateNew() noexcept
Definition GUID.cpp:76
Execution::InternallySynchronized fInternallySynchronizedLetter
use eInternallySynchronized to make letter internally synchronized