use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use crate::counter::{CounterSink, ErasedCounterTarget};
use crate::error::{DbError, DbResult};
use crate::unique::ClaimTarget;
use super::registry::{BuiltRegistry, Decl, IndexTargetDecl};
pub(super) type Owner = (String, String);
pub(super) enum BuildIssue {
Collision {
index: &'static str,
key: Vec<u8>,
owners: [Owner; 2],
},
Projection {
source: &'static str,
error: DbError,
},
}
pub(super) struct SkippedSource {
pub name: &'static str,
pub rows: u64,
pub targets: Vec<&'static str>,
}
pub(super) type TargetRows = HashMap<Vec<u8>, (Vec<u8>, Owner)>;
#[derive(Default)]
pub(super) struct ExpectedRows {
by_target: HashMap<&'static str, TargetRows>,
}
impl ExpectedRows {
pub(super) fn rows(&self, target: &str) -> Option<&TargetRows> {
self.by_target.get(target)
}
pub(super) fn contains(&self, target: &str, key: &[u8]) -> bool {
self.by_target
.get(target)
.is_some_and(|m| m.contains_key(key))
}
}
pub(super) fn declared_targets(reg: &BuiltRegistry) -> HashSet<&'static str> {
reg.reg
.decls
.iter()
.filter_map(|d| match d {
Decl::Index { targets, .. } => Some(targets.as_slice()),
_ => None,
})
.flat_map(|ts| ts.iter().map(|t| t.name))
.collect()
}
pub(super) fn target_decl<'r>(reg: &'r BuiltRegistry, name: &str) -> Option<&'r IndexTargetDecl> {
reg.reg
.decls
.iter()
.filter_map(|d| match d {
Decl::Index { targets, .. } => Some(targets),
_ => None,
})
.flat_map(|ts| ts.iter())
.find(|t| t.name == name)
}
pub(super) fn build(
reg: &BuiltRegistry,
on_issue: &mut dyn FnMut(BuildIssue) -> DbResult<()>,
) -> DbResult<(ExpectedRows, Vec<SkippedSource>)> {
let mut expected = ExpectedRows::default();
let mut skipped: Vec<SkippedSource> = Vec::new();
for d in ®.reg.decls {
let Decl::Index {
source,
targets,
project,
} = d
else {
continue;
};
let Some(src) = reg.reg.entries.iter().find(|e| e.name == *source) else {
continue;
};
let mut abort: Option<DbError> = None;
let mut issue = |i: BuildIssue, abort: &mut Option<DbError>| {
if let Err(e) = on_issue(i) {
*abort = Some(e);
}
};
let limits = src.projection_limits;
(src.scan_any)(&mut |src_kb, k, v| {
if abort.is_some() {
return;
}
let mut rows = Vec::new();
if let Err(e) = project(k, v, limits, &mut rows) {
issue(BuildIssue::Projection { source, error: e }, &mut abort);
return;
}
let owner = (source.to_string(), super::validate::hex(src_kb));
for (target_idx, key, value) in rows {
let Some(t) = targets.get(target_idx) else {
issue(
BuildIssue::Projection {
source,
error: DbError::Internal("index decl: target index out of range"),
},
&mut abort,
);
continue;
};
let rows = expected.by_target.entry(t.name).or_default();
if let Some((_, existing)) = rows.get(&key)
&& existing != &owner
{
let mut owners = [existing.clone(), owner.clone()];
owners.sort();
issue(
BuildIssue::Collision {
index: t.name,
key: key.clone(),
owners,
},
&mut abort,
);
continue;
}
rows.insert(key, (value, owner.clone()));
}
});
if let Some(e) = abort {
return Err(e);
}
let rows = (src.undecodable_rows)();
if rows > 0 {
let names: Vec<&'static str> = targets.iter().map(|t| t.name).collect();
tracing::warn!(
source = source,
rows,
targets = ?names,
"schema: source rows could not be read; the expected index set is incomplete"
);
skipped.push(SkippedSource {
name: source,
rows,
targets: names,
});
}
}
Ok((expected, skipped))
}
pub(super) type CounterKey = (&'static str, &'static str, Vec<u8>);
pub(super) fn build_counters(reg: &BuiltRegistry) -> DbResult<HashMap<CounterKey, u64>> {
let mut out: HashMap<CounterKey, u64> = HashMap::new();
for d in ®.reg.decls {
let Decl::Counter {
source,
targets,
project,
} = d
else {
continue;
};
let Some(src) = reg.reg.entries.iter().find(|e| e.name == *source) else {
continue;
};
let (max_rows, max_bytes) = src.projection_limits.unwrap_or((
crate::projection::DEFAULT_MAX_PROJECTED_ROWS,
crate::projection::DEFAULT_MAX_PROJECTED_BYTES,
));
let mut sums: HashMap<(usize, Vec<u8>), u128> = HashMap::new();
let mut abort: Option<DbError> = None;
(src.scan_any)(&mut |_src_kb, k, v| {
if abort.is_some() {
return;
}
let mut sink = CounterSink::new(targets, max_rows, max_bytes);
if let Err(e) = project(k, v, &mut sink) {
abort = Some(e);
return;
}
for (slot, w) in sink.into_acc() {
let entry = sums.entry(slot).or_insert(0u128);
match entry.checked_add(w) {
Some(sum) => *entry = sum,
None => abort = Some(DbError::Internal("counter expected-sum overflow")),
}
}
});
if let Some(e) = abort {
return Err(e);
}
for ((gi, key), sum) in sums {
let member: &std::sync::Arc<dyn ErasedCounterTarget> = match targets
.resolve_group(gi, &key)
{
Ok(Some(m)) => m,
Ok(None) => continue,
Err(DbError::CounterTargetMissing { targets, key }) => {
tracing::warn!(
group = gi,
targets = %targets,
key = %key,
"schema: dangling contribution — no live target row holds this key, skipping (see counter::recount)"
);
continue;
}
Err(e) => return Err(e),
};
if member.on_missing() == crate::counter::OnMissingTarget::Skip
&& !member.contains(&key)?
{
continue;
}
let want = u64::try_from(sum).map_err(|_| DbError::CounterOverflow {
collection: member.collection_name(),
field: member.field(),
key: super::validate::hex(&key),
})?;
out.insert((member.collection_name(), member.field(), key), want);
}
}
Ok(out)
}
pub(super) struct CounterDrift {
pub key: Vec<u8>,
pub want: u64,
pub stored: u64,
}
pub(super) fn counter_drift(
target: &std::sync::Arc<dyn ErasedCounterTarget>,
expected: &HashMap<CounterKey, u64>,
) -> DbResult<Vec<CounterDrift>> {
let mut existing: HashMap<Vec<u8>, u64> = HashMap::new();
target.for_each_counter_row(&mut |key, stored| {
existing.insert(key.to_vec(), stored);
Ok(())
})?;
let name = target.collection_name();
let field = target.field();
let mut keys: HashSet<Vec<u8>> = existing.keys().cloned().collect();
keys.extend(
expected
.keys()
.filter(|(n, f, _)| *n == name && *f == field)
.map(|(_, _, key)| key.clone()),
);
let mut out = Vec::new();
for key in keys {
let want = expected
.get(&(name, field, key.clone()))
.copied()
.unwrap_or(0);
let stored = existing.get(&key).copied().unwrap_or(0);
if want != stored {
out.push(CounterDrift { key, want, stored });
}
}
Ok(out)
}
pub(super) struct UniqueClaim {
pub source: &'static str,
pub source_key: String,
pub owner: crate::UniqueOwner,
}
pub(super) type UniqueExpected = HashMap<(&'static str, Vec<u8>), Vec<UniqueClaim>>;
pub(super) fn declared_unique_handles(
reg: &BuiltRegistry,
) -> HashMap<&'static str, Arc<dyn ClaimTarget>> {
let mut out: HashMap<&'static str, Arc<dyn ClaimTarget>> = HashMap::new();
for d in ®.reg.decls {
if let Decl::Unique { targets, .. } = d {
for t in targets.all() {
out.entry(t.collection_name())
.or_insert_with(|| Arc::clone(t));
}
}
}
out
}
pub(super) fn build_unique(reg: &BuiltRegistry) -> DbResult<(UniqueExpected, Vec<SkippedSource>)> {
use crate::unique::{ClaimSink, UniqueOwner, collection_tag};
let mut expected: UniqueExpected = HashMap::new();
let mut skipped: Vec<SkippedSource> = Vec::new();
for d in ®.reg.decls {
let Decl::Unique {
source,
targets,
project,
} = d
else {
continue;
};
let Some(src) = reg.reg.entries.iter().find(|e| e.name == *source) else {
continue;
};
let (max_rows, max_bytes) = src.projection_limits.unwrap_or((
crate::projection::DEFAULT_MAX_PROJECTED_ROWS,
crate::projection::DEFAULT_MAX_PROJECTED_BYTES,
));
let mut abort: Option<DbError> = None;
(src.scan_any)(&mut |src_kb, k, v| {
if abort.is_some() {
return;
}
let mut sink = ClaimSink::new(targets, max_rows, max_bytes);
if let Err(e) = project(k, v, &mut sink) {
abort = Some(e);
return;
}
let Some(owner) = UniqueOwner::new(collection_tag(source), src_kb, 0) else {
abort = Some(DbError::Internal(
"unique decl: source key exceeds the owner capacity",
));
return;
};
let mut this_row: std::collections::BTreeSet<(&'static str, Vec<u8>)> =
std::collections::BTreeSet::new();
for intent in sink.claims() {
this_row.insert((intent.namespace, intent.key.clone()));
}
for (namespace, key) in this_row {
expected
.entry((namespace, key))
.or_default()
.push(UniqueClaim {
source,
source_key: super::validate::hex(src_kb),
owner,
});
}
});
if let Some(e) = abort {
return Err(e);
}
let rows = (src.undecodable_rows)();
if rows > 0 {
let names: Vec<&'static str> = targets.all().map(|t| t.collection_name()).collect();
tracing::warn!(
source = source,
rows,
targets = ?names,
"schema: source rows could not be read; the expected unique-claim set is incomplete"
);
skipped.push(SkippedSource {
name: source,
rows,
targets: names,
});
}
}
Ok((expected, skipped))
}