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.tables.catalog.read().unwrap_or_else(std::sync::PoisonError::into_inner);
60 if g.get(&spec.name).is_some() {
61 return Err(KevyError::InvalidInput("table already exists".into()));
62 }
63 }
64 {
68 let g = self.indexes.catalog.read().unwrap_or_else(std::sync::PoisonError::into_inner);
69 let mut probe = g.1.clone();
70 for ispec in &compiled {
71 probe
72 .create(ispec.clone())
73 .map_err(|e| KevyError::InvalidInput(strip_err(e).into()))?;
74 }
75 }
76 {
77 let mut g =
78 self.tables.catalog.write().unwrap_or_else(std::sync::PoisonError::into_inner);
79 g.create(spec).map_err(|e| KevyError::InvalidInput(strip_err(&e).into()))?;
80 }
81 self.persist_table_sidecar();
82 for ispec in compiled {
83 self.register_spec(ispec)?;
84 }
85 self.advise_clear();
86 Ok(())
87 }
88
89 pub fn table_ensure(&self, spec: TableSpec) -> KevyResult<TableEnsure> {
101 let existing = {
102 let g = self.tables.catalog.read().unwrap_or_else(std::sync::PoisonError::into_inner);
103 g.get(&spec.name).cloned()
104 };
105 match existing {
106 None => {
107 self.table_declare(spec)?;
108 Ok(TableEnsure::Created)
109 }
110 Some(cur) if cur.sans_auto() == spec => Ok(TableEnsure::Unchanged),
111 Some(cur) => {
112 Err(KevyError::InvalidInput(kevy_index::spec_diff(&cur.sans_auto(), &spec)))
113 }
114 }
115 }
116
117 pub fn table_replace(&self, spec: TableSpec) -> KevyResult<()> {
124 compile_table(&spec).map_err(KevyError::InvalidInput)?;
125 self.table_drop(&spec.name);
126 self.table_declare(spec)
127 }
128
129 pub fn table_drop(&self, name: &[u8]) -> bool {
132 let compiled: Vec<Vec<u8>> = {
133 let g = self.tables.catalog.read().unwrap_or_else(std::sync::PoisonError::into_inner);
134 g.get(name)
135 .map(|s| {
136 compile_table(s)
137 .map(|c| c.into_iter().map(|i| i.name).collect())
138 .unwrap_or_default() })
140 .unwrap_or_default()
141 };
142 let hit = {
143 let mut g =
144 self.tables.catalog.write().unwrap_or_else(std::sync::PoisonError::into_inner);
145 g.drop_table(name)
146 };
147 if hit {
148 for iname in &compiled {
149 self.idx_drop(iname);
150 }
151 self.persist_table_sidecar();
152 self.advise_clear();
153 }
154 hit
155 }
156
157 pub fn table_list(&self) -> Vec<TableSpec> {
159 let g = self.tables.catalog.read().unwrap_or_else(std::sync::PoisonError::into_inner);
160 g.iter().cloned().collect()
161 }
162
163 #[deprecated(since = "4.1.0", note = "use `table_verify_report`, whose fields are named")]
168 #[allow(deprecated)]
169 pub fn table_verify(&self, name: &[u8]) -> KevyResult<TableVerifyReport> {
170 let r = self.table_verify_report(name)?;
171 Ok((
172 r.per_index
173 .into_iter()
174 .map(|i| {
175 (
176 i.name,
177 [
178 i.entries,
179 i.approx_bytes,
180 i.coerce_failures,
181 i.duplicates,
182 i.drift,
183 i.checked,
184 ],
185 )
186 })
187 .collect(),
188 [r.spot_rows, r.spot_type_mismatches],
189 ))
190 }
191
192 pub fn table_verify_report(&self, name: &[u8]) -> KevyResult<TableVerify> {
195 let spec = {
196 let g = self.tables.catalog.read().unwrap_or_else(std::sync::PoisonError::into_inner);
197 g.get(name).cloned()
198 }
199 .ok_or_else(|| KevyError::NotFound("no such table".into()))?;
200 let compiled = compile_table(&spec).map_err(KevyError::InvalidInput)?;
201 let mut per_index: Vec<(Vec<u8>, [u64; 10])> =
202 compiled.iter().map(|i| (i.name.clone(), [0u64; 10])).collect();
203 let mut spot = [0u64; 2];
204 for shard in self.shards.iter() {
205 let mut g = lock_write(shard);
206 let inner = &mut *g;
207 crate::ops_index_sync::sync_segs(&self.indexes, &mut inner.idx_segs, &mut inner.store);
208 for (slot, ispec) in per_index.iter_mut().zip(&compiled) {
209 shard_index_counts(inner, ispec, &mut slot.1);
210 }
211 let (r, m) = shard_spot_check(&mut inner.store, &spec);
212 spot[0] += r;
213 spot[1] += m;
214 }
215 Ok(TableVerify {
216 per_index: per_index
217 .into_iter()
218 .map(|(name, s)| IndexVerify {
219 name,
220 entries: s[0],
221 approx_bytes: s[1],
222 coerce_failures: s[2],
223 duplicates: s[3],
224 drift: s[4],
225 checked: s[5],
226 excluded: s[6],
227 absent: s[7],
228 rows: s[8],
229 missing: s[9],
230 })
231 .collect(),
232 spot_rows: spot[0],
233 spot_type_mismatches: spot[1],
234 })
235 }
236
237 #[cfg(feature = "persist")]
238 pub(crate) fn table_boot(&self) {
239 let Some(dir) = &self.config.data_dir else { return };
240 if let Ok(text) = std::fs::read_to_string(dir.join(SIDECAR))
241 && let Some(cat) = TableCatalog::from_sidecar(&text)
242 && !cat.is_empty()
243 {
244 let mut g =
245 self.tables.catalog.write().unwrap_or_else(std::sync::PoisonError::into_inner);
246 *g = cat;
247 }
248 }
249
250 #[cfg(not(feature = "persist"))]
251 pub(crate) fn table_boot(&self) {}
252
253 #[cfg(feature = "persist")]
254 pub(crate) fn persist_table_sidecar(&self) {
255 let Some(dir) = &self.config.data_dir else { return };
256 let g = self.tables.catalog.read().unwrap_or_else(std::sync::PoisonError::into_inner);
257 let tmp = dir.join("table-catalog.meta.tmp");
258 if std::fs::write(&tmp, g.to_sidecar()).is_ok() {
259 let _ = std::fs::rename(&tmp, dir.join(SIDECAR));
260 }
261 }
262
263 #[cfg(not(feature = "persist"))]
264 pub(crate) fn persist_table_sidecar(&self) {}
265}
266
267fn strip_err(e: &str) -> &str {
270 e.strip_prefix("ERR ").unwrap_or(e)
271}
272
273fn classify_prefix_rows(
299 s: &mut kevy_store::Store,
300 spec: &kevy_index::IndexSpec,
301 row_keys: &[Vec<u8>],
302 indexed: &std::collections::HashSet<&[u8]>,
303 window: Option<kevy_index::WindowAudit>,
304) -> [u64; 5] {
305 let names = spec.scalar_read_names();
306 let w = spec.primary_width();
307 let mut f = [0u64; 5];
308 let mut below = 0u64;
309 for key in row_keys {
310 f[3] += 1;
311 let cls = match s.peek_hash_fields(key, &names[..w]) {
312 Ok(Some(vals)) => spec.classify_scalar(&vals),
313 _ => kevy_index::RowDerivation::Absent,
315 };
316 match cls {
317 kevy_index::RowDerivation::Indexed(_) => {
318 let slid = window.is_some_and(|wa| {
319 let v = match s.peek_hash_fields(key, &names[..w]) {
320 Ok(Some(vals)) => spec.derive_scalar(&vals),
321 _ => None,
322 };
323 v.and_then(|v| kevy_index::window_value_of(&v, wa.shape))
324 .is_some_and(|wv| wv < wa.boundary)
325 });
326 if !indexed.contains(key.as_slice()) {
327 if slid {
328 below += 1;
329 } else {
330 f[4] += 1;
331 }
332 }
333 }
334 kevy_index::RowDerivation::CoerceFailed => f[0] += 1,
335 kevy_index::RowDerivation::Oversize => f[1] += 1,
336 kevy_index::RowDerivation::Absent => f[2] += 1,
337 }
338 }
339 f[4] += below.saturating_sub(window.map_or(0, |w| w.cold_live));
342 f
343}
344
345fn hot_floor_of(
350 inner: &crate::store_inner::Inner,
351 name: &[u8],
352 ty: kevy_index::ValType,
353) -> Option<kevy_index::WindowAudit> {
354 #[cfg(not(target_arch = "wasm32"))]
355 {
356 inner
357 .idx_segs
358 .windows
359 .iter()
360 .find(|(n, _)| n.as_slice() == name)
361 .and_then(|(_, w)| w.audit(ty))
362 }
363 #[cfg(target_arch = "wasm32")]
364 {
365 let _ = (inner, name, ty);
366 None
367 }
368}
369
370fn shard_index_counts(
371 inner: &mut crate::store_inner::Inner,
372 ispec: &kevy_index::IndexSpec,
373 sums: &mut [u64; 10],
374) {
375 let Some((spec, seg)) = inner.idx_segs.segs.iter().find(|(s, _)| s.name == ispec.name) else {
376 return;
377 };
378 let stats = seg.stats();
379 let mut entries: Vec<(Vec<u8>, kevy_index::IndexValue)> = Vec::new();
380 seg.each_entry(|k, v| entries.push((k.to_vec(), v.clone())));
381 let indexed: std::collections::HashSet<&[u8]> =
382 entries.iter().map(|(k, _)| k.as_slice()).collect();
383 let spec = spec.clone();
384 let mut pat = spec.prefix.clone();
385 pat.push(b'*');
386 let row_keys = inner.store.collect_keys(Some(&pat), None);
387 let window = hot_floor_of(inner, &spec.name, spec.ty);
388 let store = &mut inner.store;
389 let (drift, fresh) = store.peek_scope(|s| {
390 let names = spec.scalar_read_names();
391 let w = spec.primary_width();
392 let mut drift = 0u64;
393 for (key, held) in &entries {
394 let actual = match s.peek_hash_fields(key, &names[..w]) {
395 Ok(Some(vals)) => spec.derive_scalar(&vals),
396 _ => None,
397 };
398 if actual.as_ref() != Some(held) {
399 drift += 1;
400 }
401 }
402 (drift, classify_prefix_rows(s, &spec, &row_keys, &indexed, window))
403 });
404 sums[0] += stats.entries;
405 sums[1] += stats.approx_bytes;
406 sums[2] += fresh[0];
407 sums[3] += stats.duplicates;
408 sums[4] += drift;
409 sums[5] += entries.len() as u64;
410 sums[6] += fresh[1];
411 sums[7] += fresh[2];
412 sums[8] += fresh[3];
413 sums[9] += fresh[4];
414}
415
416fn shard_spot_check(store: &mut kevy_store::Store, spec: &TableSpec) -> (u64, u64) {
420 let mut pat = spec.prefix.clone();
421 pat.push(b'*');
422 let keys = store.collect_keys(Some(&pat), Some(SPOTCHECK_ROWS));
423 let names: Vec<&[u8]> = spec.columns.iter().map(|(n, _)| n.as_slice()).collect();
424 store.peek_scope(|s| {
425 let (mut rows, mut mismatches) = (0u64, 0u64);
426 for key in &keys {
427 rows += 1;
428 let Ok(Some(vals)) = s.peek_hash_fields(key, &names) else { continue };
429 for ((_, ty), val) in spec.columns.iter().zip(&vals) {
430 if let Some(raw) = val
431 && kevy_index::IndexValue::coerce(*ty, raw).is_none()
432 {
433 mismatches += 1;
434 }
435 }
436 }
437 (rows, mismatches)
438 })
439}