diff --git a/src/cursor.h b/src/cursor.h index c9f46aa..36d46ba 100644 --- a/src/cursor.h +++ b/src/cursor.h @@ -46,6 +46,7 @@ public: void renew() const; void renew(TransactionID txn) const; void close() const; + bool opened() const; std::pair first() const; std::pair last() const; diff --git a/src/cursor.hpp b/src/cursor.hpp index 57d3a42..a3bd252 100644 --- a/src/cursor.hpp +++ b/src/cursor.hpp @@ -21,25 +21,67 @@ #include "cursor.h" +/** + * \class LMDBAL::Cursor + * \brief An object to iterate storages. + * + * \tparam K type of the keys in the storage that this cursor would iterate + * \tparam V type of the values in the storage that this cursor would iterate + * + * Cursor allowes you to perform sequential querries, navigate from one element to another at reduced operation price. + * For now Cursors are read only but in the future you might also be able to write to the storage with them. + * + * Cursors are owned by the storage, they die with the storage. + * They also get closed if the storage is closed (if you close by the database for example) + * + * You can obtain an instance of this class calling LMDBAL::Storage::createCursor() + * and destory it calling LMDBAL::Storage::destoryCursor() at any time, LMDBAL::Base doesn't necessarily need to be opened. + * + * You are not supposed to instantiate or destory instances of this class yourself! + */ + +/** + * \brief Creates a cursor + * + * \param[in] parent a storage that created this cursor + */ template LMDBAL::Cursor::Cursor(Storage* parent): storage(parent), cursor(nullptr), state(closed) -{ - -} +{} +/** + * \brief Destroys a cursor + * + * If the cursor wasn't properly closed - it's going to be upon destruction + */ template LMDBAL::Cursor::~Cursor () { close(); } +/** + * \brief A private function the storage owning this cursor will call to inform this cursor that the thansaction needs to be aborted + */ template void LMDBAL::Cursor::terminated () const { close(); //for now it's the same, but if I ever going to make writable cursor - here is where it's gonna be different } +/** + * \brief Opens the cursor for operations. + * + * This is a normal way to start the sequence of operations with the cursor. + * This variant of the function creates a read only transaction just for this cursor + * + * This function should be called when the LMDBAL::Storage is already opened and before any query with this cursor! + * It will do nothing to a cursor that was already opened (no matter what way). + * + * \throws LMDBAL::Closed thrown if you try to open the cursor on a closed database + * \throws LMDBAL::Unknown thrown if there was a problem opening the cursor by the lmdb, or to begin a transaction + */ template void LMDBAL::Cursor::open () const { storage->ensureOpened(openCursorMethodName); @@ -58,6 +100,18 @@ void LMDBAL::Cursor::open () const { } } +/** + * \brief Opens the cursor for operations. + * + * This is a normal way to start the sequence of operations with the cursor. + * This variant of the function uses for queries a transaction you have obtained somewhere else. + * + * This function should be called when the LMDBAL::Storage is already opened and before any query with this cursor! + * It will do nothing to a cursor that was already opened (no matter what way). + * + * \throws LMDBAL::Closed thrown if you try to open the cursor on a closed database + * \throws LMDBAL::Unknown thrown if there was a problem opening the cursor by the lmdb + */ template void LMDBAL::Cursor::open (TransactionID txn) const { storage->ensureOpened(openCursorMethodName); @@ -74,6 +128,24 @@ void LMDBAL::Cursor::open (TransactionID txn) const { } } +/** + * \brief Renews a cursor + * + * This function aborts current transaction if the cursor was opened with it's own transaction + * (does not mess up if the transaction was public), + * creates new private transaction and rebinds this cursor to it. + * + * Theoretically you could call this method if your public transaction was aborted (or commited) + * but you wish to continue to keep working with your cursor. + * Or if you just want to rebind your cursor to a new private transaction. + * + * This function does nothing if the cursor is closed + * + * \param[in] txn a transaction you wish this cursor to be bound to + * + * \throws LMDBAL::Closed thrown if you try to renew the cursor on a closed database + * \throws LMDBAL::Unknown thrown if there was a problem beginning new transaction or if there was a problem renewing the cursor by lmdb + */ template void LMDBAL::Cursor::renew () const { storage->ensureOpened(renewCursorMethodName); @@ -98,6 +170,24 @@ void LMDBAL::Cursor::renew () const { } } +/** + * \brief Renews a cursor + * + * This function aborts current transaction if the cursor was opened with it's own transaction + * (does not mess up if the transaction was public), + * and rebinds this cursor to a passed new transaction. + * + * Theoretically you could call this method if your previous public transaction was aborted (or commited) + * but you wish to continue to keep working with your cursor. + * Or if you just want to rebind your cursor to another public transaction. + * + * This function does nothing if the cursor is closed + * + * \param[in] txn a transaction you wish this cursor to be bound to + * + * \throws LMDBAL::Closed thrown if you try to renew the cursor on a closed database + * \throws LMDBAL::Unknown thrown if there was a problem renewing the cursor by lmdb + */ template void LMDBAL::Cursor::renew (TransactionID txn) const { storage->ensureOpened(renewCursorMethodName); @@ -120,6 +210,16 @@ void LMDBAL::Cursor::renew (TransactionID txn) const { } } +/** + * \brief Termiates a sequence of operations with the cursor + * + * This is a normal way to tell that you're done with the cursor and don't want to continue the sequence of queries. + * The state of the cursor is lost after calling this method, some inner resorce is freed. + * + * If the cursor was opened with the private transaction - the owner storage will be notified of the aborted transaction. + * + * This function does nothing on a closed cursor. + */ template void LMDBAL::Cursor::close () const { switch (state) { @@ -141,31 +241,124 @@ void LMDBAL::Cursor::close () const { } } +/** + * \brief Tells if the cursor is open + */ +template +bool LMDBAL::Cursor::opened () const { + return state != closed; +} + +/** + * \brief Queries the first element in the storage + * + * Notifies the storage of the queried element calling LMDBAL::Storage::discoveredRecord() + * + * \param[out] key a reference to an object the key of queried element is going to be assigned + * \param[out] value a reference to an object the value of queried element is going to be assigned + * + * \throws LMDBAL::CursorNotReady thrown if you try to call this method on a closed cursor + * \throws LMDBAL::NotFound thrown if there are no elements in the storage + * \throws LMDBAL::Unknown thrown if there was some unexpected problem with lmdb + */ template void LMDBAL::Cursor::first (K& key, V& value) const { operateCursorRead(key, value, MDB_FIRST, firstMethodName, firstOperationName); } +/** + * \brief Queries the last element in the storage + * + * Notifies the storage of the queried element calling LMDBAL::Storage::discoveredRecord() + * + * \param[out] key a reference to an object the key of queried element is going to be assigned + * \param[out] value a reference to an object the value of queried element is going to be assigned + * + * \throws LMDBAL::CursorNotReady thrown if you try to call this method on a closed cursor + * \throws LMDBAL::NotFound thrown if there are no elements in the storage + * \throws LMDBAL::Unknown thrown if there was some unexpected problem with lmdb + */ template void LMDBAL::Cursor::last (K& key, V& value) const { operateCursorRead(key, value, MDB_LAST, lastMethodName, lastOperationName); } +/** + * \brief Queries the next element from the storage + * + * If there was no operation before this method positions the cursor on the first element and returns it + * so, it's basically doing the same as LMDBAL::Cursor::first(K key, V value). + * + * It will also throw LMDBAL::NotFound if you call this method being on the last element + * or if there are no elements in the database. + * + * Notifies the storage of the queried element calling LMDBAL::Storage::discoveredRecord() + * + * \param[out] key a reference to an object the key of queried element is going to be assigned + * \param[out] value a reference to an object the value of queried element is going to be assigned + * + * \throws LMDBAL::CursorNotReady thrown if you try to call this method on a closed cursor + * \throws LMDBAL::NotFound thrown if the cursor already was on the last element or if there are no elements in the storage + * \throws LMDBAL::Unknown thrown if there was some unexpected problem with lmdb + */ template void LMDBAL::Cursor::next (K& key, V& value) const { operateCursorRead(key, value, MDB_NEXT, nextMethodName, nextOperationName); } +/** + * \brief Queries the previous element from the storage + * + * If there was no operation before this method positions the cursor on the last element and returns it + * so, it's basically doing the same as LMDBAL::Cursor::last(K key, V value). + * + * It will also throw LMDBAL::NotFound if you call this method being on the first element + * or if there are no elements in the database. + * + * Notifies the storage of the queried element calling LMDBAL::Storage::discoveredRecord() + * + * \param[out] key a reference to an object the key of queried element is going to be assigned + * \param[out] value a reference to an object the value of queried element is going to be assigned + * + * \throws LMDBAL::CursorNotReady thrown if you try to call this method on a closed cursor + * \throws LMDBAL::NotFound thrown if the cursor already was on the first element or if there are no elements in the storage + * \throws LMDBAL::Unknown thrown if there was some unexpected problem with lmdb + */ template void LMDBAL::Cursor::prev (K& key, V& value) const { operateCursorRead(key, value, MDB_PREV, prevMethodName, prevOperationName); } +/** + * \brief Returns current cursor element from the storage + * + * If there was no operation before this method throws LMDBAL::Unknown for some reason + * + * Notifies the storage of the queried element calling LMDBAL::Storage::discoveredRecord() + * + * \param[out] key a reference to an object the key of queried element is going to be assigned + * \param[out] value a reference to an object the value of queried element is going to be assigned + * + * \throws LMDBAL::CursorNotReady thrown if you try to call this method on a closed cursor + * \throws LMDBAL::NotFound probably never thrown but there might be still some corner case I don't know about + * \throws LMDBAL::Unknown thrown if there was some unexpected problem with lmdb + */ template void LMDBAL::Cursor::current (K& key, V& value) const { operateCursorRead(key, value, MDB_GET_CURRENT, currentMethodName, currentOperationName); } +/** + * \brief Queries the first element in the storage + * + * Notifies the storage of the queried element calling LMDBAL::Storage::discoveredRecord() + * + * \returns std::pair where first is element key and second is element value + * + * \throws LMDBAL::CursorNotReady thrown if you try to call this method on a closed cursor + * \throws LMDBAL::NotFound thrown if there are no elements in the storage + * \throws LMDBAL::Unknown thrown if there was some unexpected problem with lmdb + */ template std::pair LMDBAL::Cursor::first () const { std::pair result; @@ -173,6 +366,17 @@ std::pair LMDBAL::Cursor::first () const { return result; } +/** + * \brief Queries the last element in the storage + * + * Notifies the storage of the queried element calling LMDBAL::Storage::discoveredRecord() + * + * \returns std::pair where first is element key and second is element value + * + * \throws LMDBAL::CursorNotReady thrown if you try to call this method on a closed cursor + * \throws LMDBAL::NotFound thrown if there are no elements in the storage + * \throws LMDBAL::Unknown thrown if there was some unexpected problem with lmdb + */ template std::pair LMDBAL::Cursor::last () const { std::pair result; @@ -180,6 +384,23 @@ std::pair LMDBAL::Cursor::last () const { return result; } +/** + * \brief Queries the next element from the storage + * + * If there was no operation before this method positions the cursor on the first element and returns it + * so, it's basically doing the same as LMDBAL::Cursor::first(). + * + * It will also throw LMDBAL::NotFound if you call this method being on the last element + * or if there are no elements in the database. + * + * Notifies the storage of the queried element calling LMDBAL::Storage::discoveredRecord() + * + * \returns std::pair where first is element key and second is element value + * + * \throws LMDBAL::CursorNotReady thrown if you try to call this method on a closed cursor + * \throws LMDBAL::NotFound thrown if the cursor already was on the last element or if there are no elements in the storage + * \throws LMDBAL::Unknown thrown if there was some unexpected problem with lmdb + */ template std::pair LMDBAL::Cursor::next () const { std::pair result; @@ -187,6 +408,23 @@ std::pair LMDBAL::Cursor::next () const { return result; } +/** + * \brief Queries the previous element from the storage + * + * If there was no operation before this method positions the cursor on the last element and returns it + * so, it's basically doing the same as LMDBAL::Cursor::last(). + * + * It will also throw LMDBAL::NotFound if you call this method being on the first element + * or if there are no elements in the database. + * + * Notifies the storage of the queried element calling LMDBAL::Storage::discoveredRecord() + * + * \returns std::pair where first is element key and second is element value + * + * \throws LMDBAL::CursorNotReady thrown if you try to call this method on a closed cursor + * \throws LMDBAL::NotFound thrown if the cursor already was on the first element or if there are no elements in the storage + * \throws LMDBAL::Unknown thrown if there was some unexpected problem with lmdb + */ template std::pair LMDBAL::Cursor::prev () const { std::pair result; @@ -194,6 +432,19 @@ std::pair LMDBAL::Cursor::prev () const { return result; } +/** + * \brief Returns current cursor element from the storage + * + * If there was no operation before this method throws LMDBAL::Unknown for some reason + * + * Notifies the storage of the queried element calling LMDBAL::Storage::discoveredRecord() + * + * \returns std::pair where first is element key and second is element value + * + * \throws LMDBAL::CursorNotReady thrown if you try to call this method on a closed cursor + * \throws LMDBAL::NotFound probably never thrown but there might be still some corner case I don't know about + * \throws LMDBAL::Unknown thrown if there was no positioning operation before of if there was some unexpected problem with lmdb + */ template std::pair LMDBAL::Cursor::current () const { std::pair result; @@ -201,7 +452,21 @@ std::pair LMDBAL::Cursor::current () const { return result; } - +/** + * \brief a private mothod that actually doing all the reading + * + * Queries the storage, deserializes the output, notifies the storage of the queried element calling LMDBAL::Storage::discoveredRecord() + * + * \param[out] key a reference to an object the key of queried element is going to be assigned + * \param[out] value a reference to an object the value of queried element is going to be assigned + * \param[in] operation LMDB cursor operation code + * \param[in] methodName a name of the method you called it from, just for the exception message if something goes not as expected + * \param[in] operationName a name of the opeartion, just for the exception message if something goes not as expected + * + * \throws LMDBAL::CursorNotReady thrown if you try to call this method on a closed cursor + * \throws LMDBAL::NotFound mostly thrown if the query wasn't found + * \throws LMDBAL::Unknown mostly thrown if there was some unexpected problem with lmdb + */ template void LMDBAL::Cursor::operateCursorRead( K& key, diff --git a/src/storage.cpp b/src/storage.cpp index 124dab8..d65923c 100644 --- a/src/storage.cpp +++ b/src/storage.cpp @@ -253,6 +253,18 @@ bool LMDBAL::iStorage::isDBOpened() const { void LMDBAL::iStorage::throwUnknown(int rc) const { throw Unknown(db->name, mdb_strerror(rc), name);} +/** + * \brief Throws LMDBAL::Unknown + * + * Helper function ment to be used in heirs and reduce the code a bit + * + * \param[in] message - a message you wish to appear in the exception reason + * + * \exception LMDBAL::Unknown thrown everytime + */ +void LMDBAL::iStorage::throwUnknown(const std::string& message) const { + throw Unknown(db->name, message, name);} + /** * \brief Throws LMDBAL::Exist * diff --git a/src/storage.h b/src/storage.h index 0a7d645..bf58222 100644 --- a/src/storage.h +++ b/src/storage.h @@ -48,6 +48,7 @@ protected: void throwNotFoundOrUnknown(int rc, TransactionID txn, const std::string& key) const; void throwUnknown(int rc, TransactionID txn) const; void throwUnknown(int rc) const; + void throwUnknown(const std::string& message) const; void throwDuplicate(const std::string& key) const; void throwNotFound(const std::string& key) const; void throwCursorNotReady(const std::string& method) const; diff --git a/src/storage.hpp b/src/storage.hpp index cd83f5a..5e33f08 100644 --- a/src/storage.hpp +++ b/src/storage.hpp @@ -667,7 +667,7 @@ void LMDBAL::Storage::removeRecord(const K& key) { * 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] 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 @@ -706,6 +706,11 @@ void LMDBAL::Storage::close() { 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); @@ -714,18 +719,44 @@ LMDBAL::Cursor* LMDBAL::Storage::createCursor() { return cursor; } +/** + * \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 + * + * \throws LMDBAL::Unknown thrown if you try to destroy something that this storage didn't create + */ template void LMDBAL::Storage::destroyCursor(Cursor* cursor) { - cursors.erase(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); diff --git a/test/storagecursor.cpp b/test/storagecursor.cpp index e7b1f8d..8a08724 100644 --- a/test/storagecursor.cpp +++ b/test/storagecursor.cpp @@ -8,7 +8,8 @@ class StorageCursorTest : public ::testing::Test { protected: StorageCursorTest(): ::testing::Test(), - table (db->getStorage("table1")) {} + table (db->getStorage("table1")), + emptyTable (db->getStorage("empty")) {} ~StorageCursorTest() {} @@ -16,6 +17,7 @@ protected: if (db == nullptr) { db = new LMDBAL::Base("testBase"); db->addStorage("table1"); + db->addStorage("empty"); db->open(); } } @@ -25,16 +27,21 @@ protected: db->removeDirectory(); delete db; db = nullptr; + cursor = nullptr; + emptyCursor = nullptr; } static LMDBAL::Base* db; static LMDBAL::Cursor* cursor; + static LMDBAL::Cursor* emptyCursor; LMDBAL::Storage* table; + LMDBAL::Storage* emptyTable; }; LMDBAL::Base* StorageCursorTest::db = nullptr; LMDBAL::Cursor* StorageCursorTest::cursor = nullptr; +LMDBAL::Cursor* StorageCursorTest::emptyCursor = nullptr; static const std::map data({ {245665783, "bothering nerds"}, @@ -50,13 +57,13 @@ static const std::map data({ }); TEST_F(StorageCursorTest, PopulatingTheTable) { - uint32_t amount = table->addRecords(data); EXPECT_EQ(amount, data.size()); } TEST_F(StorageCursorTest, Creation) { cursor = table->createCursor(); + emptyCursor = emptyTable->createCursor(); EXPECT_THROW(cursor->first(), LMDBAL::CursorNotReady); EXPECT_THROW(cursor->last(), LMDBAL::CursorNotReady); @@ -121,21 +128,7 @@ TEST_F(StorageCursorTest, PrevPrivate) { EXPECT_EQ(element.second, reference->second); } -TEST_F(StorageCursorTest, Destruction) { - cursor->close(); - - EXPECT_THROW(cursor->first(), LMDBAL::CursorNotReady); - EXPECT_THROW(cursor->last(), LMDBAL::CursorNotReady); - EXPECT_THROW(cursor->next(), LMDBAL::CursorNotReady); - EXPECT_THROW(cursor->prev(), LMDBAL::CursorNotReady); - EXPECT_THROW(cursor->current(), LMDBAL::CursorNotReady); -} - TEST_F(StorageCursorTest, CurrentPrivate) { - cursor->open(); - - EXPECT_THROW(cursor->current(), LMDBAL::Unknown); //yeah, nice thing to write in the doc - std::pair element = cursor->first(); std::map::const_iterator reference = data.begin(); @@ -165,3 +158,46 @@ TEST_F(StorageCursorTest, CurrentPrivate) { EXPECT_EQ(element.first, reference->first); EXPECT_EQ(element.second, reference->second); } + +TEST_F(StorageCursorTest, Destruction) { + cursor->close(); + + EXPECT_THROW(cursor->first(), LMDBAL::CursorNotReady); + EXPECT_THROW(cursor->last(), LMDBAL::CursorNotReady); + EXPECT_THROW(cursor->next(), LMDBAL::CursorNotReady); + EXPECT_THROW(cursor->prev(), LMDBAL::CursorNotReady); + EXPECT_THROW(cursor->current(), LMDBAL::CursorNotReady); +} + +TEST_F(StorageCursorTest, CornerCases) { + emptyCursor->open(); + EXPECT_THROW(emptyCursor->first(), LMDBAL::NotFound); + EXPECT_THROW(emptyCursor->last(), LMDBAL::NotFound); + EXPECT_THROW(emptyCursor->next(), LMDBAL::NotFound); + EXPECT_THROW(emptyCursor->prev(), LMDBAL::NotFound); + EXPECT_THROW(emptyCursor->current(), LMDBAL::Unknown); + emptyCursor->close(); + + cursor->open(); + EXPECT_THROW(cursor->current(), LMDBAL::Unknown); //yeah, nice thing to write in the doc + + std::map::const_reverse_iterator breference = data.rbegin(); + std::pair element(cursor->prev()); + EXPECT_EQ(element.first, breference->first); //nice thing to write in the doc, again! + EXPECT_EQ(element.second, breference->second); + element = cursor->current(); + EXPECT_EQ(element.first, breference->first); + EXPECT_EQ(element.second, breference->second); + EXPECT_THROW(cursor->next(), LMDBAL::NotFound); + cursor->close(); + + cursor->open(); + element = cursor->next(); + std::map::const_iterator reference = data.begin(); + EXPECT_EQ(element.first, reference->first); + EXPECT_EQ(element.second, reference->second); + element = cursor->current(); + EXPECT_EQ(element.first, reference->first); + EXPECT_EQ(element.second, reference->second); + EXPECT_THROW(cursor->prev(), LMDBAL::NotFound); +}