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
//! `TABLE.VERIFY`'s per-index counters — both directions of the audit.
//! Split from `cmd_table` for the 500-LOC house rule.
use kevy_store::Store;
use crate::index_runtime;
use crate::state::Ctx;
/// One compiled index's per-shard verify counters — both directions
/// (the embedded face's `shard_index_counts` is the byte-parity
/// twin, and the oracle holds them together).
///
/// index→row: the IDX.VERIFY drift recheck. row→index: every prefix row
/// classifies against the spec with the cause kept — fresh
/// `coerce_failures` (present-but-wrong-type only; the 4.0 lifetime
/// counter also swallowed absences), `excluded` (composite oversize),
/// `absent` (NULL by design), and `missing` (derives a value, has no
/// entry — the class a drift walk cannot see).
/// The row→index half of the walk, under the no-promote peek: every
/// prefix row classified by cause.
/// Returns `[coerce_fresh, excluded, absent, rows, missing]`.
/// `window` is the audit's cold-side evidence on a windowed path. Rows
/// below the boundary are absent from the hot tree by design — but only
/// if they reached a cold segment, and a row lost between the two
/// structures sits below the boundary exactly like a legitimate one.
/// So they are counted, not exempted, and reconciled against the live
/// cold entries at the end. Without any of this a windowed path reports
/// every slid row as a hole in its own index (17500 of them on a
/// 20000-row table); exempting them by position instead reports zero
/// even when rows really are lost.
pub(crate) fn classify_prefix_rows(
s: &mut Store,
spec: &kevy_index::IndexSpec,
row_keys: &[Vec<u8>],
indexed: &std::collections::HashSet<&[u8]>,
window: Option<kevy_index::WindowAudit>,
) -> [u64; 5] {
let names = spec.scalar_read_names();
let w = spec.primary_width();
let mut f = [0u64; 5];
// Holes below the window boundary: candidates for the cold side,
// reconciled against its own count once the walk is done.
let mut below = 0u64;
for key in row_keys {
f[3] += 1;
let cls = match s.peek_hash_fields(key, &names[..w]) {
Ok(Some(vals)) => spec.classify_scalar(&vals),
_ => kevy_index::RowDerivation::Absent,
};
match cls {
kevy_index::RowDerivation::Indexed(_) => {
if !indexed.contains(key.as_slice()) {
if slid_out(s, spec, key, window) {
below += 1;
} else {
f[4] += 1;
}
}
}
kevy_index::RowDerivation::CoerceFailed => f[0] += 1,
kevy_index::RowDerivation::Oversize => f[1] += 1,
kevy_index::RowDerivation::Absent => f[2] += 1,
}
}
// Every row that slid should have an entry waiting for it. The
// shortfall is exactly the rows that fell between the hot tree and
// the segment. `saturating_sub` because a cold entry can outlive
// its row (tombstoning is the writer's job, not the reader's) —
// that direction is the drift walk's, not this one's.
f[4] += below.saturating_sub(window.map_or(0, |w| w.cold_live));
f
}
/// Is this row below the window boundary — i.e. one the cold side is
/// supposed to be holding? Only asked about a row that looks missing,
/// so the extra peek is paid per candidate hole rather than per row.
/// Says nothing about whether the cold side actually has it; the
/// caller's reconciliation does that.
fn slid_out(
s: &mut Store,
spec: &kevy_index::IndexSpec,
key: &[u8],
window: Option<kevy_index::WindowAudit>,
) -> bool {
let Some(w) = window else {
return false;
};
match index_runtime::row_value(s, spec, key) {
index_runtime::RowValue::Value(v) => {
kevy_index::window_value_of(&v, w.shape).is_some_and(|wv| wv < w.boundary)
}
_ => false,
}
}
pub(crate) fn index_verify_counts(
ctx: &Ctx<'_>,
store: &mut Store,
name: &[u8],
) -> Result<[u64; 10], kevy_resp::CmdError> {
let (spec, entries, stats, window) =
index_runtime::with_ready_segment(ctx, store, name, |spec, seg, win| {
let mut entries: Vec<(Vec<u8>, kevy_index::IndexValue)> = Vec::new();
seg.each_entry(|k, v| entries.push((k.to_vec(), v.clone())));
let audit = win.and_then(|w| w.audit(spec.ty));
(spec.clone(), entries, seg.stats(), audit)
})?;
let indexed: std::collections::HashSet<&[u8]> =
entries.iter().map(|(k, _)| k.as_slice()).collect();
let mut pat = spec.prefix.clone();
pat.push(b'*');
let row_keys = store.collect_keys(Some(&pat), None);
let (drift, fresh) = store.peek_scope(|s| {
let mut drift = 0u64;
for (key, held) in &entries {
match index_runtime::row_value(s, &spec, key) {
index_runtime::RowValue::Value(actual) if &actual == held => {}
_ => drift += 1,
}
}
(drift, classify_prefix_rows(s, &spec, &row_keys, &indexed, window))
});
Ok([
stats.entries,
stats.approx_bytes,
fresh[0],
stats.duplicates,
drift,
entries.len() as u64,
fresh[1],
fresh[2],
fresh[3],
fresh[4],
])
}