1use std::marker::PhantomData;
2use std::ops::Bound;
3use std::sync::Arc;
4
5use crate::Key;
6use crate::batch::Applied;
7use crate::byte_view::ByteView;
8use crate::codec::Codec;
9use crate::compaction::CompactionIndex;
10use crate::config::Config;
11use crate::decoded_cache::DecodedCache;
12use crate::disk_loc::DiskLoc;
13use crate::error::{DbError, DbResult};
14use crate::hook::{NoHook, TypedWriteHook, VarTypedHookAdapter, WriteHook};
15use crate::map_index::{Dir, dir_from};
16use crate::scope::DirRangeIter;
17use crate::upsert::Upsert;
18use crate::var_tree::{TryVarIter, VarIter, VarShard, VarTree};
19use crate::watch::{self, CodecWatchDecoder, WatchOptions, WatchReceiver, WatchSpec};
20
21pub struct VarTypedTree<
89 K: Key,
90 T: Send + Sync,
91 C: Codec<T> + Clone,
92 H: TypedWriteHook<K, T> = NoHook,
93> {
94 inner: Arc<VarTree<K, VarTypedHookAdapter<K, T, C, H>>>,
95 codec: C,
96 decoded_cache: DecodedCache<T>,
97 _marker: PhantomData<fn() -> T>,
98}
99
100impl<K: Key, T: Send + Sync, C: Codec<T> + Clone, H: TypedWriteHook<K, T>> watch::sealed::Spec
101 for VarTypedTree<K, T, C, H>
102{
103}
104impl<K: Key, T: Send + Sync, C: Codec<T> + Clone, H: TypedWriteHook<K, T>> WatchSpec
105 for VarTypedTree<K, T, C, H>
106{
107 type Key = K;
108 type Value = T;
109 type Decoder = CodecWatchDecoder<C, T>;
110}
111impl<
112 K: Key,
113 T: Send + Sync,
114 C: Codec<T> + Clone + Sync,
115 H: TypedWriteHook<K, T> + watch::sealed::Enabled,
116> VarTypedTree<K, T, C, H>
117{
118 pub fn watch(&self, options: WatchOptions) -> WatchReceiver<Self> {
138 WatchReceiver::subscribe(
139 self.inner.hook.watch_channel().expect("enabled watch hook"),
140 CodecWatchDecoder::new(self.codec.clone()),
141 options,
142 )
143 }
144}
145
146impl<K: Key, T: Send + Sync, C: Codec<T> + Clone> VarTypedTree<K, T, C> {
147 #[doc = include_str!("../docs/exclusive-access.md")]
151 pub fn open(path: impl AsRef<std::path::Path>, config: Config, codec: C) -> DbResult<Self> {
152 Self::open_hooked_inner(path, config, codec, NoHook)
153 }
154}
155
156impl<K: Key, T: Send + Sync, C: Codec<T> + Clone, H: TypedWriteHook<K, T>>
157 VarTypedTree<K, T, C, H>
158{
159 #[doc = include_str!("../docs/exclusive-access.md")]
162 pub fn open_hooked(
163 path: impl AsRef<std::path::Path>,
164 config: Config,
165 codec: C,
166 hook: H,
167 ) -> DbResult<Self> {
168 crate::counter_dirty::check_no_counters(&hook)?;
169 crate::unique::check_no_claims(&hook)?;
170 #[cfg(feature = "armour")]
171 if H::EMITS {
172 return Err(DbError::ChangeStreamRequiresManagedOpen);
173 }
174 Self::open_hooked_inner(path, config, codec, hook)
175 }
176
177 pub(crate) fn open_hooked_inner(
181 path: impl AsRef<std::path::Path>,
182 config: Config,
183 codec: C,
184 hook: H,
185 ) -> DbResult<Self> {
186 let adapter = VarTypedHookAdapter {
187 inner: hook,
188 codec: codec.clone(),
189 _marker: PhantomData,
190 };
191 let decoded_cache = DecodedCache::new(&config.decoded_cache);
192 if config.value_and_decoded_cache_both_on() {
193 tracing::warn!(
194 "VarTyped collection has both value_cache and decoded_cache enabled: every \
195 value-cache-routed value is held twice (raw bytes + decoded T). Consider \
196 value_cache: 0 when reads go through get_shared."
197 );
198 }
199 let inner = Arc::new(VarTree::open_inner(path, config, adapter)?);
200 Ok(Self {
201 inner,
202 codec,
203 decoded_cache,
204 _marker: PhantomData,
205 })
206 }
207
208 pub fn clean_shutdown(&self) -> DbResult<()> {
210 self.inner.clean_shutdown()
211 }
212
213 pub fn close(self) -> DbResult<()> {
214 self.clean_shutdown()
215 }
216
217 pub fn flush_buffers(&self) -> DbResult<()> {
219 self.inner.flush_buffers()
220 }
221
222 pub(crate) fn durable_flush(&self) -> DbResult<()> {
223 self.inner.durable_flush()
224 }
225
226 pub fn config(&self) -> &Config {
228 self.inner.config()
229 }
230
231 pub fn compact(&self) -> DbResult<usize> {
233 self.inner.compact()
234 }
235
236 pub fn sync_hints(&self) -> DbResult<()> {
238 self.inner.sync_hints()
239 }
240
241 pub fn warmup(&self) -> DbResult<()> {
243 self.inner.warmup()
244 }
245
246 pub fn as_inner(&self) -> &VarTree<K, VarTypedHookAdapter<K, T, C, H>> {
248 &self.inner
249 }
250
251 #[cfg(test)]
252 pub(crate) fn test_inner_arc(&self) -> Arc<VarTree<K, VarTypedHookAdapter<K, T, C, H>>> {
253 Arc::clone(&self.inner)
254 }
255
256 pub(crate) fn bind_tx_self(arc: &Arc<Self>) {
258 VarTree::bind_tx_self(&arc.inner);
259 }
260
261 pub(crate) fn tx_install_projection_metadata(&self, db: &crate::armour::Db) -> DbResult<()> {
262 self.as_inner().tx_install_projection_metadata(db)
263 }
264
265 pub(crate) fn tx_preplan_projection_targets(
266 &self,
267 ) -> DbResult<Vec<Arc<dyn crate::txn::participant::TxParticipant>>> {
268 self.as_inner().tx_preplan_projection_targets()
269 }
270
271 pub(crate) fn tx_project_writes(
272 &self,
273 key: &[u8],
274 old: Option<&[u8]>,
275 new: Option<&[u8]>,
276 ) -> DbResult<Vec<crate::txn::participant::TxDerivedWrite>> {
277 self.as_inner().tx_project_writes(key, old, new)
278 }
279
280 pub(crate) fn tx_counter_deltas_for(
281 &self,
282 key: &[u8],
283 old: Option<&[u8]>,
284 new: Option<&[u8]>,
285 ) -> DbResult<Vec<crate::txn::participant::TxCounterDelta>> {
286 self.as_inner().tx_counter_deltas_for(key, old, new)
287 }
288
289 pub(crate) fn tx_claims_for(
293 &self,
294 name: &'static str,
295 key: &[u8],
296 old: Option<&[u8]>,
297 new: Option<&[u8]>,
298 ) -> DbResult<Vec<crate::txn::participant::ClaimOp>> {
299 self.as_inner().tx_claims_for(name, key, old, new)
300 }
301
302 pub fn codec(&self) -> &C {
304 &self.codec
305 }
306
307 #[doc(hidden)]
308 pub fn build_ttl_index(&self) -> DbResult<()> {
309 self.inner.build_ttl_index()
310 }
311
312 pub fn sweep_expired(&self, now_ms: u64, budget: usize) -> DbResult<crate::ttl::SweepOutcome> {
313 self.inner.sweep_expired(now_ms, budget)
314 }
315
316 pub fn set_ttl_sweep(&self, enabled: bool) -> DbResult<()> {
317 self.inner.set_ttl_sweep(enabled)
318 }
319
320 pub fn rebuild_ttl_index(&self) -> DbResult<()> {
321 self.inner.rebuild_ttl_index()
322 }
323
324 #[doc(hidden)]
325 pub fn ttl_debug_len(&self) -> usize {
326 self.inner.ttl_debug_len()
327 }
328
329 #[doc(hidden)]
330 pub fn ttl_debug_deadline(&self, key: &K) -> Option<u64> {
331 self.inner.ttl_debug_deadline(key)
332 }
333
334 #[doc(hidden)]
335 pub fn ttl_debug_record(&self, key: &K, deadline: Option<u64>) {
336 self.inner.ttl_debug_record(key, deadline)
337 }
338
339 #[doc(hidden)]
340 pub fn ttl_debug_inject_stale(&self, key: &K, deadline: u64) {
341 self.inner.ttl_debug_inject_stale(key, deadline)
342 }
343
344 pub fn get(&self, key: &K) -> Option<T> {
352 let bytes = self.inner.get(key)?;
353 self.codec.decode_from(&bytes).ok()
354 }
355
356 pub fn get_or_err(&self, key: &K) -> DbResult<T> {
359 self.try_get(key)?.ok_or(DbError::KeyNotFound)
360 }
361
362 pub fn try_get(&self, key: &K) -> DbResult<Option<T>> {
365 match self.inner.try_get(key)? {
366 Some(bytes) => self.codec.decode_from(&bytes).map(Some),
367 None => Ok(None),
368 }
369 }
370
371 pub fn get_shared(&self, key: &K) -> Option<Arc<T>> {
388 self.try_get_shared(key).ok().flatten()
389 }
390
391 pub fn try_get_shared(&self, key: &K) -> DbResult<Option<Arc<T>>> {
396 self.inner
397 .try_get_decoded(key, &self.decoded_cache, |b| self.codec.decode_from(b))
398 }
399
400 pub fn get_many_shared(&self, keys: &[K]) -> Vec<Option<Arc<T>>> {
404 self.inner
405 .get_many_decoded(keys, &self.decoded_cache, |b| self.codec.decode_from(b))
406 }
407
408 pub fn contains(&self, key: &K) -> bool {
410 self.inner.contains(key)
411 }
412
413 pub fn first(&self) -> Option<(K, T)> {
417 self.iter().next()
418 }
419
420 pub fn last(&self) -> Option<(K, T)> {
422 self.iter().next_back()
423 }
424
425 pub fn try_first(&self) -> DbResult<Option<(K, T)>> {
428 match self.inner.try_first()? {
429 Some((k, bytes)) => Ok(Some((k, self.codec.decode_from(&bytes)?))),
430 None => Ok(None),
431 }
432 }
433
434 pub fn try_last(&self) -> DbResult<Option<(K, T)>> {
436 match self.inner.try_last()? {
437 Some((k, bytes)) => Ok(Some((k, self.codec.decode_from(&bytes)?))),
438 None => Ok(None),
439 }
440 }
441
442 pub fn put(&self, key: &K, value: &T) -> DbResult<bool> {
449 let expires_at = self.inner.hook.typed_deadline(key, value);
450 let mut buf = Vec::new();
451 self.codec.encode_to(value, &mut buf)?;
452 self.inner.put_with_deadline(key, &buf, expires_at)
453 }
454
455 pub fn insert(&self, key: &K, value: &T) -> DbResult<()> {
458 let mut buf = Vec::new();
459 self.codec.encode_to(value, &mut buf)?;
460 self.inner.insert(key, &buf)
461 }
462
463 pub fn delete(&self, key: &K) -> DbResult<bool> {
465 self.inner.delete(key)
466 }
467
468 pub fn cas(&self, key: &K, expected: &T, new_value: &T) -> DbResult<()> {
475 let mut exp_buf = Vec::new();
476 self.codec.encode_to(expected, &mut exp_buf)?;
477 let mut new_buf = Vec::new();
478 self.codec.encode_to(new_value, &mut new_buf)?;
479 self.inner.cas(key, &exp_buf, &new_buf)
480 }
481
482 pub fn compare_delete(&self, key: &K, expected: &T) -> DbResult<()> {
486 let mut exp_buf = Vec::new();
487 self.codec.encode_to(expected, &mut exp_buf)?;
488 self.inner.compare_delete(key, &exp_buf)
489 }
490
491 pub fn update(&self, key: &K, f: impl FnOnce(&T) -> T) -> DbResult<Option<T>> {
498 use std::cell::Cell;
499 let out: Cell<Option<T>> = Cell::new(None);
500 let result = self.inner.try_update_inner(
501 key,
502 |bytes| {
503 let current = self.codec.decode_from(bytes)?;
504 let new_val = f(¤t);
505 let mut buf = Vec::new();
506 self.codec.encode_to(&new_val, &mut buf)?;
507 out.set(Some(new_val));
508 Ok(Some(ByteView::from_vec(buf)))
509 },
510 false,
511 )?;
512 if result.is_none() {
513 return Ok(None);
514 }
515 Ok(out.into_inner())
516 }
517
518 pub fn fetch_update(&self, key: &K, f: impl FnOnce(&T) -> T) -> DbResult<Option<T>> {
521 use std::cell::Cell;
522 let out: Cell<Option<T>> = Cell::new(None);
523 let result = self.inner.try_update_inner(
524 key,
525 |bytes| {
526 let current = self.codec.decode_from(bytes)?;
527 let new_val = f(¤t);
528 let mut buf = Vec::new();
529 self.codec.encode_to(&new_val, &mut buf)?;
530 out.set(Some(current));
531 Ok(Some(ByteView::from_vec(buf)))
532 },
533 true,
534 )?;
535 if result.is_none() {
536 return Ok(None);
537 }
538 Ok(out.into_inner())
539 }
540
541 pub fn upsert<U: crate::IntoUpsert<T>>(
546 &self,
547 key: &K,
548 f: impl FnOnce(Option<&T>) -> U,
549 ) -> crate::DbResult<U::Out> {
550 self.atomic(key, |s| crate::upsert::upsert_via_held(s, key, f))
551 }
552
553 pub fn atomic<R>(
559 &self,
560 shard_key: &K,
561 f: impl FnOnce(&mut VarTypedShard<'_, K, T, C, H>) -> DbResult<R>,
562 ) -> DbResult<R> {
563 self.inner.atomic(shard_key, |var_shard| {
564 let inner_ptr: *mut () = (var_shard as *mut VarShard<'_, _, _>).cast();
567 let mut shard = VarTypedShard {
568 tree: self,
569 inner: inner_ptr,
570 _marker: PhantomData,
571 };
572 f(&mut shard)
573 })
574 }
575
576 pub fn prefix_iter(&self, prefix: &[u8]) -> VarTypedIter<'_, K, T, C, H> {
585 VarTypedIter {
586 inner: self.inner.prefix_iter(prefix),
587 codec: &self.codec,
588 _marker: PhantomData,
589 }
590 }
591
592 pub fn iter(&self) -> VarTypedIter<'_, K, T, C, H> {
597 VarTypedIter {
598 inner: self.inner.iter(),
599 codec: &self.codec,
600 _marker: PhantomData,
601 }
602 }
603
604 pub fn range(&self, start: &K, end: &K) -> VarTypedIter<'_, K, T, C, H> {
608 VarTypedIter {
609 inner: self.inner.range(start, end),
610 codec: &self.codec,
611 _marker: PhantomData,
612 }
613 }
614
615 pub fn range_bounds(&self, start: Bound<&K>, end: Bound<&K>) -> VarTypedIter<'_, K, T, C, H> {
619 VarTypedIter {
620 inner: self.inner.range_bounds(start, end),
621 codec: &self.codec,
622 _marker: PhantomData,
623 }
624 }
625
626 pub(crate) fn range_bounds_dir(
628 &self,
629 start: Bound<&K>,
630 end: Bound<&K>,
631 dir: Dir,
632 ) -> VarTypedDirRangeIter<'_, K, T, C, H> {
633 VarTypedDirRangeIter {
634 inner: self.inner.range_bounds_dir(start, end, dir),
635 codec: &self.codec,
636 _marker: PhantomData,
637 }
638 }
639
640 pub(crate) fn keys_range_bounds_dir(
642 &self,
643 start: Bound<&K>,
644 end: Bound<&K>,
645 dir: Dir,
646 ) -> crate::skiplist::iter::SkipKeyKeys<'_, K, crate::skiplist::node::VarNode<K>> {
647 self.inner.keys_range_bounds_dir(start, end, dir)
648 }
649
650 pub(crate) fn reversed(&self) -> bool {
652 self.inner.reversed()
653 }
654
655 pub fn scope<const N: usize>(
657 &self,
658 key: &K,
659 ) -> crate::scope::Scope<'_, crate::scope::TreeScan<&Self>, N> {
660 crate::scope::tree_scope::<_, _, N>(self, key, dir_from(self.reversed()))
661 }
662
663 #[cfg(feature = "armour")]
665 pub fn tx_scope<const N: usize>(self: &Arc<Self>, key: &K) -> crate::TxScope<Self, K, N> {
666 crate::txn::scope::TxScope::from_parts(
667 self.clone(),
668 key,
669 dir_from(self.reversed()),
670 Self::tx_scope_keys,
671 )
672 }
673
674 fn tx_scope_keys<'a>(
675 collection: &'a Self,
676 start: Bound<&'a K>,
677 end: Bound<&'a K>,
678 dir: Dir,
679 ) -> Box<dyn Iterator<Item = K> + 'a> {
680 Box::new(collection.keys_range_bounds_dir(start, end, dir))
681 }
682
683 pub(crate) fn try_range_bounds(
685 &self,
686 start: Bound<&K>,
687 end: Bound<&K>,
688 ) -> TryVarTypedIter<'_, K, T, C, H> {
689 TryVarTypedIter {
690 inner: crate::scope::DirRangeIter::from_storage_match(
691 self.inner.try_range_bounds(start, end),
692 true,
693 ),
694 codec: &self.codec,
695 failed: false,
696 }
697 }
698
699 pub(crate) fn try_range_bounds_dir(
700 &self,
701 start: Bound<&K>,
702 end: Bound<&K>,
703 dir: Dir,
704 ) -> TryVarTypedIter<'_, K, T, C, H> {
705 TryVarTypedIter {
706 inner: self.inner.try_range_bounds_dir(start, end, dir),
707 codec: &self.codec,
708 failed: false,
709 }
710 }
711
712 pub fn len(&self) -> usize {
715 self.inner.len()
716 }
717
718 pub(crate) fn try_count_live_keys(&self) -> DbResult<u64> {
720 self.inner.try_count_live_keys()
721 }
722
723 pub fn is_empty(&self) -> bool {
724 self.inner.is_empty()
725 }
726
727 pub fn shard_for(&self, key: &K) -> usize {
728 self.inner.shard_for(key)
729 }
730
731 pub fn entry_len(&self, key: &K) -> Option<u32> {
734 self.inner.entry_len(key)
735 }
736
737 pub fn get_many(&self, keys: &[K]) -> Vec<Option<T>> {
742 self.inner
743 .get_many(keys)
744 .into_iter()
745 .map(|opt| opt.and_then(|b| self.codec.decode_from(&b).ok()))
746 .collect()
747 }
748
749 pub fn update_many<P>(
758 &self,
759 items: Vec<(K, P)>,
760 f: impl Fn(&K, Option<&T>, &P) -> Upsert<T>,
761 ) -> DbResult<Vec<(K, Applied<T>)>> {
762 use std::cell::RefCell;
763
764 let codec_err: RefCell<Option<DbError>> = RefCell::new(None);
769 let inner_out = self.inner.update_many(items, |k, old_bytes, p| {
770 if codec_err.borrow().is_some() {
771 return Upsert::Keep;
772 }
773 let old_t: Option<T> = match old_bytes {
774 Some(b) => match self.codec.decode_from(b) {
775 Ok(t) => Some(t),
776 Err(e) => {
777 *codec_err.borrow_mut() = Some(e);
778 return Upsert::Keep;
779 }
780 },
781 None => None,
782 };
783 match f(k, old_t.as_ref(), p) {
784 Upsert::Put(v) => {
785 let mut buf = Vec::new();
786 match self.codec.encode_to(&v, &mut buf) {
787 Ok(()) => Upsert::Put(ByteView::from_vec(buf)),
788 Err(e) => {
789 *codec_err.borrow_mut() = Some(e);
790 Upsert::Keep
791 }
792 }
793 }
794 Upsert::Keep => Upsert::Keep,
795 Upsert::Delete => Upsert::Delete,
796 }
797 })?;
798 if let Some(e) = codec_err.into_inner() {
799 return Err(e);
800 }
801 let mut out = Vec::with_capacity(inner_out.len());
805 for (k, applied) in inner_out {
806 let applied_t = match applied {
807 Applied::Written { old, new } => Applied::Written {
808 old: match old {
809 Some(b) => Some(self.codec.decode_from(&b)?),
810 None => None,
811 },
812 new: self.codec.decode_from(&new)?,
813 },
814 Applied::Kept => Applied::Kept,
815 Applied::Deleted(b) => Applied::Deleted(self.codec.decode_from(&b)?),
816 };
817 out.push((k, applied_t));
818 }
819 Ok(out)
820 }
821
822 pub fn migrate(&self, f: impl Fn(&K, &T) -> crate::MigrateAction<T>) -> DbResult<usize> {
849 self.migrate_inner(f, true)
850 }
851
852 pub(crate) fn migrate_inner(
853 &self,
854 f: impl Fn(&K, &T) -> crate::MigrateAction<T>,
855 fire_init: bool,
856 ) -> DbResult<usize> {
857 use crate::MigrateAction;
858 use std::cell::RefCell;
859
860 let codec_err: RefCell<Option<DbError>> = RefCell::new(None);
866 let count = self.inner.migrate_inner(
867 |key, bytes| {
868 if codec_err.borrow().is_some() {
869 return MigrateAction::Keep;
870 }
871 let current = match self.codec.decode_from(bytes) {
872 Ok(v) => v,
873 Err(e) => {
874 *codec_err.borrow_mut() = Some(e);
875 return MigrateAction::Keep;
876 }
877 };
878 match f(key, ¤t) {
879 MigrateAction::Keep => MigrateAction::Keep,
880 MigrateAction::Update(new) => {
881 let mut buf = Vec::new();
882 match self.codec.encode_to(&new, &mut buf) {
883 Ok(()) => MigrateAction::Update(ByteView::from_vec(buf)),
884 Err(e) => {
885 *codec_err.borrow_mut() = Some(e);
886 MigrateAction::Keep
887 }
888 }
889 }
890 MigrateAction::Delete => MigrateAction::Delete,
891 }
892 },
893 fire_init,
894 )?;
895 if let Some(e) = codec_err.into_inner() {
896 tracing::error!(error = %e, "var_typed_tree migrate: codec failure — aborting migration step");
897 return Err(e);
898 }
899 Ok(count)
900 }
901
902 pub(crate) fn replay_init(&self) {
904 self.inner.replay_init();
905 }
906
907 pub(crate) fn reconcile_projections(&self) -> DbResult<()> {
908 self.as_inner().reconcile_projections()
909 }
910
911 pub(crate) fn effective_projection_limits(&self) -> (usize, usize) {
912 self.as_inner().effective_projection_limits()
913 }
914}
915
916impl<K: Key, T: Send + Sync, C: Codec<T> + Clone, H: TypedWriteHook<K, T>> CompactionIndex<K>
917 for VarTypedTree<K, T, C, H>
918{
919 fn update_if_match(&self, key: &K, old_loc: DiskLoc, new_loc: DiskLoc) -> bool {
920 self.inner.update_if_match(key, old_loc, new_loc)
921 }
922
923 fn invalidate_blocks(&self, shard_id: u8, file_id: u32, total_bytes: u64) {
924 self.inner.invalidate_blocks(shard_id, file_id, total_bytes);
925 }
926
927 fn contains_key(&self, key: &K) -> bool {
928 self.inner.contains(key)
929 }
930
931 fn is_live(&self, shard_id: u8, key: &K, loc: DiskLoc) -> bool {
932 self.inner.is_live(shard_id, key, loc)
933 }
934}
935
936#[cfg(feature = "replication")]
937impl<K: Key, T: Send + Sync, C: Codec<T> + Clone, H: TypedWriteHook<K, T>>
938 crate::replication::ReplicationTarget for VarTypedTree<K, T, C, H>
939{
940 fn apply_entry(
941 &self,
942 shard_inner: &mut crate::shard::ShardInner,
943 shard_id: u8,
944 file_id: u32,
945 entry_offset: u64,
946 header: &crate::entry::EntryHeader,
947 key: &[u8],
948 value: &[u8],
949 ) -> DbResult<crate::replication::ApplyOutcome> {
950 self.inner.apply_entry(
951 shard_inner,
952 shard_id,
953 file_id,
954 entry_offset,
955 header,
956 key,
957 value,
958 )
959 }
960
961 fn try_apply_entry(
962 &self,
963 shard_inner: &mut crate::shard::ShardInner,
964 shard_id: u8,
965 file_id: u32,
966 entry_offset: u64,
967 header: &crate::entry::EntryHeader,
968 raw_after_header: &[u8],
969 ) -> DbResult<crate::replication::ApplyOutcome> {
970 self.inner.try_apply_entry(
971 shard_inner,
972 shard_id,
973 file_id,
974 entry_offset,
975 header,
976 raw_after_header,
977 )
978 }
979
980 fn key_len(&self) -> usize {
981 self.inner.key_len()
982 }
983
984 fn mark_replication_follower(&self) {
985 self.inner.mark_replication_follower();
986 }
987}
988
989pub struct VarTypedIter<'a, K: Key, T: Send + Sync, C: Codec<T> + Clone, H: TypedWriteHook<K, T>> {
997 inner: VarIter<'a, K, VarTypedHookAdapter<K, T, C, H>>,
998 codec: &'a C,
999 _marker: PhantomData<fn() -> T>,
1000}
1001
1002impl<K: Key, T: Send + Sync, C: Codec<T> + Clone, H: TypedWriteHook<K, T>> Iterator
1003 for VarTypedIter<'_, K, T, C, H>
1004{
1005 type Item = (K, T);
1006
1007 fn next(&mut self) -> Option<Self::Item> {
1008 loop {
1009 let (k, bytes) = self.inner.next()?;
1010 match self.codec.decode_from(&bytes) {
1011 Ok(v) => return Some((k, v)),
1012 Err(_) => {
1013 tracing::debug!(
1014 value_len = bytes.len(),
1015 "var_typed_iter: decode error, skipping entry"
1016 );
1017 }
1018 }
1019 }
1020 }
1021}
1022
1023impl<K: Key, T: Send + Sync, C: Codec<T> + Clone, H: TypedWriteHook<K, T>> DoubleEndedIterator
1024 for VarTypedIter<'_, K, T, C, H>
1025{
1026 fn next_back(&mut self) -> Option<Self::Item> {
1027 while let Some((k, bytes)) = self.inner.next_back() {
1028 match self.codec.decode_from(&bytes) {
1029 Ok(v) => return Some((k, v)),
1030 Err(_) => {
1031 tracing::debug!(
1032 value_len = bytes.len(),
1033 "var_typed_iter: decode error, skipping entry"
1034 );
1035 }
1036 }
1037 }
1038 None
1039 }
1040}
1041
1042pub(crate) struct VarTypedDirRangeIter<
1044 'a,
1045 K: Key,
1046 T: Send + Sync,
1047 C: Codec<T> + Clone,
1048 H: TypedWriteHook<K, T>,
1049> {
1050 inner: DirRangeIter<VarIter<'a, K, VarTypedHookAdapter<K, T, C, H>>>,
1051 codec: &'a C,
1052 _marker: PhantomData<fn() -> T>,
1053}
1054
1055impl<K: Key, T: Send + Sync, C: Codec<T> + Clone, H: TypedWriteHook<K, T>> Iterator
1056 for VarTypedDirRangeIter<'_, K, T, C, H>
1057{
1058 type Item = (K, T);
1059
1060 fn next(&mut self) -> Option<Self::Item> {
1061 loop {
1062 let (k, bytes) = self.inner.next()?;
1063 match self.codec.decode_from(&bytes) {
1064 Ok(v) => return Some((k, v)),
1065 Err(_) => {
1066 tracing::debug!(
1067 value_len = bytes.len(),
1068 "var_typed_iter: decode error, skipping entry"
1069 );
1070 }
1071 }
1072 }
1073 }
1074}
1075
1076#[doc(hidden)]
1079pub struct TryVarTypedIter<'a, K, T, C, H>
1080where
1081 K: Key,
1082 T: Send + Sync,
1083 C: Codec<T> + Clone,
1084 H: TypedWriteHook<K, T>,
1085{
1086 inner: crate::scope::DirRangeIter<TryVarIter<'a, K, VarTypedHookAdapter<K, T, C, H>>>,
1087 codec: &'a C,
1088 failed: bool,
1089}
1090
1091impl<K, T, C, H> Iterator for TryVarTypedIter<'_, K, T, C, H>
1092where
1093 K: Key,
1094 T: Send + Sync,
1095 C: Codec<T> + Clone,
1096 H: TypedWriteHook<K, T>,
1097{
1098 type Item = DbResult<(K, T)>;
1099
1100 fn next(&mut self) -> Option<Self::Item> {
1101 if self.failed {
1102 return None;
1103 }
1104 let row = self.inner.next()?;
1105 match row {
1106 Ok((key, bytes)) => match self.codec.decode_from(&bytes) {
1107 Ok(value) => Some(Ok((key, value))),
1108 Err(error) => {
1109 self.failed = true;
1110 Some(Err(error))
1111 }
1112 },
1113 Err(error) => {
1114 self.failed = true;
1115 Some(Err(error))
1116 }
1117 }
1118 }
1119}
1120
1121pub struct VarTypedShard<
1129 'tree,
1130 K: Key,
1131 T: Send + Sync,
1132 C: Codec<T> + Clone,
1133 H: TypedWriteHook<K, T>,
1134> {
1135 tree: &'tree VarTypedTree<K, T, C, H>,
1136 inner: *mut (),
1140 _marker: PhantomData<&'tree mut ()>,
1141}
1142
1143unsafe impl<K: Key, T: Send + Sync, C: Codec<T> + Clone, H: TypedWriteHook<K, T>> Send
1146 for VarTypedShard<'_, K, T, C, H>
1147{
1148}
1149
1150impl<K: Key, T: Send + Sync, C: Codec<T> + Clone, H: TypedWriteHook<K, T>>
1151 VarTypedShard<'_, K, T, C, H>
1152{
1153 fn inner_mut(&mut self) -> &mut VarShard<'_, K, VarTypedHookAdapter<K, T, C, H>> {
1154 unsafe { &mut *(self.inner as *mut VarShard<'_, K, VarTypedHookAdapter<K, T, C, H>>) }
1157 }
1158
1159 fn inner_ref(&self) -> &VarShard<'_, K, VarTypedHookAdapter<K, T, C, H>> {
1160 unsafe { &*(self.inner as *const VarShard<'_, K, VarTypedHookAdapter<K, T, C, H>>) }
1162 }
1163
1164 pub fn put(&mut self, key: &K, value: &T) -> DbResult<bool> {
1167 let mut buf = Vec::new();
1168 self.tree.codec.encode_to(value, &mut buf)?;
1169 self.inner_mut().put(key, &buf)
1170 }
1171
1172 pub fn insert(&mut self, key: &K, value: &T) -> DbResult<()> {
1173 let mut buf = Vec::new();
1174 self.tree.codec.encode_to(value, &mut buf)?;
1175 self.inner_mut().insert(key, &buf)
1176 }
1177
1178 pub fn delete(&mut self, key: &K) -> DbResult<bool> {
1179 self.inner_mut().delete(key)
1180 }
1181
1182 pub fn try_get(&self, key: &K) -> DbResult<Option<T>> {
1183 match self.inner_ref().try_get(key)? {
1184 Some(bytes) => self.tree.codec.decode_from(&bytes).map(Some),
1185 None => Ok(None),
1186 }
1187 }
1188
1189 pub fn get_or_err(&self, key: &K) -> DbResult<T> {
1190 let bytes = self.inner_ref().get_or_err(key)?;
1191 self.tree.codec.decode_from(&bytes)
1192 }
1193
1194 pub fn try_contains(&self, key: &K) -> DbResult<bool> {
1195 self.inner_ref().try_contains(key)
1196 }
1197
1198 pub fn update(&mut self, key: &K, f: impl FnOnce(&T) -> T) -> DbResult<Option<T>> {
1201 use std::cell::Cell;
1202 let tree = self.tree; let out: Cell<Option<T>> = Cell::new(None);
1204 let result = self.inner_mut().update_inner(
1205 key,
1206 |bytes| {
1207 let current = tree.codec.decode_from(bytes)?;
1208 let new_val = f(¤t);
1209 let mut buf = Vec::new();
1210 tree.codec.encode_to(&new_val, &mut buf)?;
1211 out.set(Some(new_val));
1212 Ok(Some(ByteView::from_vec(buf)))
1213 },
1214 false,
1215 )?;
1216 if result.is_none() {
1217 return Ok(None);
1218 }
1219 Ok(out.into_inner())
1220 }
1221
1222 pub fn fetch_update(&mut self, key: &K, f: impl FnOnce(&T) -> T) -> DbResult<Option<T>> {
1225 use std::cell::Cell;
1226 let tree = self.tree; let out: Cell<Option<T>> = Cell::new(None);
1228 let result = self.inner_mut().update_inner(
1229 key,
1230 |bytes| {
1231 let current = tree.codec.decode_from(bytes)?;
1232 let new_val = f(¤t);
1233 let mut buf = Vec::new();
1234 tree.codec.encode_to(&new_val, &mut buf)?;
1235 out.set(Some(current)); Ok(Some(ByteView::from_vec(buf)))
1237 },
1238 true,
1239 )?;
1240 if result.is_none() {
1241 return Ok(None);
1242 }
1243 Ok(out.into_inner())
1244 }
1245
1246 pub fn upsert<U: crate::IntoUpsert<T>>(
1248 &mut self,
1249 key: &K,
1250 f: impl FnOnce(Option<&T>) -> U,
1251 ) -> crate::DbResult<U::Out> {
1252 crate::upsert::upsert_via_held(self, key, f)
1253 }
1254}
1255
1256#[cfg(feature = "armour")]
1257impl<T, C, H> crate::armour::collection::Collection for VarTypedTree<T::SelfId, T, C, H>
1258where
1259 T: crate::CollectionMeta + Send + Sync,
1260 C: Codec<T> + Clone + 'static,
1261 H: TypedWriteHook<T::SelfId, T>,
1262 T::SelfId: crate::Key + Ord,
1263{
1264 fn name(&self) -> &str {
1265 T::NAME
1266 }
1267 fn len(&self) -> usize {
1268 self.len()
1269 }
1270 fn compact(&self) -> DbResult<usize> {
1271 self.compact()
1272 }
1273 fn flush(&self) -> DbResult<()> {
1274 crate::projection::collection_flush_fault()?;
1275 self.flush_buffers()?;
1276 self.sync_hints()?;
1277 Ok(())
1278 }
1279 fn periodic_flush(&self) -> DbResult<()> {
1280 self.flush_buffers()
1281 }
1282 fn durable_flush(&self) -> DbResult<()> {
1283 VarTypedTree::durable_flush(self)
1284 }
1285 fn clean_shutdown(&self) -> DbResult<()> {
1286 VarTypedTree::clean_shutdown(self)
1287 }
1288 fn sweep_expired(&self, now_ms: u64, budget: usize) -> DbResult<crate::ttl::SweepOutcome> {
1289 self.sweep_expired(now_ms, budget)
1290 }
1291 fn set_ttl_sweep(&self, enabled: bool) -> DbResult<()> {
1292 self.set_ttl_sweep(enabled)
1293 }
1294 fn rebuild_ttl_index(&self) -> DbResult<()> {
1295 self.rebuild_ttl_index()
1296 }
1297 fn reset_counter_dirty_marker(&self) -> bool {
1298 self.as_inner()
1299 .counter_dirty
1300 .get()
1301 .is_some_and(|d| d.take_marked())
1302 }
1303 fn reconcile_counters(&self) -> DbResult<u64> {
1304 self.as_inner().reconcile_counters()
1305 }
1306 fn counter_target_names(&self) -> Vec<&'static str> {
1307 self.as_inner().counter_target_names()
1308 }
1309 fn close_watches(&self) {
1310 if let Some(channel) = self.inner.hook.watch_channel() {
1311 channel.close_terminal();
1312 }
1313 }
1314}
1315
1316#[cfg(feature = "typed-tree")]
1317impl<
1318 K: Key + Send + Sync + std::hash::Hash + Eq + 'static,
1319 T: armour_core::GetType + crate::CollectionMeta + Clone + PartialEq + Send + Sync + 'static,
1320 C: Codec<T> + Clone + Sync + 'static,
1321 H: TypedWriteHook<K, T> + 'static,
1322> crate::projection::sealed::Sealed for VarTypedTree<K, T, C, H>
1323{
1324}
1325
1326impl<
1327 K: Key + Send + Sync + std::hash::Hash + Eq + 'static,
1328 T: armour_core::GetType + crate::CollectionMeta + Clone + PartialEq + Send + Sync + 'static,
1329 C: Codec<T> + Clone + Sync + 'static,
1330 H: TypedWriteHook<K, T> + 'static,
1331> crate::projection::ProjectableIndex for VarTypedTree<K, T, C, H>
1332{
1333 fn collection_name(&self) -> &'static str {
1334 T::NAME
1335 }
1336
1337 fn put_bytes(&self, key: &[u8], value: &[u8]) -> DbResult<()> {
1338 if key.len() != std::mem::size_of::<K>() {
1339 return Err(DbError::CorruptedEntry { offset: 0 });
1340 }
1341 let k = K::from_bytes(key);
1342 let v = self.codec.decode_from(value)?;
1343 self.put(&k, &v)?;
1344 Ok(())
1345 }
1346
1347 fn delete_bytes(&self, key: &[u8]) -> DbResult<()> {
1348 if key.len() != std::mem::size_of::<K>() {
1349 return Err(DbError::CorruptedEntry { offset: 0 });
1350 }
1351 let k = K::from_bytes(key);
1352 self.delete(&k)?;
1353 Ok(())
1354 }
1355
1356 fn values_equal(&self, a: &[u8], b: &[u8]) -> DbResult<bool> {
1357 if a == b {
1358 return Ok(true);
1359 }
1360 let va = self.codec.decode_from(a)?;
1361 let vb = self.codec.decode_from(b)?;
1362 Ok(va == vb)
1363 }
1364
1365 fn for_each_bytes(&self, f: &mut dyn FnMut(&[u8], &[u8]) -> DbResult<()>) -> DbResult<()> {
1366 let mut err: Option<DbError> = None;
1367 for item in self.iter() {
1368 if err.is_some() {
1369 break;
1370 }
1371 let (k, v) = item;
1372 let mut buf = Vec::new();
1373 match self
1374 .codec
1375 .encode_to(&v, &mut buf)
1376 .and_then(|()| f(k.as_bytes(), &buf))
1377 {
1378 Ok(()) => {}
1379 Err(e) => err = Some(e),
1380 }
1381 }
1382 match err {
1383 Some(e) => Err(e),
1384 None => Ok(()),
1385 }
1386 }
1387
1388 fn durable_flush(&self) -> DbResult<()> {
1389 VarTypedTree::durable_flush(self)
1390 }
1391}
1392
1393#[cfg(feature = "typed-tree")]
1394impl<
1395 K: Key + Send + Sync + std::hash::Hash + Eq + 'static,
1396 T: armour_core::GetType + crate::CollectionMeta + Clone + PartialEq + Send + Sync + 'static,
1397 C: Codec<T> + Clone + Sync + 'static,
1398 H: TypedWriteHook<K, T> + 'static,
1399> crate::projection::ProjectableIndexTx for VarTypedTree<K, T, C, H>
1400{
1401 fn as_tx_participant(self: Arc<Self>) -> Arc<dyn crate::txn::participant::TxParticipant> {
1402 self
1403 }
1404}
1405
1406#[cfg(feature = "typed-tree")]
1407impl<
1408 K: Key + Send + Sync + std::hash::Hash + Eq + 'static,
1409 T: armour_core::GetType + crate::CollectionMeta + Clone + PartialEq + Send + Sync + 'static,
1410 C: Codec<T> + Clone + Sync + 'static,
1411 H: TypedWriteHook<K, T> + 'static,
1412> crate::projection::IndexKeyed for VarTypedTree<K, T, C, H>
1413{
1414 type K = K;
1415 type V = T;
1416
1417 fn encode_value(&self, v: &T) -> DbResult<Vec<u8>> {
1418 let mut buf = Vec::new();
1419 self.codec.encode_to(v, &mut buf)?;
1420 Ok(buf)
1421 }
1422}
1423
1424#[cfg(feature = "typed-tree")]
1425impl<
1426 K: Key + Send + Sync + std::hash::Hash + Eq + 'static,
1427 T: armour_core::GetType + crate::CollectionMeta + Clone + PartialEq + Send + Sync + 'static,
1428 C: Codec<T> + Clone + Sync + 'static,
1429 H: TypedWriteHook<K, T> + 'static,
1430> crate::counter::CounterCollection for VarTypedTree<K, T, C, H>
1431{
1432 fn decode_value(&self, bytes: &[u8]) -> DbResult<T> {
1433 self.codec.decode_from(bytes)
1434 }
1435
1436 fn read_bytes(&self, key: &[u8]) -> DbResult<Option<Vec<u8>>> {
1440 if key.len() != std::mem::size_of::<K>() {
1441 return Err(DbError::CorruptedEntry { offset: 0 });
1442 }
1443 let k = K::from_bytes(key);
1444 Ok(self.inner.try_get(&k)?.map(|bv| bv.as_bytes().to_vec()))
1445 }
1446
1447 fn contains_bytes(&self, key: &[u8]) -> DbResult<bool> {
1448 if key.len() != std::mem::size_of::<K>() {
1449 return Err(DbError::CorruptedEntry { offset: 0 });
1450 }
1451 let k = K::from_bytes(key);
1452 Ok(self.contains(&k))
1453 }
1454
1455 fn upsert_counter(
1456 &self,
1457 key: &[u8],
1458 f: &mut dyn FnMut(Option<&T>) -> Upsert<T>,
1459 ) -> DbResult<()> {
1460 if key.len() != std::mem::size_of::<K>() {
1461 return Err(DbError::CorruptedEntry { offset: 0 });
1462 }
1463 let k = K::from_bytes(key);
1464 self.upsert(&k, f)?;
1465 Ok(())
1466 }
1467}
1468
1469#[cfg(test)]
1470#[cfg(feature = "rapira-codec")]
1471mod tests {
1472 use super::*;
1473 use crate::codec::{Codec, RapiraCodec};
1474 use crate::config::{CacheConfig, Config};
1475 use crate::hook::TypedWriteHook;
1476 use rapira::Rapira;
1477 use std::sync::Arc;
1478 use std::sync::atomic::{AtomicUsize, Ordering as AtomicOrdering};
1479 use tempfile::tempdir;
1480
1481 #[derive(Debug, Clone, PartialEq, Rapira)]
1482 struct Msg {
1483 id: u64,
1484 body: String,
1485 tags: Vec<String>,
1486 }
1487
1488 type MsgTree = VarTypedTree<[u8; 16], Msg, RapiraCodec>;
1489
1490 fn open(dir: &std::path::Path) -> MsgTree {
1491 VarTypedTree::open(dir, Config::test(), RapiraCodec).unwrap()
1492 }
1493
1494 fn msg(id: u64) -> Msg {
1495 Msg {
1496 id,
1497 body: format!("hello from message #{id}"),
1498 tags: vec![format!("t{id}"), "bench".into()],
1499 }
1500 }
1501
1502 #[derive(Clone)]
1505 struct CountingCodec {
1506 decodes: Arc<AtomicUsize>,
1507 }
1508
1509 impl Codec<Msg> for CountingCodec {
1510 fn encode_to(&self, value: &Msg, buf: &mut Vec<u8>) -> DbResult<()> {
1511 RapiraCodec.encode_to(value, buf)
1512 }
1513 fn decode_from(&self, bytes: &[u8]) -> DbResult<Msg> {
1514 self.decodes.fetch_add(1, AtomicOrdering::Relaxed);
1515 RapiraCodec.decode_from(bytes)
1516 }
1517 }
1518
1519 fn open_counting(
1520 dir: &std::path::Path,
1521 decoded_cache_bytes: u64,
1522 ) -> (VarTypedTree<[u8; 16], Msg, CountingCodec>, Arc<AtomicUsize>) {
1523 let decodes = Arc::new(AtomicUsize::new(0));
1524 let mut cfg = Config::test();
1525 cfg.decoded_cache = CacheConfig {
1526 max_size: decoded_cache_bytes,
1527 estimated_items: 64,
1528 };
1529 let tree = VarTypedTree::open(
1530 dir,
1531 cfg,
1532 CountingCodec {
1533 decodes: Arc::clone(&decodes),
1534 },
1535 )
1536 .unwrap();
1537 (tree, decodes)
1538 }
1539
1540 #[test]
1541 fn get_shared_hit_does_not_decode() {
1542 let dir = tempdir().unwrap();
1543 let (tree, decodes) = open_counting(dir.path(), 1 << 20);
1544 let k = [1u8; 16];
1545 tree.put(&k, &msg(1)).unwrap();
1546 let after_write = decodes.load(AtomicOrdering::Relaxed);
1547
1548 let a = tree.get_shared(&k).expect("present");
1549 let b = tree.get_shared(&k).expect("present");
1550 assert_eq!(*a, msg(1));
1551 assert_eq!(*b, msg(1));
1552 assert_eq!(
1553 decodes.load(AtomicOrdering::Relaxed) - after_write,
1554 1,
1555 "the second get_shared must be served from the decoded cache"
1556 );
1557 assert!(Arc::ptr_eq(&a, &b)); }
1559
1560 #[test]
1561 fn get_shared_works_with_the_cache_disabled() {
1562 let dir = tempdir().unwrap();
1563 let (tree, decodes) = open_counting(dir.path(), 0);
1564 let k = [2u8; 16];
1565 tree.put(&k, &msg(2)).unwrap();
1566 let after_write = decodes.load(AtomicOrdering::Relaxed);
1567
1568 for _ in 0..3 {
1569 assert_eq!(*tree.get_shared(&k).expect("present"), msg(2));
1570 }
1571 assert_eq!(
1572 decodes.load(AtomicOrdering::Relaxed) - after_write,
1573 3,
1574 "a disabled cache must still serve correct values, decoding each time"
1575 );
1576 }
1577
1578 #[test]
1579 fn shared_reads_absent_key() {
1580 let dir = tempdir().unwrap();
1581 let (tree, _) = open_counting(dir.path(), 1 << 20);
1582 let k = [3u8; 16];
1583 assert!(tree.get_shared(&k).is_none());
1584 assert!(
1585 tree.try_get_shared(&k)
1586 .expect("absent is Ok(None)")
1587 .is_none()
1588 );
1589 }
1590
1591 #[test]
1592 fn shared_reads_undecodable_value() {
1593 let dir = tempdir().unwrap();
1594 let (tree, _) = open_counting(dir.path(), 1 << 20);
1595 let k = [4u8; 16];
1596 tree.as_inner().put(&k, &[0xffu8; 8]).unwrap();
1598
1599 assert!(tree.get_shared(&k).is_none(), "lossy: undecodable -> None");
1600 assert!(
1601 tree.try_get_shared(&k).is_err(),
1602 "strict: undecodable -> Err"
1603 );
1604 }
1605
1606 #[test]
1607 fn get_many_shared_is_aligned_and_lossy() {
1608 let dir = tempdir().unwrap();
1609 let (tree, decodes) = open_counting(dir.path(), 1 << 20);
1610 let keys = [[10u8; 16], [11u8; 16], [12u8; 16]];
1611 tree.put(&keys[0], &msg(10)).unwrap();
1612 tree.put(&keys[2], &msg(12)).unwrap();
1613 let after_writes = decodes.load(AtomicOrdering::Relaxed);
1614
1615 let out = tree.get_many_shared(&keys);
1616 assert_eq!(out.len(), 3);
1617 assert_eq!(out[0].as_deref(), Some(&msg(10)));
1618 assert!(out[1].is_none(), "absent key is None at its own index");
1619 assert_eq!(out[2].as_deref(), Some(&msg(12)));
1620 assert_eq!(decodes.load(AtomicOrdering::Relaxed) - after_writes, 2);
1621
1622 let again = tree.get_many_shared(&keys);
1623 assert_eq!(again[0].as_deref(), Some(&msg(10)));
1624 assert_eq!(
1625 decodes.load(AtomicOrdering::Relaxed) - after_writes,
1626 2,
1627 "a cached batch must not decode again"
1628 );
1629 }
1630
1631 #[test]
1632 fn shared_reads_do_not_require_clone_on_t() {
1633 struct NotClone {
1636 body: Vec<u8>,
1637 }
1638 #[derive(Clone)]
1639 struct RawCodec;
1640 impl Codec<NotClone> for RawCodec {
1641 fn encode_to(&self, value: &NotClone, buf: &mut Vec<u8>) -> DbResult<()> {
1642 buf.clear();
1643 buf.extend_from_slice(&value.body);
1644 Ok(())
1645 }
1646 fn decode_from(&self, bytes: &[u8]) -> DbResult<NotClone> {
1647 Ok(NotClone {
1648 body: bytes.to_vec(),
1649 })
1650 }
1651 }
1652
1653 let dir = tempdir().unwrap();
1654 let mut cfg = Config::test();
1655 cfg.decoded_cache = CacheConfig {
1656 max_size: 1 << 20,
1657 estimated_items: 8,
1658 };
1659 let tree: VarTypedTree<[u8; 16], NotClone, RawCodec> =
1660 VarTypedTree::open(dir.path(), cfg, RawCodec).unwrap();
1661
1662 let k = [5u8; 16];
1663 tree.put(
1664 &k,
1665 &NotClone {
1666 body: vec![1, 2, 3],
1667 },
1668 )
1669 .unwrap();
1670 assert_eq!(tree.get_shared(&k).expect("present").body, vec![1, 2, 3]);
1671 assert_eq!(
1672 tree.try_get_shared(&k).expect("ok").expect("present").body,
1673 vec![1, 2, 3]
1674 );
1675 assert_eq!(
1676 tree.get_many_shared(&[k])[0].as_ref().unwrap().body,
1677 vec![1, 2, 3]
1678 );
1679 }
1680
1681 #[test]
1682 fn get_shared_absent_after_delete_redecodes_on_reinsert() {
1683 let dir = tempdir().unwrap();
1684 let (tree, decodes) = open_counting(dir.path(), 1 << 20);
1685 let k = [7u8; 16];
1686 tree.put(&k, &msg(7)).unwrap();
1687
1688 assert_eq!(*tree.get_shared(&k).expect("present"), msg(7));
1689 assert_eq!(decodes.load(AtomicOrdering::Relaxed), 1);
1690
1691 assert!(tree.delete(&k).unwrap());
1692 assert!(tree.get_shared(&k).is_none());
1693 assert!(
1694 tree.try_get_shared(&k)
1695 .expect("absent is Ok(None)")
1696 .is_none()
1697 );
1698
1699 tree.put(&k, &msg(77)).unwrap();
1700 let before = decodes.load(AtomicOrdering::Relaxed);
1701 assert_eq!(*tree.get_shared(&k).expect("reinserted"), msg(77));
1702 assert_eq!(
1703 decodes.load(AtomicOrdering::Relaxed) - before,
1704 1,
1705 "re-insert must decode again"
1706 );
1707 }
1708
1709 #[test]
1710 fn get_shared_evicts_under_pressure_and_redecodes() {
1711 fn big_msg(id: u64, body_len: usize) -> Msg {
1712 Msg {
1713 id,
1714 body: "x".repeat(body_len),
1715 tags: vec![],
1716 }
1717 }
1718
1719 let dir_probe = tempdir().unwrap();
1721 let (probe, probe_decodes) = open_counting(dir_probe.path(), 1 << 20);
1722 let body_len = 512;
1723 let k_probe = [99u8; 16];
1724 let sample = big_msg(99, body_len);
1725 probe.put(&k_probe, &sample).unwrap();
1726 let weight = u64::from(probe.entry_len(&k_probe).expect("on-disk len"));
1727 assert!(weight > 0, "entry must have non-zero on-disk weight");
1728
1729 probe.get_shared(&k_probe).expect("present");
1736 probe.get_shared(&k_probe).expect("present");
1737 assert_eq!(
1738 probe_decodes.load(AtomicOrdering::Relaxed),
1739 1,
1740 "control: a resident entry must be served without decoding"
1741 );
1742
1743 let dir = tempdir().unwrap();
1744 let (tree, decodes) = open_counting(dir.path(), weight);
1746 let k0 = [0u8; 16];
1747 let k1 = [1u8; 16];
1748 tree.put(&k0, &big_msg(0, body_len)).unwrap();
1749 tree.put(&k1, &big_msg(1, body_len)).unwrap();
1750 assert_eq!(
1751 tree.entry_len(&k0),
1752 tree.entry_len(&k1),
1753 "same-shaped payloads must weigh the same in the decoded cache"
1754 );
1755
1756 tree.get_shared(&k0).expect("present");
1757 assert_eq!(decodes.load(AtomicOrdering::Relaxed), 1);
1758
1759 let before_k1 = decodes.load(AtomicOrdering::Relaxed);
1760 tree.get_shared(&k1).expect("present");
1761 assert_eq!(decodes.load(AtomicOrdering::Relaxed) - before_k1, 1);
1762
1763 let before_reread = decodes.load(AtomicOrdering::Relaxed);
1764 tree.get_shared(&k0)
1765 .expect("evicted entry must still read correctly");
1766 assert_eq!(
1767 decodes.load(AtomicOrdering::Relaxed) - before_reread,
1768 1,
1769 "an evicted entry must re-decode (weight={weight}, budget={weight})"
1770 );
1771 }
1772
1773 #[test]
1774 fn concurrent_get_shared_on_a_cold_key_is_correct() {
1775 let dir = tempdir().unwrap();
1776 let (tree, _) = open_counting(dir.path(), 1 << 20);
1777 let k = [6u8; 16];
1778 tree.put(&k, &msg(6)).unwrap();
1779 let tree = Arc::new(tree);
1780
1781 std::thread::scope(|s| {
1784 for _ in 0..4 {
1785 let tree = Arc::clone(&tree);
1786 s.spawn(move || {
1787 for _ in 0..32 {
1788 assert_eq!(*tree.get_shared(&k).expect("present"), msg(6));
1789 }
1790 });
1791 }
1792 });
1793 }
1794
1795 #[test]
1796 fn compare_delete_match_mismatch_absent() {
1797 let dir = tempdir().unwrap();
1798 let tree = open(dir.path());
1799
1800 let k = [1u8; 16];
1801 let v = msg(1);
1802 let other = msg(99);
1803 tree.put(&k, &v).unwrap();
1804
1805 assert!(matches!(
1806 tree.compare_delete(&k, &other),
1807 Err(DbError::CasMismatch)
1808 ));
1809 assert_eq!(tree.get(&k), Some(v.clone()));
1810
1811 assert!(tree.compare_delete(&k, &v).is_ok());
1812 assert!(tree.get(&k).is_none());
1813
1814 assert!(matches!(
1815 tree.compare_delete(&k, &v),
1816 Err(DbError::KeyNotFound)
1817 ));
1818 }
1819
1820 #[test]
1821 fn entry_len_returns_some_for_existing_key() {
1822 let tmp = tempdir().expect("tmp");
1823 let tree: VarTypedTree<[u8; 8], Vec<u8>, RapiraCodec> =
1824 VarTypedTree::open(tmp.path(), Config::test(), RapiraCodec).expect("open");
1825 let key = 1u64.to_be_bytes();
1826 tree.put(&key, &vec![10u8, 20, 30, 40]).expect("put");
1827 assert_eq!(tree.entry_len(&key), Some(8u32));
1830 }
1831
1832 #[test]
1833 fn entry_len_returns_none_for_missing_key() {
1834 let tmp = tempdir().expect("tmp");
1835 let tree: VarTypedTree<[u8; 8], Vec<u8>, RapiraCodec> =
1836 VarTypedTree::open(tmp.path(), Config::test(), RapiraCodec).expect("open");
1837 let key = 99u64.to_be_bytes();
1838 assert_eq!(tree.entry_len(&key), None);
1839 }
1840
1841 #[test]
1842 fn try_range_bounds_surfaces_decode_error_without_skipping() {
1843 use crate::error::{DbError, DbResult};
1844 use crate::test_faults::{POISON, PoisonCodec};
1845 use std::ops::Bound;
1846
1847 let dir = tempdir().unwrap();
1848 let tree: VarTypedTree<[u8; 8], u64, PoisonCodec> =
1849 VarTypedTree::open(dir.path(), Config::test(), PoisonCodec).unwrap();
1850 tree.put(&1_u64.to_be_bytes(), &1).unwrap();
1851 tree.put(&2_u64.to_be_bytes(), &POISON).unwrap();
1852 tree.put(&3_u64.to_be_bytes(), &3).unwrap();
1853
1854 let rows = tree
1855 .try_range_bounds(Bound::Unbounded, Bound::Unbounded)
1856 .collect::<DbResult<Vec<_>>>();
1857
1858 assert!(matches!(rows, Err(DbError::CorruptedEntry { .. })));
1859 assert_eq!(
1860 tree.iter().count(),
1861 2,
1862 "the existing iterator remains lossy"
1863 );
1864 }
1865
1866 #[test]
1867 fn try_range_bounds_reversed_direction_fails_at_poison_from_opposite_edge() {
1868 use crate::error::DbError;
1869 use crate::test_faults::{POISON, PoisonCodec};
1870 use std::ops::Bound;
1871
1872 let dir = tempdir().unwrap();
1873 let mut cfg = Config::test();
1874 cfg.reversed = false;
1875 let tree: VarTypedTree<[u8; 8], u64, PoisonCodec> =
1876 VarTypedTree::open(dir.path(), cfg, PoisonCodec).unwrap();
1877 tree.put(&1_u64.to_be_bytes(), &1).unwrap();
1878 tree.put(&2_u64.to_be_bytes(), &POISON).unwrap();
1879 tree.put(&3_u64.to_be_bytes(), &3).unwrap();
1880
1881 let k1 = 1_u64.to_be_bytes();
1882 let mut iter = tree.try_range_bounds(Bound::Unbounded, Bound::Unbounded);
1883 match iter.next() {
1884 Some(Ok((k, v))) => {
1885 assert_eq!(k, k1);
1886 assert_eq!(v, 1);
1887 }
1888 other => panic!("expected Ok((k1, 1)), got {other:?}"),
1889 }
1890 assert!(matches!(
1891 iter.next(),
1892 Some(Err(DbError::CorruptedEntry { .. }))
1893 ));
1894 assert!(iter.next().is_none());
1895 }
1896
1897 #[test]
1898 fn typed_decode_fault_surfaced_on_result_methods_raw_bytes_on_cas() {
1899 use crate::test_faults::{POISON, PoisonCodec};
1900 let dir = tempdir().unwrap();
1901 let tree: VarTypedTree<[u8; 8], u64, PoisonCodec> =
1902 VarTypedTree::open(dir.path(), Config::test(), PoisonCodec).unwrap();
1903
1904 let k1 = 1u64.to_be_bytes();
1907 let k2 = 2u64.to_be_bytes();
1908 tree.put(&k1, &7u64).unwrap();
1909 tree.put(&k2, &POISON).unwrap();
1910
1911 assert!(matches!(
1913 tree.get_or_err(&k2),
1914 Err(DbError::CorruptedEntry { .. })
1915 ));
1916 assert!(matches!(
1917 tree.try_get(&k2),
1918 Err(DbError::CorruptedEntry { .. })
1919 ));
1920 assert!(matches!(
1921 tree.update(&k2, |v| v + 1),
1922 Err(DbError::CorruptedEntry { .. })
1923 ));
1924 assert!(matches!(
1925 tree.fetch_update(&k2, |v| v + 1),
1926 Err(DbError::CorruptedEntry { .. })
1927 ));
1928
1929 assert_eq!(tree.first(), Some((k1, 7u64)));
1932 assert_eq!(tree.first(), tree.iter().next());
1933 assert!(matches!(
1935 tree.try_first(),
1936 Err(DbError::CorruptedEntry { .. })
1937 ));
1938
1939 assert!(matches!(
1942 tree.cas(&k2, &123u64, &5u64),
1943 Err(DbError::CasMismatch)
1944 ));
1945 assert!(tree.cas(&k2, &POISON, &5u64).is_ok());
1946 }
1947
1948 #[test]
1949 fn typed_shard_get_or_err_surfaces_decode_fault() {
1950 use crate::test_faults::{POISON, PoisonCodec};
1951 let dir = tempdir().unwrap();
1952 let tree: VarTypedTree<[u8; 8], u64, PoisonCodec> =
1953 VarTypedTree::open(dir.path(), Config::test(), PoisonCodec).unwrap();
1954
1955 let k = 1u64.to_be_bytes();
1956 tree.put(&k, &POISON).unwrap();
1957
1958 let got = tree.atomic(&k, |shard| shard.get_or_err(&k));
1959 assert!(matches!(got, Err(DbError::CorruptedEntry { .. })));
1960
1961 let absent = 999u64.to_be_bytes();
1962 let got_absent = tree.atomic(&absent, |shard| shard.get_or_err(&absent));
1963 assert!(matches!(got_absent, Err(DbError::KeyNotFound)));
1964 }
1965
1966 #[test]
1967 fn var_typed_try_get_surfaces_undecodable_rapira_as_err() {
1968 let dir = tempdir().unwrap();
1973 let tree = open(dir.path());
1974 let key = [0u8; 16];
1975
1976 let bad: &[u8] = &[
1979 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0xFF, 0xFE, 0, 0, 0, 0, ];
1984 tree.as_inner().put(&key, bad).unwrap();
1986
1987 let res = tree.try_get(&key);
1988 assert!(
1989 matches!(res, Err(DbError::CorruptedEntry { .. })),
1990 "undecodable stored value must surface as Err, got {res:?}"
1991 );
1992 }
1993
1994 #[test]
1995 fn var_typed_tree_migrate_errors_on_undecodable_row() {
1996 use crate::MigrateAction;
1997 let dir = tempdir().unwrap();
1998 let tree = open(dir.path());
1999 tree.put(&[1u8; 16], &msg(1)).unwrap();
2000 let bad: &[u8] = &[
2003 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0xFF, 0xFE, 0, 0, 0, 0, ];
2008 tree.as_inner().put(&[2u8; 16], bad).unwrap();
2009
2010 let err = tree
2014 .migrate(|_k, _v| MigrateAction::Update(msg(9)))
2015 .unwrap_err();
2016 assert!(matches!(err, DbError::CorruptedEntry { .. }), "{err:?}");
2017 }
2018
2019 #[test]
2020 fn var_typed_tree_migrate_errors_on_unencodable_value() {
2021 use crate::MigrateAction;
2022 let dir = tempdir().unwrap();
2023 let tree: VarTypedTree<[u8; 8], u64, EncodeFailCodec> =
2024 VarTypedTree::open(dir.path(), Config::test(), EncodeFailCodec).unwrap();
2025 tree.put(&1u64.to_be_bytes(), &7u64).unwrap();
2026
2027 let err = tree
2030 .migrate(|_k, _v| MigrateAction::Update(ENCODE_FAIL))
2031 .unwrap_err();
2032 assert!(matches!(err, DbError::CorruptedEntry { .. }), "{err:?}");
2033 }
2034
2035 const ENCODE_FAIL: u64 = 0xBAD0_BAD0_BAD0_BAD0;
2037
2038 #[derive(Clone)]
2041 struct EncodeFailCodec;
2042
2043 impl Codec<u64> for EncodeFailCodec {
2044 fn encode_to(&self, value: &u64, buf: &mut Vec<u8>) -> DbResult<()> {
2045 if *value == ENCODE_FAIL {
2046 return Err(DbError::CorruptedEntry { offset: 0 });
2047 }
2048 buf.clear();
2049 buf.extend_from_slice(&value.to_be_bytes());
2050 Ok(())
2051 }
2052
2053 fn decode_from(&self, bytes: &[u8]) -> DbResult<u64> {
2054 let arr: [u8; 8] = bytes
2055 .try_into()
2056 .map_err(|_| DbError::CorruptedEntry { offset: 0 })?;
2057 Ok(u64::from_be_bytes(arr))
2058 }
2059 }
2060
2061 #[derive(Clone)]
2062 struct U64Codec;
2063
2064 impl Codec<u64> for U64Codec {
2065 fn encode_to(&self, value: &u64, buf: &mut Vec<u8>) -> DbResult<()> {
2066 buf.clear();
2067 buf.extend_from_slice(&value.to_be_bytes());
2068 Ok(())
2069 }
2070
2071 fn decode_from(&self, bytes: &[u8]) -> DbResult<u64> {
2072 let arr: [u8; 8] = bytes
2073 .try_into()
2074 .map_err(|_| DbError::CorruptedEntry { offset: 0 })?;
2075 Ok(u64::from_be_bytes(arr))
2076 }
2077 }
2078
2079 #[derive(Default)]
2080 struct TRecHookState {
2081 writes: AtomicUsize,
2082 last_new: crate::sync::Mutex<Option<u64>>,
2083 }
2084
2085 #[derive(Clone, Default)]
2086 struct TRecHook {
2087 state: Arc<TRecHookState>,
2088 }
2089
2090 impl TypedWriteHook<[u8; 8], u64> for TRecHook {
2091 fn on_write(&self, _key: &[u8; 8], _old: Option<&u64>, new: Option<&u64>) {
2092 self.state.writes.fetch_add(1, AtomicOrdering::Relaxed);
2093 *crate::sync::lock(&self.state.last_new) = new.copied();
2094 }
2095 }
2096
2097 fn open_var_typed_hooked(
2098 dir: &std::path::Path,
2099 hook: TRecHook,
2100 ) -> VarTypedTree<[u8; 8], u64, U64Codec, TRecHook> {
2101 let mut cfg = Config::test();
2102 cfg.shard_count = 1;
2103 VarTypedTree::open_hooked(dir, cfg, U64Codec, hook).expect("open hooked")
2104 }
2105
2106 #[test]
2107 fn var_typed_tree_atomic_fires_typed_hook() {
2108 let dir = tempdir().unwrap();
2109 let hook = TRecHook::default();
2110 let state = hook.state.clone();
2111 let tree = open_var_typed_hooked(dir.path(), hook);
2112 let k = 7u64.to_be_bytes();
2113 tree.atomic(&k, |s| {
2114 s.put(&k, &42)?;
2115 Ok(())
2116 })
2117 .expect("atomic");
2118 assert_eq!(state.writes.load(AtomicOrdering::Relaxed), 1);
2119 assert_eq!(*crate::sync::lock(&state.last_new), Some(42));
2120 }
2121
2122 #[test]
2127 fn var_typed_get_many_and_update_many() {
2128 use crate::{Applied, Upsert};
2129 let dir = tempdir().unwrap();
2130 let cfg = Config::test();
2131 let tree = VarTypedTree::<[u8; 8], String, RapiraCodec>::open(dir.path(), cfg, RapiraCodec)
2132 .unwrap();
2133 tree.insert(&1u64.to_be_bytes(), &"a".to_string()).unwrap();
2134 tree.insert(&2u64.to_be_bytes(), &"b".to_string()).unwrap();
2135
2136 let keys: Vec<[u8; 8]> = vec![2u64.to_be_bytes(), 9u64.to_be_bytes(), 1u64.to_be_bytes()];
2137 assert_eq!(
2138 tree.get_many(&keys),
2139 vec![Some("b".to_string()), None, Some("a".to_string())]
2140 );
2141
2142 let items: Vec<([u8; 8], &str)> =
2143 vec![(1u64.to_be_bytes(), "a2"), (3u64.to_be_bytes(), "c")];
2144 let out = tree
2145 .update_many(items, |_k, _cur, p| Upsert::Put(p.to_string()))
2146 .unwrap();
2147 assert_eq!(
2148 out[0].1,
2149 Applied::Written {
2150 old: Some("a".to_string()),
2151 new: "a2".to_string()
2152 }
2153 );
2154 assert_eq!(
2155 out[1].1,
2156 Applied::Written {
2157 old: None,
2158 new: "c".to_string()
2159 }
2160 );
2161 assert_eq!(tree.get(&1u64.to_be_bytes()), Some("a2".to_string()));
2162 assert_eq!(tree.get(&3u64.to_be_bytes()), Some("c".to_string()));
2163 }
2164
2165 #[test]
2166 fn var_typed_update_many_strict_on_codec_fault() {
2167 use crate::test_faults::{POISON, PoisonCodec};
2168 let dir = tempdir().unwrap();
2169 let tree: VarTypedTree<[u8; 8], u64, PoisonCodec> =
2170 VarTypedTree::open(dir.path(), Config::test(), PoisonCodec).unwrap();
2171 let k = 1u64.to_be_bytes();
2172 tree.put(&k, &POISON).unwrap();
2173
2174 let res = tree.update_many(vec![(k, 5u64)], |_k, _old, v| Upsert::Put(*v));
2177 assert!(matches!(res, Err(DbError::CorruptedEntry { .. })));
2178
2179 assert!(matches!(
2182 tree.try_get(&k),
2183 Err(DbError::CorruptedEntry { .. })
2184 ));
2185 }
2186
2187 #[test]
2188 fn var_typed_tree_shard_put_returns_existed() {
2189 let dir = tempdir().unwrap();
2190 let tree: VarTypedTree<[u8; 8], u64, U64Codec> =
2191 VarTypedTree::open(dir.path(), Config::test(), U64Codec).unwrap();
2192 let k = 1u64.to_be_bytes();
2193 let (fresh, overwrite) = tree
2194 .atomic(&k, |s| {
2195 let fresh = s.put(&k, &10u64)?;
2196 let overwrite = s.put(&k, &20u64)?;
2197 Ok((fresh, overwrite))
2198 })
2199 .expect("atomic");
2200 assert!(!fresh, "fresh insert must return false");
2201 assert!(overwrite, "overwrite must return true");
2202 }
2203
2204 #[test]
2205 fn var_typed_tree_shard_update_and_fetch_update() {
2206 let dir = tempdir().unwrap();
2207 let mut cfg = Config::test();
2208 cfg.shard_count = 1;
2209 let tree: VarTypedTree<[u8; 8], u64, U64Codec> =
2210 VarTypedTree::open(dir.path(), cfg, U64Codec).unwrap();
2211 let k = 1u64.to_be_bytes();
2212 tree.put(&k, &100u64).unwrap();
2213
2214 let (upd, fetched, missing) = tree
2219 .atomic(&k, |s| {
2220 let upd = s.update(&k, |old| old + 1)?;
2221 let fetched = s.fetch_update(&k, |old| old + 1)?;
2222 let missing = s.update(&2u64.to_be_bytes(), |old| *old)?;
2223 Ok((upd, fetched, missing))
2224 })
2225 .expect("atomic");
2226
2227 assert_eq!(upd, Some(101), "update returns new value");
2228 assert_eq!(fetched, Some(101), "fetch_update returns old value");
2229 assert_eq!(missing, None, "absent key returns None");
2230 assert_eq!(
2231 tree.get(&k),
2232 Some(102),
2233 "final stored value after two increments"
2234 );
2235 }
2236
2237 #[test]
2238 fn var_typed_tree_atomic_update_fires_hook() {
2239 let dir = tempdir().unwrap();
2240 let hook = TRecHook::default();
2241 let state = hook.state.clone();
2242 let tree = open_var_typed_hooked(dir.path(), hook);
2243 let k = 7u64.to_be_bytes();
2244
2245 tree.put(&k, &10u64).unwrap();
2247 state.writes.store(0, AtomicOrdering::Relaxed);
2249
2250 tree.atomic(&k, |s| s.update(&k, |old| old + 5))
2251 .expect("atomic update");
2252
2253 assert_eq!(
2255 state.writes.load(AtomicOrdering::Relaxed),
2256 1,
2257 "hook fired once"
2258 );
2259 assert_eq!(
2260 *crate::sync::lock(&state.last_new),
2261 Some(15),
2262 "hook saw the new value"
2263 );
2264 }
2265
2266 #[test]
2267 fn keys_skip_poison_decode_on_var_typed_tree() {
2268 use crate::test_faults::{POISON, PoisonCodec};
2269
2270 let dir = tempdir().unwrap();
2271 let tree: VarTypedTree<[u8; 8], u64, PoisonCodec> =
2272 VarTypedTree::open(dir.path(), Config::test(), PoisonCodec).unwrap();
2273 let good_key = 1_u64.to_be_bytes();
2274 let poison_key = 2_u64.to_be_bytes();
2275 tree.put(&good_key, &1).unwrap();
2276 tree.put(&poison_key, &POISON).unwrap();
2277
2278 assert_eq!(tree.iter().count(), 1, "iter skips poison");
2279
2280 let keys: Vec<_> = tree.scope::<0>(&[0u8; 8]).keys().collect();
2281 assert!(keys.contains(&good_key));
2282 assert!(
2283 keys.contains(&poison_key),
2284 "keys must include the poison row's key without decoding"
2285 );
2286 }
2287
2288 #[test]
2289 fn keys_perform_zero_disk_value_reads() {
2290 let dir = tempdir().unwrap();
2291 let mut cfg = Config::test();
2292 cfg.shard_count = 1;
2293 cfg.direct_io = true;
2294 let tree: VarTypedTree<[u8; 8], Vec<u8>, RapiraCodec> =
2295 VarTypedTree::open(dir.path(), cfg, RapiraCodec).unwrap();
2296
2297 for i in 1..=5u64 {
2298 let k = i.to_be_bytes();
2299 let v: Vec<u8> = (0..5000).map(|j| ((i * 17 + j) % 251) as u8).collect();
2300 tree.put(&k, &v).unwrap();
2301 }
2302 tree.flush_buffers().unwrap();
2303
2304 let scope_key = [0u8; 8];
2305 let before = crate::var_tree::test_disk_value_reads();
2306 let n = tree.scope::<0>(&scope_key).keys().count();
2307 assert!(n > 0);
2308 assert_eq!(
2309 crate::var_tree::test_disk_value_reads(),
2310 before,
2311 "keys() must not read values from disk"
2312 );
2313
2314 let before_iter = crate::var_tree::test_disk_value_reads();
2315 let iter_count = tree.scope::<0>(&scope_key).iter().count();
2316 assert!(iter_count > 0);
2317 assert!(
2318 crate::var_tree::test_disk_value_reads() > before_iter,
2319 "iter() must read values from disk (counter sanity check)"
2320 );
2321 }
2322}