//------------------------------------------------------------------------------ /* 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 (rounded down to KB) // Initially should be < 1KB, so rounds down to 0 // Note: These are 0 due to rounding, not because there's literally // no data BEAST_EXPECT(allKB == 0); // < 1024 bytes rounds to 0 KB BEAST_EXPECT(ledgerKB == 0); // < 1024 bytes rounds to 0 KB BEAST_EXPECT(txKB == 0); // < 1024 bytes rounds to 0 KB } else { // SQLite reports cache/engine memory which has overhead even when // empty Just verify the functions return reasonable values 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 some increase BEAST_EXPECT(newAllKB >= 1); // Should have at least 1KB total BEAST_EXPECT( newTxKB >= 0); // Transactions added (might still be < 1KB) BEAST_EXPECT( newLedgerKB >= 0); // Ledger data (might still be < 1KB) // Key relationships BEAST_EXPECT(newAllKB >= newLedgerKB + newTxKB); // Total >= parts BEAST_EXPECT(newAllKB >= allKB); // Should increase or stay same BEAST_EXPECT(newTxKB >= txKB); // Should increase or stay same } else { // SQLite: Memory usage should not decrease after adding data // Values might increase due to cache growth BEAST_EXPECT(newAllKB >= allKB); BEAST_EXPECT(newLedgerKB >= ledgerKB); BEAST_EXPECT(newTxKB >= txKB); // SQLite's getKBUsedAll is global memory, should be >= parts BEAST_EXPECT(newAllKB >= newLedgerKB); BEAST_EXPECT(newAllKB >= newTxKB); } // Test space availability // Both SQLite and RWDB use in-memory databases in standalone mode, // so file-based space checks don't apply to either backend. // Skip these checks for both. // if (backend == "rwdb") // { // BEAST_EXPECT(db.ledgerDbHasSpace(env.app().config())); // BEAST_EXPECT(db.transactionDbHasSpace(env.app().config())); // } // 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"}; // Default to all valid backends if no arg specified if (unittest_arg.empty()) return {validBackends.begin(), validBackends.end()}; std::set backends; // Use set to avoid duplicates std::stringstream ss(unittest_arg); 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