kevy_embedded/
ops_table.rs1use std::sync::RwLock;
9
10use kevy_index::{TableCatalog, TableSpec, 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
28pub type TableVerifyReport = (Vec<(Vec<u8>, [u64; 6])>, [u64; 2]);
32
33impl Store {
34 pub fn table_declare(&self, spec: TableSpec) -> KevyResult<()> {
39 #[cfg(all(feature = "tier", not(target_arch = "wasm32")))]
43 crate::ops_index_sync::tier_floor_check(&self.shards)?;
44 let compiled = compile_table(&spec);
45 {
46 let g = self
47 .tables
48 .catalog
49 .read()
50 .unwrap_or_else(std::sync::PoisonError::into_inner);
51 if g.get(&spec.name).is_some() {
52 return Err(KevyError::InvalidInput("table already exists".into()));
53 }
54 }
55 {
59 let g = self
60 .indexes
61 .catalog
62 .read()
63 .unwrap_or_else(std::sync::PoisonError::into_inner);
64 let mut probe = g.1.clone();
65 for ispec in &compiled {
66 probe
67 .create(ispec.clone())
68 .map_err(|e| KevyError::InvalidInput(strip_err(e).into()))?;
69 }
70 }
71 {
72 let mut g = self
73 .tables
74 .catalog
75 .write()
76 .unwrap_or_else(std::sync::PoisonError::into_inner);
77 g.create(spec).map_err(|e| KevyError::InvalidInput(strip_err(&e).into()))?;
78 }
79 self.persist_table_sidecar();
80 for ispec in compiled {
81 self.register_spec(ispec)?;
82 }
83 Ok(())
84 }
85
86 pub fn table_drop(&self, name: &[u8]) -> bool {
89 let compiled: Vec<Vec<u8>> = {
90 let g = self
91 .tables
92 .catalog
93 .read()
94 .unwrap_or_else(std::sync::PoisonError::into_inner);
95 g.get(name)
96 .map(|s| compile_table(s).into_iter().map(|i| i.name).collect())
97 .unwrap_or_default()
98 };
99 let hit = {
100 let mut g = self
101 .tables
102 .catalog
103 .write()
104 .unwrap_or_else(std::sync::PoisonError::into_inner);
105 g.drop_table(name)
106 };
107 if hit {
108 for iname in &compiled {
109 self.idx_drop(iname);
110 }
111 self.persist_table_sidecar();
112 }
113 hit
114 }
115
116 pub fn table_list(&self) -> Vec<TableSpec> {
118 let g = self
119 .tables
120 .catalog
121 .read()
122 .unwrap_or_else(std::sync::PoisonError::into_inner);
123 g.iter().cloned().collect()
124 }
125
126 pub fn table_verify(&self, name: &[u8]) -> KevyResult<TableVerifyReport> {
131 let spec = {
132 let g = self
133 .tables
134 .catalog
135 .read()
136 .unwrap_or_else(std::sync::PoisonError::into_inner);
137 g.get(name).cloned()
138 }
139 .ok_or_else(|| KevyError::NotFound("no such table".into()))?;
140 let compiled = compile_table(&spec);
141 let mut per_index: Vec<(Vec<u8>, [u64; 6])> =
142 compiled.iter().map(|i| (i.name.clone(), [0u64; 6])).collect();
143 let mut spot = [0u64; 2];
144 for shard in self.shards.iter() {
145 let mut g = lock_write(shard);
146 let inner = &mut *g;
147 crate::ops_index_sync::sync_segs(&self.indexes, &mut inner.idx_segs, &mut inner.store);
148 for (slot, ispec) in per_index.iter_mut().zip(&compiled) {
149 shard_index_counts(inner, ispec, &mut slot.1);
150 }
151 let (r, m) = shard_spot_check(&mut inner.store, &spec);
152 spot[0] += r;
153 spot[1] += m;
154 }
155 Ok((per_index, spot))
156 }
157
158 #[cfg(feature = "persist")]
159 pub(crate) fn table_boot(&self) {
160 let Some(dir) = &self.config.data_dir else { return };
161 if let Ok(text) = std::fs::read_to_string(dir.join(SIDECAR))
162 && let Some(cat) = TableCatalog::from_sidecar(&text)
163 && !cat.is_empty()
164 {
165 let mut g = self
166 .tables
167 .catalog
168 .write()
169 .unwrap_or_else(std::sync::PoisonError::into_inner);
170 *g = cat;
171 }
172 }
173
174 #[cfg(not(feature = "persist"))]
175 pub(crate) fn table_boot(&self) {}
176
177 #[cfg(feature = "persist")]
178 fn persist_table_sidecar(&self) {
179 let Some(dir) = &self.config.data_dir else { return };
180 let g = self
181 .tables
182 .catalog
183 .read()
184 .unwrap_or_else(std::sync::PoisonError::into_inner);
185 let tmp = dir.join("table-catalog.meta.tmp");
186 if std::fs::write(&tmp, g.to_sidecar()).is_ok() {
187 let _ = std::fs::rename(&tmp, dir.join(SIDECAR));
188 }
189 }
190
191 #[cfg(not(feature = "persist"))]
192 fn persist_table_sidecar(&self) {}
193}
194
195fn strip_err(e: &str) -> &str {
198 e.strip_prefix("ERR ").unwrap_or(e)
199}
200
201fn shard_index_counts(
203 inner: &mut crate::store_inner::Inner,
204 ispec: &kevy_index::IndexSpec,
205 sums: &mut [u64; 6],
206) {
207 let Some((spec, seg)) = inner.idx_segs.segs.iter().find(|(s, _)| s.name == ispec.name)
208 else {
209 return;
210 };
211 let stats = seg.stats();
212 let mut entries: Vec<(Vec<u8>, kevy_index::IndexValue)> = Vec::new();
213 seg.each_entry(|k, v| entries.push((k.to_vec(), v.clone())));
214 let spec = spec.clone();
215 let store = &mut inner.store;
216 let drift = store.peek_scope(|s| {
217 let names = spec.scalar_read_names();
218 let w = spec.primary_width();
219 let mut drift = 0u64;
220 for (key, held) in &entries {
221 let actual = match s.peek_hash_fields(key, &names[..w]) {
222 Ok(Some(vals)) => spec.derive_scalar(&vals),
223 _ => None,
224 };
225 if actual.as_ref() != Some(held) {
226 drift += 1;
227 }
228 }
229 drift
230 });
231 sums[0] += stats.entries;
232 sums[1] += stats.approx_bytes;
233 sums[2] += stats.coerce_failures;
234 sums[3] += stats.duplicates;
235 sums[4] += drift;
236 sums[5] += entries.len() as u64;
237}
238
239fn shard_spot_check(store: &mut kevy_store::Store, spec: &TableSpec) -> (u64, u64) {
243 let mut pat = spec.prefix.clone();
244 pat.push(b'*');
245 let keys = store.collect_keys(Some(&pat), Some(SPOTCHECK_ROWS));
246 let names: Vec<&[u8]> = spec.columns.iter().map(|(n, _)| n.as_slice()).collect();
247 store.peek_scope(|s| {
248 let (mut rows, mut mismatches) = (0u64, 0u64);
249 for key in &keys {
250 rows += 1;
251 let Ok(Some(vals)) = s.peek_hash_fields(key, &names) else { continue };
252 for ((_, ty), val) in spec.columns.iter().zip(&vals) {
253 if let Some(raw) = val
254 && kevy_index::IndexValue::coerce(*ty, raw).is_none()
255 {
256 mismatches += 1;
257 }
258 }
259 }
260 (rows, mismatches)
261 })
262}