Skip to content

Commit abfb6d3

Browse files
feat(cli): Allow walking a range of an MDBX table using db mdbx get (#20233)
1 parent 0f0eb7a commit abfb6d3

File tree

8 files changed

+224
-44
lines changed

8 files changed

+224
-44
lines changed

crates/cli/commands/src/db/get.rs

Lines changed: 159 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,17 @@ use reth_db::{
88
RawDupSort,
99
};
1010
use reth_db_api::{
11-
table::{Decompress, DupSort, Table},
12-
tables, RawKey, RawTable, Receipts, TableViewer, Transactions,
11+
cursor::{DbCursorRO, DbDupCursorRO},
12+
database::Database,
13+
table::{Compress, Decompress, DupSort, Table},
14+
tables,
15+
transaction::DbTx,
16+
RawKey, RawTable, Receipts, TableViewer, Transactions,
1317
};
1418
use reth_db_common::DbTool;
1519
use reth_node_api::{HeaderTy, ReceiptTy, TxTy};
1620
use reth_node_builder::NodeTypesWithDB;
21+
use reth_primitives_traits::ValueWithSubKey;
1722
use reth_provider::{providers::ProviderNodeTypes, StaticFileProviderFactory};
1823
use reth_static_file_types::StaticFileSegment;
1924
use tracing::error;
@@ -39,6 +44,14 @@ enum Subcommand {
3944
#[arg(value_parser = maybe_json_value_parser)]
4045
subkey: Option<String>,
4146

47+
/// Optional end key for range query (exclusive upper bound)
48+
#[arg(value_parser = maybe_json_value_parser)]
49+
end_key: Option<String>,
50+
51+
/// Optional end subkey for range query (exclusive upper bound)
52+
#[arg(value_parser = maybe_json_value_parser)]
53+
end_subkey: Option<String>,
54+
4255
/// Output bytes instead of human-readable decoded value
4356
#[arg(long)]
4457
raw: bool,
@@ -61,8 +74,8 @@ impl Command {
6174
/// Execute `db get` command
6275
pub fn execute<N: ProviderNodeTypes>(self, tool: &DbTool<N>) -> eyre::Result<()> {
6376
match self.subcommand {
64-
Subcommand::Mdbx { table, key, subkey, raw } => {
65-
table.view(&GetValueViewer { tool, key, subkey, raw })?
77+
Subcommand::Mdbx { table, key, subkey, end_key, end_subkey, raw } => {
78+
table.view(&GetValueViewer { tool, key, subkey, end_key, end_subkey, raw })?
6679
}
6780
Subcommand::StaticFile { segment, key, raw } => {
6881
let (key, mask): (u64, _) = match segment {
@@ -154,6 +167,8 @@ struct GetValueViewer<'a, N: NodeTypesWithDB> {
154167
tool: &'a DbTool<N>,
155168
key: String,
156169
subkey: Option<String>,
170+
end_key: Option<String>,
171+
end_subkey: Option<String>,
157172
raw: bool,
158173
}
159174

@@ -163,53 +178,158 @@ impl<N: ProviderNodeTypes> TableViewer<()> for GetValueViewer<'_, N> {
163178
fn view<T: Table>(&self) -> Result<(), Self::Error> {
164179
let key = table_key::<T>(&self.key)?;
165180

166-
let content = if self.raw {
167-
self.tool
168-
.get::<RawTable<T>>(RawKey::from(key))?
169-
.map(|content| hex::encode_prefixed(content.raw_value()))
181+
// A non-dupsort table cannot have subkeys. The `subkey` arg becomes the `end_key`. First we
182+
// check that `end_key` and `end_subkey` weren't previously given, as that wouldn't be
183+
// valid.
184+
if self.end_key.is_some() || self.end_subkey.is_some() {
185+
return Err(eyre::eyre!("Only END_KEY can be given for non-DUPSORT tables"));
186+
}
187+
188+
let end_key = self.subkey.clone();
189+
190+
// Check if we're doing a range query
191+
if let Some(ref end_key_str) = end_key {
192+
let end_key = table_key::<T>(end_key_str)?;
193+
194+
// Use walk_range to iterate over the range
195+
self.tool.provider_factory.db_ref().view(|tx| {
196+
let mut cursor = tx.cursor_read::<T>()?;
197+
let walker = cursor.walk_range(key..end_key)?;
198+
199+
for result in walker {
200+
let (k, v) = result?;
201+
let json_val = if self.raw {
202+
let raw_key = RawKey::from(k);
203+
serde_json::json!({
204+
"key": hex::encode_prefixed(raw_key.raw_key()),
205+
"val": hex::encode_prefixed(v.compress().as_ref()),
206+
})
207+
} else {
208+
serde_json::json!({
209+
"key": &k,
210+
"val": &v,
211+
})
212+
};
213+
214+
println!("{}", serde_json::to_string_pretty(&json_val)?);
215+
}
216+
217+
Ok::<_, eyre::Report>(())
218+
})??;
170219
} else {
171-
self.tool.get::<T>(key)?.as_ref().map(serde_json::to_string_pretty).transpose()?
172-
};
220+
// Single key lookup
221+
let content = if self.raw {
222+
self.tool
223+
.get::<RawTable<T>>(RawKey::from(key))?
224+
.map(|content| hex::encode_prefixed(content.raw_value()))
225+
} else {
226+
self.tool.get::<T>(key)?.as_ref().map(serde_json::to_string_pretty).transpose()?
227+
};
173228

174-
match content {
175-
Some(content) => {
176-
println!("{content}");
177-
}
178-
None => {
179-
error!(target: "reth::cli", "No content for the given table key.");
180-
}
181-
};
229+
match content {
230+
Some(content) => {
231+
println!("{content}");
232+
}
233+
None => {
234+
error!(target: "reth::cli", "No content for the given table key.");
235+
}
236+
};
237+
}
182238

183239
Ok(())
184240
}
185241

186-
fn view_dupsort<T: DupSort>(&self) -> Result<(), Self::Error> {
242+
fn view_dupsort<T: DupSort>(&self) -> Result<(), Self::Error>
243+
where
244+
T::Value: reth_primitives_traits::ValueWithSubKey<SubKey = T::SubKey>,
245+
{
187246
// get a key for given table
188247
let key = table_key::<T>(&self.key)?;
189248

190-
// process dupsort table
191-
let subkey = table_subkey::<T>(self.subkey.as_deref())?;
249+
// Check if we're doing a range query
250+
if let Some(ref end_key_str) = self.end_key {
251+
let end_key = table_key::<T>(end_key_str)?;
252+
let start_subkey = table_subkey::<T>(Some(
253+
self.subkey.as_ref().expect("must have been given if end_key is given").as_str(),
254+
))?;
255+
let end_subkey_parsed = self
256+
.end_subkey
257+
.as_ref()
258+
.map(|s| table_subkey::<T>(Some(s.as_str())))
259+
.transpose()?;
192260

193-
let content = if self.raw {
194-
self.tool
195-
.get_dup::<RawDupSort<T>>(RawKey::from(key), RawKey::from(subkey))?
196-
.map(|content| hex::encode_prefixed(content.raw_value()))
261+
self.tool.provider_factory.db_ref().view(|tx| {
262+
let mut cursor = tx.cursor_dup_read::<T>()?;
263+
264+
// Seek to the starting key. If there is actually a key at the starting key then
265+
// seek to the subkey within it.
266+
if let Some((decoded_key, _)) = cursor.seek(key.clone())? &&
267+
decoded_key == key
268+
{
269+
cursor.seek_by_key_subkey(key.clone(), start_subkey.clone())?;
270+
}
271+
272+
// Get the current position to start iteration
273+
let mut current = cursor.current()?;
274+
275+
while let Some((decoded_key, decoded_value)) = current {
276+
// Extract the subkey using the ValueWithSubKey trait
277+
let decoded_subkey = decoded_value.get_subkey();
278+
279+
// Check if we've reached the end (exclusive)
280+
if (&decoded_key, Some(&decoded_subkey)) >=
281+
(&end_key, end_subkey_parsed.as_ref())
282+
{
283+
break;
284+
}
285+
286+
// Output the entry with both key and subkey
287+
let json_val = if self.raw {
288+
let raw_key = RawKey::from(decoded_key.clone());
289+
serde_json::json!({
290+
"key": hex::encode_prefixed(raw_key.raw_key()),
291+
"val": hex::encode_prefixed(decoded_value.compress().as_ref()),
292+
})
293+
} else {
294+
serde_json::json!({
295+
"key": &decoded_key,
296+
"val": &decoded_value,
297+
})
298+
};
299+
300+
println!("{}", serde_json::to_string_pretty(&json_val)?);
301+
302+
// Move to next entry
303+
current = cursor.next()?;
304+
}
305+
306+
Ok::<_, eyre::Report>(())
307+
})??;
197308
} else {
198-
self.tool
199-
.get_dup::<T>(key, subkey)?
200-
.as_ref()
201-
.map(serde_json::to_string_pretty)
202-
.transpose()?
203-
};
309+
// Single key/subkey lookup
310+
let subkey = table_subkey::<T>(self.subkey.as_deref())?;
204311

205-
match content {
206-
Some(content) => {
207-
println!("{content}");
208-
}
209-
None => {
210-
error!(target: "reth::cli", "No content for the given table subkey.");
211-
}
212-
};
312+
let content = if self.raw {
313+
self.tool
314+
.get_dup::<RawDupSort<T>>(RawKey::from(key), RawKey::from(subkey))?
315+
.map(|content| hex::encode_prefixed(content.raw_value()))
316+
} else {
317+
self.tool
318+
.get_dup::<T>(key, subkey)?
319+
.as_ref()
320+
.map(serde_json::to_string_pretty)
321+
.transpose()?
322+
};
323+
324+
match content {
325+
Some(content) => {
326+
println!("{content}");
327+
}
328+
None => {
329+
error!(target: "reth::cli", "No content for the given table subkey.");
330+
}
331+
};
332+
}
213333
Ok(())
214334
}
215335
}

crates/primitives-traits/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ pub use alloy_primitives::{logs_bloom, Log, LogData};
164164
pub mod proofs;
165165

166166
mod storage;
167-
pub use storage::StorageEntry;
167+
pub use storage::{StorageEntry, ValueWithSubKey};
168168

169169
pub mod sync;
170170

crates/primitives-traits/src/storage.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
use alloy_primitives::{B256, U256};
22

3+
/// Trait for `DupSort` table values that contain a subkey.
4+
///
5+
/// This trait allows extracting the subkey from a value during database iteration,
6+
/// enabling proper range queries and filtering on `DupSort` tables.
7+
pub trait ValueWithSubKey {
8+
/// The type of the subkey.
9+
type SubKey;
10+
11+
/// Extract the subkey from the value.
12+
fn get_subkey(&self) -> Self::SubKey;
13+
}
14+
315
/// Account storage entry.
416
///
517
/// `key` is the subkey when used as a value in the `StorageChangeSets` table.
@@ -21,6 +33,14 @@ impl StorageEntry {
2133
}
2234
}
2335

36+
impl ValueWithSubKey for StorageEntry {
37+
type SubKey = B256;
38+
39+
fn get_subkey(&self) -> Self::SubKey {
40+
self.key
41+
}
42+
}
43+
2444
impl From<(B256, U256)> for StorageEntry {
2545
fn from((key, value): (B256, U256)) -> Self {
2646
Self { key, value }

crates/storage/db-api/src/tables/mod.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,10 @@ pub trait TableViewer<R> {
9494
/// Operate on the dupsort table in a generic way.
9595
///
9696
/// By default, the `view` function is invoked unless overridden.
97-
fn view_dupsort<T: DupSort>(&self) -> Result<R, Self::Error> {
97+
fn view_dupsort<T: DupSort>(&self) -> Result<R, Self::Error>
98+
where
99+
T::Value: reth_primitives_traits::ValueWithSubKey<SubKey = T::SubKey>,
100+
{
98101
self.view::<T>()
99102
}
100103
}

crates/storage/db-models/src/accounts.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use alloy_primitives::Address;
2-
use reth_primitives_traits::Account;
2+
use reth_primitives_traits::{Account, ValueWithSubKey};
33

44
/// Account as it is saved in the database.
55
///
@@ -15,6 +15,14 @@ pub struct AccountBeforeTx {
1515
pub info: Option<Account>,
1616
}
1717

18+
impl ValueWithSubKey for AccountBeforeTx {
19+
type SubKey = Address;
20+
21+
fn get_subkey(&self) -> Self::SubKey {
22+
self.address
23+
}
24+
}
25+
1826
// NOTE: Removing reth_codec and manually encode subkey
1927
// and compress second part of the value. If we have compression
2028
// over whole value (Even SubKey) that would mess up fetching of values with seek_by_key_subkey

crates/trie/common/src/storage.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use super::{BranchNodeCompact, StoredNibblesSubKey};
2+
use reth_primitives_traits::ValueWithSubKey;
23

34
/// Account storage trie node.
45
///
@@ -12,6 +13,14 @@ pub struct StorageTrieEntry {
1213
pub node: BranchNodeCompact,
1314
}
1415

16+
impl ValueWithSubKey for StorageTrieEntry {
17+
type SubKey = StoredNibblesSubKey;
18+
19+
fn get_subkey(&self) -> Self::SubKey {
20+
self.nibbles.clone()
21+
}
22+
}
23+
1524
// NOTE: Removing reth_codec and manually encode subkey
1625
// and compress second part of the value. If we have compression
1726
// over whole value (Even SubKey) that would mess up fetching of values with seek_by_key_subkey
@@ -46,6 +55,14 @@ pub struct TrieChangeSetsEntry {
4655
pub node: Option<BranchNodeCompact>,
4756
}
4857

58+
impl ValueWithSubKey for TrieChangeSetsEntry {
59+
type SubKey = StoredNibblesSubKey;
60+
61+
fn get_subkey(&self) -> Self::SubKey {
62+
self.nibbles.clone()
63+
}
64+
}
65+
4966
#[cfg(any(test, feature = "reth-codec"))]
5067
impl reth_codecs::Compact for TrieChangeSetsEntry {
5168
fn to_compact<B>(&self, buf: &mut B) -> usize

docs/vocs/docs/pages/cli/op-reth/db/get/mdbx.mdx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ Gets the content of a database table for the given key
66
$ op-reth db get mdbx --help
77
```
88
```txt
9-
Usage: op-reth db get mdbx [OPTIONS] <TABLE> <KEY> [SUBKEY]
9+
Usage: op-reth db get mdbx [OPTIONS] <TABLE> <KEY> [SUBKEY] [END_KEY] [END_SUBKEY]
1010
1111
Arguments:
1212
<TABLE>
@@ -18,6 +18,12 @@ Arguments:
1818
[SUBKEY]
1919
The subkey to get content for
2020
21+
[END_KEY]
22+
Optional end key for range query (exclusive upper bound)
23+
24+
[END_SUBKEY]
25+
Optional end subkey for range query (exclusive upper bound)
26+
2127
Options:
2228
--raw
2329
Output bytes instead of human-readable decoded value

0 commit comments

Comments
 (0)