1use std::sync::{Mutex, RwLock};
9
10use kevy_index::{
11 AdviseLog, IndexVerify, TableCatalog, TableEnsure, TableSpec, TableVerify, compile_table,
12};
13
14use crate::store::{Store, lock_write};
15use crate::{KevyError, KevyResult};
16
17#[derive(Default)]
20pub(crate) struct TableReg {
21 pub(crate) catalog: RwLock<TableCatalog>,
22 pub(crate) advise: Mutex<AdviseLog>,
26}
27
28const SPOTCHECK_ROWS: usize = 64;
30
31#[cfg(feature = "persist")]
32const SIDECAR: &str = "table-catalog.meta";
33
34#[deprecated(since = "4.1.0", note = "use `table_verify_report`, whose fields are named")]
42pub type TableVerifyReport = (Vec<(Vec<u8>, [u64; 6])>, [u64; 2]);
43
44impl Store {
45 pub fn table_declare(&self, spec: TableSpec) -> KevyResult<()> {
50 #[cfg(all(feature = "tier", not(target_arch = "wasm32")))]
54 crate::ops_index_sync::tier_floor_check(&self.shards)?;
55 let compiled = compile_table(&spec).map_err(KevyError::InvalidInput)?;
58 {
59 let g = self
60 .tables
61 .catalog
62 .read()
63 .unwrap_or_else(std::sync::PoisonError::into_inner);
64 if g.get(&spec.name).is_some() {
65 return Err(KevyError::InvalidInput("table already exists".into()));
66 }
67 }
68 {
72 let g = self
73 .indexes
74 .catalog
75 .read()
76 .unwrap_or_else(std::sync::PoisonError::into_inner);
77 let mut probe = g.1.clone();
78 for ispec in &compiled {
79 probe
80 .create(ispec.clone())
81 .map_err(|e| KevyError::InvalidInput(strip_err(e).into()))?;
82 }
83 }
84 {
85 let mut g = self
86 .tables
87 .catalog
88 .write()
89 .unwrap_or_else(std::sync::PoisonError::into_inner);
90 g.create(spec).map_err(|e| KevyError::InvalidInput(strip_err(&e).into()))?;
91 }
92 self.persist_table_sidecar();
93 for ispec in compiled {
94 self.register_spec(ispec)?;
95 }
96 self.advise_clear();
97 Ok(())
98 }
99
100 pub fn table_ensure(&self, spec: TableSpec) -> KevyResult<TableEnsure> {
112 let existing = {
113 let g = self
114 .tables
115 .catalog
116 .read()
117 .unwrap_or_else(std::sync::PoisonError::into_inner);
118 g.get(&spec.name).cloned()
119 };
120 match existing {
121 None => {
122 self.table_declare(spec)?;
123 Ok(TableEnsure::Created)
124 }
125 Some(cur) if cur.sans_auto() == spec => Ok(TableEnsure::Unchanged),
126 Some(cur) => Err(KevyError::InvalidInput(
127 kevy_index::spec_diff(&cur.sans_auto(), &spec),
128 )),
129 }
130 }
131
132 pub fn table_replace(&self, spec: TableSpec) -> KevyResult<()> {
139 compile_table(&spec).map_err(KevyError::InvalidInput)?;
140 self.table_drop(&spec.name);
141 self.table_declare(spec)
142 }
143
144 pub fn table_drop(&self, name: &[u8]) -> bool {
147 let compiled: Vec<Vec<u8>> = {
148 let g = self
149 .tables
150 .catalog
151 .read()
152 .unwrap_or_else(std::sync::PoisonError::into_inner);
153 g.get(name)
154 .map(|s| {
155 compile_table(s)
156 .map(|c| c.into_iter().map(|i| i.name).collect())
157 .unwrap_or_default() })
159 .unwrap_or_default()
160 };
161 let hit = {
162 let mut g = self
163 .tables
164 .catalog
165 .write()
166 .unwrap_or_else(std::sync::PoisonError::into_inner);
167 g.drop_table(name)
168 };
169 if hit {
170 for iname in &compiled {
171 self.idx_drop(iname);
172 }
173 self.persist_table_sidecar();
174 self.advise_clear();
175 }
176 hit
177 }
178
179 pub fn table_list(&self) -> Vec<TableSpec> {
181 let g = self
182 .tables
183 .catalog
184 .read()
185 .unwrap_or_else(std::sync::PoisonError::into_inner);
186 g.iter().cloned().collect()
187 }
188
189 #[deprecated(since = "4.1.0", note = "use `table_verify_report`, whose fields are named")]
194 #[allow(deprecated)]
195 pub fn table_verify(&self, name: &[u8]) -> KevyResult<TableVerifyReport> {
196 let r = self.table_verify_report(name)?;
197 Ok((
198 r.per_index
199 .into_iter()
200 .map(|i| {
201 (i.name, [i.entries, i.approx_bytes, i.coerce_failures, i.duplicates, i.drift, i.checked])
202 })
203 .collect(),
204 [r.spot_rows, r.spot_type_mismatches],
205 ))
206 }
207
208 pub fn table_verify_report(&self, name: &[u8]) -> KevyResult<TableVerify> {
211 let spec = {
212 let g = self
213 .tables
214 .catalog
215 .read()
216 .unwrap_or_else(std::sync::PoisonError::into_inner);
217 g.get(name).cloned()
218 }
219 .ok_or_else(|| KevyError::NotFound("no such table".into()))?;
220 let compiled = compile_table(&spec).map_err(KevyError::InvalidInput)?;
221 let mut per_index: Vec<(Vec<u8>, [u64; 10])> =
222 compiled.iter().map(|i| (i.name.clone(), [0u64; 10])).collect();
223 let mut spot = [0u64; 2];
224 for shard in self.shards.iter() {
225 let mut g = lock_write(shard);
226 let inner = &mut *g;
227 crate::ops_index_sync::sync_segs(&self.indexes, &mut inner.idx_segs, &mut inner.store);
228 for (slot, ispec) in per_index.iter_mut().zip(&compiled) {
229 shard_index_counts(inner, ispec, &mut slot.1);
230 }
231 let (r, m) = shard_spot_check(&mut inner.store, &spec);
232 spot[0] += r;
233 spot[1] += m;
234 }
235 Ok(TableVerify {
236 per_index: per_index
237 .into_iter()
238 .map(|(name, s)| IndexVerify {
239 name,
240 entries: s[0],
241 approx_bytes: s[1],
242 coerce_failures: s[2],
243 duplicates: s[3],
244 drift: s[4],
245 checked: s[5],
246 excluded: s[6],
247 absent: s[7],
248 rows: s[8],
249 missing: s[9],
250 })
251 .collect(),
252 spot_rows: spot[0],
253 spot_type_mismatches: spot[1],
254 })
255 }
256
257 #[cfg(feature = "persist")]
258 pub(crate) fn table_boot(&self) {
259 let Some(dir) = &self.config.data_dir else { return };
260 if let Ok(text) = std::fs::read_to_string(dir.join(SIDECAR))
261 && let Some(cat) = TableCatalog::from_sidecar(&text)
262 && !cat.is_empty()
263 {
264 let mut g = self
265 .tables
266 .catalog
267 .write()
268 .unwrap_or_else(std::sync::PoisonError::into_inner);
269 *g = cat;
270 }
271 }
272
273 #[cfg(not(feature = "persist"))]
274 pub(crate) fn table_boot(&self) {}
275
276 #[cfg(feature = "persist")]
277 pub(crate) fn persist_table_sidecar(&self) {
278 let Some(dir) = &self.config.data_dir else { return };
279 let g = self
280 .tables
281 .catalog
282 .read()
283 .unwrap_or_else(std::sync::PoisonError::into_inner);
284 let tmp = dir.join("table-catalog.meta.tmp");
285 if std::fs::write(&tmp, g.to_sidecar()).is_ok() {
286 let _ = std::fs::rename(&tmp, dir.join(SIDECAR));
287 }
288 }
289
290 #[cfg(not(feature = "persist"))]
291 pub(crate) fn persist_table_sidecar(&self) {}
292}
293
294fn strip_err(e: &str) -> &str {
297 e.strip_prefix("ERR ").unwrap_or(e)
298}
299
300fn classify_prefix_rows(
326 s: &mut kevy_store::Store,
327 spec: &kevy_index::IndexSpec,
328 row_keys: &[Vec<u8>],
329 indexed: &std::collections::HashSet<&[u8]>,
330 window: Option<kevy_index::WindowAudit>,
331) -> [u64; 5] {
332 let names = spec.scalar_read_names();
333 let w = spec.primary_width();
334 let mut f = [0u64; 5];
335 let mut below = 0u64;
336 for key in row_keys {
337 f[3] += 1;
338 let cls = match s.peek_hash_fields(key, &names[..w]) {
339 Ok(Some(vals)) => spec.classify_scalar(&vals),
340 _ => kevy_index::RowDerivation::Absent,
342 };
343 match cls {
344 kevy_index::RowDerivation::Indexed(_) => {
345 let slid = window.is_some_and(|wa| {
346 let v = match s.peek_hash_fields(key, &names[..w]) {
347 Ok(Some(vals)) => spec.derive_scalar(&vals),
348 _ => None,
349 };
350 v.and_then(|v| kevy_index::window_value_of(&v, wa.shape))
351 .is_some_and(|wv| wv < wa.boundary)
352 });
353 if !indexed.contains(key.as_slice()) {
354 if slid {
355 below += 1;
356 } else {
357 f[4] += 1;
358 }
359 }
360 }
361 kevy_index::RowDerivation::CoerceFailed => f[0] += 1,
362 kevy_index::RowDerivation::Oversize => f[1] += 1,
363 kevy_index::RowDerivation::Absent => f[2] += 1,
364 }
365 }
366 f[4] += below.saturating_sub(window.map_or(0, |w| w.cold_live));
369 f
370}
371
372fn hot_floor_of(
377 inner: &crate::store_inner::Inner,
378 name: &[u8],
379 ty: kevy_index::ValType,
380) -> Option<kevy_index::WindowAudit> {
381 #[cfg(not(target_arch = "wasm32"))]
382 {
383 inner
384 .idx_segs
385 .windows
386 .iter()
387 .find(|(n, _)| n.as_slice() == name)
388 .and_then(|(_, w)| w.audit(ty))
389 }
390 #[cfg(target_arch = "wasm32")]
391 {
392 let _ = (inner, name, ty);
393 None
394 }
395}
396
397fn shard_index_counts(
398 inner: &mut crate::store_inner::Inner,
399 ispec: &kevy_index::IndexSpec,
400 sums: &mut [u64; 10],
401) {
402 let Some((spec, seg)) = inner.idx_segs.segs.iter().find(|(s, _)| s.name == ispec.name)
403 else {
404 return;
405 };
406 let stats = seg.stats();
407 let mut entries: Vec<(Vec<u8>, kevy_index::IndexValue)> = Vec::new();
408 seg.each_entry(|k, v| entries.push((k.to_vec(), v.clone())));
409 let indexed: std::collections::HashSet<&[u8]> =
410 entries.iter().map(|(k, _)| k.as_slice()).collect();
411 let spec = spec.clone();
412 let mut pat = spec.prefix.clone();
413 pat.push(b'*');
414 let row_keys = inner.store.collect_keys(Some(&pat), None);
415 let window = hot_floor_of(inner, &spec.name, spec.ty);
416 let store = &mut inner.store;
417 let (drift, fresh) = store.peek_scope(|s| {
418 let names = spec.scalar_read_names();
419 let w = spec.primary_width();
420 let mut drift = 0u64;
421 for (key, held) in &entries {
422 let actual = match s.peek_hash_fields(key, &names[..w]) {
423 Ok(Some(vals)) => spec.derive_scalar(&vals),
424 _ => None,
425 };
426 if actual.as_ref() != Some(held) {
427 drift += 1;
428 }
429 }
430 (drift, classify_prefix_rows(s, &spec, &row_keys, &indexed, window))
431 });
432 sums[0] += stats.entries;
433 sums[1] += stats.approx_bytes;
434 sums[2] += fresh[0];
435 sums[3] += stats.duplicates;
436 sums[4] += drift;
437 sums[5] += entries.len() as u64;
438 sums[6] += fresh[1];
439 sums[7] += fresh[2];
440 sums[8] += fresh[3];
441 sums[9] += fresh[4];
442}
443
444fn shard_spot_check(store: &mut kevy_store::Store, spec: &TableSpec) -> (u64, u64) {
448 let mut pat = spec.prefix.clone();
449 pat.push(b'*');
450 let keys = store.collect_keys(Some(&pat), Some(SPOTCHECK_ROWS));
451 let names: Vec<&[u8]> = spec.columns.iter().map(|(n, _)| n.as_slice()).collect();
452 store.peek_scope(|s| {
453 let (mut rows, mut mismatches) = (0u64, 0u64);
454 for key in &keys {
455 rows += 1;
456 let Ok(Some(vals)) = s.peek_hash_fields(key, &names) else { continue };
457 for ((_, ty), val) in spec.columns.iter().zip(&vals) {
458 if let Some(raw) = val
459 && kevy_index::IndexValue::coerce(*ty, raw).is_none()
460 {
461 mismatches += 1;
462 }
463 }
464 }
465 (rows, mismatches)
466 })
467}