#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace xrpl { class SociDB_test final : public TestSuite { private: static void setupSQLiteConfig(BasicConfig& config, boost::filesystem::path const& dbPath) { config.overwrite("sqdb", "backend", "sqlite"); auto value = dbPath.string(); if (!value.empty()) config.legacy("database_path", value); } static void cleanupDatabaseDir(boost::filesystem::path const& dbPath) { using namespace boost::filesystem; if (!exists(dbPath) || !is_directory(dbPath) || !is_empty(dbPath)) return; remove(dbPath); } static void setupDatabaseDir(boost::filesystem::path const& dbPath) { using namespace boost::filesystem; if (!exists(dbPath)) { create_directory(dbPath); return; } if (!is_directory(dbPath)) { // someone created a file where we want to put out directory Throw("Cannot create directory: " + dbPath.string()); } } static boost::filesystem::path getDatabasePath() { return boost::filesystem::current_path() / "socidb_test_databases"; } public: SociDB_test() { try { setupDatabaseDir(getDatabasePath()); } catch (std::exception const&) // NOLINT(bugprone-empty-catch) { } } ~SociDB_test() override { try { cleanupDatabaseDir(getDatabasePath()); } catch (std::exception const&) // NOLINT(bugprone-empty-catch) { } } void testSQLiteFileNames() { // confirm that files are given the correct extensions testcase("sqliteFileNames"); BasicConfig c; setupSQLiteConfig(c, getDatabasePath()); std::vector> const d( {{"peerfinder", ".sqlite"}, {"state", ".db"}, {"random", ".db"}, {"validators", ".sqlite"}}); for (auto const& i : d) { DBConfig const sc(c, i.first); BEAST_EXPECT(boost::ends_with(sc.connectionString(), i.first + i.second)); } } void testSQLiteSession() { testcase("open"); BasicConfig c; setupSQLiteConfig(c, getDatabasePath()); DBConfig const sc(c, "SociTestDB"); std::vector const stringData({"String1", "String2", "String3"}); std::vector const intData({1, 2, 3}); auto checkValues = [this, &stringData, &intData](soci::session& s) { // Check values in db std::vector stringResult(20 * stringData.size()); std::vector intResult(20 * intData.size()); s << "SELECT StringData, IntData FROM SociTestTable;", soci::into(stringResult), soci::into(intResult); BEAST_EXPECT( stringResult.size() == stringData.size() && intResult.size() == intData.size()); for (int i = 0; i < stringResult.size(); ++i) { auto si = std::distance( stringData.begin(), std::ranges::find(stringData, stringResult[i])); auto ii = std::distance(intData.begin(), std::ranges::find(intData, intResult[i])); BEAST_EXPECT(si == ii && si < stringResult.size()); } }; { soci::session s; sc.open(s); s << "CREATE TABLE IF NOT EXISTS SociTestTable (" " Key INTEGER PRIMARY KEY," " StringData TEXT," " IntData INTEGER" ");"; s << "INSERT INTO SociTestTable (StringData, IntData) VALUES " "(:stringData, :intData);", soci::use(stringData), soci::use(intData); checkValues(s); } { // Check values in db after session was closed soci::session s; sc.open(s); checkValues(s); } { namespace bfs = boost::filesystem; // Remove the database bfs::path const dbPath(sc.connectionString()); if (bfs::is_regular_file(dbPath)) bfs::remove(dbPath); } } void testSQLiteSelect() { testcase("select"); BasicConfig c; setupSQLiteConfig(c, getDatabasePath()); DBConfig const sc(c, "SociTestDB"); std::vector const ubid( {(std::uint64_t)std::numeric_limits::max(), 20, 30}); std::vector const bid({-10, -20, -30}); std::vector const uid({std::numeric_limits::max(), 2, 3}); std::vector const id({-1, -2, -3}); { soci::session s; sc.open(s); s << "DROP TABLE IF EXISTS STT;"; s << "CREATE TABLE STT (" " I INTEGER," " UI INTEGER UNSIGNED," " BI BIGINT," " UBI BIGINT UNSIGNED" ");"; s << "INSERT INTO STT (I, UI, BI, UBI) VALUES " "(:id, :idu, :bid, :bidu);", soci::use(id), soci::use(uid), soci::use(bid), soci::use(ubid); try { std::int32_t ig = 0; std::uint32_t uig = 0; std::int64_t big = 0; std::uint64_t ubig = 0; s << "SELECT I, UI, BI, UBI from STT;", soci::into(ig), soci::into(uig), soci::into(big), soci::into(ubig); BEAST_EXPECT(ig == id[0] && uig == uid[0] && big == bid[0] && ubig == ubid[0]); } catch (std::exception&) { fail(); } try { // SOCI requires boost::optional (not std::optional) as // parameters. boost::optional ig; // Known bug: https://github.com/SOCI/soci/issues/926 // boost::optional uig; uint32_t uig = 0; boost::optional big; boost::optional ubig; s << "SELECT I, UI, BI, UBI from STT;", soci::into(ig), soci::into(uig), soci::into(big), soci::into(ubig); BEAST_EXPECT(*ig == id[0] && uig == uid[0] && *big == bid[0] && *ubig == ubid[0]); } catch (std::exception&) { fail(); } // There are too many issues when working with soci::row and // boost::tuple. DO NOT USE soci row! I had a set of workarounds to // make soci row less error prone, I'm keeping these tests in case I // try to add soci::row and boost::tuple back into soci. #if 0 try { std::int32_t ig = 0; std::uint32_t uig = 0; std::int64_t big = 0; std::uint64_t ubig = 0; soci::row r; s << "SELECT I, UI, BI, UBI from STT", soci::into (r); ig = r.get(0); uig = r.get(1); big = r.get(2); ubig = r.get(3); BEAST_EXPECT(ig == id[0] && uig == uid[0] && big == bid[0] && ubig == ubid[0]); } catch (std::exception&) { fail (); } try { std::int32_t ig = 0; std::uint32_t uig = 0; std::int64_t big = 0; std::uint64_t ubig = 0; soci::row r; s << "SELECT I, UI, BI, UBI from STT", soci::into (r); ig = r.get("I"); uig = r.get("UI"); big = r.get("BI"); ubig = r.get("UBI"); BEAST_EXPECT(ig == id[0] && uig == uid[0] && big == bid[0] && ubig == ubid[0]); } catch (std::exception&) { fail (); } try { boost::tuple d; s << "SELECT I, UI, BI, UBI from STT", soci::into (d); BEAST_EXPECT(get<0>(d) == id[0] && get<1>(d) == uid[0] && get<2>(d) == bid[0] && get<3>(d) == ubid[0]); } catch (std::exception&) { fail (); } #endif } { namespace bfs = boost::filesystem; // Remove the database bfs::path const dbPath(sc.connectionString()); if (bfs::is_regular_file(dbPath)) bfs::remove(dbPath); } } void testSQLiteDeleteWithSubselect() { testcase("deleteWithSubselect"); BasicConfig c; setupSQLiteConfig(c, getDatabasePath()); DBConfig const sc(c, "SociTestDB"); { soci::session s; sc.open(s); std::string_view const dbInit[] = { "BEGIN TRANSACTION;", "CREATE TABLE Ledgers ( \ LedgerHash CHARACTER(64) PRIMARY KEY, \ LedgerSeq BIGINT UNSIGNED \ );", "CREATE INDEX SeqLedger ON Ledgers(LedgerSeq);"}; for (auto const c : dbInit) s << c; char lh[65]; memset(lh, 'a', 64); lh[64] = '\0'; int toIncIndex = 63; int const numRows = 16; std::vector ledgerHashes; std::vector ledgerIndexes; ledgerHashes.reserve(numRows); ledgerIndexes.reserve(numRows); for (int i = 0; i < numRows; ++i) { ++lh[toIncIndex]; if (lh[toIncIndex] == 'z') --toIncIndex; ledgerHashes.emplace_back(lh); ledgerIndexes.emplace_back(i); } s << "INSERT INTO Ledgers (LedgerHash, LedgerSeq) VALUES " "(:lh, :li);", soci::use(ledgerHashes), soci::use(ledgerIndexes); std::vector ledgersLS(numRows * 2); s << "SELECT LedgerSeq FROM Ledgers;", soci::into(ledgersLS); BEAST_EXPECT(ledgersLS.size() == numRows); } namespace bfs = boost::filesystem; // Remove the database bfs::path const dbPath(sc.connectionString()); if (bfs::is_regular_file(dbPath)) bfs::remove(dbPath); } void run() override { testSQLiteFileNames(); testSQLiteSession(); testSQLiteSelect(); testSQLiteDeleteWithSubselect(); } }; BEAST_DEFINE_TESTSUITE(SociDB, core, xrpl); } // namespace xrpl