use std::sync::RwLock;
use kevy_index::{TableCatalog, TableSpec, compile_table};
use crate::store::{Store, lock_write};
use crate::{KevyError, KevyResult};
#[derive(Default)]
pub(crate) struct TableReg {
pub(crate) catalog: RwLock<TableCatalog>,
}
const SPOTCHECK_ROWS: usize = 64;
#[cfg(feature = "persist")]
const SIDECAR: &str = "table-catalog.meta";
pub type TableVerifyReport = (Vec<(Vec<u8>, [u64; 6])>, [u64; 2]);
impl Store {
pub fn table_declare(&self, spec: TableSpec) -> KevyResult<()> {
#[cfg(all(feature = "tier", not(target_arch = "wasm32")))]
crate::ops_index_sync::tier_floor_check(&self.shards)?;
let compiled = compile_table(&spec);
{
let g = self
.tables
.catalog
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
if g.get(&spec.name).is_some() {
return Err(KevyError::InvalidInput("table already exists".into()));
}
}
{
let g = self
.indexes
.catalog
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let mut probe = g.1.clone();
for ispec in &compiled {
probe
.create(ispec.clone())
.map_err(|e| KevyError::InvalidInput(strip_err(e).into()))?;
}
}
{
let mut g = self
.tables
.catalog
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
g.create(spec).map_err(|e| KevyError::InvalidInput(strip_err(&e).into()))?;
}
self.persist_table_sidecar();
for ispec in compiled {
self.register_spec(ispec)?;
}
Ok(())
}
pub fn table_drop(&self, name: &[u8]) -> bool {
let compiled: Vec<Vec<u8>> = {
let g = self
.tables
.catalog
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
g.get(name)
.map(|s| compile_table(s).into_iter().map(|i| i.name).collect())
.unwrap_or_default()
};
let hit = {
let mut g = self
.tables
.catalog
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
g.drop_table(name)
};
if hit {
for iname in &compiled {
self.idx_drop(iname);
}
self.persist_table_sidecar();
}
hit
}
pub fn table_list(&self) -> Vec<TableSpec> {
let g = self
.tables
.catalog
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
g.iter().cloned().collect()
}
pub fn table_verify(&self, name: &[u8]) -> KevyResult<TableVerifyReport> {
let spec = {
let g = self
.tables
.catalog
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
g.get(name).cloned()
}
.ok_or_else(|| KevyError::NotFound("no such table".into()))?;
let compiled = compile_table(&spec);
let mut per_index: Vec<(Vec<u8>, [u64; 6])> =
compiled.iter().map(|i| (i.name.clone(), [0u64; 6])).collect();
let mut spot = [0u64; 2];
for shard in self.shards.iter() {
let mut g = lock_write(shard);
let inner = &mut *g;
crate::ops_index_sync::sync_segs(&self.indexes, &mut inner.idx_segs, &mut inner.store);
for (slot, ispec) in per_index.iter_mut().zip(&compiled) {
shard_index_counts(inner, ispec, &mut slot.1);
}
let (r, m) = shard_spot_check(&mut inner.store, &spec);
spot[0] += r;
spot[1] += m;
}
Ok((per_index, spot))
}
#[cfg(feature = "persist")]
pub(crate) fn table_boot(&self) {
let Some(dir) = &self.config.data_dir else { return };
if let Ok(text) = std::fs::read_to_string(dir.join(SIDECAR))
&& let Some(cat) = TableCatalog::from_sidecar(&text)
&& !cat.is_empty()
{
let mut g = self
.tables
.catalog
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*g = cat;
}
}
#[cfg(not(feature = "persist"))]
pub(crate) fn table_boot(&self) {}
#[cfg(feature = "persist")]
fn persist_table_sidecar(&self) {
let Some(dir) = &self.config.data_dir else { return };
let g = self
.tables
.catalog
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
let tmp = dir.join("table-catalog.meta.tmp");
if std::fs::write(&tmp, g.to_sidecar()).is_ok() {
let _ = std::fs::rename(&tmp, dir.join(SIDECAR));
}
}
#[cfg(not(feature = "persist"))]
fn persist_table_sidecar(&self) {}
}
fn strip_err(e: &str) -> &str {
e.strip_prefix("ERR ").unwrap_or(e)
}
fn shard_index_counts(
inner: &mut crate::store_inner::Inner,
ispec: &kevy_index::IndexSpec,
sums: &mut [u64; 6],
) {
let Some((spec, seg)) = inner.idx_segs.segs.iter().find(|(s, _)| s.name == ispec.name)
else {
return;
};
let stats = seg.stats();
let mut entries: Vec<(Vec<u8>, kevy_index::IndexValue)> = Vec::new();
seg.each_entry(|k, v| entries.push((k.to_vec(), v.clone())));
let spec = spec.clone();
let store = &mut inner.store;
let drift = store.peek_scope(|s| {
let names = spec.scalar_read_names();
let w = spec.primary_width();
let mut drift = 0u64;
for (key, held) in &entries {
let actual = match s.peek_hash_fields(key, &names[..w]) {
Ok(Some(vals)) => spec.derive_scalar(&vals),
_ => None,
};
if actual.as_ref() != Some(held) {
drift += 1;
}
}
drift
});
sums[0] += stats.entries;
sums[1] += stats.approx_bytes;
sums[2] += stats.coerce_failures;
sums[3] += stats.duplicates;
sums[4] += drift;
sums[5] += entries.len() as u64;
}
fn shard_spot_check(store: &mut kevy_store::Store, spec: &TableSpec) -> (u64, u64) {
let mut pat = spec.prefix.clone();
pat.push(b'*');
let keys = store.collect_keys(Some(&pat), Some(SPOTCHECK_ROWS));
let names: Vec<&[u8]> = spec.columns.iter().map(|(n, _)| n.as_slice()).collect();
store.peek_scope(|s| {
let (mut rows, mut mismatches) = (0u64, 0u64);
for key in &keys {
rows += 1;
let Ok(Some(vals)) = s.peek_hash_fields(key, &names) else { continue };
for ((_, ty), val) in spec.columns.iter().zip(&vals) {
if let Some(raw) = val
&& kevy_index::IndexValue::coerce(*ty, raw).is_none()
{
mismatches += 1;
}
}
}
(rows, mismatches)
})
}