9.5 KiB
SQL Database
SQLite via SOCI for ledger/transaction history. Only SQLite is supported; any non-sqlite backend value in config throws at parse time (detail::getSociInit in SociDB.cpp).
Key Invariants
- Two main databases:
lgrdb_(ledger) andtxdb_(transactions, optional viauseTxTablesconfig) - Transaction tables are optional; disabling them disables transaction history and
account_txqueries - WAL checkpointing offloads to
JobQueue(jtWAL); at most one checkpoint job in flight perDatabaseCon(guarded byrunning_mutex inWALCheckpointer) - Database init failure is fatal (throws exception, prevents construction)
- Free disk space < 512 MB triggers fatal error on write operations
- File extension inconsistency:
validatorsandpeerfinderuse.sqlite; all other DBs use.db. Historical artifact enforced indetail::getSociInit
Schema
Ledgers: seq, hash, parent hash, total coins, close time, etc. Indexed byLedgerSeqTransactions: TransID, TransType, FromAcct, FromSeq, LedgerSeq, Status, RawTxn, TxnMeta. Indexed byLedgerSeqAccountTransactions: TransID, Account, LedgerSeq, TxnSeq. Triple-indexed foraccount_txqueries- Secondary DBs: Wallet (node identity, manifests), PeerFinder (bootstrap cache), State (deletion tracking)
- Schema defined in
src/xrpld/app/main/DBInit.h - No schema migration system;
CREATE TABLE IF NOT EXISTSsilently preserves old schemas with missing columns. Exception: PeerFinder has schema versioning via aSchemaVersiontable.
Configuration
| Option | Section | Values | Default |
|---|---|---|---|
backend |
[sqdb] / [relational_db] |
sqlite only |
sqlite |
page_size |
[sqlite] |
512–65536, power of 2 | 4096 |
safety_level |
[sqlite] |
high, medium, low | high |
journal_size_limit |
[sqlite] |
integer >= 0 | 1582080 |
safety_level: low changes journal_mode and synchronous settings — can lose data on crash.
WAL Checkpointing Architecture
The checkpointer subsystem is the trickiest part of this module. SQLite's WAL hook is a C callback registered on the native sqlite3* connection, but the work runs on a JobQueue thread that may still be executing when the owning DatabaseCon is destroyed.
Two-file split
SociDB.cpp:WALCheckpointerclass (anonymous namespace) — installs the hook, implementsschedule()andcheckpoint(), holds theweak_ptr<soci::session>.DatabaseCon.cpp:CheckpointersCollectionclass — process-wide singleton registry (checkpointers, namespace-scope variable) mapping monotonically-incrementing integer IDs toshared_ptr<Checkpointer>; exposescreate,fromId,erase. AllDatabaseConinstances share this one registry.
DatabaseCon.cpp has no direct SQLite dependency; it only manages the Checkpointer abstract interface.
ID-based hook indirection
WALCheckpointeris registered withsqlite3_wal_hookusing itsstd::uintptr_t id_cast tovoid*, not a rawthispointer.- The C hook calls
checkpointerFromId()→CheckpointersCollection::fromId()(process-wide singleton). If lookup returns null (connection torn down), the hook deregisters itself viasqlite3_wal_hook(conn, nullptr, nullptr). - Prevents use-after-free: the hook may fire on a writer thread after
DatabaseConbegins destruction.
Session ownership split
DatabaseConholdsstd::shared_ptr<soci::session>;WALCheckpointerholds onlystd::weak_ptr<soci::session>.- If the checkpointer held a
shared_ptr, an in-flight job would keep the WAL lock alive, blocking a freshly-opened replacementDatabaseConon the same file. WALCheckpointer::checkpoint()callssession_.lock()and bails silently if expired.
Destructor sequence (DatabaseCon::~DatabaseCon)
Order matters:
checkpointers.erase(checkpointer_->id())— future hook invocations now no-op and self-deregister.- Take
weak_ptr<Checkpointer> wk(checkpointer_), thencheckpointer_.reset(). - Busy-poll
wk.use_count() != 0with 100 ms sleeps until all in-flight job lambdas release theirshared_ptr<Checkpointer>.
The 100 ms poll is deliberate (rare event; simpler than a condvar). Without this wait, reopening the same SQLite file immediately after destruction can fail because the old checkpoint job may still hold the WAL lock.
setupCheckpointing() — deferred wiring
- Separated from constructors so checkpointing is opt-in.
- Constructors accepting
CheckpointerSetupopen the DB first, then callsetupCheckpointing(JobQueue*, ServiceRegistry&). - Null
JobQueue*throwsstd::logic_error(programming error, not runtime). - The checkpointer must be inserted into
CheckpointersCollectionbeforesetupCheckpointingreturns, because the WAL hook is armed inside theWALCheckpointerconstructor and writes can fire it immediately.
Checkpoint job behavior
- Triggered by
sqlite3_wal_hookafter every WAL write;static checkpointPageCount = 1000mirrors SQLite's auto-checkpoint threshold. schedule()usesrunning_bool under mutex to enforce single in-flight job; ifJobQueuerejects the job,running_is reset.- Enqueued lambda captures
std::weak_ptr<Checkpointer>; destroyedDatabaseConcauses the job to exit without touching the session. checkpoint()callssqlite3_wal_checkpoint_v2withSQLITE_CHECKPOINT_PASSIVE.SQLITE_LOCKEDlogged at trace (expected under reader contention); other errors logged as warnings.running_reset under mutex after each attempt.- Net effect: routes checkpoint work off the writer thread onto
jtWAL. Without this, SQLite does it synchronously on whichever thread crosses the page threshold.
SOCI Adapter Notes (SociDB.cpp)
DBConfigis two-phase: parse params, open later.detail::getSociInitanddetail::getSociSqliteInitresolve backend + path; the.sqlitevs.dbextension fork lives ingetSociInit.getSociSqliteInitthrowsstd::runtime_errorif the database name is empty.- Two free-function
open()overloads: config-based (delegates throughDBConfig) and explicit-string (enforces same "sqlite only" constraint). Both paths calls.open(soci::sqlite3, connectionString). getConnection(session&)recovers the rawsqlite3*viadynamic_cast<soci::sqlite3_session_backend*>— the only intentional break in the SOCI abstraction. Throwsstd::logic_errorif the cast fails. Required for WAL hooks andsqlite3_db_status.getKBUsedAll()→sqlite3_memory_used()(process-global).getKBUsedDB()→SQLITE_DBSTATUS_CACHE_USED(per-connection).- Four
convert()overloads bridgesoci::blob↔std::vector<uint8_t>/std::string. Empty blobs requireblob.trim(0)rather thanblob.write(nullptr, 0). SociDB.cppopens with#pragma clang diagnostic ignored "-Wdeprecated"because SOCI headers use deprecated constructs; scoped to this TU only.
Common Bug Patterns
- No schema migration system;
CREATE TABLE IF NOT EXISTSsilently preserves old schemas with missing columns. New columns on existing deployments require manualALTER TABLEor explicit documentation that the column may be absent. page_sizemust be power of 2 between 512–65536; invalid values cause init failure.- Online deletion coordinates between NodeStore rotation and SQL table pruning; race conditions here lose history.
- Empty database name passed to
detail::getSociSqliteInitthrows — no silent fallback. - A
WALCheckpointerregistered withsqlite3_wal_hookcan outlive itsDatabaseConif a checkpoint job is in flight; teardown must wait for the job to drain (see Destructor sequence above). - Opening a new
DatabaseConto the same file immediately after destroying the old one can fail if the destructor busy-poll is skipped or shortened — the old checkpoint job may still hold the WAL lock.
Key Patterns
Schema Evolution Caveat
// No migration system — old databases keep old schemas.
// CREATE TABLE IF NOT EXISTS silently skips if table exists with old columns.
// New columns require manual ALTER TABLE or must be treated as optional/absent.
// PeerFinder is the exception: it has a SchemaVersion table.
Disk Space Guard
// Required on all write paths.
if (freeDiskSpace < minDiskFree)
Throw<std::runtime_error>("Not enough disk space for database write");
WAL Hook Cookie
// Always pass an integer ID, never `this`.
// DatabaseCon may be destroyed while a hook invocation is mid-flight on a writer thread.
sqlite3_wal_hook(conn, &walHookCallback,
reinterpret_cast<void*>(checkpointer->id()));
Penetrating the SOCI Abstraction
// getConnection() is the only intentional SOCI abstraction break.
// Required for sqlite3_wal_hook and sqlite3_db_status APIs.
auto* be = dynamic_cast<soci::sqlite3_session_backend*>(s.get_backend());
if (!be || !be->conn_) throw std::logic_error("Not a sqlite3 session");
sqlite3* conn = be->conn_;
Key Files
| File | Purpose |
|---|---|
src/libxrpl/rdb/SociDB.cpp |
SOCI/SQLite adapter, WALCheckpointer, blob conversion, memory stats |
src/libxrpl/rdb/DatabaseCon.cpp |
Connection lifecycle, CheckpointersCollection, destructor drain |
src/xrpld/app/main/DBInit.h |
Schema definitions (CREATE TABLE statements) |
src/xrpld/app/rdb/backend/detail/SQLiteDatabase.cpp |
Main SQLiteDatabase implementation |
src/xrpld/app/rdb/backend/detail/Node.cpp |
Ledger/tx read-write operations |
src/xrpld/app/rdb/detail/State.cpp |
Deletion state tracking |
src/xrpld/core/detail/DatabaseCon.cpp |
Legacy reference; lifecycle now in libxrpl |