//------------------------------------------------------------------------------ /* This file is part of rippled: https://github.com/ripple/rippled Copyright (c) 2025 Ripple Labs Inc. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL , DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ //============================================================================== #include #include #include #include #include #include #include namespace ripple { namespace test { class RelationalDatabase_test : public beast::unit_test::suite { private: // Helper to get SQLiteDatabase* (works for both SQLite and RWDB since RWDB // inherits from SQLiteDatabase) static SQLiteDatabase* getInterface(Application& app) { return dynamic_cast(&app.getRelationalDatabase()); } static SQLiteDatabase* getInterface(RelationalDatabase& db) { return dynamic_cast(&db); } static std::unique_ptr makeConfig(std::string const& backend) { auto config = test::jtx::envconfig(); // Sqlite backend doesn't need a database_path as it will just use // in-memory databases when in standalone mode anyway. config->overwrite(SECTION_RELATIONAL_DB, "backend", backend); return config; } public: RelationalDatabase_test() = default; void testBasicInitialization( std::string const& backend, std::unique_ptr config) { testcase("Basic initialization and empty database - " + backend); using namespace test::jtx; Env env(*this, std::move(config)); auto& db = env.app().getRelationalDatabase(); // Test empty database state BEAST_EXPECT(db.getMinLedgerSeq() == 2); BEAST_EXPECT(db.getMaxLedgerSeq() == 2); BEAST_EXPECT(db.getNewestLedgerInfo()->seq == 2); auto* sqliteDb = getInterface(db); BEAST_EXPECT(sqliteDb != nullptr); if (sqliteDb) { BEAST_EXPECT(!sqliteDb->getTransactionsMinLedgerSeq().has_value()); BEAST_EXPECT( !sqliteDb->getAccountTransactionsMinLedgerSeq().has_value()); auto ledgerCount = sqliteDb->getLedgerCountMinMax(); BEAST_EXPECT(ledgerCount.numberOfRows == 1); BEAST_EXPECT(ledgerCount.minLedgerSequence == 2); BEAST_EXPECT(ledgerCount.maxLedgerSequence == 2); } } void testLedgerSequenceOperations( std::string const& backend, std::unique_ptr config) { testcase("Ledger sequence operations - " + backend); using namespace test::jtx; config->LEDGER_HISTORY = 1000; Env env(*this, std::move(config)); auto& db = env.app().getRelationalDatabase(); // Create initial ledger Account alice("alice"); env.fund(XRP(10000), alice); env.close(); // Test basic sequence operations auto minSeq = db.getMinLedgerSeq(); auto maxSeq = db.getMaxLedgerSeq(); BEAST_EXPECT(minSeq.has_value()); BEAST_EXPECT(maxSeq.has_value()); BEAST_EXPECT(*minSeq == 2); BEAST_EXPECT(*maxSeq == 3); // Create more ledgers env(pay(alice, Account("bob"), XRP(1000))); env.close(); env(pay(alice, Account("carol"), XRP(500))); env.close(); // Verify sequence updates minSeq = db.getMinLedgerSeq(); maxSeq = db.getMaxLedgerSeq(); BEAST_EXPECT(*minSeq == 2); BEAST_EXPECT(*maxSeq == 5); auto* sqliteDb = getInterface(db); if (sqliteDb) { auto ledgerCount = sqliteDb->getLedgerCountMinMax(); BEAST_EXPECT(ledgerCount.numberOfRows == 4); BEAST_EXPECT(ledgerCount.minLedgerSequence == 2); BEAST_EXPECT(ledgerCount.maxLedgerSequence == 5); } } void testLedgerInfoOperations( std::string const& backend, std::unique_ptr config) { testcase("Ledger info retrieval operations - " + backend); using namespace test::jtx; config->LEDGER_HISTORY = 1000; Env env(*this, std::move(config)); auto* db = getInterface(env.app()); Account alice("alice"); env.fund(XRP(10000), alice); env.close(); // Test getNewestLedgerInfo auto newestLedger = db->getNewestLedgerInfo(); BEAST_EXPECT(newestLedger.has_value()); BEAST_EXPECT(newestLedger->seq == 3); // Test getLedgerInfoByIndex auto ledgerByIndex = db->getLedgerInfoByIndex(3); BEAST_EXPECT(ledgerByIndex.has_value()); BEAST_EXPECT(ledgerByIndex->seq == 3); BEAST_EXPECT(ledgerByIndex->hash == newestLedger->hash); // Test getLedgerInfoByHash auto ledgerByHash = db->getLedgerInfoByHash(newestLedger->hash); BEAST_EXPECT(ledgerByHash.has_value()); BEAST_EXPECT(ledgerByHash->seq == 3); BEAST_EXPECT(ledgerByHash->hash == newestLedger->hash); // Test getLimitedOldestLedgerInfo auto oldestLedger = db->getLimitedOldestLedgerInfo(2); BEAST_EXPECT(oldestLedger.has_value()); BEAST_EXPECT(oldestLedger->seq == 2); // Test getLimitedNewestLedgerInfo auto limitedNewest = db->getLimitedNewestLedgerInfo(2); BEAST_EXPECT(limitedNewest.has_value()); BEAST_EXPECT(limitedNewest->seq == 3); // Test invalid queries auto invalidLedger = db->getLedgerInfoByIndex(999); BEAST_EXPECT(!invalidLedger.has_value()); uint256 invalidHash; auto invalidHashLedger = db->getLedgerInfoByHash(invalidHash); BEAST_EXPECT(!invalidHashLedger.has_value()); } void testHashOperations( std::string const& backend, std::unique_ptr config) { testcase("Hash retrieval operations - " + backend); using namespace test::jtx; config->LEDGER_HISTORY = 1000; Env env(*this, std::move(config)); auto& db = env.app().getRelationalDatabase(); Account alice("alice"); env.fund(XRP(10000), alice); env.close(); env(pay(alice, Account("bob"), XRP(1000))); env.close(); // Test getHashByIndex auto hash1 = db.getHashByIndex(3); auto hash2 = db.getHashByIndex(4); BEAST_EXPECT(hash1 != uint256()); BEAST_EXPECT(hash2 != uint256()); BEAST_EXPECT(hash1 != hash2); // Test getHashesByIndex (single) auto hashPair = db.getHashesByIndex(4); BEAST_EXPECT(hashPair.has_value()); BEAST_EXPECT(hashPair->ledgerHash == hash2); BEAST_EXPECT(hashPair->parentHash == hash1); // Test getHashesByIndex (range) auto hashRange = db.getHashesByIndex(3, 4); BEAST_EXPECT(hashRange.size() == 2); BEAST_EXPECT(hashRange[3].ledgerHash == hash1); BEAST_EXPECT(hashRange[4].ledgerHash == hash2); BEAST_EXPECT(hashRange[4].parentHash == hash1); // Test invalid hash queries auto invalidHash = db.getHashByIndex(999); BEAST_EXPECT(invalidHash == uint256()); auto invalidHashPair = db.getHashesByIndex(999); BEAST_EXPECT(!invalidHashPair.has_value()); auto emptyRange = db.getHashesByIndex(10, 5); // max < min BEAST_EXPECT(emptyRange.empty()); } void testTransactionOperations( std::string const& backend, std::unique_ptr config) { testcase("Transaction storage and retrieval - " + backend); using namespace test::jtx; config->LEDGER_HISTORY = 1000; Env env(*this, std::move(config)); auto& db = env.app().getRelationalDatabase(); Account alice("alice"); Account bob("bob"); env.fund(XRP(10000), alice, bob); env.close(); auto* sqliteDb = getInterface(db); BEAST_EXPECT(sqliteDb != nullptr); if (!sqliteDb) return; // Test initial transaction counts after funding auto initialTxCount = sqliteDb->getTransactionCount(); auto initialAcctTxCount = sqliteDb->getAccountTransactionCount(); BEAST_EXPECT(initialTxCount == 4); BEAST_EXPECT(initialAcctTxCount == 6); // Create transactions env(pay(alice, bob, XRP(1000))); env.close(); env(pay(bob, alice, XRP(500))); env.close(); // Test transaction counts after creation auto txCount = sqliteDb->getTransactionCount(); auto acctTxCount = sqliteDb->getAccountTransactionCount(); BEAST_EXPECT(txCount == 6); BEAST_EXPECT(acctTxCount == 10); // Test transaction retrieval uint256 invalidTxId; error_code_i ec; auto invalidTxResult = sqliteDb->getTransaction(invalidTxId, std::nullopt, ec); BEAST_EXPECT(std::holds_alternative(invalidTxResult)); // Test transaction history auto txHistory = db.getTxHistory(0); BEAST_EXPECT(!txHistory.empty()); BEAST_EXPECT(txHistory.size() == 6); // Test with valid transaction range auto minSeq = sqliteDb->getTransactionsMinLedgerSeq(); auto maxSeq = db.getMaxLedgerSeq(); if (minSeq && maxSeq) { ClosedInterval range(*minSeq, *maxSeq); auto rangeResult = sqliteDb->getTransaction(invalidTxId, range, ec); auto searched = std::get(rangeResult); BEAST_EXPECT( searched == TxSearched::all || searched == TxSearched::some); } } void testAccountTransactionOperations( std::string const& backend, std::unique_ptr config) { testcase("Account transaction operations - " + backend); using namespace test::jtx; config->LEDGER_HISTORY = 1000; Env env(*this, std::move(config)); auto& db = env.app().getRelationalDatabase(); Account alice("alice"); Account bob("bob"); Account carol("carol"); env.fund(XRP(10000), alice, bob, carol); env.close(); auto* sqliteDb = getInterface(db); BEAST_EXPECT(sqliteDb != nullptr); if (!sqliteDb) return; // Create multiple transactions involving alice env(pay(alice, bob, XRP(1000))); env.close(); env(pay(bob, alice, XRP(500))); env.close(); env(pay(alice, carol, XRP(250))); env.close(); auto minSeq = db.getMinLedgerSeq(); auto maxSeq = db.getMaxLedgerSeq(); if (!minSeq || !maxSeq) return; // Test getOldestAccountTxs RelationalDatabase::AccountTxOptions options{ alice.id(), *minSeq, *maxSeq, 0, 10, false}; auto oldestTxs = sqliteDb->getOldestAccountTxs(options); BEAST_EXPECT(oldestTxs.size() == 5); // Test getNewestAccountTxs auto newestTxs = sqliteDb->getNewestAccountTxs(options); BEAST_EXPECT(newestTxs.size() == 5); // Test binary format versions auto oldestTxsB = sqliteDb->getOldestAccountTxsB(options); BEAST_EXPECT(oldestTxsB.size() == 5); auto newestTxsB = sqliteDb->getNewestAccountTxsB(options); BEAST_EXPECT(newestTxsB.size() == 5); // Test with limit options.limit = 1; auto limitedTxs = sqliteDb->getOldestAccountTxs(options); BEAST_EXPECT(limitedTxs.size() == 1); // Test with offset options.limit = 10; options.offset = 1; auto offsetTxs = sqliteDb->getOldestAccountTxs(options); BEAST_EXPECT(offsetTxs.size() == 4); // Test with invalid account { Account invalidAccount("invalid"); RelationalDatabase::AccountTxOptions invalidOptions{ invalidAccount.id(), *minSeq, *maxSeq, 0, 10, false}; auto invalidAccountTxs = sqliteDb->getOldestAccountTxs(invalidOptions); BEAST_EXPECT(invalidAccountTxs.empty()); } } void testAccountTransactionPaging( std::string const& backend, std::unique_ptr config) { testcase("Account transaction paging operations - " + backend); using namespace test::jtx; config->LEDGER_HISTORY = 1000; Env env(*this, std::move(config)); auto& db = env.app().getRelationalDatabase(); Account alice("alice"); Account bob("bob"); env.fund(XRP(10000), alice, bob); env.close(); auto* sqliteDb = getInterface(db); BEAST_EXPECT(sqliteDb != nullptr); if (!sqliteDb) return; // Create multiple transactions for paging for (int i = 0; i < 5; ++i) { env(pay(alice, bob, XRP(100 + i))); env.close(); } auto minSeq = db.getMinLedgerSeq(); auto maxSeq = db.getMaxLedgerSeq(); if (!minSeq || !maxSeq) return; RelationalDatabase::AccountTxPageOptions pageOptions{ alice.id(), *minSeq, *maxSeq, std::nullopt, 2, false}; // Test oldestAccountTxPage auto [oldestPage, oldestMarker] = sqliteDb->oldestAccountTxPage(pageOptions); BEAST_EXPECT(oldestPage.size() == 2); BEAST_EXPECT(oldestMarker.has_value() == true); // Test newestAccountTxPage auto [newestPage, newestMarker] = sqliteDb->newestAccountTxPage(pageOptions); BEAST_EXPECT(newestPage.size() == 2); BEAST_EXPECT(newestMarker.has_value() == true); // Test binary versions auto [oldestPageB, oldestMarkerB] = sqliteDb->oldestAccountTxPageB(pageOptions); BEAST_EXPECT(oldestPageB.size() == 2); auto [newestPageB, newestMarkerB] = sqliteDb->newestAccountTxPageB(pageOptions); BEAST_EXPECT(newestPageB.size() == 2); // Test with marker continuation if (oldestMarker.has_value()) { pageOptions.marker = oldestMarker; auto [continuedPage, continuedMarker] = sqliteDb->oldestAccountTxPage(pageOptions); BEAST_EXPECT(continuedPage.size() == 2); } } void testDeletionOperations( std::string const& backend, std::unique_ptr config) { testcase("Deletion operations - " + backend); using namespace test::jtx; config->LEDGER_HISTORY = 1000; Env env(*this, std::move(config)); auto& db = env.app().getRelationalDatabase(); Account alice("alice"); Account bob("bob"); env.fund(XRP(10000), alice, bob); env.close(); auto* sqliteDb = getInterface(db); BEAST_EXPECT(sqliteDb != nullptr); if (!sqliteDb) return; // Create multiple ledgers and transactions for (int i = 0; i < 3; ++i) { env(pay(alice, bob, XRP(100 + i))); env.close(); } auto initialTxCount = sqliteDb->getTransactionCount(); BEAST_EXPECT(initialTxCount == 7); auto initialAcctTxCount = sqliteDb->getAccountTransactionCount(); BEAST_EXPECT(initialAcctTxCount == 12); auto initialLedgerCount = sqliteDb->getLedgerCountMinMax(); BEAST_EXPECT(initialLedgerCount.numberOfRows == 5); auto maxSeq = db.getMaxLedgerSeq(); if (!maxSeq || *maxSeq <= 2) return; // Test deleteTransactionByLedgerSeq sqliteDb->deleteTransactionByLedgerSeq(*maxSeq); auto txCountAfterDelete = sqliteDb->getTransactionCount(); BEAST_EXPECT(txCountAfterDelete == 6); // Test deleteTransactionsBeforeLedgerSeq sqliteDb->deleteTransactionsBeforeLedgerSeq(*maxSeq - 1); auto txCountAfterBulkDelete = sqliteDb->getTransactionCount(); BEAST_EXPECT(txCountAfterBulkDelete == 1); // Test deleteAccountTransactionsBeforeLedgerSeq sqliteDb->deleteAccountTransactionsBeforeLedgerSeq(*maxSeq - 1); auto acctTxCountAfterDelete = sqliteDb->getAccountTransactionCount(); BEAST_EXPECT(acctTxCountAfterDelete == 4); // Test deleteBeforeLedgerSeq auto minSeq = db.getMinLedgerSeq(); if (minSeq) { sqliteDb->deleteBeforeLedgerSeq(*minSeq + 1); auto ledgerCountAfterDelete = sqliteDb->getLedgerCountMinMax(); BEAST_EXPECT(ledgerCountAfterDelete.numberOfRows == 4); } } void testDatabaseSpaceOperations( std::string const& backend, std::unique_ptr config) { testcase("Database space and size operations - " + backend); using namespace test::jtx; Env env(*this, std::move(config)); auto& db = env.app().getRelationalDatabase(); auto* sqliteDb = getInterface(db); BEAST_EXPECT(sqliteDb != nullptr); if (!sqliteDb) return; // Test size queries auto allKB = sqliteDb->getKBUsedAll(); auto ledgerKB = sqliteDb->getKBUsedLedger(); auto txKB = sqliteDb->getKBUsedTransaction(); if (backend == "rwdb") { // RWDB reports actual data memory // After closing genesis ledger, should have some data BEAST_EXPECT(allKB == 0); BEAST_EXPECT(ledgerKB == 0); BEAST_EXPECT(txKB == 0); } else { // Will not assert on sqlite usage except that it's non-zero BEAST_EXPECT(allKB > 0); BEAST_EXPECT(ledgerKB > 0); BEAST_EXPECT(txKB > 0); } // Create some data and verify size increases Account alice("alice"); env.fund(XRP(10000), alice); env.close(); auto newAllKB = sqliteDb->getKBUsedAll(); auto newLedgerKB = sqliteDb->getKBUsedLedger(); auto newTxKB = sqliteDb->getKBUsedTransaction(); if (backend == "rwdb") { // RWDB reports actual data memory // After adding data, should see increases BEAST_EXPECT(newAllKB == 2); BEAST_EXPECT(newLedgerKB == 0); BEAST_EXPECT(newTxKB == 1); // For RWDB: All = Ledger + Transaction + overhead // So All should be >= Ledger + Transaction BEAST_EXPECT(newAllKB >= newLedgerKB + newTxKB); } else { // SQLite: The values should stay roughly the same or increase // slightly since SQLite reports global memory usage BEAST_EXPECT(newAllKB >= allKB); // Should not decrease BEAST_EXPECT(newLedgerKB >= ledgerKB); // Should not decrease BEAST_EXPECT(newTxKB >= txKB); // Should not decrease } // Test space availability // Note: SQLite in standalone mode uses in-memory databases, // so file-based space checks don't apply if (backend == "rwdb") { // RWDB uses actual files, so space checks are valid BEAST_EXPECT(db.ledgerDbHasSpace(env.app().config())); BEAST_EXPECT(db.transactionDbHasSpace(env.app().config())); } // Skip space checks for SQLite as it uses in-memory databases in // standalone mode // Test database closure operations (should not throw) try { sqliteDb->closeLedgerDB(); sqliteDb->closeTransactionDB(); } catch (std::exception const& e) { BEAST_EXPECT(false); // Should not throw } } void testTransactionMinLedgerSeq( std::string const& backend, std::unique_ptr config) { testcase("Transaction minimum ledger sequence tracking - " + backend); using namespace test::jtx; config->LEDGER_HISTORY = 1000; Env env(*this, std::move(config)); auto& db = env.app().getRelationalDatabase(); auto* sqliteDb = getInterface(db); BEAST_EXPECT(sqliteDb != nullptr); if (!sqliteDb) return; // Initially should have no transactions BEAST_EXPECT(!sqliteDb->getTransactionsMinLedgerSeq().has_value()); BEAST_EXPECT( !sqliteDb->getAccountTransactionsMinLedgerSeq().has_value()); Account alice("alice"); Account bob("bob"); env.fund(XRP(10000), alice, bob); env.close(); // Create first transaction env(pay(alice, bob, XRP(1000))); env.close(); auto txMinSeq = sqliteDb->getTransactionsMinLedgerSeq(); auto acctTxMinSeq = sqliteDb->getAccountTransactionsMinLedgerSeq(); BEAST_EXPECT(txMinSeq.has_value()); BEAST_EXPECT(acctTxMinSeq.has_value()); BEAST_EXPECT(*txMinSeq == 3); BEAST_EXPECT(*acctTxMinSeq == 3); // Create more transactions env(pay(bob, alice, XRP(500))); env.close(); env(pay(alice, bob, XRP(250))); env.close(); // Min sequences should remain the same (first transaction ledger) auto newTxMinSeq = sqliteDb->getTransactionsMinLedgerSeq(); auto newAcctTxMinSeq = sqliteDb->getAccountTransactionsMinLedgerSeq(); BEAST_EXPECT(newTxMinSeq == txMinSeq); BEAST_EXPECT(newAcctTxMinSeq == acctTxMinSeq); } std::vector static getBackends(std::string const& unittest_arg) { // Valid backends static const std::set validBackends = {"sqlite", "rwdb"}; std::set backends; // Use set to avoid duplicates std::string argStr = unittest_arg.empty() ? "sqlite,rwdb" : unittest_arg; std::stringstream ss(argStr); std::string backend; while (std::getline(ss, backend, ',')) { if (!backend.empty()) { // Validate backend if (validBackends.contains(backend)) { backends.insert(backend); } } } // Return as vector (sorted due to set) return {backends.begin(), backends.end()}; } void run() override { auto backends = getBackends(arg()); if (backends.empty()) { fail("no valid backend specified: '" + arg() + "'"); } for (auto const& backend : backends) { testBasicInitialization(backend, makeConfig(backend)); testLedgerSequenceOperations(backend, makeConfig(backend)); testLedgerInfoOperations(backend, makeConfig(backend)); testHashOperations(backend, makeConfig(backend)); testTransactionOperations(backend, makeConfig(backend)); testAccountTransactionOperations(backend, makeConfig(backend)); testAccountTransactionPaging(backend, makeConfig(backend)); testDeletionOperations(backend, makeConfig(backend)); testDatabaseSpaceOperations(backend, makeConfig(backend)); testTransactionMinLedgerSeq(backend, makeConfig(backend)); } } }; BEAST_DEFINE_TESTSUITE(RelationalDatabase, rdb, ripple); } // namespace test } // namespace ripple