Skip to main content

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}