/* * LMDB Abstraction Layer. * Copyright (C) 2023 Yury Gubich * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #ifndef LMDBAL_STORAGE_HPP #define LMDBAL_STORAGE_HPP #include "storage.h" #include "exceptions.h" #define UNUSED(x) (void)(x) /** * \class LMDBAL::Storage * \brief This is a basic key value storage. * * \tparam K type of the keys of the storage * \tparam V type of the values of the storage * * You can receive an instance of this class calling LMDBAL::Base::addStorage(const std::string&) * if the database is yet closed and you're defining the storages you're going to need. * Or you can call LMDBAL::Base::getStorage(const std::string&) if you didn't save a pointer to the storage at first * * You are not supposed to instantiate or destory instances of this class yourself! */ /** * \brief Creates a storage * * \param[in] parent - LMDBAL::Base pointer for the owning database (borrowed) * \param[in] name - the name of the storage * \param[in] duplicates - true if storage supports duplicates, false otherwise (false by default) */ template LMDBAL::Storage::Storage(Base* parent, const std::string& name, bool duplicates): iStorage(parent, name, duplicates), keySerializer(), valueSerializer(), cursors() {} /** * \brief Destroys a storage */ template LMDBAL::Storage::~Storage() { for (Cursor* cursor : cursors) delete cursor; } /** * \brief Adds a key-value record to the storage * * Take a note that if the storage already had a record you want to add LMDBAL::Exist is thrown * * \param[in] key key of the record * \param[in] value value of the record * * \exception LMDBAL::Exist thrown if the storage already has a record with the given key * \exception LMDBAL::Closed thrown if the database was not opened * \exception LMDBAL::Unknown thrown if something unexpected happend within lmdb */ template void LMDBAL::Storage::addRecord(const K& key, const V& value) { ensureOpened(addRecordMethodName); TransactionID txn = beginTransaction(); try { Storage::addRecord(key, value, txn); } catch (...) { abortTransaction(txn); throw; } commitTransaction(txn); } /** * \brief Adds a key-value record to the storage (transaction variant) * * This function schedules an addition of a key-value record, but doesn't immidiately adds it. * You can obtain LMDBAL::TransactionID calling LMDBAL::Base::beginTransaction(). * * Take a note that if the storage already had a record you want to add LMDBAL::Exist is thrown * * \param[in] key key of the record * \param[in] value value of the record * \param[in] txn transaction ID, needs to be a writable transaction! * * \exception LMDBAL::Exist thrown if the storage already has a record with the given key * \exception LMDBAL::Closed thrown if the database was not opened * \exception LMDBAL::Unknown thrown if something unexpected happend within lmdb */ template void LMDBAL::Storage::addRecord(const K& key, const V& value, TransactionID txn) { ensureOpened(addRecordMethodName); MDB_val lmdbKey = keySerializer.setData(key); MDB_val lmdbData = valueSerializer.setData(value); int rc = mdb_put(txn, dbi, &lmdbKey, &lmdbData, MDB_NOOVERWRITE); if (rc != MDB_SUCCESS) throwDuplicateOrUnknown(rc, toString(key)); } /** * \brief Adds a key-value record to the storage, overwrites if it already exists * \param[in] key key of the record * \param[in] value value of the record * \returns true if the record was added, false otherwise * * \exception LMDBAL::Closed thrown if the database was not opened * \exception LMDBAL::Unknown thrown if something unexpected happend within lmdb */ template bool LMDBAL::Storage::forceRecord(const K& key, const V& value) { ensureOpened(forceRecordMethodName); TransactionID txn = beginTransaction(); bool added; try { added = Storage::forceRecord(key, value, txn); } catch (...) { abortTransaction(txn); throw; } commitTransaction(txn); return added; } /** * \brief Adds a key-value record to the storage, overwrites if it already exists (transaction variant) * * This function schedules an addition of a key-value record, but doesn't immidiately adds it. * You can obtain LMDBAL::TransactionID calling LMDBAL::Base::beginTransaction(). * If the record did already exist in the database the actual overwrite will be done only after calling LMDBAL::Base::commitTransaction(). * * \param[in] key key of the record * \param[in] value value of the record * \param[in] txn transaction ID, needs to be a writable transaction! * \returns true if the record was added, false otherwise * * \exception LMDBAL::Closed thrown if the database was not opened * \exception LMDBAL::Unknown thrown if something unexpected happend within lmdb */ template bool LMDBAL::Storage::forceRecord(const K& key, const V& value, TransactionID txn) { ensureOpened(forceRecordMethodName); bool added; MDB_val lmdbKey = keySerializer.setData(key); MDB_val lmdbData; int rc = mdb_get(txn, dbi, &lmdbKey, &lmdbData); switch (rc) { case MDB_SUCCESS: added = false; break; case MDB_NOTFOUND: added = true; break; default: added = false; throwUnknown(rc); } lmdbData = valueSerializer.setData(value); rc = mdb_put(txn, dbi, &lmdbKey, &lmdbData, 0); if (rc != MDB_SUCCESS) throwUnknown(rc); return added; } /** * \brief Changes key-value record to the storage. * * Take a note that if the storage didn't have a record you want to change LMDBAL::NotFound is thrown * * \param[in] key key of the record * \param[in] value new value of the record * * \exception LMDBAL::NotFound thrown if the storage doesn't have a record with the given key * \exception LMDBAL::Closed thrown if the database was not opened * \exception LMDBAL::Unknown thrown if something unexpected happend within lmdb */ template void LMDBAL::Storage::changeRecord(const K& key, const V& value) { ensureOpened(changeRecordMethodName); TransactionID txn = beginTransaction(); try { Storage::changeRecord(key, value, txn); } catch (...) { abortTransaction(txn); throw; } commitTransaction(txn); } /** * \brief Changes key-value record to the storage (transaction variant) * * This function schedules a modification of a key-value record, but doesn't immidiately changes it. * You can obtain LMDBAL::TransactionID calling LMDBAL::Base::beginTransaction(). * * Take a note that if the storage didn't have a record you want to change LMDBAL::NotFound is thrown * * \param[in] key key of the record * \param[in] value new value of the record * \param[in] txn transaction ID, needs to be a writable transaction! * * \exception LMDBAL::NotFound thrown if the storage doesn't have a record with the given key * \exception LMDBAL::Closed thrown if the database was not opened * \exception LMDBAL::Unknown thrown if something unexpected happend within lmdb */ template void LMDBAL::Storage::changeRecord(const K& key, const V& value, TransactionID txn) { ensureOpened(changeRecordMethodName); MDB_cursor* cursor; int rc = mdb_cursor_open(txn, dbi, &cursor); if (rc != MDB_SUCCESS) throwUnknown(rc); MDB_val lmdbKey = keySerializer.setData(key); rc = mdb_cursor_get(cursor, &lmdbKey, nullptr, MDB_SET); if (rc != MDB_SUCCESS) throwNotFoundOrUnknown(rc, toString(key)); MDB_val lmdbData = valueSerializer.setData(value); rc = mdb_cursor_put(cursor, &lmdbKey, &lmdbData, MDB_CURRENT); mdb_cursor_close(cursor); if (rc != MDB_SUCCESS) throwUnknown(rc); } /** * \brief Gets the record from the database * Take a note that if the storage didn't have a record you want to change LMDBAL::NotFound is thrown * * \param[in] key key of the record you look for * \returns the value from the storage * * \exception LMDBAL::NotFound thrown if the storage doesn't have a record with the given key * \exception LMDBAL::Closed thrown if the database was not opened * \exception LMDBAL::Unknown thrown if something unexpected happend within lmdb */ template V LMDBAL::Storage::getRecord(const K& key) const { ensureOpened(getRecordMethodName); V value; Storage::getRecord(key, value); return value; } /** * \brief Gets the record from the database (reference variant) * Take a note that if the storage didn't have a record you want to change LMDBAL::NotFound is thrown * * \param[in] key key of the record you look for * \param[out] value the value from the storage * * \exception LMDBAL::NotFound thrown if the storage doesn't have a record with the given key * \exception LMDBAL::Closed thrown if the database was not opened * \exception LMDBAL::Unknown thrown if something unexpected happend within lmdb */ template void LMDBAL::Storage::getRecord(const K& key, V& value) const { ensureOpened(getRecordMethodName); TransactionID txn = beginReadOnlyTransaction(); try { Storage::getRecord(key, value, txn); } catch (...) { abortTransaction(txn); throw; } abortTransaction(txn); } /** * \brief Gets the record from the database (transaction variant) * * You can obtain LMDBAL::TransactionID calling LMDBAL::Base::beginReadOnlyTransaction() or LMDBAL::Base::beginTransaction(). * If you just want to read data you should prefer LMDBAL::Base::beginReadOnlyTransaction(). * * Take a note that if the storage didn't have a record you want to change LMDBAL::NotFound is thrown * * \param[in] key key of the record you look for * \param[in] txn transaction ID, can be read only transaction * \returns the value from the storage * * \exception LMDBAL::NotFound thrown if the storage doesn't have a record with the given key * \exception LMDBAL::Closed thrown if the database was not opened * \exception LMDBAL::Unknown thrown if something unexpected happend within lmdb */ template V LMDBAL::Storage::getRecord(const K& key, TransactionID txn) const { ensureOpened(getRecordMethodName); V value; Storage::getRecord(key, value, txn); return value; } /** * \brief Gets the record from the database (transaction, reference variant) * * You can obtain LMDBAL::TransactionID calling LMDBAL::Base::beginReadOnlyTransaction() or LMDBAL::Base::beginTransaction(). * If you just want to read data you should prefer LMDBAL::Base::beginReadOnlyTransaction(). * * Take a note that if the storage didn't have a record you want to change LMDBAL::NotFound is thrown * * \param[in] key key of the record you look for * \param[out] value the value from the storage * \param[in] txn transaction ID, can be read only transaction * * \exception LMDBAL::NotFound thrown if the storage doesn't have a record with the given key * \exception LMDBAL::Closed thrown if the database was not opened * \exception LMDBAL::Unknown thrown if something unexpected happend within lmdb */ template void LMDBAL::Storage::getRecord(const K& key, V& value, TransactionID txn) const { ensureOpened(getRecordMethodName); MDB_val lmdbKey = keySerializer.setData(key); MDB_val lmdbData; int rc = mdb_get(txn, dbi, &lmdbKey, &lmdbData); if (rc != MDB_SUCCESS) throwNotFoundOrUnknown(rc, toString(key)); valueSerializer.deserialize(lmdbData, value); } /** * \brief Chechs if storage has value * * \param[in] key key of the record you look for * \returns true if there was a record with given key, false otherwise * * \exception LMDBAL::Closed thrown if the database was not opened * \exception LMDBAL::Unknown thrown if something unexpected happend within lmdb */ template bool LMDBAL::Storage::checkRecord(const K& key) const { ensureOpened(checkRecordMethodName); TransactionID txn = beginReadOnlyTransaction(); bool result; try { result = Storage::checkRecord(key, txn); } catch (...) { abortTransaction(txn); throw; } abortTransaction(txn); return result; } /** * \brief Chechs if storage has value (transaction variant) * * You can obtain LMDBAL::TransactionID calling LMDBAL::Base::beginReadOnlyTransaction() or LMDBAL::Base::beginTransaction(). * If you just want to read data you should prefer LMDBAL::Base::beginReadOnlyTransaction(). * * \param[in] key key of the record you look for * \param[in] txn transaction ID, can be read only transaction * \returns true if there was a record with given key, false otherwise * * \exception LMDBAL::Closed thrown if the database was not opened * \exception LMDBAL::Unknown thrown if something unexpected happend within lmdb */ template bool LMDBAL::Storage::checkRecord(const K& key, TransactionID txn) const { ensureOpened(checkRecordMethodName); MDB_val lmdbKey = keySerializer.setData(key); MDB_val lmdbData; int rc = mdb_get(txn, dbi, &lmdbKey, &lmdbData); if (rc == MDB_SUCCESS) return true; if (rc != MDB_NOTFOUND) throwUnknown(rc); return false; } /** * \brief Reads whole storage into a map * * Basically just reads all database in an std::map, usefull when you store small storages * * \exception LMDBAL::Closed thrown if the database was not opened * \exception LMDBAL::Unknown thrown if something unexpected happend within lmdb */ template std::map LMDBAL::Storage::readAll() const { ensureOpened(readAllMethodName); std::map result; Storage::readAll(result); return result; } /** * \brief Reads whole storage into a map (reference variant) * * Basically just reads all database in an std::map, usefull when you store small storages * * \param[out] result a map that is going to contain all data * * \exception LMDBAL::Closed thrown if the database was not opened * \exception LMDBAL::Unknown thrown if something unexpected happend within lmdb */ template void LMDBAL::Storage::readAll(std::map& result) const { ensureOpened(readAllMethodName); TransactionID txn = beginReadOnlyTransaction(); try { Storage::readAll(result, txn); } catch (...) { abortTransaction(txn); throw; } abortTransaction(txn); } /** * \brief Reads whole storage into a map (transaction variant) * * Basically just reads all database in an std::map, usefull when you store small storages * You can obtain LMDBAL::TransactionID calling LMDBAL::Base::beginReadOnlyTransaction() or LMDBAL::Base::beginTransaction(). * If you just want to read data you should prefer LMDBAL::Base::beginReadOnlyTransaction(). * * \param[in] txn transaction ID, can be read only transaction * * \exception LMDBAL::Closed thrown if the database was not opened * \exception LMDBAL::Unknown thrown if something unexpected happend within lmdb */ template std::map LMDBAL::Storage::readAll(TransactionID txn) const { ensureOpened(readAllMethodName); std::map result; Storage::readAll(result, txn); return result; } /** * \brief Reads whole storage into a map (transaction, reference variant) * * Basically just reads all database in an std::map, usefull when you store small storages * You can obtain LMDBAL::TransactionID calling LMDBAL::Base::beginReadOnlyTransaction() or LMDBAL::Base::beginTransaction(). * If you just want to read data you should prefer LMDBAL::Base::beginReadOnlyTransaction(). * * \param[out] result a map that is going to contain all data * \param[in] txn transaction ID, can be read only transaction * * \exception LMDBAL::Closed thrown if the database was not opened * \exception LMDBAL::Unknown thrown if something unexpected happend within lmdb */ template void LMDBAL::Storage::readAll(std::map& result, TransactionID txn) const { ensureOpened(readAllMethodName); MDB_cursor* cursor; MDB_val lmdbKey, lmdbData; int rc = mdb_cursor_open(txn, dbi, &cursor); if (rc != MDB_SUCCESS) throwUnknown(rc); rc = mdb_cursor_get(cursor, &lmdbKey, &lmdbData, MDB_FIRST); while (rc == MDB_SUCCESS) { K key; keySerializer.deserialize(lmdbKey, key); V& value = result[key]; valueSerializer.deserialize(lmdbData, value); rc = mdb_cursor_get(cursor, &lmdbKey, &lmdbData, MDB_NEXT); } mdb_cursor_close(cursor); if (rc != MDB_NOTFOUND) throwUnknown(rc); } /** * \brief Replaces the content of the whole storage with the given * * Basically this function drops the database and adds all the records from the given map * * \param[in] data new data of the storage * * \exception LMDBAL::Closed thrown if the database was not opened * \exception LMDBAL::Unknown thrown if something unexpected happend within lmdb */ template void LMDBAL::Storage::replaceAll(const std::map& data) { ensureOpened(replaceAllMethodName); TransactionID txn = beginTransaction(); try { Storage::replaceAll(data, txn); } catch (...) { abortTransaction(txn); throw; } commitTransaction(txn); } /** * \brief Replaces the content of the whole storage with the given (transaction variant) * * Basically this function drops the database and adds all the records from the given map * This function schedules a data replacement, but doesn't immidiately execute it. * You can obtain LMDBAL::TransactionID calling LMDBAL::Base::beginTransaction(). * * \param[in] data new data of the storage * \param[in] txn transaction ID, needs to be a writable transaction! * * \exception LMDBAL::Closed thrown if the database was not opened * \exception LMDBAL::Unknown thrown if something unexpected happend within lmdb */ template void LMDBAL::Storage::replaceAll(const std::map& data, TransactionID txn) { ensureOpened(replaceAllMethodName); int rc = drop(txn); if (rc != MDB_SUCCESS) throwUnknown(rc); MDB_val lmdbKey, lmdbData; for (const std::pair& pair : data) { lmdbKey = keySerializer.setData(pair.first); lmdbData = valueSerializer.setData(pair.second); rc = mdb_put(txn, dbi, &lmdbKey, &lmdbData, MDB_NOOVERWRITE); //TODO may be appending with cursor makes sence here? if (rc != MDB_SUCCESS) throwUnknown(rc); } } /** * \brief Adds records in bulk * * \param[in] data the data to be added * \param[in] overwrite if false function throws LMDBAL::Exist on repeated key, if true - overwrites it * \returns new actual amount of records in the storage * * \exception LMDBAL::Closed thrown if the database was not opened * \exception LMDBAL::Exist thrown if overwrite==false and at least one of the keys of data already exists in the storage * \exception LMDBAL::Unknown thrown if something unexpected happend within lmdb */ template uint32_t LMDBAL::Storage::addRecords(const std::map& data, bool overwrite) { ensureOpened(addRecordsMethodName); TransactionID txn = beginTransaction(); uint32_t amount; try { amount = Storage::addRecords(data, txn, overwrite); } catch (...) { abortTransaction(txn); throw; } commitTransaction(txn); return amount; } /** * \brief Adds records in bulk (transaction variant) * * This function schedules a data addition, but doesn't immidiately execute it. * You can obtain LMDBAL::TransactionID calling LMDBAL::Base::beginTransaction(). * * \param[in] data the data to be added * \param[in] txn transaction ID, needs to be a writable transaction! * \param[in] overwrite if false function throws LMDBAL::Exist on repeated key, if true - overwrites it * \returns new actual amount of records in the storage * * \exception LMDBAL::Closed thrown if the database was not opened * \exception LMDBAL::Exist thrown if overwrite==false and at least one of the keys of data already exists in the storage * \exception LMDBAL::Unknown thrown if something unexpected happend within lmdb */ template uint32_t LMDBAL::Storage::addRecords(const std::map& data, TransactionID txn, bool overwrite) { ensureOpened(addRecordsMethodName); MDB_val lmdbKey, lmdbData; int rc; for (const std::pair& pair : data) { lmdbKey = keySerializer.setData(pair.first); lmdbData = valueSerializer.setData(pair.second); rc = mdb_put(txn, dbi, &lmdbKey, &lmdbData, overwrite ? 0 : MDB_NOOVERWRITE); if (rc == MDB_KEYEXIST) throwDuplicate(toString(pair.first)); if (rc != MDB_SUCCESS) throwUnknown(rc); } MDB_stat stat; rc = mdb_stat(txn, dbi, &stat); if (rc != MDB_SUCCESS) throwUnknown(rc); return stat.ms_entries; } /** * \brief Removes one of the records * * Take a note that if the storage didn't have a record you want to remove LMDBAL::NotFound is thrown * * \param[in] key key of the record you wish to be removed * * \exception LMDBAL::Closed thrown if the database was not opened * \exception LMDBAL::NotFound thrown if the record with given key wasn't found * \exception LMDBAL::Unknown thrown if something unexpected happend within lmdb */ template void LMDBAL::Storage::removeRecord(const K& key) { ensureOpened(removeRecordMethodName); TransactionID txn = beginTransaction(); try { Storage::removeRecord(key, txn); } catch (...) { abortTransaction(txn); throw; } commitTransaction(txn); } /** * \brief Removes one of the records (transaction variant) * * Take a note that if the storage didn't have a record you want to remove LMDBAL::NotFound is thrown * This function schedules a record removal, but doesn't immidiately execute it. * You can obtain LMDBAL::TransactionID calling LMDBAL::Base::beginTransaction(). * * \param[in] key key of the record you wish to be removed * \param[in] txn transaction ID, needs to be a writable transaction! * * \exception LMDBAL::Closed thrown if the database was not opened * \exception LMDBAL::NotFound thrown if the record with given key wasn't found * \exception LMDBAL::Unknown thrown if something unexpected happend within lmdb */ template void LMDBAL::Storage::removeRecord(const K& key, TransactionID txn) { ensureOpened(removeRecordMethodName); MDB_val lmdbKey = keySerializer.setData(key); int rc = mdb_del(txn, dbi, &lmdbKey, NULL); if (rc != MDB_SUCCESS) throwNotFoundOrUnknown(rc, toString(key)); } /** * \brief A private virtual function I need to open each storage in the database * * \param[in] transaction - lmdb transaction to call mdb_dbi_open * \returns MDB_SUCCESS if everything went smooth or MDB_ -like error code */ template int LMDBAL::Storage::open(MDB_txn* transaction) { return makeStorage(transaction, duplicates); } /** * \brief A private virtual function I need to close each storage in the database */ template void LMDBAL::Storage::close() { for (Cursor* cursor : cursors) cursor->terminated(); iStorage::close(); } /** * \brief Creates cursor * * \returns LMDBAL::Cursor for this storage and returs you a pointer to a created cursor */ template LMDBAL::Cursor* LMDBAL::Storage::createCursor() { Cursor* cursor = new Cursor(this); cursors.insert(cursor); return cursor; } /** * \brief Reads current storage flags it was opened with * * This function exists mostly for testing purposes * * \returns Third out parameter of mdb_dbi_flags function * * \exception LMDBAL::Closed thrown if the database was not opened * \exception LMDBAL::Unknown if the result of mdb_dbi_flags was not successfull */ template uint32_t LMDBAL::Storage::flags() const { ensureOpened(flagsMethodName); uint32_t result; TransactionID txn = beginReadOnlyTransaction(); int res = mdb_dbi_flags(txn, dbi, &result); abortTransaction(txn); if (res != MDB_SUCCESS) throwUnknown(res); return result; } /** * \brief Destroys cursor * * This a normal way to discard a cursor you don't need anymore * * \param[in] cursor a pointer to a cursor you want to destroy * * \exception LMDBAL::Unknown thrown if you try to destroy something that this storage didn't create */ template void LMDBAL::Storage::destroyCursor(Cursor* cursor) { typename std::set*>::const_iterator itr = cursors.find(cursor); if (itr == cursors.end()) throwUnknown("An attempt to destroy a cursor the storage doesn't own"); cursors.erase(itr); delete cursor; } /** * \brief A private virtual function that cursor calls when he reads a record, does nothing here but populates the LMDBAL::Cache * * \param[in] key a key of discovered record * \param[in] value a value of discovered record */ template void LMDBAL::Storage::discoveredRecord(const K& key, const V& value) const { UNUSED(key); UNUSED(value); } /** * \brief A private virtual function that cursor calls when he reads a record, does nothing here but populates the LMDBAL::Cache * * \param[in] key a key of discovered record * \param[in] value a value of discovered record * \param[in] txn TransactionID under which the dicovery happened, to avoid not commited changes collisions */ template void LMDBAL::Storage::discoveredRecord(const K& key, const V& value, TransactionID txn) const { UNUSED(key); UNUSED(value); UNUSED(txn); } /** * \brief A functiion to actually open MDB_dbi storage * * \tparam K type of keys in opening storage * * \param[in] transaction - lmdb transaction to call mdb_dbi_open, must be a writable transaction! * \param[in] duplicates - true if you wish to enable duplicates support for the storage * * \returns MDB_SUCCESS if everything went smooth or MDB_ -like error code * * This is a way to optimise database using MDB_INTEGERKEY flag, * when the key is actually kind of an integer * This infrastructure also allowes us to customize mdb_dbi_open call in the future */ template inline int LMDBAL::iStorage::makeStorage(MDB_txn* transaction, bool duplicates) { unsigned int flags = MDB_CREATE; if constexpr (std::is_integral::value) flags |= MDB_INTEGERKEY; if (duplicates) { flags |= MDB_DUPSORT; if constexpr (std::is_integral::value) flags |= MDB_INTEGERDUP | MDB_DUPFIXED; } return mdb_dbi_open(transaction, name.c_str(), flags, &dbi); } /** * \brief A method to cast a value (which can be a value or a key) to string. * * This function is mainly used in exceptions, to report which key was duplicated or not found. * You can define your own specializations to this function in case std::to_string doesn't cover your case * * \param[in] value a value that should be converted to string * \returns a string presentation of value */ template inline std::string LMDBAL::iStorage::toString(const T& value) { return std::to_string(value); } /** * \brief A method to cast a value (which can be a value or a key) to string. * * QString spectialization * * \param[in] value a value that should be converted to string * \returns a string presentation of value */ template<> inline std::string LMDBAL::iStorage::toString(const QString& value) { return value.toStdString(); } /** * \brief A method to cast a value (which can be a value or a key) to string. * * std::string spectialization * * \param[in] value a value that should be converted to string * \returns a string presentation of value */ template<> inline std::string LMDBAL::iStorage::toString(const std::string& value) { return value; } #endif //LMDBAL_STORAGE_HPP