Skip to main content

ethrex_storage/api/
mod.rs

1//! # Storage Backend API
2//!
3//! This module provides a thin, minimal interface for storage backends:
4//!
5//! - Thin: Minimal set of operations that databases must provide
6//! - Simple: Avoids type-system complexity and focuses on core functionality
7//!
8//! Rather than implementing business logic in each database backend, this API
9//! provides low-level primitives that higher-level code can build upon.
10//! This eliminates code duplication and makes adding new database backends trivial.
11//!
12//! The API differentiates between three types of database access:
13//!
14//! - Read views ([`StorageReadView`]): read-only views of the database,
15//!   with no atomicity guarantees between operations.
16//! - Write batches ([`StorageWriteBatch`]): write batch functionality, with
17//!   atomicity guarantees at commit time.
18//! - Locked views ([`StorageLockedView`]): read-only views of a point in time (snapshots), right now it's
19//!   only used during snap-sync.
20
21use crate::error::StoreError;
22use std::{fmt::Debug, path::Path, sync::Arc};
23
24pub mod tables;
25
26/// Type alias for the result of a prefix iterator.
27pub type PrefixResult = Result<(Box<[u8]>, Box<[u8]>), StoreError>;
28
29/// This trait provides a minimal set of operations required from a database backend.
30/// Implementations should focus on providing efficient access to the underlying storage
31/// without implementing business logic.
32pub trait StorageBackend: Debug + Send + Sync {
33    /// Removes all data from the specified table.
34    fn clear_table(&self, table: &'static str) -> Result<(), StoreError>;
35
36    /// Opens a new read view.
37    fn begin_read(&self) -> Result<Arc<dyn StorageReadView>, StoreError>;
38
39    /// Creates a new write batch.
40    fn begin_write(&self) -> Result<Box<dyn StorageWriteBatch + 'static>, StoreError>;
41
42    /// Creates a locked snapshot for a specific table.
43    ///
44    /// This provides a persistent read-only view of a single table, optimized
45    /// for batch read operations. The snapshot remains valid until dropped.
46    fn begin_locked(
47        &self,
48        table_name: &'static str,
49    ) -> Result<Box<dyn StorageLockedView + 'static>, StoreError>;
50
51    // TODO: remove this and provide historic data via diff-layers
52    /// Creates a checkpoint of the current database state at the specified path.
53    fn create_checkpoint(&self, path: &Path) -> Result<(), StoreError>;
54
55    /// Durably persists all buffered writes to disk, so a subsequent process
56    /// start needs no crash recovery. Called on graceful shutdown. Defaults to a
57    /// no-op for backends that are already durable or purely in-memory.
58    fn flush(&self) -> Result<(), StoreError> {
59        Ok(())
60    }
61}
62
63/// Read-only transaction interface.
64/// Provides methods to read data from the database
65pub trait StorageReadView: Send + Sync {
66    /// Retrieves a value by key from the specified table.
67    fn get(&self, table: &'static str, key: &[u8]) -> Result<Option<Vec<u8>>, StoreError>;
68
69    /// Retrieves multiple values by key from the specified table.
70    /// Returns results in the same order as the input keys.
71    /// Backends that support batched reads (e.g. RocksDB `multi_get_cf`)
72    /// should override this for better throughput. Callers should not
73    /// assume `multi_get` is asymptotically faster than `get`; on backends
74    /// without a batched read primitive (e.g. the in-memory backend) the
75    /// default impl below is equivalent to N independent `get` calls.
76    fn multi_get(
77        &self,
78        table: &'static str,
79        keys: &[&[u8]],
80    ) -> Vec<Result<Option<Vec<u8>>, StoreError>> {
81        keys.iter().map(|k| self.get(table, k)).collect()
82    }
83
84    /// Returns an iterator over all key-value pairs with the given prefix.
85    fn prefix_iterator(
86        &self,
87        table: &'static str,
88        prefix: &[u8],
89    ) -> Result<Box<dyn Iterator<Item = PrefixResult> + '_>, StoreError>;
90
91    /// Returns the lowest key in `table` by lexicographic order, or `None` if the table is
92    /// empty. Backends that support forward iteration (e.g. RocksDB `IteratorMode::Start`)
93    /// should implement this in O(1).
94    fn first_key(&self, table: &'static str) -> Result<Option<Vec<u8>>, StoreError>;
95
96    /// Returns the highest key in `table` by lexicographic order, or `None` if the table is
97    /// empty. Backends that support reverse iteration (e.g. RocksDB `IteratorMode::End`) should
98    /// implement this in O(1).
99    fn last_key(&self, table: &'static str) -> Result<Option<Vec<u8>>, StoreError>;
100}
101
102/// Write transaction interface.
103///
104/// Note that this does not provide read access, since we don't currently use that functionality.
105///
106/// Changes are not persisted until [`commit()`](StorageWriteBatch::commit) is called.
107pub trait StorageWriteBatch: Send {
108    /// Stores a key-value pair in the specified table.
109    fn put(&mut self, table: &'static str, key: &[u8], value: &[u8]) -> Result<(), StoreError> {
110        self.put_batch(table, vec![(key.to_vec(), value.to_vec())])
111    }
112
113    /// Stores multiple key-value pairs in the specified table within the transaction.
114    fn put_batch(
115        &mut self,
116        table: &'static str,
117        batch: Vec<(Vec<u8>, Vec<u8>)>,
118    ) -> Result<(), StoreError>;
119
120    /// Removes a key-value pair from the specified table.
121    fn delete(&mut self, table: &'static str, key: &[u8]) -> Result<(), StoreError>;
122
123    /// Removes every key in `[start, end)` from the specified table.
124    ///
125    /// Half-open range; `end` is exclusive. Equivalent to enumerating each key
126    /// in the range and calling [`delete`], but backends with native range-delete
127    /// support (e.g. RocksDB's `delete_range_cf`) can implement it more efficiently.
128    ///
129    /// Lexicographic byte order is used for the range bounds — callers using
130    /// numeric keys must encode them in a representation whose lex order matches
131    /// numeric order (e.g. `u64::to_be_bytes()`).
132    fn delete_range(
133        &mut self,
134        table: &'static str,
135        start: &[u8],
136        end: &[u8],
137    ) -> Result<(), StoreError>;
138
139    /// Appends a merge operand for the given key in the specified table.
140    ///
141    /// The actual combine step is deferred — backends with a registered merge
142    /// operator (RocksDB) apply it at read or compaction time; backends without
143    /// (InMemory) dispatch by table and apply inline.
144    ///
145    /// Currently used for `TRANSACTION_LOCATIONS`. Calling on a table without
146    /// a registered merge function is an error.
147    fn merge(&mut self, table: &'static str, key: &[u8], operand: &[u8]) -> Result<(), StoreError>;
148
149    /// Commits all changes made in this transaction.
150    fn commit(&mut self) -> Result<(), StoreError>;
151}
152
153/// Locked snapshot interface for batch read operations.
154/// Provides read-only access to a specific table with a persistent snapshot.
155/// This is optimized for scenarios where many reads are performed on the same
156/// table, such as trie traversal operations.
157/// This is currently only used in snapsync stage.
158// TODO: Check if we can remove this trait and use [`StorageReadView`] instead.
159pub trait StorageLockedView: Send + Sync {
160    /// Retrieves a value by key from the locked table.
161    fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, StoreError>;
162}