1use std::sync::{Mutex, RwLock};
9
10use kevy_index::{AdviseLog, IndexVerify, TableCatalog, TableEnsure, TableSpec, TableVerify};
11
12use crate::store::{Store, lock_write};
13use crate::{KevyError, KevyResult};
14
15#[derive(Debug, Default)]
18pub(crate) struct TableReg {
19 pub(crate) catalog: RwLock<TableCatalog>,
20 pub(crate) advise: Mutex<AdviseLog>,
24}
25
26const SPOTCHECK_ROWS: usize = 64;
28
29#[deprecated(since = "4.1.0", note = "use `table_verify_report`, whose fields are named")]
37pub type TableVerifyReport = (Vec<(Vec<u8>, [u64; 6])>, [u64; 2]);
38
39impl Store {
40 pub fn table_declare(&self, spec: TableSpec) -> KevyResult<()> {
45 self.catalog_change(|| self.declare_table(spec))
46 }
47
48 fn declare_table(&self, spec: TableSpec) -> KevyResult<()> {
49 #[cfg(all(feature = "tier", not(target_arch = "wasm32")))]
53 crate::ops_index_sync::tier_floor_check(&self.shards)?;
54 let compiled = spec.compile().map_err(|e| KevyError::InvalidInput(e.to_string()))?;
57 {
58 let g = self.tables.catalog.read().unwrap_or_else(std::sync::PoisonError::into_inner);
59 if g.get(&spec.name).is_some() {
60 return Err(KevyError::InvalidInput("table already exists".into()));
61 }
62 }
63 {
67 let g = self.indexes.catalog.read().unwrap_or_else(std::sync::PoisonError::into_inner);
68 let mut probe = g.1.clone();
69 for ispec in &compiled {
70 probe.create(ispec.clone()).map_err(|e| KevyError::InvalidInput(e.to_string()))?;
71 }
72 }
73 {
74 let mut g =
75 self.tables.catalog.write().unwrap_or_else(std::sync::PoisonError::into_inner);
76 g.create(spec).map_err(|e| KevyError::InvalidInput(e.to_string()))?;
77 }
78 for ispec in compiled {
79 self.register_spec(ispec)?;
80 }
81 self.advise_clear();
82 Ok(())
83 }
84
85 pub fn table_ensure(&self, spec: TableSpec) -> KevyResult<TableEnsure> {
97 let existing = {
98 let g = self.tables.catalog.read().unwrap_or_else(std::sync::PoisonError::into_inner);
99 g.get(&spec.name).cloned()
100 };
101 match existing {
102 None => {
103 self.table_declare(spec)?;
104 Ok(TableEnsure::Created)
105 }
106 Some(cur) if cur.sans_auto() == spec => Ok(TableEnsure::Unchanged),
107 Some(cur) => {
108 Err(KevyError::InvalidInput(kevy_index::spec_diff(&cur.sans_auto(), &spec)))
109 }
110 }
111 }
112
113 pub fn table_replace(&self, spec: TableSpec) -> KevyResult<()> {
120 spec.compile().map_err(|e| KevyError::InvalidInput(e.to_string()))?;
121 self.catalog_change(|| {
122 self.drop_table(&spec.name);
123 self.declare_table(spec)
124 })
125 }
126
127 pub fn table_drop(&self, name: &[u8]) -> KevyResult<bool> {
131 self.catalog_change(|| Ok(self.drop_table(name)))
132 }
133
134 fn drop_table(&self, name: &[u8]) -> bool {
135 let compiled: Vec<Vec<u8>> = {
136 let g = self.tables.catalog.read().unwrap_or_else(std::sync::PoisonError::into_inner);
137 g.get(name)
138 .map(|s| {
139 s.compile()
140 .map(|c| c.into_iter().map(|i| i.name().to_vec()).collect())
141 .unwrap_or_default() })
143 .unwrap_or_default()
144 };
145 let hit = {
146 let mut g =
147 self.tables.catalog.write().unwrap_or_else(std::sync::PoisonError::into_inner);
148 g.drop_table(name)
149 };
150 if hit {
151 for iname in &compiled {
152 self.drop_index(iname);
153 }
154 self.advise_clear();
155 }
156 hit
157 }
158
159 pub fn table_list(&self) -> Vec<TableSpec> {
161 let g = self.tables.catalog.read().unwrap_or_else(std::sync::PoisonError::into_inner);
162 g.iter().cloned().collect()
163 }
164
165 #[deprecated(since = "4.1.0", note = "use `table_verify_report`, whose fields are named")]
170 #[allow(deprecated)]
171 pub fn table_verify(&self, name: &[u8]) -> KevyResult<TableVerifyReport> {
172 let r = self.table_verify_report(name)?;
173 Ok((
174 r.per_index
175 .into_iter()
176 .map(|i| {
177 (
178 i.name,
179 [
180 i.entries,
181 i.approx_bytes,
182 i.coerce_failures,
183 i.duplicates,
184 i.drift,
185 i.checked,
186 ],
187 )
188 })
189 .collect(),
190 [r.spot_rows, r.spot_type_mismatches],
191 ))
192 }
193
194 pub fn table_verify_report(&self, name: &[u8]) -> KevyResult<TableVerify> {
197 let spec = {
198 let g = self.tables.catalog.read().unwrap_or_else(std::sync::PoisonError::into_inner);
199 g.get(name).cloned()
200 }
201 .ok_or_else(|| KevyError::NotFound("no such table".into()))?;
202 let compiled = spec.compile().map_err(|e| KevyError::InvalidInput(e.to_string()))?;
203 let mut per_index: Vec<(Vec<u8>, [u64; 10])> =
204 compiled.iter().map(|i| (i.name().to_vec(), [0u64; 10])).collect();
205 let mut spot = [0u64; 2];
206 for shard in self.shards.iter() {
207 let mut g = lock_write(shard);
208 let inner = &mut *g;
209 crate::ops_index_sync::sync_segs(&self.indexes, &mut inner.idx_segs, &mut inner.store);
210 for (slot, ispec) in per_index.iter_mut().zip(&compiled) {
211 shard_index_counts(inner, ispec, &mut slot.1);
212 }
213 let (r, m) = shard_spot_check(&mut inner.store, &spec);
214 spot[0] += r;
215 spot[1] += m;
216 }
217 let mut report = TableVerify::default();
218 report.per_index = per_index
219 .into_iter()
220 .map(|(name, s)| {
221 let mut v = IndexVerify::new(name);
222 (v.entries, v.approx_bytes, v.coerce_failures) = (s[0], s[1], s[2]);
223 (v.duplicates, v.drift, v.checked) = (s[3], s[4], s[5]);
224 (v.excluded, v.absent, v.rows, v.missing) = (s[6], s[7], s[8], s[9]);
225 v
226 })
227 .collect();
228 (report.spot_rows, report.spot_type_mismatches) = (spot[0], spot[1]);
229 Ok(report)
230 }
231}
232
233fn classify_prefix_rows(
259 s: &mut kevy_store::Store,
260 spec: &kevy_index::IndexSpec,
261 row_keys: &[Vec<u8>],
262 indexed: &std::collections::HashSet<&[u8]>,
263 window: Option<kevy_index::WindowAudit>,
264) -> [u64; 5] {
265 let names = spec.scalar_read_names();
266 let w = spec.primary_width();
267 let mut f = [0u64; 5];
268 let mut below = 0u64;
269 for key in row_keys {
270 f[3] += 1;
271 let cls = match s.peek_hash_fields(key, &names[..w]) {
272 Ok(Some(vals)) => spec.classify_scalar(&vals),
273 _ => kevy_index::RowDerivation::Absent,
275 };
276 match cls {
277 kevy_index::RowDerivation::Indexed(_) => {
278 let slid = window.is_some_and(|wa| {
279 let v = match s.peek_hash_fields(key, &names[..w]) {
280 Ok(Some(vals)) => spec.derive_scalar(&vals),
281 _ => None,
282 };
283 v.and_then(|v| v.window_value(wa.shape)).is_some_and(|wv| wv < wa.boundary)
284 });
285 if !indexed.contains(key.as_slice()) {
286 if slid {
287 below += 1;
288 } else {
289 f[4] += 1;
290 }
291 }
292 }
293 kevy_index::RowDerivation::CoerceFailed => f[0] += 1,
294 kevy_index::RowDerivation::Oversize => f[1] += 1,
295 kevy_index::RowDerivation::Absent => f[2] += 1,
296 }
297 }
298 f[4] += below.saturating_sub(window.map_or(0, |w| w.cold_live));
301 f
302}
303
304fn hot_floor_of(
309 inner: &crate::store_inner::Inner,
310 name: &[u8],
311 ty: kevy_index::ValType,
312) -> Option<kevy_index::WindowAudit> {
313 #[cfg(not(target_arch = "wasm32"))]
314 {
315 inner
316 .idx_segs
317 .windows
318 .iter()
319 .find(|(n, _)| n.as_slice() == name)
320 .and_then(|(_, w)| w.audit(ty))
321 }
322 #[cfg(target_arch = "wasm32")]
323 {
324 let _ = (inner, name, ty);
325 None
326 }
327}
328
329fn shard_index_counts(
330 inner: &mut crate::store_inner::Inner,
331 ispec: &kevy_index::IndexSpec,
332 sums: &mut [u64; 10],
333) {
334 let Some((spec, seg)) = inner.idx_segs.segs.iter().find(|(s, _)| s.name() == ispec.name())
335 else {
336 return;
337 };
338 let stats = seg.stats();
339 let mut entries: Vec<(Vec<u8>, kevy_index::IndexValue)> = Vec::new();
340 seg.each_entry(|k, v| entries.push((k.to_vec(), v.clone())));
341 let indexed: std::collections::HashSet<&[u8]> =
342 entries.iter().map(|(k, _)| k.as_slice()).collect();
343 let spec = spec.clone();
344 let mut pat = spec.prefix().to_vec();
345 pat.push(b'*');
346 let row_keys = inner.store.collect_keys(Some(&pat), None);
347 let window = hot_floor_of(inner, spec.name(), spec.ty());
348 let store = &mut inner.store;
349 let (drift, fresh) = store.peek_scope(|s| {
350 let names = spec.scalar_read_names();
351 let w = spec.primary_width();
352 let mut drift = 0u64;
353 for (key, held) in &entries {
354 let actual = match s.peek_hash_fields(key, &names[..w]) {
355 Ok(Some(vals)) => spec.derive_scalar(&vals),
356 _ => None,
357 };
358 if actual.as_ref() != Some(held) {
359 drift += 1;
360 }
361 }
362 (drift, classify_prefix_rows(s, &spec, &row_keys, &indexed, window))
363 });
364 sums[0] += stats.entries;
365 sums[1] += stats.approx_bytes;
366 sums[2] += fresh[0];
367 sums[3] += stats.duplicates;
368 sums[4] += drift;
369 sums[5] += entries.len() as u64;
370 sums[6] += fresh[1];
371 sums[7] += fresh[2];
372 sums[8] += fresh[3];
373 sums[9] += fresh[4];
374}
375
376fn shard_spot_check(store: &mut kevy_store::Store, spec: &TableSpec) -> (u64, u64) {
380 let mut pat = spec.prefix.clone();
381 pat.push(b'*');
382 let keys = store.collect_keys(Some(&pat), Some(SPOTCHECK_ROWS));
383 let names: Vec<&[u8]> = spec.columns.iter().map(|(n, _)| n.as_slice()).collect();
384 store.peek_scope(|s| {
385 let (mut rows, mut mismatches) = (0u64, 0u64);
386 for key in &keys {
387 rows += 1;
388 let Ok(Some(vals)) = s.peek_hash_fields(key, &names) else { continue };
389 for ((_, ty), val) in spec.columns.iter().zip(&vals) {
390 if let Some(raw) = val
391 && kevy_index::IndexValue::coerce(*ty, raw).is_none()
392 {
393 mismatches += 1;
394 }
395 }
396 }
397 (rows, mismatches)
398 })
399}