Skip to main content

reth_cli_commands/db/
get.rs

1use alloy_primitives::{hex, Address, BlockHash, B256};
2use clap::Parser;
3use reth_db::{
4    static_file::{
5        AccountChangesetMask, ColumnSelectorOne, ColumnSelectorTwo, HeaderWithHashMask,
6        ReceiptMask, TransactionMask, TransactionSenderMask,
7    },
8    RawDupSort,
9};
10use reth_db_api::{
11    cursor::{DbCursorRO, DbDupCursorRO},
12    database::Database,
13    models::{storage_sharded_key::StorageShardedKey, ShardedKey},
14    table::{Compress, Decompress, DupSort, Table},
15    tables,
16    transaction::DbTx,
17    RawKey, RawTable, TableViewer,
18};
19use reth_db_common::DbTool;
20use reth_node_api::{HeaderTy, ReceiptTy, TxTy};
21use reth_node_builder::NodeTypesWithDB;
22use reth_primitives_traits::ValueWithSubKey;
23use reth_provider::{
24    providers::ProviderNodeTypes, ChangeSetReader, RocksDBProviderFactory,
25    StaticFileProviderFactory,
26};
27use reth_static_file_types::StaticFileSegment;
28use reth_storage_api::StorageChangeSetReader;
29use tracing::error;
30
31/// The arguments for the `reth db get` command
32#[derive(Parser, Debug)]
33pub struct Command {
34    #[command(subcommand)]
35    subcommand: Subcommand,
36}
37
38#[derive(clap::Subcommand, Debug)]
39enum Subcommand {
40    /// Gets the content of a database table for the given key
41    Mdbx {
42        table: tables::Tables,
43
44        /// The key to get content for
45        #[arg(value_parser = maybe_json_value_parser)]
46        key: String,
47
48        /// The subkey to get content for
49        #[arg(value_parser = maybe_json_value_parser)]
50        subkey: Option<String>,
51
52        /// Optional end key for range query (exclusive upper bound)
53        #[arg(value_parser = maybe_json_value_parser)]
54        end_key: Option<String>,
55
56        /// Optional end subkey for range query (exclusive upper bound)
57        #[arg(value_parser = maybe_json_value_parser)]
58        end_subkey: Option<String>,
59
60        /// Output bytes instead of human-readable decoded value
61        #[arg(long)]
62        raw: bool,
63    },
64    /// Gets the content of a static file segment for the given key
65    StaticFile {
66        segment: StaticFileSegment,
67
68        /// The key to get content for
69        #[arg(value_parser = maybe_json_value_parser)]
70        key: String,
71
72        /// The subkey to get content for, for example address in changeset
73        #[arg(value_parser = maybe_json_value_parser)]
74        subkey: Option<String>,
75
76        /// Output bytes instead of human-readable decoded value
77        #[arg(long)]
78        raw: bool,
79    },
80    /// Gets the content of a RocksDB table for the given key
81    ///
82    /// For history tables (accounts-history, storages-history), you can pass a plain address
83    /// instead of a full JSON ShardedKey. Use --block to query a specific block number
84    /// (seeks to the shard containing that block), or --all-shards to list all shards for
85    /// the address.
86    ///
87    /// Examples:
88    ///   reth db get rocksdb accounts-history 0xdBBE3D8c2d2b22A2611c5A94A9a12C2fCD49Eb29
89    ///   reth db get rocksdb accounts-history 0xdBBE...Eb29 --block 1000000
90    ///   reth db get rocksdb accounts-history 0xdBBE...Eb29 --all-shards
91    ///   reth db get rocksdb storages-history 0xdBBE...Eb29 --storage-key 0x0000...0003
92    Rocksdb {
93        /// The RocksDB table
94        #[arg(value_enum)]
95        table: RocksDbTable,
96
97        /// The key to get content for. For history tables, this can be a plain address.
98        #[arg(value_parser = maybe_json_value_parser)]
99        key: String,
100
101        /// Target block number for history tables. Seeks to the shard containing this block.
102        /// Defaults to the latest shard if not specified.
103        #[arg(long)]
104        block: Option<u64>,
105
106        /// Storage key for storages-history table lookups.
107        #[arg(long)]
108        storage_key: Option<String>,
109
110        /// List all shards for the given key (history tables only).
111        #[arg(long)]
112        all_shards: bool,
113
114        /// Output bytes instead of human-readable decoded value
115        #[arg(long)]
116        raw: bool,
117    },
118}
119
120/// RocksDB tables that can be queried.
121#[derive(Debug, Clone, Copy, clap::ValueEnum)]
122pub enum RocksDbTable {
123    /// Transaction hash to transaction number mapping
124    TransactionHashNumbers,
125    /// Account history indices
126    AccountsHistory,
127    /// Storage history indices
128    StoragesHistory,
129}
130
131impl Command {
132    /// Execute `db get` command
133    pub fn execute<N: ProviderNodeTypes>(self, tool: &DbTool<N>) -> eyre::Result<()> {
134        match self.subcommand {
135            Subcommand::Mdbx { table, key, subkey, end_key, end_subkey, raw } => {
136                table.view(&GetValueViewer { tool, key, subkey, end_key, end_subkey, raw })?
137            }
138            Subcommand::Rocksdb { table, key, block, storage_key, all_shards, raw } => {
139                get_rocksdb(tool, table, &key, block, storage_key.as_deref(), all_shards, raw)?;
140            }
141            Subcommand::StaticFile { segment, key, subkey, raw } => {
142                if let StaticFileSegment::StorageChangeSets = segment {
143                    let storage_key = subkey
144                        .as_deref()
145                        .map(table_subkey::<tables::StorageChangeSets>)
146                        .transpose()?;
147                    let key = table_key::<tables::StorageChangeSets>(&key)?;
148
149                    let provider = tool.provider_factory.static_file_provider();
150
151                    if let Some(storage_key) = storage_key {
152                        let entry = provider.get_storage_before_block(
153                            key.block_number(),
154                            key.address(),
155                            storage_key,
156                        )?;
157
158                        if let Some(entry) = entry {
159                            let se: reth_primitives_traits::StorageEntry = entry;
160                            println!("{}", serde_json::to_string_pretty(&se)?);
161                        } else {
162                            error!(target: "reth::cli", "No content for the given table key.");
163                        }
164                        return Ok(());
165                    }
166
167                    let changesets = provider.storage_changeset(key.block_number())?;
168                    let serializable: Vec<_> = changesets
169                        .into_iter()
170                        .map(|(addr, entry)| {
171                            let se: reth_primitives_traits::StorageEntry = entry;
172                            (addr, se)
173                        })
174                        .collect();
175                    println!("{}", serde_json::to_string_pretty(&serializable)?);
176                    return Ok(());
177                }
178
179                let (key, subkey, mask): (u64, _, _) = match segment {
180                    StaticFileSegment::Headers => (
181                        table_key::<tables::Headers>(&key)?,
182                        None,
183                        <HeaderWithHashMask<HeaderTy<N>>>::MASK,
184                    ),
185                    StaticFileSegment::Transactions => (
186                        table_key::<tables::Transactions>(&key)?,
187                        None,
188                        <TransactionMask<TxTy<N>>>::MASK,
189                    ),
190                    StaticFileSegment::Receipts => (
191                        table_key::<tables::Receipts>(&key)?,
192                        None,
193                        <ReceiptMask<ReceiptTy<N>>>::MASK,
194                    ),
195                    StaticFileSegment::TransactionSenders => (
196                        table_key::<tables::TransactionSenders>(&key)?,
197                        None,
198                        TransactionSenderMask::MASK,
199                    ),
200                    StaticFileSegment::AccountChangeSets => {
201                        let subkey = subkey
202                            .as_deref()
203                            .map(table_subkey::<tables::AccountChangeSets>)
204                            .transpose()?;
205                        (
206                            table_key::<tables::AccountChangeSets>(&key)?,
207                            subkey,
208                            AccountChangesetMask::MASK,
209                        )
210                    }
211                    StaticFileSegment::StorageChangeSets => {
212                        unreachable!("storage changesets handled above");
213                    }
214                };
215
216                // handle account changesets differently if a subkey is provided.
217                if let StaticFileSegment::AccountChangeSets = segment {
218                    let Some(subkey) = subkey else {
219                        // get all changesets for the block
220                        let changesets = tool
221                            .provider_factory
222                            .static_file_provider()
223                            .account_block_changeset(key)?;
224
225                        println!("{}", serde_json::to_string_pretty(&changesets)?);
226                        return Ok(())
227                    };
228
229                    let account = tool
230                        .provider_factory
231                        .static_file_provider()
232                        .get_account_before_block(key, subkey)?;
233
234                    if let Some(account) = account {
235                        println!("{}", serde_json::to_string_pretty(&account)?);
236                    } else {
237                        error!(target: "reth::cli", "No content for the given table key.");
238                    }
239
240                    return Ok(())
241                }
242
243                let content = tool.provider_factory.static_file_provider().find_static_file(
244                    segment,
245                    |provider| {
246                        let mut cursor = provider.cursor()?;
247                        cursor.get(key.into(), mask).map(|result| {
248                            result.map(|vec| {
249                                vec.iter().map(|slice| slice.to_vec()).collect::<Vec<_>>()
250                            })
251                        })
252                    },
253                )?;
254
255                match content {
256                    Some(content) => {
257                        if raw {
258                            println!("{}", hex::encode_prefixed(&content[0]));
259                        } else {
260                            match segment {
261                                StaticFileSegment::Headers => {
262                                    let header = HeaderTy::<N>::decompress(content[0].as_slice())?;
263                                    let block_hash = BlockHash::decompress(content[1].as_slice())?;
264                                    println!(
265                                        "Header\n{}\n\nBlockHash\n{}",
266                                        serde_json::to_string_pretty(&header)?,
267                                        serde_json::to_string_pretty(&block_hash)?
268                                    );
269                                }
270                                StaticFileSegment::Transactions => {
271                                    let transaction = TxTy::<N>::decompress(content[0].as_slice())?;
272                                    println!("{}", serde_json::to_string_pretty(&transaction)?);
273                                }
274                                StaticFileSegment::Receipts => {
275                                    let receipt =
276                                        ReceiptTy::<N>::decompress(content[0].as_slice())?;
277                                    println!("{}", serde_json::to_string_pretty(&receipt)?);
278                                }
279                                StaticFileSegment::TransactionSenders => {
280                                    let sender =
281                                        <<tables::TransactionSenders as Table>::Value>::decompress(
282                                            content[0].as_slice(),
283                                        )?;
284                                    println!("{}", serde_json::to_string_pretty(&sender)?);
285                                }
286                                StaticFileSegment::AccountChangeSets => {
287                                    unreachable!("account changeset static files are special cased before this match")
288                                }
289                                StaticFileSegment::StorageChangeSets => {
290                                    unreachable!("storage changeset static files are special cased before this match")
291                                }
292                            }
293                        }
294                    }
295                    None => {
296                        error!(target: "reth::cli", "No content for the given table key.");
297                    }
298                };
299            }
300        }
301
302        Ok(())
303    }
304}
305
306/// Gets a value from a RocksDB table by key.
307fn get_rocksdb<N: ProviderNodeTypes>(
308    tool: &DbTool<N>,
309    table: RocksDbTable,
310    key: &str,
311    block: Option<u64>,
312    storage_key: Option<&str>,
313    all_shards: bool,
314    raw: bool,
315) -> eyre::Result<()> {
316    let rocksdb = tool.provider_factory.rocksdb_provider();
317
318    match table {
319        RocksDbTable::TransactionHashNumbers => {
320            if block.is_some() || all_shards || storage_key.is_some() {
321                return Err(eyre::eyre!(
322                    "--block, --all-shards, and --storage-key are only supported for history tables"
323                ));
324            }
325            get_rocksdb_table::<tables::TransactionHashNumbers>(&rocksdb, key, raw)
326        }
327        RocksDbTable::AccountsHistory => {
328            if storage_key.is_some() {
329                return Err(eyre::eyre!("--storage-key is only supported for storages-history"));
330            }
331            get_rocksdb_account_history(&rocksdb, key, block, all_shards, raw)
332        }
333        RocksDbTable::StoragesHistory => {
334            get_rocksdb_storage_history(&rocksdb, key, storage_key, block, all_shards, raw)
335        }
336    }
337}
338
339/// Try to parse a key string as a plain address, falling back to JSON `ShardedKey` parsing.
340fn parse_address(key: &str) -> eyre::Result<Address> {
341    // Strip surrounding quotes that `maybe_json_value_parser` may have added
342    let stripped = key.trim_matches('"');
343    stripped.parse::<Address>().map_err(|e| eyre::eyre!("failed to parse address: {e}"))
344}
345
346/// Returns true if the key is shaped like a hex address, so a parse failure is a typo rather than
347/// a JSON key.
348fn looks_like_address(key: &str) -> bool {
349    key.trim_matches('"').starts_with("0x")
350}
351
352/// Gets account history from RocksDB with ergonomic key parsing.
353///
354/// Accepts a plain address and uses seek to find the relevant shard.
355fn get_rocksdb_account_history(
356    rocksdb: &reth_provider::providers::RocksDBProvider,
357    key: &str,
358    block: Option<u64>,
359    all_shards: bool,
360    raw: bool,
361) -> eyre::Result<()> {
362    // Try parsing as a plain address first, fall back to full JSON ShardedKey
363    match parse_address(key) {
364        Ok(address) => {
365            let block_number = block.unwrap_or(u64::MAX);
366            let seek_key = ShardedKey::new(address, block_number);
367
368            if all_shards {
369                // Iterate all shards: seek from (address, 0) until address changes
370                let start = ShardedKey::new(address, 0);
371                let iter = rocksdb.iter_from::<tables::AccountsHistory>(start)?;
372                for result in iter {
373                    let (k, v) = result?;
374                    if k.key != address {
375                        break;
376                    }
377                    println!(
378                        "{}",
379                        serde_json::to_string_pretty(&serde_json::json!({
380                            "highest_block_number": k.highest_block_number,
381                            "value": v,
382                        }))?
383                    );
384                }
385            } else {
386                // Seek to the first shard with highest_block_number >= target
387                let mut iter = rocksdb.iter_from::<tables::AccountsHistory>(seek_key)?;
388                match iter.next() {
389                    Some(Ok((k, v))) if k.key == address => {
390                        if raw {
391                            let raw_val = rocksdb.get_raw::<tables::AccountsHistory>(k)?;
392                            if let Some(bytes) = raw_val {
393                                println!("{}", hex::encode_prefixed(&bytes));
394                            }
395                        } else {
396                            println!("{}", serde_json::to_string_pretty(&v)?);
397                        }
398                    }
399                    Some(Err(e)) => return Err(e.into()),
400                    _ => {
401                        error!(target: "reth::cli", "No content for the given table key.");
402                    }
403                }
404            }
405            Ok(())
406        }
407        Err(e) if looks_like_address(key) => Err(e),
408        Err(_) => {
409            // Fall back to full JSON key parsing (e.g.
410            // `{"key":"0x...","highest_block_number":...}`)
411            if all_shards || block.is_some() {
412                return Err(eyre::eyre!(
413                    "--block and --all-shards require a plain address, not a JSON key"
414                ));
415            }
416            get_rocksdb_table::<tables::AccountsHistory>(rocksdb, key, raw)
417        }
418    }
419}
420
421/// Gets storage history from RocksDB with ergonomic key parsing.
422///
423/// Accepts a plain address + optional `--storage-key` and uses seek.
424fn get_rocksdb_storage_history(
425    rocksdb: &reth_provider::providers::RocksDBProvider,
426    key: &str,
427    storage_key: Option<&str>,
428    block: Option<u64>,
429    all_shards: bool,
430    raw: bool,
431) -> eyre::Result<()> {
432    match parse_address(key) {
433        Ok(address) => {
434            let storage_key = storage_key
435                .map(|s| s.trim_matches('"').parse::<B256>())
436                .transpose()
437                .map_err(|e| eyre::eyre!("failed to parse storage key: {e}"))?
438                .unwrap_or_default();
439            let block_number = block.unwrap_or(u64::MAX);
440            let seek_key = StorageShardedKey::new(address, storage_key, block_number);
441
442            if all_shards {
443                let start = StorageShardedKey::new(address, storage_key, 0);
444                let iter = rocksdb.iter_from::<tables::StoragesHistory>(start)?;
445                for result in iter {
446                    let (k, v) = result?;
447                    if k.address != address || k.sharded_key.key != storage_key {
448                        break;
449                    }
450                    println!(
451                        "{}",
452                        serde_json::to_string_pretty(&serde_json::json!({
453                            "highest_block_number": k.sharded_key.highest_block_number,
454                            "value": v,
455                        }))?
456                    );
457                }
458            } else {
459                let mut iter = rocksdb.iter_from::<tables::StoragesHistory>(seek_key)?;
460                match iter.next() {
461                    Some(Ok((k, v)))
462                        if k.address == address && k.sharded_key.key == storage_key =>
463                    {
464                        if raw {
465                            let raw_val = rocksdb.get_raw::<tables::StoragesHistory>(k)?;
466                            if let Some(bytes) = raw_val {
467                                println!("{}", hex::encode_prefixed(&bytes));
468                            }
469                        } else {
470                            println!("{}", serde_json::to_string_pretty(&v)?);
471                        }
472                    }
473                    Some(Err(e)) => return Err(e.into()),
474                    _ => {
475                        error!(target: "reth::cli", "No content for the given table key.");
476                    }
477                }
478            }
479            Ok(())
480        }
481        Err(e) if looks_like_address(key) => Err(e),
482        Err(_) => {
483            if all_shards || block.is_some() || storage_key.is_some() {
484                return Err(eyre::eyre!(
485                    "--block, --all-shards, and --storage-key require a plain address, not a JSON key"
486                ));
487            }
488            get_rocksdb_table::<tables::StoragesHistory>(rocksdb, key, raw)
489        }
490    }
491}
492
493/// Gets a value from a specific RocksDB table by exact key and prints it.
494fn get_rocksdb_table<T: Table>(
495    rocksdb: &reth_provider::providers::RocksDBProvider,
496    key_str: &str,
497    raw: bool,
498) -> eyre::Result<()> {
499    let key = table_key::<T>(key_str)?;
500
501    if raw {
502        let content = rocksdb.get_raw::<T>(key)?;
503        match content {
504            Some(bytes) => println!("{}", hex::encode_prefixed(&bytes)),
505            None => error!(target: "reth::cli", "No content for the given table key."),
506        }
507    } else {
508        let content = rocksdb.get::<T>(key)?;
509        match content {
510            Some(value) => println!("{}", serde_json::to_string_pretty(&value)?),
511            None => error!(target: "reth::cli", "No content for the given table key."),
512        }
513    }
514
515    Ok(())
516}
517
518/// Get an instance of key for given table
519pub(crate) fn table_key<T: Table>(key: &str) -> Result<T::Key, eyre::Error> {
520    serde_json::from_str(key).map_err(|e| eyre::eyre!(e))
521}
522
523/// Get an instance of subkey for given dupsort table
524fn table_subkey<T: DupSort>(subkey: &str) -> Result<T::SubKey, eyre::Error> {
525    serde_json::from_str(subkey).map_err(|e| eyre::eyre!(e))
526}
527
528struct GetValueViewer<'a, N: NodeTypesWithDB> {
529    tool: &'a DbTool<N>,
530    key: String,
531    subkey: Option<String>,
532    end_key: Option<String>,
533    end_subkey: Option<String>,
534    raw: bool,
535}
536
537impl<N: ProviderNodeTypes> TableViewer<()> for GetValueViewer<'_, N> {
538    type Error = eyre::Report;
539
540    fn view<T: Table>(&self) -> Result<(), Self::Error> {
541        let key = table_key::<T>(&self.key)?;
542
543        // A non-dupsort table cannot have subkeys. The `subkey` arg becomes the `end_key`. First we
544        // check that `end_key` and `end_subkey` weren't previously given, as that wouldn't be
545        // valid.
546        if self.end_key.is_some() || self.end_subkey.is_some() {
547            return Err(eyre::eyre!("Only END_KEY can be given for non-DUPSORT tables"));
548        }
549
550        let end_key = self.subkey.clone();
551
552        // Check if we're doing a range query
553        if let Some(ref end_key_str) = end_key {
554            let end_key = table_key::<T>(end_key_str)?;
555
556            // Use walk_range to iterate over the range
557            self.tool.provider_factory.db_ref().view(|tx| {
558                let mut cursor = tx.cursor_read::<T>()?;
559                let walker = cursor.walk_range(key..end_key)?;
560
561                for result in walker {
562                    let (k, v) = result?;
563                    let json_val = if self.raw {
564                        let raw_key = RawKey::from(k);
565                        serde_json::json!({
566                            "key": hex::encode_prefixed(raw_key.raw_key()),
567                            "val": hex::encode_prefixed(v.compress().as_ref()),
568                        })
569                    } else {
570                        serde_json::json!({
571                            "key": &k,
572                            "val": &v,
573                        })
574                    };
575
576                    println!("{}", serde_json::to_string_pretty(&json_val)?);
577                }
578
579                Ok::<_, eyre::Report>(())
580            })??;
581        } else {
582            // Single key lookup
583            let content = if self.raw {
584                self.tool
585                    .get::<RawTable<T>>(RawKey::from(key))?
586                    .map(|content| hex::encode_prefixed(content.raw_value()))
587            } else {
588                self.tool.get::<T>(key)?.as_ref().map(serde_json::to_string_pretty).transpose()?
589            };
590
591            match content {
592                Some(content) => {
593                    println!("{content}");
594                }
595                None => {
596                    error!(target: "reth::cli", "No content for the given table key.");
597                }
598            };
599        }
600
601        Ok(())
602    }
603
604    fn view_dupsort<T: DupSort>(&self) -> Result<(), Self::Error>
605    where
606        T::Value: reth_primitives_traits::ValueWithSubKey<SubKey = T::SubKey>,
607    {
608        // get a key for given table
609        let key = table_key::<T>(&self.key)?;
610
611        // Check if we're doing a range query
612        if let Some(ref end_key_str) = self.end_key {
613            let end_key = table_key::<T>(end_key_str)?;
614            let Some(start_subkey) = self.subkey.as_deref() else {
615                eyre::bail!("a subkey is required when end_key is given");
616            };
617            let start_subkey = table_subkey::<T>(start_subkey)?;
618            let end_subkey_parsed =
619                self.end_subkey.as_deref().map(table_subkey::<T>).transpose()?;
620
621            self.tool.provider_factory.db_ref().view(|tx| {
622                let mut cursor = tx.cursor_dup_read::<T>()?;
623
624                // Seek to the starting key. If there is actually a key at the starting key then
625                // seek to the subkey within it.
626                if let Some((decoded_key, _)) = cursor.seek(key.clone())? &&
627                    decoded_key == key
628                {
629                    cursor.seek_by_key_subkey(key.clone(), start_subkey.clone())?;
630                }
631
632                // Get the current position to start iteration
633                let mut current = cursor.current()?;
634
635                while let Some((decoded_key, decoded_value)) = current {
636                    // Extract the subkey using the ValueWithSubKey trait
637                    let decoded_subkey = decoded_value.get_subkey();
638
639                    // Check if we've reached the end (exclusive)
640                    if (&decoded_key, Some(&decoded_subkey)) >=
641                        (&end_key, end_subkey_parsed.as_ref())
642                    {
643                        break;
644                    }
645
646                    // Output the entry with both key and subkey
647                    let json_val = if self.raw {
648                        let raw_key = RawKey::from(decoded_key.clone());
649                        serde_json::json!({
650                            "key": hex::encode_prefixed(raw_key.raw_key()),
651                            "val": hex::encode_prefixed(decoded_value.compress().as_ref()),
652                        })
653                    } else {
654                        serde_json::json!({
655                            "key": &decoded_key,
656                            "val": &decoded_value,
657                        })
658                    };
659
660                    println!("{}", serde_json::to_string_pretty(&json_val)?);
661
662                    // Move to next entry
663                    current = cursor.next()?;
664                }
665
666                Ok::<_, eyre::Report>(())
667            })??;
668        } else {
669            // Single key/subkey lookup
670            let Some(subkey) = self.subkey.as_deref() else {
671                eyre::bail!("a subkey is required for dupsort tables");
672            };
673            let subkey = table_subkey::<T>(subkey)?;
674
675            let content = if self.raw {
676                self.tool
677                    .get_dup::<RawDupSort<T>>(RawKey::from(key), RawKey::from(subkey))?
678                    .map(|content| hex::encode_prefixed(content.raw_value()))
679            } else {
680                self.tool
681                    .get_dup::<T>(key, subkey)?
682                    .as_ref()
683                    .map(serde_json::to_string_pretty)
684                    .transpose()?
685            };
686
687            match content {
688                Some(content) => {
689                    println!("{content}");
690                }
691                None => {
692                    error!(target: "reth::cli", "No content for the given table subkey.");
693                }
694            };
695        }
696        Ok(())
697    }
698}
699
700/// Map the user input value to json
701pub(crate) fn maybe_json_value_parser(value: &str) -> Result<String, eyre::Error> {
702    if serde_json::from_str::<serde::de::IgnoredAny>(value).is_ok() {
703        Ok(value.to_string())
704    } else {
705        serde_json::to_string(&value).map_err(|e| eyre::eyre!(e))
706    }
707}
708
709#[cfg(test)]
710mod tests {
711
712    use super::*;
713    use alloy_primitives::{address, b256, B256};
714    use clap::{Args, Parser};
715    use reth_db_api::{
716        models::{storage_sharded_key::StorageShardedKey, ShardedKey},
717        AccountsHistory, HashedAccounts, Headers, StageCheckpoints, StoragesHistory,
718    };
719
720    /// A helper type to parse Args more easily
721    #[derive(Parser)]
722    struct CommandParser<T: Args> {
723        #[command(flatten)]
724        args: T,
725    }
726
727    #[test]
728    fn parse_numeric_key_args() {
729        assert_eq!(table_key::<Headers>("123").unwrap(), 123);
730        assert_eq!(
731            table_key::<HashedAccounts>(
732                "\"0x0ac361fe774b78f8fc4e86c1916930d150865c3fc2e21dca2e58833557608bac\""
733            )
734            .unwrap(),
735            b256!("0x0ac361fe774b78f8fc4e86c1916930d150865c3fc2e21dca2e58833557608bac")
736        );
737    }
738
739    #[test]
740    fn parse_string_key_args() {
741        assert_eq!(
742            table_key::<StageCheckpoints>("\"MerkleExecution\"").unwrap(),
743            "MerkleExecution"
744        );
745    }
746
747    #[test]
748    fn parse_json_key_args() {
749        assert_eq!(
750            table_key::<StoragesHistory>(r#"{ "address": "0x01957911244e546ce519fbac6f798958fafadb41", "sharded_key": { "key": "0x0000000000000000000000000000000000000000000000000000000000000003", "highest_block_number": 18446744073709551615 } }"#).unwrap(),
751            StorageShardedKey::new(
752                address!("0x01957911244e546ce519fbac6f798958fafadb41"),
753                B256::with_last_byte(3),
754                18446744073709551615
755            )
756        );
757    }
758
759    #[test]
760    fn parse_json_key_for_account_history() {
761        assert_eq!(
762            table_key::<AccountsHistory>(r#"{ "key": "0x4448e1273fd5a8bfdb9ed111e96889c960eee145", "highest_block_number": 18446744073709551615 }"#).unwrap(),
763            ShardedKey::new(
764                address!("0x4448e1273fd5a8bfdb9ed111e96889c960eee145"),
765                18446744073709551615
766            )
767        );
768    }
769}