1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
use super::tui::DbListTUI;
use alloy_primitives::hex;
use clap::Parser;
use eyre::WrapErr;
use reth_chainspec::ChainSpec;
use reth_db::{DatabaseEnv, RawValue, TableViewer, Tables};
use reth_db_api::{database::Database, table::Table};
use reth_db_common::{DbTool, ListFilter};
use reth_node_builder::{NodeTypesWithDBAdapter, NodeTypesWithEngine};
use std::{cell::RefCell, sync::Arc};
use tracing::error;

#[derive(Parser, Debug)]
/// The arguments for the `reth db list` command
pub struct Command {
    /// The table name
    table: Tables,
    /// Skip first N entries
    #[arg(long, short, default_value_t = 0)]
    skip: usize,
    /// Reverse the order of the entries. If enabled last table entries are read.
    #[arg(long, short, default_value_t = false)]
    reverse: bool,
    /// How many items to take from the walker
    #[arg(long, short, default_value_t = 5)]
    len: usize,
    /// Search parameter for both keys and values. Prefix it with `0x` to search for binary data,
    /// and text otherwise.
    ///
    /// ATTENTION! For compressed tables (`Transactions` and `Receipts`), there might be
    /// missing results since the search uses the raw uncompressed value from the database.
    #[arg(long)]
    search: Option<String>,
    /// Minimum size of row in bytes
    #[arg(long, default_value_t = 0)]
    min_row_size: usize,
    /// Minimum size of key in bytes
    #[arg(long, default_value_t = 0)]
    min_key_size: usize,
    /// Minimum size of value in bytes
    #[arg(long, default_value_t = 0)]
    min_value_size: usize,
    /// Returns the number of rows found.
    #[arg(long, short)]
    count: bool,
    /// Dump as JSON instead of using TUI.
    #[arg(long, short)]
    json: bool,
    /// Output bytes instead of human-readable decoded value
    #[arg(long)]
    raw: bool,
}

impl Command {
    /// Execute `db list` command
    pub fn execute<N: NodeTypesWithEngine<ChainSpec = ChainSpec>>(
        self,
        tool: &DbTool<NodeTypesWithDBAdapter<N, Arc<DatabaseEnv>>>,
    ) -> eyre::Result<()> {
        self.table.view(&ListTableViewer { tool, args: &self })
    }

    /// Generate [`ListFilter`] from command.
    pub fn list_filter(&self) -> ListFilter {
        let search = self
            .search
            .as_ref()
            .map(|search| {
                if let Some(search) = search.strip_prefix("0x") {
                    return hex::decode(search).unwrap()
                }
                search.as_bytes().to_vec()
            })
            .unwrap_or_default();

        ListFilter {
            skip: self.skip,
            len: self.len,
            search,
            min_row_size: self.min_row_size,
            min_key_size: self.min_key_size,
            min_value_size: self.min_value_size,
            reverse: self.reverse,
            only_count: self.count,
        }
    }
}

struct ListTableViewer<'a, N: NodeTypesWithEngine> {
    tool: &'a DbTool<NodeTypesWithDBAdapter<N, Arc<DatabaseEnv>>>,
    args: &'a Command,
}

impl<N: NodeTypesWithEngine> TableViewer<()> for ListTableViewer<'_, N> {
    type Error = eyre::Report;

    fn view<T: Table>(&self) -> Result<(), Self::Error> {
        self.tool.provider_factory.db_ref().view(|tx| {
            let table_db = tx.inner.open_db(Some(self.args.table.name())).wrap_err("Could not open db.")?;
            let stats = tx.inner.db_stat(&table_db).wrap_err(format!("Could not find table: {}", stringify!($table)))?;
            let total_entries = stats.entries();
            let final_entry_idx = total_entries.saturating_sub(1);
            if self.args.skip > final_entry_idx {
                error!(
                    target: "reth::cli",
                    "Start index {start} is greater than the final entry index ({final_entry_idx}) in the table {table}",
                    start = self.args.skip,
                    final_entry_idx = final_entry_idx,
                    table = self.args.table.name()
                );
                return Ok(())
            }


            let list_filter = self.args.list_filter();

            if self.args.json || self.args.count {
                let (list, count) = self.tool.list::<T>(&list_filter)?;

                if self.args.count {
                    println!("{count} entries found.")
                } else if self.args.raw {
                    let list = list.into_iter().map(|row| (row.0, RawValue::new(row.1).into_value())).collect::<Vec<_>>();
                    println!("{}", serde_json::to_string_pretty(&list)?);
                } else {
                    println!("{}", serde_json::to_string_pretty(&list)?);
                }
                Ok(())
            } else {
                let list_filter = RefCell::new(list_filter);
                DbListTUI::<_, T>::new(|skip, len| {
                    list_filter.borrow_mut().update_page(skip, len);
                    self.tool.list::<T>(&list_filter.borrow()).unwrap().0
                }, self.args.skip, self.args.len, total_entries, self.args.raw).run()
            }
        })??;

        Ok(())
    }
}