Skip to main content

reth_cli_commands/db/
list.rs

1use super::tui::DbListTUI;
2use alloy_primitives::hex;
3use clap::{builder::RangedU64ValueParser, Parser};
4use eyre::WrapErr;
5use reth_chainspec::EthereumHardforks;
6use reth_db::{transaction::DbTx, DatabaseEnv};
7use reth_db_api::{database::Database, table::Table, RawValue, TableViewer, Tables};
8use reth_db_common::{DbTool, ListFilter};
9use reth_node_builder::{NodeTypes, NodeTypesWithDBAdapter};
10use std::cell::RefCell;
11use tracing::error;
12
13#[derive(Parser, Debug)]
14/// The arguments for the `reth db list` command
15pub struct Command {
16    /// The table name
17    table: Tables,
18    /// Skip first N entries
19    #[arg(long, short, default_value_t = 0)]
20    skip: usize,
21    /// Reverse the order of the entries. If enabled last table entries are read.
22    #[arg(long, short, default_value_t = false)]
23    reverse: bool,
24    /// How many items to take from the walker
25    #[arg(long, short, default_value_t = 5, value_parser = RangedU64ValueParser::<usize>::new().range(1..))]
26    len: usize,
27    /// Search parameter for both keys and values. Prefix it with `0x` to search for binary data,
28    /// and text otherwise.
29    ///
30    /// ATTENTION! For compressed tables (`Transactions` and `Receipts`), there might be
31    /// missing results since the search uses the raw uncompressed value from the database.
32    #[arg(long)]
33    search: Option<String>,
34    /// Minimum size of row in bytes
35    #[arg(long, default_value_t = 0)]
36    min_row_size: usize,
37    /// Minimum size of key in bytes
38    #[arg(long, default_value_t = 0)]
39    min_key_size: usize,
40    /// Minimum size of value in bytes
41    #[arg(long, default_value_t = 0)]
42    min_value_size: usize,
43    /// Returns the number of rows found.
44    #[arg(long, short)]
45    count: bool,
46    /// Dump as JSON instead of using TUI.
47    #[arg(long, short)]
48    json: bool,
49    /// Output bytes instead of human-readable decoded value
50    #[arg(long)]
51    raw: bool,
52}
53
54impl Command {
55    /// Execute `db list` command
56    pub fn execute<N: NodeTypes<ChainSpec: EthereumHardforks>>(
57        self,
58        tool: &DbTool<NodeTypesWithDBAdapter<N, DatabaseEnv>>,
59    ) -> eyre::Result<()> {
60        self.table.view(&ListTableViewer { tool, args: &self })
61    }
62
63    /// Generate [`ListFilter`] from command.
64    pub fn list_filter(&self) -> eyre::Result<ListFilter> {
65        let search = match self.search.as_deref() {
66            Some(search) => {
67                if let Some(search) = search.strip_prefix("0x") {
68                    hex::decode(search).wrap_err(
69                        "Invalid hex content after 0x prefix in --search (expected valid hex like 0xdeadbeef).",
70                    )?
71                } else {
72                    search.as_bytes().to_vec()
73                }
74            }
75            None => Vec::new(),
76        };
77
78        Ok(ListFilter {
79            skip: self.skip,
80            len: self.len,
81            search,
82            min_row_size: self.min_row_size,
83            min_key_size: self.min_key_size,
84            min_value_size: self.min_value_size,
85            reverse: self.reverse,
86            only_count: self.count,
87        })
88    }
89}
90
91struct ListTableViewer<'a, N: NodeTypes> {
92    tool: &'a DbTool<NodeTypesWithDBAdapter<N, DatabaseEnv>>,
93    args: &'a Command,
94}
95
96impl<N: NodeTypes> ListTableViewer<'_, N> {
97    /// Lists the viewed table, using `json` to dump a filtered list of rows as pretty JSON along
98    /// with the number of rows found, and `page` to fetch the rows of a TUI page as strings.
99    fn list(
100        &self,
101        json: &dyn Fn(&ListFilter) -> eyre::Result<(String, usize)>,
102        page: &dyn Fn(&ListFilter) -> Vec<(String, String)>,
103    ) -> eyre::Result<()> {
104        self.tool.provider_factory.db_ref().view(|tx| {
105            // We may be using the tui for a long time
106            tx.disable_long_read_transaction_safety();
107
108            let table_db = tx.inner().open_db(Some(self.args.table.name())).wrap_err("Could not open db.")?;
109                    let stats = tx.inner().db_stat(table_db.dbi()).wrap_err(format!("Could not find table: {}", self.args.table.name()))?;
110            let total_entries = stats.entries();
111            let final_entry_idx = total_entries.saturating_sub(1);
112            if self.args.skip > final_entry_idx {
113                error!(
114                    target: "reth::cli",
115                    "Start index {start} is greater than the final entry index ({final_entry_idx}) in the table {table}",
116                    start = self.args.skip,
117                    final_entry_idx = final_entry_idx,
118                    table = self.args.table.name()
119                );
120                return Ok(())
121            }
122
123
124            let list_filter = self.args.list_filter()?;
125
126            if self.args.json || self.args.count {
127                let (list, count) = json(&list_filter)?;
128
129                if self.args.count {
130                    println!("{count} entries found.")
131                } else {
132                    println!("{list}");
133                }
134                Ok(())
135            } else {
136                let list_filter = RefCell::new(list_filter);
137                DbListTUI::new(Box::new(|skip, len| {
138                    list_filter.borrow_mut().update_page(skip, len);
139                    page(&list_filter.borrow())
140                }), self.args.skip, self.args.len, total_entries).run()
141            }
142        })??;
143
144        Ok(())
145    }
146}
147
148impl<N: NodeTypes> TableViewer<()> for ListTableViewer<'_, N> {
149    type Error = eyre::Report;
150
151    fn view<T: Table>(&self) -> Result<(), Self::Error> {
152        self.list(
153            &|filter| {
154                let (list, count) = self.tool.list::<T>(filter)?;
155
156                let list = if self.args.raw {
157                    let list = list
158                        .into_iter()
159                        .map(|row| (row.0, RawValue::new(row.1).into_value()))
160                        .collect::<Vec<_>>();
161                    serde_json::to_string_pretty(&list)?
162                } else {
163                    serde_json::to_string_pretty(&list)?
164                };
165                Ok((list, count))
166            },
167            &|filter| {
168                self.tool
169                    .list::<T>(filter)
170                    .unwrap()
171                    .0
172                    .into_iter()
173                    .map(|(key, value)| {
174                        let value = if self.args.raw {
175                            serde_json::to_string(RawValue::<T::Value>::from(value).raw_value())
176                        } else {
177                            serde_json::to_string_pretty(&value)
178                        };
179                        (
180                            format!("{key:?}"),
181                            value.unwrap_or_else(|error| {
182                                format!("Error serializing value: {error}")
183                            }),
184                        )
185                    })
186                    .collect()
187            },
188        )
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn command_rejects_zero_len() {
198        let result = Command::try_parse_from(["list", "Headers", "--len", "0"]);
199
200        assert!(result.is_err());
201    }
202
203    #[test]
204    fn command_accepts_positive_len() {
205        let command = Command::try_parse_from(["list", "Headers", "--len", "10"]).unwrap();
206
207        assert_eq!(command.len, 10);
208    }
209}