use std::collections::{HashMap, HashSet};
use crate::armour::db::Db;
use crate::error::{DbError, DbResult};
use super::registry::{BuiltRegistry, Decl, scan_target_actual};
use super::unique_repair;
use super::validate::hex;
pub use super::unique_repair::{NamespaceConflict, NamespaceRepairStats};
#[derive(Debug, Clone, Default)]
pub struct RepairOptions {
pub delete_orphans: bool,
}
#[derive(Debug, Clone, Default)]
pub struct IndexRepairStats {
pub name: &'static str,
pub puts: u64,
pub rewrites: u64,
pub deletes: u64,
pub declined_orphans: u64,
}
#[derive(Debug, Clone, Default)]
pub struct CounterRepairStats {
pub collection: &'static str,
pub field: &'static str,
pub fixed: u64,
}
#[derive(Debug, Clone)]
pub struct UndecodableSource {
pub name: &'static str,
pub rows: u64,
pub protected_targets: Vec<&'static str>,
}
#[derive(Debug, Clone, Default)]
pub struct RepairReport {
pub per_index: Vec<IndexRepairStats>,
pub per_counter: Vec<CounterRepairStats>,
pub per_namespace: Vec<NamespaceRepairStats>,
pub undecodable_sources: Vec<UndecodableSource>,
}
pub fn repair(db: &Db, reg: &BuiltRegistry, opts: RepairOptions) -> DbResult<RepairReport> {
let _ = db;
use super::expected::{self, BuildIssue};
let (expected, skipped) = expected::build(reg, &mut |issue| match issue {
BuildIssue::Collision { index, key, owners } => Err(DbError::IndexKeyCollision {
index,
key: hex(&key),
owners: owners.to_vec(),
}),
BuildIssue::Projection { error, .. } => Err(error),
})?;
let protected: HashSet<&'static str> = skipped
.iter()
.flat_map(|s| s.targets.iter().copied())
.collect();
let mut undecodable_sources: Vec<UndecodableSource> = skipped
.into_iter()
.map(|s| UndecodableSource {
name: s.name,
rows: s.rows,
protected_targets: s.targets,
})
.collect();
let mut actual_by_target: HashMap<&'static str, HashMap<Vec<u8>, Vec<u8>>> = HashMap::new();
let mut stats_by_name: HashMap<&'static str, IndexRepairStats> = HashMap::new();
let all_targets = expected::declared_targets(reg);
for target_name in &all_targets {
let Some(rows) = expected.rows(target_name) else {
continue;
};
let Some(t) = expected::target_decl(reg, target_name) else {
continue;
};
let actual = scan_target_actual(reg, &mut actual_by_target, t.name, &t.value_to_bytes);
for (key, (value, _owner)) in rows {
let action = match actual.get(key) {
None => Some(true),
Some(actual_bytes) => match (t.value_eq)(value, actual_bytes) {
Ok(false) => Some(false),
Ok(true) => None,
Err(e) => return Err(e),
},
};
if let Some(missing) = action {
t.index.put_bytes(key, value)?;
let stats = stats_by_name
.entry(t.name)
.or_insert_with(|| IndexRepairStats {
name: t.name,
..Default::default()
});
if missing {
stats.puts += 1;
} else {
stats.rewrites += 1;
}
}
}
}
for target_name in all_targets {
let Some(tdecl) = expected::target_decl(reg, target_name) else {
continue;
};
let actual = scan_target_actual(
reg,
&mut actual_by_target,
target_name,
&tdecl.value_to_bytes,
);
let complete =
reg.complete_indices.contains(target_name) && !protected.contains(target_name);
let orphans: Vec<Vec<u8>> = actual
.keys()
.filter(|kb| !expected.contains(target_name, kb))
.cloned()
.collect();
for kb in orphans {
let stats = stats_by_name
.entry(target_name)
.or_insert_with(|| IndexRepairStats {
name: target_name,
..Default::default()
});
if complete && opts.delete_orphans {
tdecl.index.delete_bytes(&kb)?;
stats.deletes += 1;
} else {
stats.declined_orphans += 1;
}
}
}
let mut per_index: Vec<_> = stats_by_name.into_values().collect();
per_index.sort_by_key(|s| s.name);
let counter_expected = expected::build_counters(reg)?;
let mut per_counter: Vec<CounterRepairStats> = Vec::new();
for d in ®.reg.decls {
let Decl::Counter {
source, targets, ..
} = d
else {
continue;
};
let Some(src_entry) = reg.reg.entries.iter().find(|e| e.name == *source) else {
continue;
};
let skipped_rows = (src_entry.undecodable_rows)();
if skipped_rows > 0 {
let target_names: Vec<&'static str> =
targets.all().map(|t| t.collection_name()).collect();
match undecodable_sources.iter_mut().find(|s| s.name == *source) {
Some(existing) => {
for name in target_names {
if !existing.protected_targets.contains(&name) {
existing.protected_targets.push(name);
}
}
}
None => undecodable_sources.push(UndecodableSource {
name: source,
rows: skipped_rows,
protected_targets: target_names,
}),
}
continue;
}
for t in targets.all() {
let drift = expected::counter_drift(t, &counter_expected)?;
let mut fixed = 0u64;
for d in drift {
let delta = i128::from(d.want) - i128::from(d.stored);
match t.apply_delta(&d.key, delta) {
Ok(true) => fixed += 1,
Ok(false) => {}
Err(DbError::CounterTargetMissing { targets, key }) => {
tracing::warn!(
collection = t.collection_name(),
field = t.field(),
targets = %targets,
key = %key,
"repair: dangling contribution — target row missing, skipping (see schema::validate)"
);
}
Err(e) => return Err(e),
}
}
per_counter.push(CounterRepairStats {
collection: t.collection_name(),
field: t.field(),
fixed,
});
}
}
per_counter.sort_by(|a, b| (a.collection, a.field).cmp(&(b.collection, b.field)));
let (per_namespace, unique_skipped) = unique_repair::repair_namespaces(reg, &opts)?;
for s in unique_skipped {
match undecodable_sources.iter_mut().find(|u| u.name == s.name) {
Some(existing) => {
for name in s.targets {
if !existing.protected_targets.contains(&name) {
existing.protected_targets.push(name);
}
}
}
None => undecodable_sources.push(UndecodableSource {
name: s.name,
rows: s.rows,
protected_targets: s.targets,
}),
}
}
Ok(RepairReport {
per_index,
per_counter,
per_namespace,
undecodable_sources,
})
}