kevy_index/table_verify.rs
1//! Named verify-report types, shared by both faces.
2//!
3//! The first release shipped the embedded report as
4//! `(Vec<(Vec<u8>, [u64; 6])>, [u64; 2])` — six unnamed counters per
5//! index and two more for the spot check. The dogfood report's F10
6//! ("two counters side by side with different time semantics and
7//! nothing saying so") is the direct consequence: an anonymous array
8//! has nowhere to write what a number means. These structs are where
9//! that goes.
10
11/// Per-index verification counters, one row of `TABLE.VERIFY`.
12///
13/// ```
14/// let mut v = kevy_index::IndexVerify::new("t.at");
15/// v.entries = 3;
16/// assert_eq!((v.name.as_slice(), v.entries, v.drift), (&b"t.at"[..], 3, 0));
17/// ```
18#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
19#[non_exhaustive]
20pub struct IndexVerify {
21 /// Compiled index name (`<table>.<column-or-orderpath>`).
22 ///
23 /// ```
24 /// use kevy_index::{IndexVerify, parse_table_declare};
25 /// let t = parse_table_declare(&[
26 /// b"TABLE.DECLARE", b"t", b"PREFIX", b"t:", b"PK", b"id", b"COLUMN", b"id", b"i64",
27 /// b"COLUMN", b"at", b"i64", b"INDEX", b"at", b"range",
28 /// ])?;
29 /// // a verifier opens one row per compiled index, under the compiled name
30 /// let rows: Vec<IndexVerify> = t.compile()?.iter().map(|i| IndexVerify::new(i.name())).collect();
31 /// assert_eq!(rows[0].name, b"t.at");
32 /// # Ok::<(), Box<dyn std::error::Error>>(())
33 /// ```
34 pub name: Vec<u8>,
35 /// Entries currently held by the index.
36 ///
37 /// ```
38 /// let mut v = kevy_index::IndexVerify::new("t.at");
39 /// (v.rows, v.entries, v.absent) = (5, 4, 1);
40 /// // every walked row that derives a value is held
41 /// assert_eq!(v.rows - v.absent - v.coerce_failures - v.excluded - v.missing, v.entries);
42 /// ```
43 pub entries: u64,
44 /// Approximate resident bytes of the index structure.
45 ///
46 /// ```
47 /// let mut v = kevy_index::IndexVerify::new("t.at");
48 /// (v.entries, v.approx_bytes) = (1_000, 48_000);
49 /// assert_eq!(v.approx_bytes / v.entries, 48, "resident bytes per entry");
50 /// ```
51 pub approx_bytes: u64,
52 /// Rows whose driving column is **present but fails to coerce** to
53 /// the declared type. Recomputed fresh on every call —
54 /// the 4.0 counter of this name was a lifetime tally that also
55 /// swallowed absent-column rows, which is how a healthy migration
56 /// once read as 30 152 live failures.
57 ///
58 /// ```
59 /// use kevy_index::{IndexValue, ValType};
60 /// let mut v = kevy_index::IndexVerify::new("t.at");
61 /// // the rows hold `at` = "12" and `at` = "soon"; only the second fails to coerce
62 /// for raw in [&b"12"[..], b"soon"] {
63 /// if IndexValue::coerce(ValType::I64, raw).is_none() {
64 /// v.coerce_failures += 1;
65 /// }
66 /// }
67 /// assert_eq!(v.coerce_failures, 1);
68 /// ```
69 pub coerce_failures: u64,
70 /// Rows excluded because a string component exceeded the composite
71 /// bound (`MAX_STR_COMPONENT`). Fresh. The counter that turns a
72 /// silent two-row entries gap into a named number.
73 ///
74 /// ```
75 /// let mut v = kevy_index::IndexVerify::new("t.by_name");
76 /// (v.rows, v.entries, v.excluded) = (10, 8, 2);
77 /// // the two-row gap between rows and entries has a name
78 /// assert_eq!(v.rows - v.entries, v.excluded);
79 /// ```
80 pub excluded: u64,
81 /// Rows whose driving column is absent — NULL semantics, excluded
82 /// by design and *not* a failure. Fresh; named so absence can never
83 /// again masquerade as coercion failure.
84 ///
85 /// ```
86 /// let mut v = kevy_index::IndexVerify::new("t.at");
87 /// (v.rows, v.entries, v.absent) = (5, 3, 2);
88 /// // rows without the column are excluded by design, not failures
89 /// assert_eq!((v.rows - v.entries, v.coerce_failures), (v.absent, 0));
90 /// ```
91 pub absent: u64,
92 /// Prefix rows this verify walked for the row→index direction.
93 ///
94 /// ```
95 /// let mut v = kevy_index::IndexVerify::new("t.at");
96 /// (v.rows, v.entries) = (4, 4);
97 /// assert_eq!(v.rows, v.entries, "every walked row is indexed");
98 /// ```
99 pub rows: u64,
100 /// Rows that derive a value yet have **no entry** in the index —
101 /// the "writer forgot this path" class a drift walk structurally
102 /// cannot see, because it iterates entries and a missing entry is
103 /// not there to iterate. Fresh.
104 ///
105 /// ```
106 /// let mut v = kevy_index::IndexVerify::new("t.at");
107 /// (v.rows, v.entries, v.checked, v.missing) = (4, 3, 3, 1);
108 /// // an entry that was never written is invisible to `drift`, visible here
109 /// assert_eq!((v.drift, v.missing), (0, 1));
110 /// ```
111 pub missing: u64,
112 /// Distinct sort keys held by more than one row. Non-zero means the
113 /// sort is not a total order — a paged reader can skip or repeat
114 /// rows at page boundaries. Add a bounded tie-break column.
115 ///
116 /// ```
117 /// use kevy_index::{ValType, order_key};
118 /// let mut v = kevy_index::IndexVerify::new("t.at");
119 /// // two rows share the sort key 7: the order between them is not defined
120 /// let keys = [order_key(ValType::I64, b"7"), order_key(ValType::I64, b"7"), order_key(ValType::I64, b"9")];
121 /// v.duplicates = (1..keys.len()).filter(|&i| keys[i] == keys[i - 1]).count() as u64;
122 /// assert_eq!(v.duplicates, 1);
123 /// ```
124 pub duplicates: u64,
125 /// Entries whose held value disagrees with re-deriving from the row
126 /// right now. Recomputed fresh on every call.
127 ///
128 /// ```
129 /// let mut v = kevy_index::IndexVerify::new("t.at");
130 /// (v.checked, v.drift) = (100, 0);
131 /// assert_eq!(v.drift, 0, "every checked entry matches its row");
132 /// ```
133 pub drift: u64,
134 /// Entries the drift recheck examined this call.
135 ///
136 /// ```
137 /// let mut v = kevy_index::IndexVerify::new("t.at");
138 /// (v.entries, v.checked, v.drift) = (100, 100, 2);
139 /// assert_eq!(v.drift * 100 / v.checked, 2, "percent of this pass that drifted");
140 /// ```
141 pub checked: u64,
142}
143
144impl IndexVerify {
145 /// All-zero counters for compiled index `name`.
146 ///
147 /// ```
148 /// assert_eq!(kevy_index::IndexVerify::new("t.x").checked, 0);
149 /// ```
150 pub fn new(name: impl Into<Vec<u8>>) -> IndexVerify {
151 IndexVerify { name: name.into(), ..IndexVerify::default() }
152 }
153}
154
155/// The whole `TABLE.VERIFY` answer.
156///
157/// ```
158/// let mut t = kevy_index::TableVerify::default();
159/// t.per_index.push(kevy_index::IndexVerify::new("t.at"));
160/// assert_eq!((t.per_index.len(), t.spot_rows), (1, 0));
161/// ```
162#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
163#[non_exhaustive]
164pub struct TableVerify {
165 /// One row per compiled index of the table.
166 ///
167 /// ```
168 /// use kevy_index::{IndexVerify, TableVerify, parse_table_declare};
169 /// let t = parse_table_declare(&[
170 /// b"TABLE.DECLARE", b"t", b"PREFIX", b"t:", b"PK", b"id", b"COLUMN", b"id", b"i64",
171 /// b"COLUMN", b"at", b"i64", b"INDEX", b"at", b"range", b"INDEX", b"id", b"unique",
172 /// ])?;
173 /// let mut report = TableVerify::default();
174 /// report.per_index = t.compile()?.iter().map(|i| IndexVerify::new(i.name())).collect();
175 /// assert_eq!(report.per_index.len(), 2);
176 /// # Ok::<(), Box<dyn std::error::Error>>(())
177 /// ```
178 pub per_index: Vec<IndexVerify>,
179 /// Rows the bounded column-type spot check sampled this call.
180 ///
181 /// ```
182 /// let mut report = kevy_index::TableVerify::default();
183 /// (report.spot_rows, report.spot_type_mismatches) = (64, 0);
184 /// assert_eq!(report.spot_type_mismatches, 0, "all 64 sampled rows match their types");
185 /// ```
186 pub spot_rows: u64,
187 /// Of those, rows holding a value that contradicts the declared
188 /// column type.
189 ///
190 /// ```
191 /// use kevy_index::{IndexValue, ValType};
192 /// let mut report = kevy_index::TableVerify::default();
193 /// // a sampled row holds "abc" in a column declared i64
194 /// for raw in [&b"1"[..], b"abc"] {
195 /// report.spot_rows += 1;
196 /// if IndexValue::coerce(ValType::I64, raw).is_none() {
197 /// report.spot_type_mismatches += 1;
198 /// }
199 /// }
200 /// assert_eq!((report.spot_rows, report.spot_type_mismatches), (2, 1));
201 /// ```
202 pub spot_type_mismatches: u64,
203}
204
205/// What `table_ensure` found (the boot verb — see the embedded docs).
206///
207/// The decision the boot verb makes, in terms of this crate:
208///
209/// ```
210/// use kevy_index::{TableCatalog, TableEnsure, TableSpec, parse_table_declare, spec_diff};
211///
212/// fn ensure(cat: &mut TableCatalog, spec: TableSpec) -> Result<TableEnsure, String> {
213/// match cat.get(&spec.name) {
214/// None => {
215/// cat.create(spec).map_err(|e| e.to_string())?;
216/// Ok(TableEnsure::Created)
217/// }
218/// Some(cur) if cur.sans_auto() == spec => Ok(TableEnsure::Unchanged),
219/// Some(cur) => Err(spec_diff(cur, &spec)),
220/// }
221/// }
222///
223/// let decl = |pk: &'static [u8]| {
224/// parse_table_declare(&[b"TABLE.DECLARE", b"t", b"PREFIX", b"t:", b"PK", pk, b"COLUMN", pk, b"i64"])
225/// };
226/// let mut cat = TableCatalog::new();
227/// assert_eq!(ensure(&mut cat, decl(b"id")?)?, TableEnsure::Created);
228/// assert_eq!(ensure(&mut cat, decl(b"id")?)?, TableEnsure::Unchanged);
229/// assert!(ensure(&mut cat, decl(b"uid")?).is_err());
230/// # Ok::<(), Box<dyn std::error::Error>>(())
231/// ```
232#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
233#[non_exhaustive]
234pub enum TableEnsure {
235 /// The table did not exist; it was declared and its indexes built.
236 ///
237 /// ```
238 /// use kevy_index::TableEnsure;
239 /// // the wire reply a boot path answers with
240 /// let reply = |e: TableEnsure| match e {
241 /// TableEnsure::Unchanged => "+UNCHANGED",
242 /// _ => "+OK",
243 /// };
244 /// assert_eq!(reply(TableEnsure::Created), "+OK");
245 /// ```
246 Created,
247 /// An identical declaration already exists; nothing was touched.
248 ///
249 /// ```
250 /// use kevy_index::TableEnsure;
251 /// let first_boot = TableEnsure::Created;
252 /// let every_later_boot = TableEnsure::Unchanged;
253 /// assert_ne!(first_boot, every_later_boot, "only the first boot builds anything");
254 /// ```
255 Unchanged,
256}
257
258/// Name the first difference between an admitted spec and a proposed
259/// one — the message `table_ensure` refuses with. Specific enough to
260/// act on, short enough for a boot log.
261///
262/// ```
263/// use kevy_index::{parse_table_declare, spec_diff};
264/// let cur = parse_table_declare(&[
265/// b"TABLE.DECLARE", b"t", b"PREFIX", b"t:", b"PK", b"id", b"COLUMN", b"id", b"i64",
266/// ])?;
267/// let new = parse_table_declare(&[
268/// b"TABLE.DECLARE", b"t", b"PREFIX", b"t2:", b"PK", b"id", b"COLUMN", b"id", b"i64",
269/// ])?;
270/// let msg = spec_diff(&cur, &new);
271/// assert!(msg.starts_with("ERR table 't' exists with a different spec (PREFIX differ)"));
272/// # Ok::<(), Box<dyn std::error::Error>>(())
273/// ```
274#[must_use]
275pub fn spec_diff(cur: &crate::TableSpec, new: &crate::TableSpec) -> String {
276 let part = if cur.prefix != new.prefix {
277 "PREFIX"
278 } else if cur.pk != new.pk {
279 "PK"
280 } else if cur.columns != new.columns {
281 "COLUMNS"
282 } else if cur.indexes != new.indexes {
283 "INDEXES"
284 } else if cur.orderpaths != new.orderpaths {
285 "ORDERPATHS"
286 } else {
287 "SPEC"
288 };
289 format!(
290 "ERR table '{}' exists with a different spec ({part} differ); \
291 TABLE.REPLACE rebuilds, TABLE.DROP + DECLARE is the manual form",
292 String::from_utf8_lossy(&cur.name)
293 )
294}