Added DBDriverSqlite3 tests

This commit is contained in:
matlabbe
2026-05-17 09:53:57 -07:00
parent 59ba6e54bd
commit c102c687d9
5 changed files with 526 additions and 227 deletions
+9 -7
View File
@@ -185,15 +185,17 @@ if(BUILD_TESTING)
set(TEST_DATA_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/data") set(TEST_DATA_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/data")
endif() endif()
OPTION(ENABLE_COVERAGE "Build with gcov/llvm coverage instrumentation (for CI)" OFF) OPTION(ENABLE_COVERAGE "Build with gcov/lcov coverage (GCC/Clang, Debug only; see coverage-report.sh)" OFF)
IF(ENABLE_COVERAGE) IF(ENABLE_COVERAGE)
IF(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang") IF(NOT CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
MESSAGE(STATUS "Coverage instrumentation enabled") MESSAGE(FATAL_ERROR "ENABLE_COVERAGE requires GCC or Clang with gcov/lcov (current: ${CMAKE_CXX_COMPILER_ID}).")
ADD_COMPILE_OPTIONS(--coverage -O0 -g)
ADD_LINK_OPTIONS(--coverage)
ELSE()
MESSAGE(WARNING "ENABLE_COVERAGE is only supported with GCC or Clang")
ENDIF() ENDIF()
IF(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
MESSAGE(FATAL_ERROR "ENABLE_COVERAGE requires CMAKE_BUILD_TYPE=Debug (current: \"${CMAKE_BUILD_TYPE}\").")
ENDIF()
MESSAGE(STATUS "Coverage instrumentation enabled")
ADD_COMPILE_OPTIONS(--coverage -O0 -g)
ADD_LINK_OPTIONS(--coverage)
ENDIF() ENDIF()
####### DEPENDENCIES ####### ####### DEPENDENCIES #######
+103 -2
View File
@@ -37,17 +37,118 @@ typedef struct sqlite3 sqlite3;
namespace rtabmap { namespace rtabmap {
/**
* @class DBDriverSqlite3
* @brief SQLite3 implementation of @ref DBDriver for RTAB-Map map databases.
*
* This is the default driver returned by @ref DBDriver::create(). It stores signatures,
* links, visual words, statistics and sensor payloads in a single \c .db file using
* the SQLite C API.
*
* **Storage modes**
* - **File-backed** (default): the database is read/written directly on disk at @ref getUrl().
* - **In-memory**: when @ref isInMemory() is true, SQLite uses \c :memory: and the file at
* @ref getUrl() (if any) is loaded at open and optionally saved on @ref closeConnection().
*
* **SQLite PRAGMA tuning** (applied on connect and when setters are called while connected):
* - @ref setCacheSize() — page cache size in pages
* - @ref setJournalMode() — rollback journal mode (DELETE … OFF)
* - @ref setSynchronous() — fsync policy (OFF / NORMAL / FULL)
* - @ref setTempStore() — storage for temporary tables and indices
*
* Configure via @ref Parameters::kDbSqlite3InMemory(), @ref Parameters::kDbSqlite3CacheSize(),
* @ref Parameters::kDbSqlite3JournalMode(), @ref Parameters::kDbSqlite3Synchronous() and
* @ref Parameters::kDbSqlite3TempStore(), or call the setters directly.
*
* @see DBDriver
* @see DBDriver::create()
*/
class RTABMAP_CORE_EXPORT DBDriverSqlite3: public DBDriver { class RTABMAP_CORE_EXPORT DBDriverSqlite3: public DBDriver {
public: public:
/**
* @brief Construct driver with optional SQLite-specific parameters.
* @param parameters Map of parameters (see class description).
*/
DBDriverSqlite3(const ParametersMap & parameters = ParametersMap()); DBDriverSqlite3(const ParametersMap & parameters = ParametersMap());
virtual ~DBDriverSqlite3(); virtual ~DBDriverSqlite3();
/** @brief Apply SQLite parameters from the map; forwards to @ref DBDriver::parseParameters(). */
virtual void parseParameters(const ParametersMap & parameters); virtual void parseParameters(const ParametersMap & parameters);
/**
* @brief True when the database runs in RAM instead of on disk.
* @return True if @ref getUrl() is empty or @ref setDbInMemory(true) was used.
*/
virtual bool isInMemory() const {return getUrl().empty() || _dbInMemory;} virtual bool isInMemory() const {return getUrl().empty() || _dbInMemory;}
/**
* @brief Enable or disable in-memory mode.
* If connected, the connection is closed and reopened with the new mode.
*/
void setDbInMemory(bool dbInMemory); void setDbInMemory(bool dbInMemory);
/**
* @brief Set SQLite rollback journal mode (`PRAGMA journal_mode`).
*
* Controls how SQLite stores the transaction journal used for atomic commit and rollback.
* See https://www.sqlite.org/pragma.html#pragma_journal_mode
*
* @param journalMode Accepted values (invalid values are ignored). RTAB-Map default:
* @ref Parameters::defaultDbSqlite3JournalMode() = **3 (MEMORY)** (@ref Parameters::kDbSqlite3JournalMode()).
* - **0 — DELETE** (SQLite default): the journal file is deleted at the end of each transaction.
* Good general-purpose balance of safety and speed.
* - **1 — TRUNCATE**: the journal is truncated to zero length instead of being unlinked; can be
* faster on some filesystems than DELETE.
* - **2 — PERSIST**: the journal file is not deleted; only its header is zeroed after commit,
* reducing create/delete overhead at the cost of always keeping a journal file on disk.
* - **3 — MEMORY** (RTAB-Map default): the journal is held in RAM only (not written to disk). Faster, but the
* database cannot be rolled back after a crash and may corrupt if the process dies mid-write.
* - **4 — OFF**: no rollback journal. Fastest, but a crash or power loss during a write can
* leave the database inconsistent; transactions cannot be rolled back atomically.
*/
void setJournalMode(int journalMode); void setJournalMode(int journalMode);
/**
* @brief Set the number of database pages kept in SQLite's page cache (`PRAGMA cache_size`).
*
* A larger cache reduces disk I/O when the working set fits in memory. The effective memory
* is approximately `cacheSize * page_size` bytes (page size is usually 4096 bytes unless
* changed with `PRAGMA page_size`). Only positive values are used (page count); see
* https://www.sqlite.org/pragma.html#pragma_cache_size
*
* @param cacheSize Number of pages to cache. RTAB-Map default:
* @ref Parameters::defaultDbSqlite3CacheSize() = **10000** (@ref Parameters::kDbSqlite3CacheSize()).
*/
void setCacheSize(unsigned int cacheSize); void setCacheSize(unsigned int cacheSize);
/**
* @brief Set how aggressively SQLite syncs the database file to disk (`PRAGMA synchronous`).
*
* Trade-off between durability after a crash or power loss and write performance.
* See https://www.sqlite.org/pragma.html#pragma_synchronous
*
* @param synchronous Accepted values (invalid values are ignored). RTAB-Map default:
* @ref Parameters::defaultDbSqlite3Synchronous() = **0 (OFF)** (@ref Parameters::kDbSqlite3Synchronous()).
* - **0 — OFF** (RTAB-Map default): SQLite does not wait for data to reach persistent storage. Fastest; a system
* crash or power loss during a transaction may corrupt the database.
* - **1 — NORMAL**: syncs at the most critical moments (SQLite default in many builds). A crash
* may lose the last transaction but the database file structure usually stays valid.
* - **2 — FULL**: syncs after every transaction commit. Slowest; strongest guarantee that a
* committed transaction survives a power loss (when the OS honors fsync).
*/
void setSynchronous(int synchronous); void setSynchronous(int synchronous);
/**
* @brief Set where SQLite stores temporary tables and indices (`PRAGMA temp_store`).
*
* Affects internal temp storage used for some queries and operations, not RTAB-Map map data.
* See https://www.sqlite.org/pragma.html#pragma_temp_store
*
* @param tempStore Accepted values (invalid values are ignored). RTAB-Map default:
* @ref Parameters::defaultDbSqlite3TempStore() = **2 (MEMORY)** (@ref Parameters::kDbSqlite3TempStore()).
* - **0 — DEFAULT**: use SQLite's compile-time default (often FILE, i.e. on-disk temp files).
* - **1 — FILE**: store temporary tables and indices in temporary files in the directory
* given by `PRAGMA temp_store_directory` or the system temp folder.
* - **2 — MEMORY** (RTAB-Map default): store temporary tables and indices in RAM. Can speed up heavy queries
* but increases memory use; large temp structures may still spill to disk depending on build.
*/
void setTempStore(int tempStore); void setTempStore(int tempStore);
protected: protected:
@@ -199,8 +300,8 @@ private:
int loadOrSaveDb(sqlite3 *pInMemory, const std::string & fileName, int isSave) const; int loadOrSaveDb(sqlite3 *pInMemory, const std::string & fileName, int isSave) const;
protected: protected:
sqlite3 * _ppDb; sqlite3 * _ppDb; ///< Open SQLite connection (null when disconnected)
std::string _version; std::string _version; ///< Schema version read from the database
private: private:
unsigned long _memoryUsedEstimate; unsigned long _memoryUsedEstimate;
+20 -4
View File
@@ -272,10 +272,26 @@ class RTABMAP_CORE_EXPORT Parameters
//Database //Database
RTABMAP_PARAM(DbSqlite3, InMemory, bool, false, "Using database in the memory instead of a file on the hard disk."); RTABMAP_PARAM(DbSqlite3, InMemory, bool, false, "Using database in the memory instead of a file on the hard disk.");
RTABMAP_PARAM(DbSqlite3, CacheSize, unsigned int, 10000, "Sqlite cache size (default is 2000)."); RTABMAP_PARAM(DbSqlite3, CacheSize, unsigned int, 10000,
RTABMAP_PARAM(DbSqlite3, JournalMode, int, 3, "0=DELETE, 1=TRUNCATE, 2=PERSIST, 3=MEMORY, 4=OFF (see sqlite3 doc : \"PRAGMA journal_mode\")"); "PRAGMA cache_size: number of database pages kept in SQLite's page cache (approx. cacheSize * page_size bytes, often ~4 KiB per page). "
RTABMAP_PARAM(DbSqlite3, Synchronous, int, 0, "0=OFF, 1=NORMAL, 2=FULL (see sqlite3 doc : \"PRAGMA synchronous\")"); "Larger values reduce disk I/O when the working set fits in RAM. SQLite built-in default is typically 2000 pages.");
RTABMAP_PARAM(DbSqlite3, TempStore, int, 2, "0=DEFAULT, 1=FILE, 2=MEMORY (see sqlite3 doc : \"PRAGMA temp_store\")"); RTABMAP_PARAM(DbSqlite3, JournalMode, int, 3,
"PRAGMA journal_mode: rollback journal storage. See sqlite.org/pragma.html#pragma_journal_mode for more details. "
"0=DELETE (SQLite default): journal file deleted after each commit. "
"1=TRUNCATE: journal truncated to zero length. "
"2=PERSIST: journal file kept, header zeroed after commit. "
"3=MEMORY: journal in RAM only; faster, weaker crash safety. "
"4=OFF: no journal; fastest, risk of corruption on crash.");
RTABMAP_PARAM(DbSqlite3, Synchronous, int, 0,
"PRAGMA synchronous: how aggressively SQLite syncs the database to disk. See sqlite.org/pragma.html#pragma_synchronous for more details. "
"0=OFF: no wait for persistent storage; fastest, corruption possible on power loss. "
"1=NORMAL: sync at critical moments (common SQLite default with WAL). "
"2=FULL (SQLite safest default): sync after every commit; slowest.");
RTABMAP_PARAM(DbSqlite3, TempStore, int, 2,
"PRAGMA temp_store: where SQLite stores temporary tables and indices. See sqlite.org/pragma.html#pragma_temp_store for more details. "
"0=DEFAULT: SQLite compile-time default (often on-disk temp files). "
"1=FILE: temporary files in the system temp directory. "
"2=MEMORY: temporary data in RAM when possible.");
RTABMAP_PARAM_STR(Db, TargetVersion, "", "Target database version for backward compatibility purpose. Only Major and minor versions are used and should be set (e.g., 0.19 vs 0.20 or 1.0 vs 2.0). Patch version is ignored (e.g., 0.20.1 and 0.20.3 will generate a 0.20 database)."); RTABMAP_PARAM_STR(Db, TargetVersion, "", "Target database version for backward compatibility purpose. Only Major and minor versions are used and should be set (e.g., 0.19 vs 0.20 or 1.0 vs 2.0). Patch version is ignored (e.g., 0.20.1 and 0.20.3 will generate a 0.20 database).");
// Keypoints descriptors/detectors // Keypoints descriptors/detectors
+5
View File
@@ -126,6 +126,11 @@ add_executable(test_dbdriver test_dbdriver.cpp)
target_link_libraries(test_dbdriver gtest_main rtabmap_core) target_link_libraries(test_dbdriver gtest_main rtabmap_core)
add_test(NAME test_dbdriver COMMAND test_dbdriver) add_test(NAME test_dbdriver COMMAND test_dbdriver)
#DBDriverSqlite3.h
add_executable(test_dbdriversqlite3 test_dbdriversqlite3.cpp)
target_link_libraries(test_dbdriversqlite3 gtest_main rtabmap_core)
add_test(NAME test_dbdriversqlite3 COMMAND test_dbdriversqlite3)
#Signature.h #Signature.h
add_executable(test_signature test_signature.cpp) add_executable(test_signature test_signature.cpp)
target_link_libraries(test_signature gtest_main rtabmap_core) target_link_libraries(test_signature gtest_main rtabmap_core)
+175
View File
@@ -0,0 +1,175 @@
#include <gtest/gtest.h>
#include <rtabmap/core/DBDriver.h>
#include <rtabmap/core/DBDriverSqlite3.h>
#include <rtabmap/core/Signature.h>
#include <rtabmap/core/Parameters.h>
#include <rtabmap/utilite/UFile.h>
#include <rtabmap/utilite/UConversion.h>
using namespace rtabmap;
namespace {
std::string uniqueDbPath()
{
static int counter = 0;
return uFormat("/tmp/rtabmap_dbdriversqlite3_test_%d_%d.db", getpid(), ++counter);
}
class DBDriverSqlite3Fixture : public ::testing::Test
{
protected:
void SetUp() override
{
dbPath_ = uniqueDbPath();
driver_ = new DBDriverSqlite3();
ASSERT_TRUE(driver_->openConnection(dbPath_, true));
}
void TearDown() override
{
if(driver_)
{
driver_->closeConnection(false);
delete driver_;
driver_ = nullptr;
}
if(!dbPath_.empty())
{
UFile::erase(dbPath_.c_str());
}
}
void saveSignature(Signature * s)
{
driver_->asyncSave(s);
driver_->emptyTrashes(false);
}
std::string dbPath_;
DBDriverSqlite3 * driver_ = nullptr;
};
} // namespace
TEST(DBDriverSqlite3Test, CreateFactoryReturnsSqliteDriver)
{
DBDriver * driver = DBDriver::create();
ASSERT_NE(driver, nullptr);
EXPECT_NE(dynamic_cast<DBDriverSqlite3 *>(driver), nullptr);
driver->closeConnection(false);
delete driver;
}
TEST(DBDriverSqlite3Test, EmptyUrlIsInMemory)
{
DBDriverSqlite3 driver;
ASSERT_TRUE(driver.openConnection(""));
EXPECT_TRUE(driver.isInMemory());
EXPECT_TRUE(driver.isConnected());
EXPECT_FALSE(driver.getDatabaseVersion().empty());
driver.closeConnection(false);
}
TEST(DBDriverSqlite3Test, FileBackedIsNotInMemory)
{
const std::string path = uniqueDbPath();
DBDriverSqlite3 driver;
ASSERT_TRUE(driver.openConnection(path, true));
EXPECT_FALSE(driver.isInMemory());
EXPECT_EQ(driver.getUrl(), path);
driver.closeConnection(false);
UFile::erase(path.c_str());
}
TEST(DBDriverSqlite3Test, ParseParametersEnablesInMemory)
{
const std::string path = uniqueDbPath();
ParametersMap params;
params.insert(ParametersPair(Parameters::kDbSqlite3InMemory(), "true"));
DBDriverSqlite3 driver(params);
ASSERT_TRUE(driver.openConnection(path, true));
EXPECT_TRUE(driver.isInMemory());
EXPECT_TRUE(driver.isConnected());
driver.asyncSave(new Signature(1));
driver.emptyTrashes(false);
EXPECT_EQ(driver.getTotalNodesSize(), 1);
driver.closeConnection(false);
UFile::erase(path.c_str());
}
TEST(DBDriverSqlite3Test, InMemorySaveToFileOnClose)
{
const std::string path = uniqueDbPath();
ParametersMap params;
params.insert(ParametersPair(Parameters::kDbSqlite3InMemory(), "true"));
DBDriverSqlite3 driver(params);
ASSERT_TRUE(driver.openConnection(path, true));
EXPECT_TRUE(driver.isInMemory());
driver.asyncSave(new Signature(1, 5, 1, 50.0, "sqlite_mem", Transform(1.f, 0.f, 0.f, 0.f, 0.f, 0.f)));
driver.emptyTrashes(false);
driver.closeConnection(true, path);
DBDriverSqlite3 driver2;
ASSERT_TRUE(driver2.openConnection(path));
EXPECT_FALSE(driver2.isInMemory());
Signature * loaded = driver2.loadSignature(1);
ASSERT_NE(loaded, nullptr);
EXPECT_EQ(loaded->getLabel(), "sqlite_mem");
delete loaded;
driver2.closeConnection(false);
UFile::erase(path.c_str());
}
TEST_F(DBDriverSqlite3Fixture, SetPragmasWhileConnected)
{
EXPECT_NO_THROW(driver_->setCacheSize(4000));
EXPECT_NO_THROW(driver_->setJournalMode(1));
EXPECT_NO_THROW(driver_->setSynchronous(1));
EXPECT_NO_THROW(driver_->setTempStore(2));
EXPECT_TRUE(driver_->isConnected());
}
TEST_F(DBDriverSqlite3Fixture, SaveAndLoadSignature)
{
saveSignature(new Signature(1, 5, 7, 100.0, "node1", Transform(1.f, 2.f, 3.f, 0.f, 0.f, 0.f)));
EXPECT_EQ(driver_->getTotalNodesSize(), 1);
Signature * loaded = driver_->loadSignature(1);
ASSERT_NE(loaded, nullptr);
EXPECT_EQ(loaded->id(), 1);
EXPECT_EQ(loaded->getLabel(), "node1");
EXPECT_EQ(loaded->getWeight(), 7);
delete loaded;
}
TEST_F(DBDriverSqlite3Fixture, ReopenPreservesData)
{
saveSignature(new Signature(1, 5, 3, 50.0, "persist", Transform(0.5f, 0.f, 0.f, 0.f, 0.f, 0.f)));
const std::string path = dbPath_;
driver_->closeConnection(true);
delete driver_;
driver_ = nullptr;
driver_ = new DBDriverSqlite3();
ASSERT_TRUE(driver_->openConnection(path));
Signature * loaded = driver_->loadSignature(1);
ASSERT_NE(loaded, nullptr);
EXPECT_EQ(loaded->getLabel(), "persist");
delete loaded;
}
TEST_F(DBDriverSqlite3Fixture, ExecuteNoResultPragma)
{
EXPECT_NO_THROW(driver_->executeNoResult("PRAGMA cache_size=8000;"));
EXPECT_TRUE(driver_->isConnected());
}