#pragma once #include "./lmdb-safe.hh" #include #include #include #include namespace LMDBSafe { /*! * \brief The type used to store IDs. "0" indicates "no such ID". */ using IDType = std::uint32_t; /*! * \brief Converts \a t to an std::string. * * This is the serialization interface. You need to define this function for the * types you'd like to store. */ template std::string serToString(const T &t); /*! * \brief Initializes \a ret from \a str. * * This is the deserialization interface. You need to define this function for the * types you'd like to store. */ template void serFromString(string_view str, T &ret); /// \cond /// Define some "shortcuts" (to avoid full-blown serialization stuff for trivial cases): template <> inline std::string serToString(const std::string_view &t) { return std::string(t); } template <> inline std::string serToString(const std::string &t) { return t; } template <> inline std::string serToString(const std::uint8_t &t) { return std::string(reinterpret_cast(&t), sizeof(t)); } template <> inline std::string serToString(const std::uint16_t &t) { auto str = std::string(sizeof(t), '\0'); CppUtilities::LE::getBytes(t, str.data()); return str; } template <> inline std::string serToString(const std::uint32_t &t) { auto str = std::string(sizeof(t), '\0'); CppUtilities::LE::getBytes(t, str.data()); return str; } template <> inline std::string serToString(const std::uint64_t &t) { auto str = std::string(sizeof(t), '\0'); CppUtilities::LE::getBytes(t, str.data()); return str; } template <> inline void serFromString(string_view str, std::string &ret) { ret = std::string(str); } template <> inline void serFromString<>(string_view str, std::uint8_t &ret) { if (str.size() != sizeof(ret)) { throw CppUtilities::ConversionException(CppUtilities::argsToString("value not 8-bit, got ", str.size(), " bytes instead")); } ret = static_cast(*str.data()); } template <> inline void serFromString<>(string_view str, std::uint16_t &ret) { if (str.size() != sizeof(ret)) { throw CppUtilities::ConversionException(CppUtilities::argsToString("value not 16-bit, got ", str.size(), " bytes instead")); } ret = CppUtilities::LE::toUInt16(str.data()); } template <> inline void serFromString<>(string_view str, std::uint32_t &ret) { if (str.size() != sizeof(ret)) { throw CppUtilities::ConversionException(CppUtilities::argsToString("value not 32-bit, got ", str.size(), " bytes instead")); } ret = CppUtilities::LE::toUInt32(str.data()); } template <> inline void serFromString<>(string_view str, std::uint64_t &ret) { if (str.size() != sizeof(ret)) { throw CppUtilities::ConversionException(CppUtilities::argsToString("value not 64-bit, got ", str.size(), " bytes instead")); } ret = CppUtilities::LE::toUInt64(str.data()); } /// \endcond /*! * \brief Converts \a t to an std::string. * * This is the serialization interface for keys. You need to define this function * for the types you'd like to use as keys. */ template inline std::string keyConv(const T &t); template ::value, T>::type * = nullptr> inline string_view keyConv(const T &t) { return string_view(reinterpret_cast(&t), sizeof(t)); } /// \cond /// Define keyConv for trivial cases: template ::value, T>::type * = nullptr> inline string_view keyConv(const T &t) { return t; } template ::value, T>::type * = nullptr> inline string_view keyConv(string_view t) { return t; } /// \endcond /*! * \brief The LMDBIndexOps struct implements index operations, but only the operations that * are broadcast to all indexes. * * Specifically, to deal with databases with less than the maximum number of interfaces, this * only includes calls that should be ignored for empty indexes. * * This class only needs methods that must happen for all indexes at once. So specifically, *not* * size or get. People ask for those themselves, and should not do that on indexes that * don't exist. */ template struct LMDB_SAFE_EXPORT LMDBIndexOps { explicit LMDBIndexOps(Parent *parent) : d_parent(parent) { } void put(MDBRWTransaction &txn, const Class &t, IDType id, unsigned int flags = 0) { txn->put(d_idx, keyConv(d_parent->getMember(t)), id, flags); } void del(MDBRWTransaction &txn, const Class &t, IDType id) { const auto rc = txn->del(d_idx, keyConv(d_parent->getMember(t)), id); if (rc && rc != MDB_NOTFOUND) { throw LMDBError("Error deleting from index: ", rc); } } void clear(MDBRWTransaction &txn) { if (const auto rc = mdb_drop(*txn, d_idx, 0)) { throw LMDBError("Error clearing index: ", rc); } } void openDB(std::shared_ptr &env, string_view str, unsigned int flags) { d_idx = env->openDB(str, flags); } MDBDbi d_idx; Parent *d_parent; }; /*! * \brief The index_on struct is used to declare an index on a member variable of a particular class. */ template struct index_on : LMDBIndexOps> { index_on() : LMDBIndexOps>(this) { } static Type getMember(const Class &c) { return c.*PtrToMember; } typedef Type type; }; /*! * \brief The index_on_base_member struct is used to declare an index on a member variable of a base class of a particular class. */ template struct index_on_base_member : LMDBIndexOps> { index_on_base_member() : LMDBIndexOps>(this) { } static Type getMember(const BaseClass &c) { return c.*PtrToMember; } typedef Type type; }; /*! * \brief The index_on_function struct is used to declare an index which is dynamically computed via * a function. * \remarks This struct makes likely not much sense in its current form. */ template struct index_on_function : LMDBIndexOps> { index_on_function() : LMDBIndexOps>(this) { } static Type getMember(const Class &c) { Func f; return f(c); } typedef Type type; }; /*! * \brief The TypedDBI class is the main class. * \tparam T Specifies the type to store within the database. * \tparam I Specifies an index, should be an instantiation of index_on. * * Open issues: * - Perhaps use the separate index concept from multi_index. * - Perhaps get either to be of same type so for(auto& a : x) works * make it more value "like" with unique_ptr. */ template class LMDB_SAFE_EXPORT TypedDBI { public: // declare tuple for indexes using tuple_t = std::tuple; template using index_t = typename std::tuple_element_t::type; private: tuple_t d_tuple; /// \cond template struct IndexIterator { static inline void apply(Tuple &tuple, auto &&func) { IndexIterator::apply(tuple, std::forward(func)); func(std::get(tuple)); } }; template struct IndexIterator { static inline void apply(Tuple &tuple, auto &&func) { func(std::get<0>(tuple)); } }; template struct IndexIterator { static inline void apply(Tuple &tuple, auto &&func) { CPP_UTILITIES_UNUSED(tuple) CPP_UTILITIES_UNUSED(func) } }; void forEachIndex(auto &&func) { IndexIterator>::apply(d_tuple, std::forward(func)); } /// \endcond public: TypedDBI(std::shared_ptr env, string_view name) : d_env(env) , d_name(name) { d_main = d_env->openDB(name, MDB_CREATE | MDB_INTEGERKEY); std::size_t index = 0; forEachIndex([&](auto &&i) { i.openDB(d_env, CppUtilities::argsToString(name, '_', index++), MDB_CREATE | MDB_DUPFIXED | MDB_DUPSORT); }); } /*! * \brief The ReadonlyOperations struct defines read-only operations. */ template struct ReadonlyOperations { ReadonlyOperations(Parent &parent) : d_parent(parent) { } //! Number of entries in main database std::size_t size() { MDB_stat stat; mdb_stat(**d_parent.d_txn, d_parent.d_parent->d_main, &stat); return stat.ms_entries; } //! Number of entries in the various indexes - should be the same template std::size_t size() { MDB_stat stat; mdb_stat(**d_parent.d_txn, std::get(d_parent.d_parent->d_tuple).d_idx, &stat); return stat.ms_entries; } /*! * \brief Returns the highest ID or 0 if the database is empty. */ IDType maxID() { auto cursor = (*d_parent.d_txn)->getCursor(d_parent.d_parent->d_main); MDBOutVal idval, maxcontent; auto id = IDType(0); if (!cursor.get(idval, maxcontent, MDB_LAST)) { id = idval.get(); } return id; } /*! * \brief Returns the next highest ID in the database. * \remarks Never returns 0 so it can be used as special "no such ID" value. * \throws Throws LMDBError when running out of IDs. */ IDType nextID() { const auto id = maxID(); if (id < std::numeric_limits::max()) { return id + 1; } throw LMDBError("Running out of IDs"); } /*! * \brief Returns an ID not used in the database so far. * \remarks * - Lower IDs are reused but an extensive search for "gabs" is avoided. * - Never returns 0 so it can be used as special "no such ID" value. * \throws Throws LMDBError when running out of IDs. */ IDType newID() { auto cursor = (*d_parent.d_txn)->getCursor(d_parent.d_parent->d_main); MDBOutVal idval, maxcontent; auto id = IDType(1); if (!cursor.get(idval, maxcontent, MDB_FIRST)) { id = idval.get(); } if (id > 1) { return id - 1; } if (!cursor.get(idval, maxcontent, MDB_LAST)) { id = idval.get(); } else { id = 0; } if (id < std::numeric_limits::max()) { return id + 1; } throw LMDBError("Running out of IDs"); } //! Get item with id, from main table directly template bool get(IDType id, ElementType &t) { auto data = MDBOutVal(); if ((*d_parent.d_txn)->get(d_parent.d_parent->d_main, id, data)) { return false; } serFromString(data.get(), t); return true; } //! Get item through index N, then via the main database template IDType get(const index_t &key, ElementType &out) { auto idValue = MDBOutVal(); if (!(*d_parent.d_txn)->get(std::get(d_parent.d_parent->d_tuple).d_idx, keyConv(key), idValue)) { const auto id = idValue.get(); if (get(id, out)) { return id; } } return 0; } //! Cardinality of index N template IDType cardinality() { auto cursor = (*d_parent.d_txn)->getCursor(std::get(d_parent.d_parent->d_tuple).d_idx); bool first = true; MDBOutVal key, data; IDType count = 0; while (!cursor.get(key, data, first ? MDB_FIRST : MDB_NEXT_NODUP)) { ++count; first = false; } return count; } /*! * \brief The eiter_t struct is the end iterator. */ struct eiter_t {}; //! Store the object as immediate member of iter_t (as opposed to using an std::unique_ptr or std::shared_ptr) template struct DirectStorage {}; /*! * \brief The iter_t struct is the iterator type for walking through the database rows. * \remarks * - The iterator can be on the main database or on an index. It returns the data directly on * the main database and indirectly when on an index. * - An iterator can be limited to one key or iterate over the entire database. * - The iter_t struct requires you to put the cursor in the right place first. * - The object can be stored as direct member of iter_t or as std::unique_ptr or std::shared_ptr * by specifying the corresponding template as \tp ElementType. The pointer can then be accessed * via getPointer(). Note that the returned pointer object is re-used when the iterator is incremented * or decremented unless the owned object is moved into another pointer object. */ template