diff --git a/Builds/CMake/RippledCore.cmake b/Builds/CMake/RippledCore.cmake index 6fc33cdee..d0446aff3 100644 --- a/Builds/CMake/RippledCore.cmake +++ b/Builds/CMake/RippledCore.cmake @@ -995,6 +995,11 @@ if (tests) subdir: resource #]===============================] src/test/resource/Logic_test.cpp + #[===============================[ + test sources: + subdir: rdb + #]===============================] + src/test/rdb/RelationalDatabase_test.cpp #[===============================[ test sources: subdir: rpc diff --git a/Builds/levelization/results/ordering.txt b/Builds/levelization/results/ordering.txt index 12df1a86e..e409855a7 100644 --- a/Builds/levelization/results/ordering.txt +++ b/Builds/levelization/results/ordering.txt @@ -186,6 +186,10 @@ test.protocol > ripple.crypto test.protocol > ripple.json test.protocol > ripple.protocol test.protocol > test.toplevel +test.rdb > ripple.app +test.rdb > ripple.core +test.rdb > test.jtx +test.rdb > test.toplevel test.resource > ripple.basics test.resource > ripple.beast test.resource > ripple.resource diff --git a/src/test/app/SHAMapStore_test.cpp b/src/test/app/SHAMapStore_test.cpp index 9f4269667..8a3ca0f89 100644 --- a/src/test/app/SHAMapStore_test.cpp +++ b/src/test/app/SHAMapStore_test.cpp @@ -206,8 +206,6 @@ public: auto const firstSeq = waitForReady(env); auto lastRotated = firstSeq - 1; - // Create ledgers 4-11 with transactions (8 ledgers) - // These transactions will survive the first rotation but not the second for (auto i = firstSeq + 1; i < deleteInterval + firstSeq; ++i) { env.fund(XRP(10000), noripple("test" + std::to_string(i))); @@ -220,47 +218,6 @@ public: SQLiteDatabase* const db = dynamic_cast(&env.app().getRelationalDatabase()); - - // Simple helper to show what's in the database - auto showDBState = [&env, &db]( - const std::string& when, - const std::string& expectation = "") { - auto [ledgerCount, firstLedger, lastLedger] = - db->getLedgerCountMinMax(); - auto txCount = db->getTransactionCount(); - auto accTxCount = db->getAccountTransactionCount(); - - std::cout << "\n" << when << ":" << std::endl; - std::cout << " Ledgers: " << firstLedger << "-" << lastLedger - << " (keeping " << ledgerCount << " ledgers)" - << std::endl; - std::cout << " Transactions: " << txCount; - std::cout << ", Account Transactions: " << accTxCount; - if (!expectation.empty()) - { - std::cout << " " << expectation; - } - std::cout << std::endl; - - // Also show RPC counts to see in-memory objects - auto const result = env.rpc("get_counts")[jss::result]; - if (result.isMember("ripple::STTx") || - result.isMember("ripple::Transaction")) - { - std::cout << " In-memory objects: " - << "STTx=" - << (result.isMember("ripple::STTx") - ? result["ripple::STTx"].asInt() - : 0) - << ", Transaction=" - << (result.isMember("ripple::Transaction") - ? result["ripple::Transaction"].asInt() - : 0) - << std::endl; - } - }; - - showDBState("Initial state"); BEAST_EXPECT(*db->getTransactionsMinLedgerSeq() == 3); for (auto i = 3; i < deleteInterval + lastRotated; ++i) @@ -272,18 +229,10 @@ public: getHash(ledgers[i]).length()); } - // After creating 8 more ledgers with transactions - showDBState("Before first rotation"); - - // Verify expected state ledgerCheck(env, deleteInterval + 1, 2); transactionCheck(env, deleteInterval); accountTransactionCheck(env, 2 * deleteInterval); - // Additional verification - BEAST_EXPECT(db->getTransactionCount() == deleteInterval); - BEAST_EXPECT(db->getAccountTransactionCount() == 2 * deleteInterval); - { // Closing one more ledger triggers a rotate env.close(); @@ -299,21 +248,11 @@ public: lastRotated = store.getLastRotated(); BEAST_EXPECT(lastRotated == 11); - showDBState( - "After FIRST rotation", - "(EXPECTED: still have 8 txns from ledgers 3-11)"); - - // First rotation: Deleted ledgers 1-2, kept 3-11 - // Transactions from ledgers 3-11 should STILL EXIST + // That took care of the fake hashes ledgerCheck(env, deleteInterval + 1, 3); - transactionCheck(env, deleteInterval); // Still have transactions! + transactionCheck(env, deleteInterval); accountTransactionCheck(env, 2 * deleteInterval); - // Additional verification - BEAST_EXPECT(db->getTransactionCount() == deleteInterval); - BEAST_EXPECT(db->getAccountTransactionCount() == 2 * deleteInterval); - BEAST_EXPECT(*db->getTransactionsMinLedgerSeq() == 3); - // The last iteration of this loop should trigger a rotate for (auto i = lastRotated - 1; i < lastRotated + deleteInterval - 1; ++i) @@ -337,18 +276,9 @@ public: BEAST_EXPECT(store.getLastRotated() == deleteInterval + lastRotated); - showDBState( - "After SECOND rotation", - "(EXPECTED: 0 txns - ledgers 3-11 were deleted)"); - ledgerCheck(env, deleteInterval + 1, lastRotated); - transactionCheck(env, 0); // NOW transactions should be gone! - accountTransactionCheck(env, 0); // Account transactions too! - - // Additional verification - all transactions should be gone - BEAST_EXPECT(db->getTransactionCount() == 0); - BEAST_EXPECT(db->getAccountTransactionCount() == 0); - BEAST_EXPECT(!db->getTransactionsMinLedgerSeq().has_value()); + transactionCheck(env, 0); + accountTransactionCheck(env, 0); } void diff --git a/src/test/rdb/RelationalDatabase_test.cpp b/src/test/rdb/RelationalDatabase_test.cpp new file mode 100644 index 000000000..9eee7c6d1 --- /dev/null +++ b/src/test/rdb/RelationalDatabase_test.cpp @@ -0,0 +1,632 @@ +//------------------------------------------------------------------------------ +/* + 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 + +namespace ripple { +namespace test { + +class RelationalDatabase_test : public beast::unit_test::suite +{ +public: + void + testBasicInitialization() + { + testcase("Basic initialization and empty database"); + + using namespace test::jtx; + auto config = envconfig(); + config->LEDGER_HISTORY = 1000; + + 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 = dynamic_cast(&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() + { + testcase("Ledger sequence operations"); + + using namespace test::jtx; + auto config = envconfig(); + 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 = dynamic_cast(&db); + if (sqliteDb) + { + auto ledgerCount = sqliteDb->getLedgerCountMinMax(); + BEAST_EXPECT(ledgerCount.numberOfRows == 4); + BEAST_EXPECT(ledgerCount.minLedgerSequence == 2); + BEAST_EXPECT(ledgerCount.maxLedgerSequence == 5); + } + } + + void + testLedgerInfoOperations() + { + testcase("Ledger info retrieval operations"); + + using namespace test::jtx; + auto config = envconfig(); + config->LEDGER_HISTORY = 1000; + + Env env(*this, std::move(config)); + auto* db = + dynamic_cast(&env.app().getRelationalDatabase()); + + 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() + { + testcase("Hash retrieval operations"); + + using namespace test::jtx; + auto config = envconfig(); + 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() + { + testcase("Transaction storage and retrieval"); + + using namespace test::jtx; + auto config = envconfig(); + 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 = dynamic_cast(&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() + { + testcase("Account transaction operations"); + + using namespace test::jtx; + auto config = envconfig(); + 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 = dynamic_cast(&db); + 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() + { + testcase("Account transaction paging operations"); + + using namespace test::jtx; + auto config = envconfig(); + 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 = dynamic_cast(&db); + 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() + { + testcase("Deletion operations"); + + using namespace test::jtx; + auto config = envconfig(); + 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 = dynamic_cast(&db); + 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() + { + testcase("Database space and size operations"); + + using namespace test::jtx; + auto config = envconfig(); + config->LEDGER_HISTORY = 1000; + + Env env(*this, std::move(config)); + auto& db = env.app().getRelationalDatabase(); + + auto* sqliteDb = dynamic_cast(&db); + if (!sqliteDb) + return; + + // Test space availability (should always be true for in-memory) + BEAST_EXPECT(db.ledgerDbHasSpace(env.app().config())); + BEAST_EXPECT(db.transactionDbHasSpace(env.app().config())); + + // Test size queries + auto allKB = sqliteDb->getKBUsedAll(); + auto ledgerKB = sqliteDb->getKBUsedLedger(); + auto txKB = sqliteDb->getKBUsedTransaction(); + + 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(); + + BEAST_EXPECT(newAllKB == 1); + BEAST_EXPECT(newLedgerKB == 0); + BEAST_EXPECT(newTxKB == 0); + + // 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() + { + testcase("Transaction minimum ledger sequence tracking"); + + using namespace test::jtx; + auto config = envconfig(); + config->LEDGER_HISTORY = 1000; + + Env env(*this, std::move(config)); + auto& db = env.app().getRelationalDatabase(); + + auto* sqliteDb = dynamic_cast(&db); + 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); + } + + void + run() override + { + testBasicInitialization(); + testLedgerSequenceOperations(); + testLedgerInfoOperations(); + testHashOperations(); + testTransactionOperations(); + testAccountTransactionOperations(); + testAccountTransactionPaging(); + testDeletionOperations(); + testDatabaseSpaceOperations(); + testTransactionMinLedgerSeq(); + } +}; + +BEAST_DEFINE_TESTSUITE(RelationalDatabase, rdb, ripple); + +} // namespace test +} // namespace ripple \ No newline at end of file