1use crate::{KevyError, KevyResult};
11use std::io;
12use std::sync::RwLock;
13
14use kevy_index::{
15 IndexValue, MaterializedSet, Membership, SortOrder, Tree, ViewCatalog, ViewMode, ViewSpec,
16};
17
18use crate::ops_index::ShardSegs;
19use crate::store::{Store, lock_write};
20
21#[derive(Debug, Default)]
23pub(crate) struct ViewReg {
24 pub(crate) catalog: RwLock<(u64, ViewCatalog)>,
25}
26
27#[derive(Debug, Default)]
29pub(crate) struct ShardViews {
30 pub(crate) version: u64,
31 pub(crate) dirs_at: (u64, u64),
34 pub(crate) views: Vec<ViewState>,
35 #[cfg(all(feature = "tier", not(target_arch = "wasm32")))]
39 pub(crate) stats_dirty: bool,
40 #[cfg(all(feature = "tier", not(target_arch = "wasm32")))]
41 pub(crate) reserved_cache: u64,
42}
43
44#[derive(Debug)]
45
46pub(crate) struct ViewState {
47 spec: ViewSpec,
48 mat: Option<MaterializedSet>,
49 needs_rebuild: bool,
50}
51
52impl ShardViews {
53 #[inline]
56 pub(crate) fn mark_stats_dirty(&mut self) {
57 #[cfg(all(feature = "tier", not(target_arch = "wasm32")))]
58 {
59 self.stats_dirty = true;
60 }
61 }
62
63 #[cfg(all(feature = "tier", not(target_arch = "wasm32")))]
67 pub(crate) fn reserved_bytes(&mut self) -> u64 {
68 if !self.stats_dirty {
69 return self.reserved_cache;
70 }
71 let sum = self
72 .views
73 .iter()
74 .map(|v| v.mat.as_ref().map_or(0, MaterializedSet::approx_bytes))
75 .sum();
76 self.reserved_cache = sum;
77 self.stats_dirty = false;
78 sum
79 }
80}
81
82pub type ViewPage = (Vec<(Vec<u8>, IndexValue)>, Option<(IndexValue, Vec<u8>)>);
100
101impl Store {
102 pub fn view_create(
105 &self,
106 name: &[u8],
107 tree: Tree,
108 order_by: &[u8],
109 order: SortOrder,
110 mode: ViewMode,
111 ) -> KevyResult<()> {
112 self.catalog_change(|| self.create_view(name, tree, order_by, order, mode))
113 }
114
115 fn create_view(
116 &self,
117 name: &[u8],
118 tree: Tree,
119 order_by: &[u8],
120 order: SortOrder,
121 mode: ViewMode,
122 ) -> KevyResult<()> {
123 self.check_view_refs(&tree, order_by)?;
124 let spec = ViewSpec::new(name, tree, order_by).with_order(order).with_mode(mode);
125 {
126 let mut g =
127 self.views.catalog.write().unwrap_or_else(std::sync::PoisonError::into_inner);
128 let (ver, cat) = &mut *g;
129 cat.create(spec).map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
130 *ver += 1;
131 }
132 for shard in self.shards.iter() {
133 let mut g = lock_write(shard);
134 let inner = &mut *g;
135 crate::ops_index::sync_segs(&self.indexes, &mut inner.idx_segs, &mut inner.store);
136 sync_views(&self.views, &mut inner.view_segs, &mut inner.idx_segs);
137 }
138 Ok(())
139 }
140
141 fn check_view_refs(&self, tree: &Tree, order_by: &[u8]) -> KevyResult<()> {
144 let mut names: Vec<Vec<u8>> = vec![order_by.to_vec()];
145 tree.each_leaf(&mut |l| names.push(l.index.clone()));
146 let g = self.indexes.catalog.read().unwrap_or_else(std::sync::PoisonError::into_inner);
147 for n in &names {
148 if g.1.get(n).is_none() {
149 return Err(KevyError::InvalidInput("view references unknown index".into()));
150 }
151 }
152 Ok(())
153 }
154
155 pub fn view_drop(&self, name: &[u8]) -> KevyResult<bool> {
158 self.catalog_change(|| {
159 let mut g =
160 self.views.catalog.write().unwrap_or_else(std::sync::PoisonError::into_inner);
161 let (ver, cat) = &mut *g;
162 let hit = cat.drop_view(name);
163 if hit {
164 *ver += 1;
165 }
166 Ok(hit)
167 })
168 }
169
170 pub fn view_query(
173 &self,
174 name: &[u8],
175 after: Option<&(IndexValue, Vec<u8>)>,
176 limit: usize,
177 ) -> KevyResult<ViewPage> {
178 let limit = limit.clamp(1, 100_000);
179 let mut desc = false;
180 let mut all: Vec<(IndexValue, Vec<u8>)> = Vec::new();
181 let mut found = false;
182 for shard in self.shards.iter() {
183 let mut g = lock_write(shard);
184 let inner = &mut *g;
185 crate::ops_index::sync_segs(&self.indexes, &mut inner.idx_segs, &mut inner.store);
186 sync_views(&self.views, &mut inner.view_segs, &mut inner.idx_segs);
187 let Some(i) = inner.view_segs.views.iter().position(|v| v.spec.name == name) else {
188 continue;
189 };
190 found = true;
191 if inner.view_segs.views[i].needs_rebuild {
192 inner.view_segs.mark_stats_dirty();
193 rebuild(&mut inner.view_segs.views[i], &inner.idx_segs);
194 }
195 let vs = &inner.view_segs.views[i];
196 desc = vs.spec.order == SortOrder::Desc;
197 match &vs.mat {
198 Some(m) => all.extend(m.page(after, limit)),
199 None => stream_virtual(&vs.spec, &inner.idx_segs, after, limit, &mut all),
200 }
201 }
202 if !found {
203 return Err(KevyError::NotFound("no such view".into()));
204 }
205 all.sort();
206 if desc {
207 all.reverse();
208 }
209 all.truncate(limit);
210 let next = if all.len() == limit { all.last().cloned() } else { None };
211 Ok((all.into_iter().map(|(v, k)| (k, v)).collect(), next))
212 }
213
214 pub fn view_list(&self) -> Vec<(Vec<u8>, ViewMode, usize)> {
216 let g = self.views.catalog.read().unwrap_or_else(std::sync::PoisonError::into_inner);
217 g.1.iter().map(|s| (s.name.clone(), s.mode, s.tree.leaves())).collect()
218 }
219
220 pub fn view_spec(&self, name: &[u8]) -> Option<kevy_index::ViewSpec> {
222 let g = self.views.catalog.read().unwrap_or_else(std::sync::PoisonError::into_inner);
223 g.1.get(name).cloned()
224 }
225
226 pub fn view_count(&self, name: &[u8]) -> KevyResult<u64> {
228 Ok(self.view_query(name, None, 100_000)?.0.len() as u64)
229 }
230}
231
232fn resolver<'a>(segs: &'a ShardSegs) -> impl Fn(&[u8]) -> Option<&'a kevy_index::Segment> {
233 move |name: &[u8]| segs.segs.iter().find(|(s, _)| s.name() == name).map(|(_, seg)| seg)
234}
235
236fn eval_shard(spec: &ViewSpec, segs: &ShardSegs) -> Vec<(IndexValue, Vec<u8>)> {
237 let r = resolver(segs);
238 let members = spec.tree.eval(&&r);
239 members
240 .into_iter()
241 .filter_map(|k| r(&spec.order_by).and_then(|s| s.key_dir()?.get(&k)).map(|v| (v, k)))
242 .collect()
243}
244
245fn stream_virtual(
249 spec: &ViewSpec,
250 segs: &ShardSegs,
251 after: Option<&(IndexValue, Vec<u8>)>,
252 limit: usize,
253 all: &mut Vec<(IndexValue, Vec<u8>)>,
254) {
255 let r = resolver(segs);
256 if let Some(order_seg) = r(&spec.order_by) {
257 let cursor = after.map(|(v, k)| kevy_index::Cursor::new(v.clone(), k.clone()));
258 let mut got = 0usize;
259 let mut scan = order_seg.scan(cursor.as_ref(), spec.order);
260 while let Some((v, k)) = scan.next_entry() {
261 if spec.tree.contains(k, &&r) {
262 all.push((v.clone(), k.to_vec()));
263 got += 1;
264 if got == limit {
265 break;
266 }
267 }
268 }
269 }
270}
271
272fn rebuild(vs: &mut ViewState, segs: &ShardSegs) {
273 let spec = vs.spec.clone();
274 let Some(mat) = &mut vs.mat else {
275 vs.needs_rebuild = false;
276 return;
277 };
278 mat.clear();
279 let mut rows = eval_shard(&spec, segs);
280 rows.sort();
281 if let ViewMode::Materialized { top_k } = spec.mode
282 && top_k > 0
283 {
284 if spec.order == SortOrder::Desc {
285 rows.reverse();
286 }
287 rows.truncate((top_k + top_k / 4) as usize);
288 }
289 for (v, k) in rows {
290 mat.apply(&k, Membership::Member(Some(v)));
291 }
292 vs.needs_rebuild = false;
293}
294
295pub(crate) fn sync_views(reg: &ViewReg, sv: &mut ShardViews, segs: &mut ShardSegs) {
297 let g = reg.catalog.read().unwrap_or_else(std::sync::PoisonError::into_inner);
298 let (ver, cat) = &*g;
299 if sv.dirs_at != (*ver, segs.version) {
301 let mut read: Vec<Vec<u8>> = Vec::new();
302 for v in cat.iter() {
303 read.push(v.order_by.clone());
304 v.tree.each_leaf(&mut |l| read.push(l.index.clone()));
305 }
306 for (spec, seg) in &mut segs.segs {
307 seg.set_key_dir(read.iter().any(|n| n.as_slice() == spec.name()));
308 }
309 sv.dirs_at = (*ver, segs.version);
310 }
311 let segs = &*segs;
312 if sv.version == *ver {
313 return;
314 }
315 sv.mark_stats_dirty();
316 let mut next = Vec::new();
317 for spec in cat.iter() {
318 match sv.views.iter().position(|v| v.spec == *spec) {
319 Some(i) => next.push(sv.views.swap_remove(i)),
320 None => {
321 let mat = match spec.mode {
323 ViewMode::Materialized { top_k } => {
324 Some(MaterializedSet::new(top_k, spec.order))
325 }
326 _ => None,
327 };
328 let mut vs = ViewState { spec: spec.clone(), needs_rebuild: mat.is_some(), mat };
329 if vs.needs_rebuild {
330 rebuild(&mut vs, segs);
331 }
332 next.push(vs);
333 }
334 }
335 }
336 sv.views = next;
337 sv.version = *ver;
338}
339
340pub(crate) fn on_commit(reg: &ViewReg, sv: &mut ShardViews, segs: &mut ShardSegs, parts: &[&[u8]]) {
342 {
343 let g = reg.catalog.read().unwrap_or_else(std::sync::PoisonError::into_inner);
344 if g.1.is_empty() {
345 return;
346 }
347 }
348 sync_views(reg, sv, segs);
349 let segs = &*segs;
350 let verb = parts.first().copied().unwrap_or(b"");
351 if verb.eq_ignore_ascii_case(b"FLUSHALL") || verb.eq_ignore_ascii_case(b"FLUSHDB") {
352 for vs in &mut sv.views {
353 if let Some(m) = &mut vs.mat {
354 m.clear();
355 }
356 }
357 sv.mark_stats_dirty();
358 return;
359 }
360 let mut touched = false;
362 let views = &mut sv.views;
363 crate::ops_index::each_written_key_pub(verb, parts, |key| {
364 for vs in &mut *views {
365 let Some(mat) = &mut vs.mat else { continue };
366 touched = true;
367 let r = resolver(segs);
368 let membership = if vs.spec.tree.contains(key, &&r) {
369 Membership::Member(r(&vs.spec.order_by).and_then(|s| s.key_dir()?.get(key)))
370 } else {
371 Membership::NonMember
372 };
373 if mat.apply(key, membership) {
374 vs.needs_rebuild = true;
375 }
376 }
377 });
378 if touched {
379 sv.mark_stats_dirty();
380 }
381}