1use std::sync::RwLock;
9
10use kevy_index::{IndexVerify, TableCatalog, TableEnsure, TableSpec, TableVerify, compile_table};
11
12use crate::store::{Store, lock_write};
13use crate::{KevyError, KevyResult};
14
15#[derive(Default)]
18pub(crate) struct TableReg {
19 pub(crate) catalog: RwLock<TableCatalog>,
20}
21
22const SPOTCHECK_ROWS: usize = 64;
24
25#[cfg(feature = "persist")]
26const SIDECAR: &str = "table-catalog.meta";
27
28#[deprecated(since = "4.1.0", note = "use `table_verify_report`, whose fields are named")]
36pub type TableVerifyReport = (Vec<(Vec<u8>, [u64; 6])>, [u64; 2]);
37
38impl Store {
39 pub fn table_declare(&self, spec: TableSpec) -> KevyResult<()> {
44 #[cfg(all(feature = "tier", not(target_arch = "wasm32")))]
48 crate::ops_index_sync::tier_floor_check(&self.shards)?;
49 let compiled = compile_table(&spec).map_err(KevyError::InvalidInput)?;
52 {
53 let g = self
54 .tables
55 .catalog
56 .read()
57 .unwrap_or_else(std::sync::PoisonError::into_inner);
58 if g.get(&spec.name).is_some() {
59 return Err(KevyError::InvalidInput("table already exists".into()));
60 }
61 }
62 {
66 let g = self
67 .indexes
68 .catalog
69 .read()
70 .unwrap_or_else(std::sync::PoisonError::into_inner);
71 let mut probe = g.1.clone();
72 for ispec in &compiled {
73 probe
74 .create(ispec.clone())
75 .map_err(|e| KevyError::InvalidInput(strip_err(e).into()))?;
76 }
77 }
78 {
79 let mut g = self
80 .tables
81 .catalog
82 .write()
83 .unwrap_or_else(std::sync::PoisonError::into_inner);
84 g.create(spec).map_err(|e| KevyError::InvalidInput(strip_err(&e).into()))?;
85 }
86 self.persist_table_sidecar();
87 for ispec in compiled {
88 self.register_spec(ispec)?;
89 }
90 Ok(())
91 }
92
93 pub fn table_ensure(&self, spec: TableSpec) -> KevyResult<TableEnsure> {
105 let existing = {
106 let g = self
107 .tables
108 .catalog
109 .read()
110 .unwrap_or_else(std::sync::PoisonError::into_inner);
111 g.get(&spec.name).cloned()
112 };
113 match existing {
114 None => {
115 self.table_declare(spec)?;
116 Ok(TableEnsure::Created)
117 }
118 Some(cur) if cur == spec => Ok(TableEnsure::Unchanged),
119 Some(cur) => Err(KevyError::InvalidInput(
120 kevy_index::spec_diff(&cur, &spec),
121 )),
122 }
123 }
124
125 pub fn table_replace(&self, spec: TableSpec) -> KevyResult<()> {
132 compile_table(&spec).map_err(KevyError::InvalidInput)?;
133 self.table_drop(&spec.name);
134 self.table_declare(spec)
135 }
136
137 pub fn table_drop(&self, name: &[u8]) -> bool {
140 let compiled: Vec<Vec<u8>> = {
141 let g = self
142 .tables
143 .catalog
144 .read()
145 .unwrap_or_else(std::sync::PoisonError::into_inner);
146 g.get(name)
147 .map(|s| {
148 compile_table(s)
149 .map(|c| c.into_iter().map(|i| i.name).collect())
150 .unwrap_or_default() })
152 .unwrap_or_default()
153 };
154 let hit = {
155 let mut g = self
156 .tables
157 .catalog
158 .write()
159 .unwrap_or_else(std::sync::PoisonError::into_inner);
160 g.drop_table(name)
161 };
162 if hit {
163 for iname in &compiled {
164 self.idx_drop(iname);
165 }
166 self.persist_table_sidecar();
167 }
168 hit
169 }
170
171 pub fn table_list(&self) -> Vec<TableSpec> {
173 let g = self
174 .tables
175 .catalog
176 .read()
177 .unwrap_or_else(std::sync::PoisonError::into_inner);
178 g.iter().cloned().collect()
179 }
180
181 #[deprecated(since = "4.1.0", note = "use `table_verify_report`, whose fields are named")]
186 #[allow(deprecated)]
187 pub fn table_verify(&self, name: &[u8]) -> KevyResult<TableVerifyReport> {
188 let r = self.table_verify_report(name)?;
189 Ok((
190 r.per_index
191 .into_iter()
192 .map(|i| {
193 (i.name, [i.entries, i.approx_bytes, i.coerce_failures, i.duplicates, i.drift, i.checked])
194 })
195 .collect(),
196 [r.spot_rows, r.spot_type_mismatches],
197 ))
198 }
199
200 pub fn table_verify_report(&self, name: &[u8]) -> KevyResult<TableVerify> {
203 let spec = {
204 let g = self
205 .tables
206 .catalog
207 .read()
208 .unwrap_or_else(std::sync::PoisonError::into_inner);
209 g.get(name).cloned()
210 }
211 .ok_or_else(|| KevyError::NotFound("no such table".into()))?;
212 let compiled = compile_table(&spec).map_err(KevyError::InvalidInput)?;
213 let mut per_index: Vec<(Vec<u8>, [u64; 10])> =
214 compiled.iter().map(|i| (i.name.clone(), [0u64; 10])).collect();
215 let mut spot = [0u64; 2];
216 for shard in self.shards.iter() {
217 let mut g = lock_write(shard);
218 let inner = &mut *g;
219 crate::ops_index_sync::sync_segs(&self.indexes, &mut inner.idx_segs, &mut inner.store);
220 for (slot, ispec) in per_index.iter_mut().zip(&compiled) {
221 shard_index_counts(inner, ispec, &mut slot.1);
222 }
223 let (r, m) = shard_spot_check(&mut inner.store, &spec);
224 spot[0] += r;
225 spot[1] += m;
226 }
227 Ok(TableVerify {
228 per_index: per_index
229 .into_iter()
230 .map(|(name, s)| IndexVerify {
231 name,
232 entries: s[0],
233 approx_bytes: s[1],
234 coerce_failures: s[2],
235 duplicates: s[3],
236 drift: s[4],
237 checked: s[5],
238 excluded: s[6],
239 absent: s[7],
240 rows: s[8],
241 missing: s[9],
242 })
243 .collect(),
244 spot_rows: spot[0],
245 spot_type_mismatches: spot[1],
246 })
247 }
248
249 #[cfg(feature = "persist")]
250 pub(crate) fn table_boot(&self) {
251 let Some(dir) = &self.config.data_dir else { return };
252 if let Ok(text) = std::fs::read_to_string(dir.join(SIDECAR))
253 && let Some(cat) = TableCatalog::from_sidecar(&text)
254 && !cat.is_empty()
255 {
256 let mut g = self
257 .tables
258 .catalog
259 .write()
260 .unwrap_or_else(std::sync::PoisonError::into_inner);
261 *g = cat;
262 }
263 }
264
265 #[cfg(not(feature = "persist"))]
266 pub(crate) fn table_boot(&self) {}
267
268 #[cfg(feature = "persist")]
269 fn persist_table_sidecar(&self) {
270 let Some(dir) = &self.config.data_dir else { return };
271 let g = self
272 .tables
273 .catalog
274 .read()
275 .unwrap_or_else(std::sync::PoisonError::into_inner);
276 let tmp = dir.join("table-catalog.meta.tmp");
277 if std::fs::write(&tmp, g.to_sidecar()).is_ok() {
278 let _ = std::fs::rename(&tmp, dir.join(SIDECAR));
279 }
280 }
281
282 #[cfg(not(feature = "persist"))]
283 fn persist_table_sidecar(&self) {}
284}
285
286fn strip_err(e: &str) -> &str {
289 e.strip_prefix("ERR ").unwrap_or(e)
290}
291
292fn classify_prefix_rows(
312 s: &mut kevy_store::Store,
313 spec: &kevy_index::IndexSpec,
314 row_keys: &[Vec<u8>],
315 indexed: &std::collections::HashSet<&[u8]>,
316) -> [u64; 5] {
317 let names = spec.scalar_read_names();
318 let w = spec.primary_width();
319 let mut f = [0u64; 5];
320 for key in row_keys {
321 f[3] += 1;
322 let cls = match s.peek_hash_fields(key, &names[..w]) {
323 Ok(Some(vals)) => spec.classify_scalar(&vals),
324 _ => kevy_index::RowDerivation::Absent,
326 };
327 match cls {
328 kevy_index::RowDerivation::Indexed(_) => {
329 if !indexed.contains(key.as_slice()) {
330 f[4] += 1;
331 }
332 }
333 kevy_index::RowDerivation::CoerceFailed => f[0] += 1,
334 kevy_index::RowDerivation::Oversize => f[1] += 1,
335 kevy_index::RowDerivation::Absent => f[2] += 1,
336 }
337 }
338 f
339}
340
341fn shard_index_counts(
342 inner: &mut crate::store_inner::Inner,
343 ispec: &kevy_index::IndexSpec,
344 sums: &mut [u64; 10],
345) {
346 let Some((spec, seg)) = inner.idx_segs.segs.iter().find(|(s, _)| s.name == ispec.name)
347 else {
348 return;
349 };
350 let stats = seg.stats();
351 let mut entries: Vec<(Vec<u8>, kevy_index::IndexValue)> = Vec::new();
352 seg.each_entry(|k, v| entries.push((k.to_vec(), v.clone())));
353 let indexed: std::collections::HashSet<&[u8]> =
354 entries.iter().map(|(k, _)| k.as_slice()).collect();
355 let spec = spec.clone();
356 let mut pat = spec.prefix.clone();
357 pat.push(b'*');
358 let row_keys = inner.store.collect_keys(Some(&pat), None);
359 let store = &mut inner.store;
360 let (drift, fresh) = store.peek_scope(|s| {
361 let names = spec.scalar_read_names();
362 let w = spec.primary_width();
363 let mut drift = 0u64;
364 for (key, held) in &entries {
365 let actual = match s.peek_hash_fields(key, &names[..w]) {
366 Ok(Some(vals)) => spec.derive_scalar(&vals),
367 _ => None,
368 };
369 if actual.as_ref() != Some(held) {
370 drift += 1;
371 }
372 }
373 (drift, classify_prefix_rows(s, &spec, &row_keys, &indexed))
374 });
375 sums[0] += stats.entries;
376 sums[1] += stats.approx_bytes;
377 sums[2] += fresh[0];
378 sums[3] += stats.duplicates;
379 sums[4] += drift;
380 sums[5] += entries.len() as u64;
381 sums[6] += fresh[1];
382 sums[7] += fresh[2];
383 sums[8] += fresh[3];
384 sums[9] += fresh[4];
385}
386
387fn shard_spot_check(store: &mut kevy_store::Store, spec: &TableSpec) -> (u64, u64) {
391 let mut pat = spec.prefix.clone();
392 pat.push(b'*');
393 let keys = store.collect_keys(Some(&pat), Some(SPOTCHECK_ROWS));
394 let names: Vec<&[u8]> = spec.columns.iter().map(|(n, _)| n.as_slice()).collect();
395 store.peek_scope(|s| {
396 let (mut rows, mut mismatches) = (0u64, 0u64);
397 for key in &keys {
398 rows += 1;
399 let Ok(Some(vals)) = s.peek_hash_fields(key, &names) else { continue };
400 for ((_, ty), val) in spec.columns.iter().zip(&vals) {
401 if let Some(raw) = val
402 && kevy_index::IndexValue::coerce(*ty, raw).is_none()
403 {
404 mismatches += 1;
405 }
406 }
407 }
408 (rows, mismatches)
409 })
410}