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 if (not sfOptions.fForceCreateNew) {
433 DoReadFromFS ();
434 }
435 }
436 virtual ~SingleFileDatabaseRep_ () override
437 {
438 if (fDirty_) {
439 DoWriteToFS ();
440 }
441 }
442 virtual shared_ptr<const EngineProperties> GetEngineProperties () const override
443 {
444 struct MyEngineProperties_ final : EngineProperties {
445 virtual String GetEngineName () const override
446 {
447 return "LocalDocumentDB.SingleFile"sv;
448 }
449 };
450 static const shared_ptr<const EngineProperties> kProps_ = Memory::MakeSharedPtr<const MyEngineProperties_> ();
451 return kProps_;
452 }
453 virtual Database::Document::Connection::Options GetOptions () const override
454 {
455 scoped_lock declareContext{fMemoryDB_->fMaybeLock_};
456 Database::Document::Connection::Options o = fMemoryDB_->GetOptions ();
457 o.fOperationLoggingCallback = fOperationLoggingCallback_;
458 return o;
459 }
460 virtual uintmax_t GetSpaceConsumed () const override
461 {
462 if (fDirty_) {
463 IgnoreExceptionsExceptThreadAbortForCall (const_cast<SingleFileDatabaseRep_*> (this)->DoWriteToFS ()); // cannot get size otherwise
464 }
465 error_code ignoredEC;
466 return filesystem::file_size (fExternalFile_, ignoredEC);
467 }
468 virtual Set<String> GetCollections () override
469 {
470 return fMemoryDB_->GetCollections ();
471 }
472 virtual Document::Collection::Ptr CreateCollection (const String& name) override
473 {
474 scoped_lock declareContext{fMemoryDB_->fMaybeLock_};
475 return WrapExecute_ (
476 [&] () {
477 fMemoryDB_->CreateCollection (name);
478 DataChangedSoMaybeWrite2Disk ();
479 return GetCollection (name);
480 },
481 name, true);
482 }
483 virtual void DropCollection (const String& name) override
484 {
485 scoped_lock declareContext{fMemoryDB_->fMaybeLock_};
486 WrapExecute_ (
487 [&] () {
488 fMemoryDB_->DropCollection (name);
489 DataChangedSoMaybeWrite2Disk ();
490 },
491 name, true);
492 }
493 virtual Document::Collection::Ptr GetCollection (const String& name) override
494 {
495 scoped_lock declareContext{fMemoryDB_->fMaybeLock_};
496 Document::Collection::Ptr memDBCollection = fMemoryDB_->GetCollection (name);
497 return Document::Collection::Ptr{Memory::MakeSharedPtr<MyCollectionRep_> (
498 Debug::UncheckedDynamicPointerCast<SingleFileDatabaseRep_> (shared_from_this ()), name, memDBCollection)};
499 }
500 virtual Document::Transaction mkTransaction () override
501 {
502 return Document::Transaction{make_unique<MyTransactionRep_> ()};
503 }
504 virtual void Flush () override
505 {
506 DoWriteToFS ();
507 }
508 void DoReadFromFS ()
509 {
510 using namespace IO::FileSystem;
511#if USE_NOISY_TRACE_IN_THIS_MODULE_
512 TraceContextBumper ctx{"LocalDocumentDB::SingleFileDatabaseRep_::DoReadFromFS", "path={}"_f, fExternalFile_};
513#endif
514 scoped_lock declareContext{fMemoryDB_->fMaybeLock_};
515 if (filesystem::exists (fExternalFile_)) {
516 fMemoryDB_->fCollections_.clear ();
517 for (KeyValuePair<String, VariantValue> collectionAndDocument :
518 fReader_.Read (FileInputStream::New (fExternalFile_)).template As<Mapping<String, VariantValue>> ()) {
519 fMemoryDB_->fCollections_.Add (
520 collectionAndDocument.fKey,
521 collectionAndDocument.fValue.As<Mapping<String, VariantValue>> ().template Map<Mapping<GUID, Document::Document>> (
523 return {GUID{kvp.fKey}, kvp.fValue.As<Document::Document> ()};
524 }));
525 }
526 }
527 }
528 void DataChangedSoMaybeWrite2Disk ()
529 {
530 if (fFlushOnEachWrite_) {
531 DoWriteToFS ();
532 }
533 else {
534 fDirty_ = true;
535 }
536 }
537 void DoWriteToFS ()
538 {
539#if USE_NOISY_TRACE_IN_THIS_MODULE_
540 TraceContextBumper ctx{"LocalDocumentDB::SingleFileDatabaseRep_::DoWriteToFS", "path={}"_f, fExternalFile_};
541#endif
542 if (not this->fReadOnly_) {
543 scoped_lock declareContext{fMemoryDB_->fMaybeLock_};
544 using namespace IO::FileSystem;
545 ThroughTmpFileWriter tmpFile{fExternalFile_};
546 IO::FileSystem::FileOutputStream::Ptr outStream = IO::FileSystem::FileOutputStream::New (tmpFile.GetFilePath ());
547 Mapping<String, VariantValue> collectionsAsVV;
548 for (const KeyValuePair<String, Mapping<GUID, Document::Document>>& collection : fMemoryDB_->fCollections_) {
549 Mapping<GUID, Document::Document> collectionValue = collection.fValue;
550 Mapping<String, VariantValue> collWithStringKey;
551 for (const KeyValuePair<GUID, Document::Document>& kvp : collectionValue) {
552 collWithStringKey.Add (kvp.fKey.ToString (), VariantValue{kvp.fValue});
553 }
554 collectionsAsVV.Add (collection.fKey, VariantValue{collWithStringKey});
555 }
556 this->fWriter_.Write (VariantValue{collectionsAsVV}, outStream);
557 outStream.Close (); // close like this so we can throw exception - cannot throw if we count on DTOR
558#if qStroika_Foundation_Common_Platform_Windows
559 tmpFile.fRetryOnSharingViolationFor = fRetryOnSharingViolationFor_;
560#endif
561 tmpFile.Commit (); // any exceptions cause the tmp file to be automatically cleaned up
562 }
563 fDirty_ = false;
564 }
565 template <typename FUN>
566 inline auto WrapExecute_ (FUN&& f, const optional<String>& collectionName, bool write) -> invoke_result_t<FUN>
567 {
568 if (fOperationLoggingCallback_) {
569 Database::Document::Connection::Options o = fMemoryDB_->GetOptions ();
570 o.fOperationLoggingCallback = fOperationLoggingCallback_;
571 return Document::Connection::Private_::WrapLoggingExecuteHelper_ (forward<FUN> (f), this, o, collectionName, write);
572 }
573 else {
574 return f ();
575 }
576 }
577 };
578
579 // Store each collection in a folder under the root folder
580 // Mostly intrinsically internally synchronized, but maybe could use locks here. Corner cases?
581 // like update, while adds happening. Won't cause CORRUPTION, but unclear what guarantees we want
582 // to offer about what completes before what?
583 template <InternallySynchronized SYNC_STYLE>
584 struct DirectoryFilesystemDatabaseRep_ final : Database::Document::LocalDocumentDB::IRep {
586 const filesystem::path fRoot_;
587 const DataExchange::Variant::Reader fReader_;
588 const DataExchange::Variant::Writer fWriter_;
589#if qStroika_Foundation_Common_Platform_Windows
590 const optional<Time::DurationSeconds> fRetryOnSharingViolationFor_;
591#endif
592
593 struct MyCollectionRep_ final : Document::Collection::IRep {
594 const shared_ptr<DirectoryFilesystemDatabaseRep_> fDBRep_; // save to bump reference count (lifetime safety)
595 const String fName_;
596 const filesystem::path fCollectionRoot_;
597
598 MyCollectionRep_ (const shared_ptr<DirectoryFilesystemDatabaseRep_>& dbRep, const String& name)
599 : fDBRep_{dbRep}
600 , fName_{name}
601 , fCollectionRoot_{dbRep->GetCollectionFilePath_ (name)}
602 {
603 }
604 virtual String GetName () const override
605 {
606 return fName_;
607 }
608 virtual IDType Add (const Document::Document& v) override
609 {
610#if USE_NOISY_TRACE_IN_THIS_MODULE_
611 TraceContextBumper ctx{"LocalDocumentDB::DirectoryFilesystemDatabaseRep_::MyCollectionRep_::Add"};
612#endif
613 optional<VariantValue> vID = v.Lookup (Document::kID);
614 Require (not vID.has_value () or fDBRep_->fOptions_.fAddAllowsExternallySpecifiedIDs);
615 GUID id = vID.has_value () ? GUID{vID->As<String> ()} : GUID::GenerateNew ();
616 fDBRep_->WrapExecute_ (
617 [&] () {
618 Document::Document doc2Add = v;
619 if (vID) {
620 doc2Add.Remove (Document::kID); // already in parent KEY so don't store redundantly
621 }
622 DoWriteToFS_ (id, VariantValue{doc2Add});
623 },
624 fName_, true);
625 return id.As<IDType> ();
626 }
627 virtual optional<Document::Document> Get (const IDType& id, const optional<Projection>& projection) override
628 {
629#if USE_NOISY_TRACE_IN_THIS_MODULE_
630 TraceContextBumper ctx{"LocalDocumentDB::DirectoryFilesystemDatabaseRep_::MyCollectionRep_::Get"};
631#endif
632 return fDBRep_->WrapExecute_ (
633 [&] () -> optional<Document::Document> {
634 if (auto od = DoReadFromFS_ (GUID{id})) {
635 Document::Document d = *od;
636 d.Add (Document::kID, id);
637 if (projection) {
638 d = projection->Apply (d);
639 }
640 return d;
641 }
642 return nullopt;
643 },
644 fName_, false);
645 }
646 virtual Sequence<Document::Document> GetAll (const optional<Filter>& filter, const optional<Projection>& projection) override
647 {
648#if USE_NOISY_TRACE_IN_THIS_MODULE_
649 TraceContextBumper ctx{"LocalDocumentDB::DirectoryFilesystemDatabaseRep_::MyCollectionRep_::GetAll",
650 "filter={}, projection={}"_f, filter, projection};
651#endif
652 return fDBRep_->WrapExecute_ (
653 [&] () {
655 for (const auto& entry : filesystem::directory_iterator{fCollectionRoot_}) {
656 if (entry.path ().extension () == ".json"sv) { // Check if the entry is a JSON file
658 fDBRep_->fReader_.Read (IO::FileSystem::FileInputStream::New (entry.path ())).template As<Document::Document> ();
659 d.Add (Document::kID, entry.path ().stem ().string ());
660 if (not filter or filter->Matches (d)) {
661 if (projection) {
662 d = projection->Apply (d);
663 }
664 result += d;
665 }
666 }
667 }
668 return result;
669 },
670 fName_, false);
671 }
672 virtual void Update (const IDType& id, const Document::Document& newV, const optional<Set<String>>& onlyTheseFields) override
673 {
674#if USE_NOISY_TRACE_IN_THIS_MODULE_
675 TraceContextBumper ctx{"LocalDocumentDB::DirectoryFilesystemDatabaseRep_::MyCollectionRep_::Update",
676 "id={},newV={}, onlyTheseFields={}"_f, id, newV, onlyTheseFields};
677#endif
678 fDBRep_->WrapExecute_ (
679 [&] () {
680 Document::Document updatedDoc =
681 onlyTheseFields ? Memory::ValueOfOrThrow (DoReadFromFS_ (id), RuntimeErrorException{"no such id"sv}) : newV;
682 Document::Document updateWithDoc = newV;
683 if (onlyTheseFields) {
684 updateWithDoc.RetainAll (*onlyTheseFields);
685 }
686 updatedDoc.AddAll (updateWithDoc);
687 if (onlyTheseFields) {
688 // any fields listed in onlyTheseFields, but not present in newV need to be removed
689 Set<String> removeMe = *onlyTheseFields - newV.Keys ();
690 updatedDoc.RemoveAll (removeMe);
691 }
692 DoWriteToFS_ (GUID{id}, VariantValue{updatedDoc});
693 },
694 fName_, true);
695 }
696 virtual void Remove (const IDType& id) override
697 {
698#if USE_NOISY_TRACE_IN_THIS_MODULE_
699 TraceContextBumper ctx{"LocalDocumentDB::DirectoryFilesystemDatabaseRep_::MyCollectionRep_::Remove", "id={}"_f, id};
700#endif
701 fDBRep_->WrapExecute_ ([&] () { (void)filesystem::remove (GetDocumentFilePath_ (GUID{id})); }, fName_, true);
702 }
703 filesystem::path GetDocumentFilePath_ (const GUID& id) const
704 {
705 return fCollectionRoot_ / (id.As<String> () + ".json"sv).As<filesystem::path> ();
706 }
707 optional<Document::Document> DoReadFromFS_ (const GUID& id)
708 {
709 using namespace IO::FileSystem;
710 filesystem::path docFilePath = GetDocumentFilePath_ (id);
711#if USE_NOISY_TRACE_IN_THIS_MODULE_
712 TraceContextBumper ctx{"LocalDocumentDB::DirectoryFilesystemDatabaseRep_::DoReadFromFS", "path={}"_f, docFilePath};
713#endif
714 if (filesystem::exists (docFilePath)) {
715 return fDBRep_->fReader_.Read (FileInputStream::New (docFilePath)).template As<Document::Document> ();
716 }
717 return nullopt;
718 }
719 void DoWriteToFS_ (const GUID& id, const VariantValue& vv)
720 {
721 filesystem::path docFilePath = GetDocumentFilePath_ (id);
722#if USE_NOISY_TRACE_IN_THIS_MODULE_
723 TraceContextBumper ctx{"LocalDocumentDB::DirectoryFilesystemDatabaseRep_::DoWriteToFS", "path={}"_f, docFilePath};
724#endif
725 using namespace IO::FileSystem;
726 ThroughTmpFileWriter tmpFile{docFilePath};
727 IO::FileSystem::FileOutputStream::Ptr outStream = IO::FileSystem::FileOutputStream::New (tmpFile.GetFilePath ());
728 fDBRep_->fWriter_.Write (vv, outStream);
729 outStream.Close (); // close like this so we can throw exception - cannot throw if we count on DTOR
730#if qStroika_Foundation_Common_Platform_Windows
731 tmpFile.fRetryOnSharingViolationFor = fDBRep_->fRetryOnSharingViolationFor_;
732#endif
733 tmpFile.Commit (); // any exceptions cause the tmp file to be automatically cleaned up
734 }
735 };
736
737 struct MyTransactionRep_ final : Database::Document::Transaction::IRep {
738 virtual void Commit () override
739 {
740 // nothing todo
741 }
742 virtual void Rollback () override
743 {
745 }
746 virtual Disposition GetDisposition () const override
747 {
748 return Disposition::eCompleted;
749 }
750 };
751
752 filesystem::path GetCollectionFilePath_ (const String& collectionName) const
753 {
754 // @todo - in future - consider mapping name to URL-safe name
755 // @todo use PCTEncode2String
756 return fRoot_ / (collectionName.As<filesystem::path> ());
757 }
758
759 DirectoryFilesystemDatabaseRep_ () = delete;
760 DirectoryFilesystemDatabaseRep_ (const DirectoryFilesystemDatabaseRep_&) = delete;
761 DirectoryFilesystemDatabaseRep_ (const Document::LocalDocumentDB::Options& options,
762 const Document::LocalDocumentDB::Options::DirectoryFileStorage& dfOptions)
763 : fOptions_{options}
764 , fRoot_{dfOptions.fRoot}
765 , fReader_{get<DataExchange::Variant::Reader> (dfOptions.fSerialization)}
766 , fWriter_{get<DataExchange::Variant::Writer> (dfOptions.fSerialization)}
767#if qStroika_Foundation_Common_Platform_Windows
768 , fRetryOnSharingViolationFor_{dfOptions.fRetryOnSharingViolationFor}
769#endif
770 {
771 filesystem::create_directories (fRoot_);
772 }
773 virtual shared_ptr<const EngineProperties> GetEngineProperties () const override
774 {
775 struct MyEngineProperties_ final : EngineProperties {
776 virtual String GetEngineName () const override
777 {
778 return "LocalDocumentDB.Folder"sv;
779 }
780 };
781 static const shared_ptr<const EngineProperties> kProps_ = Memory::MakeSharedPtr<const MyEngineProperties_> ();
782 return kProps_;
783 }
784 virtual Database::Document::Connection::Options GetOptions () const override
785 {
786 return fOptions_;
787 }
788 virtual uintmax_t GetSpaceConsumed () const override
789 {
790 uintmax_t totalSize{};
791 try {
792 for (const auto& entry : filesystem::recursive_directory_iterator (fRoot_, filesystem::directory_options::skip_permission_denied)) {
793 // Check if the current entry is a regular file before getting its size
794 if (filesystem::is_regular_file (entry.status ())) {
795 // file_size() throws an exception or returns (uintmax_t)-1 on error
796 std::uintmax_t file_size = entry.file_size ();
797 totalSize += file_size;
798 }
799 }
800 }
801 catch (const filesystem::filesystem_error& e) {
802 DbgTrace ("suppressing error in GetSpaceConsumed () = returning zero: {}"_f, e);
803 return 0;
804 }
805 return totalSize;
806 }
807 virtual Set<String> GetCollections () override
808 {
809 Set<String> result;
810 for (const auto& entry : filesystem::directory_iterator{fRoot_}) {
811 if (filesystem::is_directory (entry.path ())) { // Check if the entry is a directory
812 result += String{entry.path ().filename ()};
813 }
814 }
815 return result;
816 }
817 virtual Document::Collection::Ptr CreateCollection (const String& name) override
818 {
819 return WrapExecute_ (
820 [&] () {
821 filesystem::create_directories (GetCollectionFilePath_ (name));
822 return Document::Collection::Ptr{Memory::MakeSharedPtr<MyCollectionRep_> (
823 Debug::UncheckedDynamicPointerCast<DirectoryFilesystemDatabaseRep_> (shared_from_this ()), name)};
824 },
825 name, true);
826 }
827 virtual void DropCollection (const String& name) override
828 {
829 WrapExecute_ ([&] () { filesystem::remove_all (GetCollectionFilePath_ (name)); }, name, true);
830 }
831 virtual Document::Collection::Ptr GetCollection (const String& name) override
832 {
833 Require (GetCollections ().Contains (name));
834 return Document::Collection::Ptr{Memory::MakeSharedPtr<MyCollectionRep_> (
835 Debug::UncheckedDynamicPointerCast<DirectoryFilesystemDatabaseRep_> (shared_from_this ()), name)};
836 }
837 virtual Document::Transaction mkTransaction () override
838 {
839 return Document::Transaction{make_unique<MyTransactionRep_> ()};
840 }
841 virtual void Flush () override
842 {
843 // nothing todo - already flushed to FS on each operation
844 }
845 template <typename FUN>
846 inline auto WrapExecute_ (FUN&& f, const optional<String>& collectionName, bool write) -> invoke_result_t<FUN>
847 {
848 return Document::Connection::Private_::WrapLoggingExecuteHelper_ (forward<FUN> (f), this, fOptions_, collectionName, write);
849 }
850 };
851}
852
853/*
854 ********************************************************************************
855 *********************** SQL::LocalDocumentDB::New ******************************
856 ********************************************************************************
857 */
858auto Document::LocalDocumentDB::New (const Options& options) -> Ptr
859{
860 switch (options.fInternallySynchronizedLetter) {
861 case eInternallySynchronized:
862 if (get_if<Options::MemoryStorage> (&options.fStorage)) {
863 return Ptr{Memory::MakeSharedPtr<MemoryDatabaseRep_<eInternallySynchronized>> (options)};
864 }
865 else if (auto fop = get_if<Options::SingleFileStorage> (&options.fStorage)) {
866 return Ptr{Memory::MakeSharedPtr<SingleFileDatabaseRep_<eInternallySynchronized>> (options, *fop)};
867 }
868 else if (auto dop = get_if<Options::DirectoryFileStorage> (&options.fStorage)) {
869 return Ptr{Memory::MakeSharedPtr<DirectoryFilesystemDatabaseRep_<eInternallySynchronized>> (options, *dop)};
870 }
872 return nullptr;
873 case eNotKnownInternallySynchronized:
874 if (get_if<Options::MemoryStorage> (&options.fStorage)) {
875 return Ptr{Memory::MakeSharedPtr<MemoryDatabaseRep_<Execution::eNotKnownInternallySynchronized>> (options)};
876 }
877 else if (auto fop = get_if<Options::SingleFileStorage> (&options.fStorage)) {
878 return Ptr{Memory::MakeSharedPtr<SingleFileDatabaseRep_<eNotKnownInternallySynchronized>> (options, *fop)};
879 }
880 else if (auto dop = get_if<Options::DirectoryFileStorage> (&options.fStorage)) {
881 return Ptr{Memory::MakeSharedPtr<DirectoryFilesystemDatabaseRep_<eNotKnownInternallySynchronized>> (options, *dop)};
882 }
884 return nullptr;
885 default:
887 return nullptr;
888 }
889
891 return nullptr;
892}
#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