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
92/// Write transaction interface.
93///
94/// Note that this does not provide read access, since we don't currently use that functionality.
95///
96/// Changes are not persisted until [`commit()`](StorageWriteBatch::commit) is called.
97pub trait StorageWriteBatch: Send {
98 /// Stores a key-value pair in the specified table.
99 fn put(&mut self, table: &'static str, key: &[u8], value: &[u8]) -> Result<(), StoreError> {
100 self.put_batch(table, vec![(key.to_vec(), value.to_vec())])
101 }
102
103 /// Stores multiple key-value pairs in the specified table within the transaction.
104 fn put_batch(
105 &mut self,
106 table: &'static str,
107 batch: Vec<(Vec<u8>, Vec<u8>)>,
108 ) -> Result<(), StoreError>;
109
110 /// Removes a key-value pair from the specified table.
111 fn delete(&mut self, table: &'static str, key: &[u8]) -> Result<(), StoreError>;
112
113 /// Appends a merge operand for the given key in the specified table.
114 ///
115 /// The actual combine step is deferred — backends with a registered merge
116 /// operator (RocksDB) apply it at read or compaction time; backends without
117 /// (InMemory) dispatch by table and apply inline.
118 ///
119 /// Currently used for `TRANSACTION_LOCATIONS`. Calling on a table without
120 /// a registered merge function is an error.
121 fn merge(&mut self, table: &'static str, key: &[u8], operand: &[u8]) -> Result<(), StoreError>;
122
123 /// Commits all changes made in this transaction.
124 fn commit(&mut self) -> Result<(), StoreError>;
125}
126
127/// Locked snapshot interface for batch read operations.
128/// Provides read-only access to a specific table with a persistent snapshot.
129/// This is optimized for scenarios where many reads are performed on the same
130/// table, such as trie traversal operations.
131/// This is currently only used in snapsync stage.
132// TODO: Check if we can remove this trait and use [`StorageReadView`] instead.
133pub trait StorageLockedView: Send + Sync {
134 /// Retrieves a value by key from the locked table.
135 fn get(&self, key: &[u8]) -> Result<Option<Vec<u8>>, StoreError>;
136}