use clap::Parser;
use reth_db::{open_db_read_only, tables_to_generic, DatabaseEnv, Tables};
use reth_db_api::{cursor::DbCursorRO, database::Database, table::Table, transaction::DbTx};
use reth_db_common::DbTool;
use reth_node_builder::{NodeTypesWithDBAdapter, NodeTypesWithEngine};
use reth_node_core::{
args::DatabaseArgs,
dirs::{DataDirPath, PlatformPath},
};
use std::{
collections::HashMap,
fmt::Debug,
fs::{self, File},
hash::Hash,
io::Write,
path::{Path, PathBuf},
sync::Arc,
};
use tracing::{info, warn};
#[derive(Parser, Debug)]
pub struct Command {
#[arg(long, verbatim_doc_comment)]
secondary_datadir: PlatformPath<DataDirPath>,
#[command(flatten)]
second_db: DatabaseArgs,
#[arg(long, verbatim_doc_comment)]
table: Option<Tables>,
#[arg(long, verbatim_doc_comment)]
output: PlatformPath<PathBuf>,
}
impl Command {
pub fn execute<T: NodeTypesWithEngine>(
self,
tool: &DbTool<NodeTypesWithDBAdapter<T, Arc<DatabaseEnv>>>,
) -> eyre::Result<()> {
warn!("Make sure the node is not running when running `reth db diff`!");
let second_db_path: PathBuf = self.secondary_datadir.join("db").into();
let second_db = open_db_read_only(&second_db_path, self.second_db.database_args())?;
let tables = match &self.table {
Some(table) => std::slice::from_ref(table),
None => Tables::ALL,
};
for table in tables {
let mut primary_tx = tool.provider_factory.db_ref().tx()?;
let mut secondary_tx = second_db.tx()?;
primary_tx.disable_long_read_transaction_safety();
secondary_tx.disable_long_read_transaction_safety();
let output_dir = self.output.clone();
tables_to_generic!(table, |Table| find_diffs::<Table>(
primary_tx,
secondary_tx,
output_dir
))?;
}
Ok(())
}
}
fn find_diffs<T: Table>(
primary_tx: impl DbTx,
secondary_tx: impl DbTx,
output_dir: impl AsRef<Path>,
) -> eyre::Result<()>
where
T::Key: Hash,
T::Value: PartialEq,
{
let table = T::NAME;
info!("Analyzing table {table}...");
let result = find_diffs_advanced::<T>(&primary_tx, &secondary_tx)?;
info!("Done analyzing table {table}!");
info!("");
info!("Diff results for {table}:");
fs::create_dir_all(output_dir.as_ref())?;
let file_name = format!("{table}.txt");
let mut file = File::create(output_dir.as_ref().join(file_name.clone()))?;
let discrepancies = result.discrepancies.len();
let extra_elements = result.extra_elements.len();
writeln!(file, "Diff results for {table}")?;
if discrepancies > 0 {
writeln!(file, "Found {discrepancies} discrepancies in table {table}")?;
info!("Found {discrepancies} discrepancies in table {table}");
} else {
writeln!(file, "No discrepancies found in table {table}")?;
info!("No discrepancies found in table {table}");
}
if extra_elements > 0 {
writeln!(file, "Found {extra_elements} extra elements in table {table}")?;
info!("Found {extra_elements} extra elements in table {table}");
} else {
writeln!(file, "No extra elements found in table {table}")?;
info!("No extra elements found in table {table}");
}
info!("Writing diff results for {table} to {file_name}...");
if discrepancies > 0 {
writeln!(file, "Discrepancies:")?;
}
for discrepancy in result.discrepancies.values() {
writeln!(file, "{discrepancy:?}")?;
}
if extra_elements > 0 {
writeln!(file, "Extra elements:")?;
}
for extra_element in result.extra_elements.values() {
writeln!(file, "{extra_element:?}")?;
}
let full_file_name = output_dir.as_ref().join(file_name);
info!("Done writing diff results for {table} to {}", full_file_name.display());
Ok(())
}
fn find_diffs_advanced<T: Table>(
primary_tx: &impl DbTx,
secondary_tx: &impl DbTx,
) -> eyre::Result<TableDiffResult<T>>
where
T::Value: PartialEq,
T::Key: Hash,
{
let mut primary_zip_cursor =
primary_tx.cursor_read::<T>().expect("Was not able to obtain a cursor.");
let primary_walker = primary_zip_cursor.walk(None)?;
let mut secondary_zip_cursor =
secondary_tx.cursor_read::<T>().expect("Was not able to obtain a cursor.");
let secondary_walker = secondary_zip_cursor.walk(None)?;
let zipped_cursor = primary_walker.zip(secondary_walker);
let mut primary_cursor =
primary_tx.cursor_read::<T>().expect("Was not able to obtain a cursor.");
let mut secondary_cursor =
secondary_tx.cursor_read::<T>().expect("Was not able to obtain a cursor.");
let mut result = TableDiffResult::<T>::default();
for (primary_entry, secondary_entry) in zipped_cursor {
let (primary_key, primary_value) = primary_entry?;
let (secondary_key, secondary_value) = secondary_entry?;
if primary_key != secondary_key {
let crossed_secondary =
secondary_cursor.seek_exact(primary_key.clone())?.map(|(_, value)| value);
result.try_push_discrepancy(
primary_key.clone(),
Some(primary_value),
crossed_secondary,
);
let crossed_primary =
primary_cursor.seek_exact(secondary_key.clone())?.map(|(_, value)| value);
result.try_push_discrepancy(
secondary_key.clone(),
crossed_primary,
Some(secondary_value),
);
} else {
result.try_push_discrepancy(primary_key, Some(primary_value), Some(secondary_value));
}
}
Ok(result)
}
#[derive(Debug)]
struct TableDiffElement<T: Table> {
key: T::Key,
#[allow(dead_code)]
first: T::Value,
#[allow(dead_code)]
second: T::Value,
}
struct TableDiffResult<T: Table>
where
T::Key: Hash,
{
discrepancies: HashMap<T::Key, TableDiffElement<T>>,
extra_elements: HashMap<T::Key, ExtraTableElement<T>>,
}
impl<T> Default for TableDiffResult<T>
where
T: Table,
T::Key: Hash,
{
fn default() -> Self {
Self { discrepancies: HashMap::default(), extra_elements: HashMap::default() }
}
}
impl<T: Table> TableDiffResult<T>
where
T::Key: Hash,
{
fn push_discrepancy(&mut self, discrepancy: TableDiffElement<T>) {
self.discrepancies.insert(discrepancy.key.clone(), discrepancy);
}
fn push_extra_element(&mut self, element: ExtraTableElement<T>) {
self.extra_elements.insert(element.key().clone(), element);
}
}
impl<T> TableDiffResult<T>
where
T: Table,
T::Key: Hash,
T::Value: PartialEq,
{
fn try_push_discrepancy(
&mut self,
key: T::Key,
first: Option<T::Value>,
second: Option<T::Value>,
) {
if self.discrepancies.contains_key(&key) {
return
}
if self.extra_elements.contains_key(&key) {
return
}
match (first, second) {
(Some(first), Some(second)) => {
if first != second {
self.push_discrepancy(TableDiffElement { key, first, second });
}
}
(Some(first), None) => {
self.push_extra_element(ExtraTableElement::First { key, value: first });
}
(None, Some(second)) => {
self.push_extra_element(ExtraTableElement::Second { key, value: second });
}
(None, None) => {}
}
}
}
#[derive(Debug)]
enum ExtraTableElement<T: Table> {
#[allow(dead_code)]
First { key: T::Key, value: T::Value },
#[allow(dead_code)]
Second { key: T::Key, value: T::Value },
}
impl<T: Table> ExtraTableElement<T> {
const fn key(&self) -> &T::Key {
match self {
Self::First { key, .. } | Self::Second { key, .. } => key,
}
}
}