Skip to main content

armdb/
var_typed_tree.rs

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
21/// A tree with fixed-size keys and typed values `T`. Values are encoded via
22/// a [`Codec`] and stored on disk (variable length), with a block cache
23/// (≤ 8 KB values) and a value cache (> 8 KB values) for reads — unlike
24/// [`TypedTree`](crate::TypedTree) which keeps values in memory.
25///
26/// Thin wrapper around [`VarTree<K, VarTypedHookAdapter<K, T, C, H>>`].
27/// Each `VarTypedTree` owns its storage engine — one tree = one database directory.
28///
29/// Facade mutations and `atomic()` forward to the inner Var tree, so they ride
30/// Var's `Family` / `Ctx` seam (borrowed facade events; `ByteView::from` only in
31/// `own` for Held) without a separate VarTyped `Family` impl.
32///
33/// # When to use
34///
35/// - Typed values too large to keep in RAM → `VarTypedTree` (disk-resident).
36/// - Typed values that fit in RAM → [`TypedTree`](crate::TypedTree) (in-memory).
37/// - Fixed-size zerocopy values → [`ZeroTree`](crate::ZeroTree).
38///
39/// # Error handling
40///
41/// Follows `VarTree` / `TypedTree` precedent (variant A):
42/// - `get` / `first` / `last` → `None` if key missing **or** decode fails.
43/// - Iterators skip entries with decode errors (`continue` in `next()`).
44/// - `migrate` is strict: a decode/encode error aborts it with `Err`.
45///
46/// # Decoded value cache
47///
48/// [`get_shared`](Self::get_shared) / [`try_get_shared`](Self::try_get_shared) /
49/// [`get_many_shared`](Self::get_many_shared) serve `Arc<T>` from an optional
50/// per-collection cache of **decoded** values (`Config::decoded_cache`, off by
51/// default), keyed by the value's physical location — so an update, a delete or
52/// a compaction invalidates nothing explicitly. The byte-level `get` / `try_get`
53/// / iterators are unaffected and never consult it.
54///
55/// # Write hooks
56///
57/// Uses [`TypedWriteHook<K, T>`] via [`VarTypedHookAdapter`]. The hook receives
58/// `&T` directly; the adapter decodes raw bytes via the codec. `on_write` fires
59/// on `put`/`insert`/`delete`/`cas`/`compare_delete`/`update`/`fetch_update` and inside `atomic()`.
60///
61/// # Usage
62///
63/// ```ignore
64/// let tree = VarTypedTree::<[u8; 16], Message, RapiraCodec>::open(
65///     "data/messages",
66///     Config::large_values().build(),
67///     RapiraCodec,
68/// )?;
69/// tree.put(&key, &msg)?;
70/// if let Some(m) = tree.get(&key) {
71///     println!("{:?}", m);
72/// }
73/// tree.close()?;
74/// ```
75///
76/// # Iteration
77///
78/// `iter()`, `range()`, and `prefix_iter()` return [`VarTypedIter`] with
79/// `Item = (K, T)`. Each `next()` / `next_back()` may perform disk I/O on a
80/// block-cache miss and performs a decode.
81///
82/// # Batch operations
83///
84/// [`get_many`](Self::get_many) does one sorted finger-walk over the index for a
85/// list of keys; [`update_many`](Self::update_many) applies a per-key closure
86/// grouped by shard (per-shard isolation, no global atomicity). See
87/// `docs/superpowers/specs/26-06-24-armdb-batch-tree-ops.md`.
88pub 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    /// Subscribe to this collection's mutations.
119    ///
120    /// Infallible and repeatable; every receiver is independent and owns a
121    /// bounded ring. Delivery is **at-most-once** — a full ring drops the
122    /// oldest event and reports [`WatchError::Lagged`] at the gap.
123    ///
124    /// The consumer recipe is `watch()` **first**, then scan, and rescan on
125    /// `Lagged`. It is complete because of what this call guarantees on return:
126    /// every write that decided not to publish to this receiver has already
127    /// made its row visible.
128    ///
129    /// An event is a pointer, not a snapshot — a rebuild re-reads rows by key
130    /// instead of applying payloads. There is no ordering across keys or
131    /// collections, and transaction boundaries are invisible. A `new_only` hook
132    /// reports every non-delete as [`WatchKind::Upsert`] — and, for this Var
133    /// family, is what keeps a watched write from reading the old value.
134    ///
135    /// Subscriptions end with the database: `Db::shutdown` (and `Drop`) closes
136    /// every channel; `Db::close` leaves receivers live. See `docs/watch.md`.
137    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    /// Open or create a `VarTypedTree` at the given path.
148    /// Recovers the index from existing data files on disk.
149    ///
150    #[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    /// Open or create a `VarTypedTree` with a typed write hook.
160    ///
161    #[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    /// Skips [`check_no_counters`](crate::counter_dirty::check_no_counters) —
178    /// only `Db::open_managed` may call this, immediately followed by
179    /// `ManagedOpen::bind_counter_dirty` before the collection is published.
180    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    /// Graceful shutdown: write hint files (if enabled), flush write buffers + fsync.
209    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    /// Flush all shard write buffers to disk (without fsync).
218    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    /// Get the database configuration.
227    pub fn config(&self) -> &Config {
228        self.inner.config()
229    }
230
231    /// Trigger a compaction pass across all shards.
232    pub fn compact(&self) -> DbResult<usize> {
233        self.inner.compact()
234    }
235
236    /// Write hint files for all active shard files. Call during graceful shutdown.
237    pub fn sync_hints(&self) -> DbResult<()> {
238        self.inner.sync_hints()
239    }
240
241    /// Pre-populate the block cache with blocks containing live values.
242    pub fn warmup(&self) -> DbResult<()> {
243        self.inner.warmup()
244    }
245
246    /// Access the underlying `VarTree` for raw byte operations.
247    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    /// Register the `Arc` handle the inner [`VarTree`] needs for transaction deferred replay.
257    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    /// Claim ops for one decided write inside a transaction (T10 Task 6) —
290    /// `name` is passed straight through to [`VarTree::tx_claims_for`], which
291    /// cannot name `T` itself.
292    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    /// Access the codec used for encoding / decoding values.
303    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    // -- Reads ----------------------------------------------------------------
345
346    /// Get and decode a value by key.
347    /// Returns `None` if the key is missing or decoding fails.
348    ///
349    /// **O(log n)** — lock-free SkipList lookup; brief shard lock + `pread` on block-cache
350    /// miss, then decode.
351    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    /// Get a value by key, returning `Err(KeyNotFound)` if absent or
357    /// `Err(CorruptedEntry)` if present but undecodable.
358    pub fn get_or_err(&self, key: &K) -> DbResult<T> {
359        self.try_get(key)?.ok_or(DbError::KeyNotFound)
360    }
361
362    /// Strict read: `Ok(None)` only when absent; `Err` when present but the value
363    /// cannot be read or decoded.
364    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    /// Get a value as a shared `Arc<T>`, decoding **only on a cache miss**.
372    ///
373    /// Lossy, like [`get`](Self::get): `None` when the key is missing, the value
374    /// is unreadable, or it fails to decode. Works with the decoded cache
375    /// disabled (`Config::decoded_cache.max_size == 0`) — it then decodes on
376    /// every call, exactly like `get`, and wraps the result in an `Arc`.
377    ///
378    /// **Arc identity is opportunistic.** The returned value is the version the
379    /// index pointed at when the read took its `DiskLoc`; that is the only
380    /// invariant. Repeated calls are pointer-equal while the entry stays
381    /// resident, but eviction, a concurrent cold miss (misses are deliberately
382    /// not coalesced) and compaction each produce a new `Arc` with no write
383    /// involved. A handle taken before a write keeps observing the old version.
384    ///
385    /// **O(log n)** — SkipList lookup, then a hash lookup on a hit; on a miss,
386    /// the ordinary byte read plus one decode.
387    pub fn get_shared(&self, key: &K) -> Option<Arc<T>> {
388        self.try_get_shared(key).ok().flatten()
389    }
390
391    /// Strict shared read: `Ok(None)` only when the key is absent; `Err` when it
392    /// is present but cannot be read or decoded. Shared twin of
393    /// [`try_get`](Self::try_get); see [`get_shared`](Self::get_shared) for the
394    /// `Arc` identity contract.
395    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    /// Batch shared point-read: one sorted finger-walk over the index, result
401    /// aligned to `keys`. Lossy per element, like
402    /// [`get_many`](Self::get_many); cached keys are not decoded again.
403    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    /// Check if a key exists.
409    pub fn contains(&self, key: &K) -> bool {
410        self.inner.contains(key)
411    }
412
413    /// Return the first entry in index order, or `None` if empty or every boundary
414    /// entry is unproducible. Skips unreadable/undecodable entries, matching
415    /// `iter().next()`.
416    pub fn first(&self) -> Option<(K, T)> {
417        self.iter().next()
418    }
419
420    /// Return the last entry in index order (symmetric to `first`).
421    pub fn last(&self) -> Option<(K, T)> {
422        self.iter().next_back()
423    }
424
425    /// Strict first entry: `Ok(None)` only when there is no live entry; `Err` when
426    /// the boundary entry cannot be read or decoded.
427    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    /// Strict last entry (symmetric to `try_first`).
435    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    // -- Writes ---------------------------------------------------------------
443
444    /// Insert or update a key-value pair.
445    ///
446    /// Returns `true` if a previous value for `key` existed (overwrite),
447    /// `false` for a fresh insert.
448    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    /// Insert a key-value pair only if the key does not exist.
456    /// Returns `Err(KeyExists)` if the key is already present.
457    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    /// Delete a key. Returns `true` if the key existed.
464    pub fn delete(&self, key: &K) -> DbResult<bool> {
465        self.inner.delete(key)
466    }
467
468    /// Compare-and-swap based on encoded bytes. Relies on deterministic codec output.
469    /// Returns `Ok(())` on success, `Err(CasMismatch)` if current != expected,
470    /// `Err(KeyNotFound)` if key doesn't exist.
471    ///
472    /// **Caveat:** inherits `VarTree::cas` behavior — holds the shard lock while
473    /// reading the current value; block-cache miss causes disk I/O under the lock.
474    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    /// Compare-and-delete based on encoded bytes. Relies on deterministic codec output.
483    /// Returns `Ok(())` on success, `Err(CasMismatch)` if current != expected,
484    /// `Err(KeyNotFound)` if the key doesn't exist.
485    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    /// Atomically read-modify-write. Returns `Some(new_value)` if key existed,
492    /// `None` if the key is absent; `Err(CorruptedEntry)` if the current value
493    /// cannot be decoded.
494    ///
495    /// **Caveat:** inherits `VarTree::update` behavior — shard lock held during
496    /// possible disk I/O.
497    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(&current);
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    /// Like [`update()`](Self::update), but returns `Some(old_value)` instead
519    /// of the new one.
520    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(&current);
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    /// Decide-and-write: insert, update, keep, or delete based on the closure's
542    /// [`Upsert`](crate::Upsert) decision. The closure must be short — the shard
543    /// lock is held across it, so it must not panic and must not call back into
544    /// this collection (the shard mutex is not reentrant).
545    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    // -- Atomic ---------------------------------------------------------------
554
555    /// Atomically execute multiple operations on a single shard.
556    /// All keys must route to the same shard as `shard_key`.
557    /// The closure must be short — shard lock is held for its duration.
558    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            // SAFETY: erase VarShard lifetime via `*mut ()` — see VarTypedShard doc.
565            // The pointer is valid for the duration of this closure.
566            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    // -- Iteration ------------------------------------------------------------
577
578    /// Iterate entries whose keys start with `prefix`.
579    ///
580    /// `reversed=true` (default): yields matching keys in DESC order.
581    /// The delegated iterator has `next()` O(1); its first reverse seek is expected O(log n), and
582    /// an m-row reverse walk is expected/amortized O(log n + m). Disk I/O on cache miss.
583    /// Entries with decode errors are skipped.
584    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    /// Iterate all entries in index order. Disk I/O on cache miss.
593    /// The delegated iterator has `next()` O(1); its first reverse seek is expected O(log n), and
594    /// an m-row reverse walk is expected/amortized O(log n + m).
595    /// Entries with decode errors are skipped.
596    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    /// Iterate entries in `[start, end)` — start inclusive, end exclusive.
605    /// The delegated iterator has `next()` O(1); its first reverse seek is expected O(log n), and
606    /// an m-row reverse walk is expected/amortized O(log n + m). Disk I/O on cache miss.
607    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    /// Iterate entries in range defined by `start` and `end` bounds.
616    /// The delegated iterator has `next()` O(1); its first reverse seek is expected O(log n), and
617    /// an m-row reverse walk is expected/amortized O(log n + m). Disk I/O on cache miss.
618    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    /// Range iterator with explicit direction override. See [`VarTree::range_bounds_dir`].
627    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    /// Key-only range iterator with explicit direction override.
641    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    /// Whether index iteration yields descending key order.
651    pub(crate) fn reversed(&self) -> bool {
652        self.inner.reversed()
653    }
654
655    /// Field-granular prefix / range / page builder over this tree's key space.
656    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    /// Arc-owned prefix/range builder for [`crate::Tx::delete_scope`].
664    #[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    /// Strict range iterator: surfaces storage/decode errors instead of skipping.
684    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    // -- Info -----------------------------------------------------------------
713
714    pub fn len(&self) -> usize {
715        self.inner.len()
716    }
717
718    /// Strict live-key count for read-only inspection (key-only skiplist walk).
719    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    /// Encoded value byte length for `key`, or `None` if absent.
732    /// Reads only the in-memory index entry (`DiskLoc::len`); no disk I/O.
733    pub fn entry_len(&self, key: &K) -> Option<u32> {
734        self.inner.entry_len(key)
735    }
736
737    // -- Batch ----------------------------------------------------------------
738
739    /// Batch point-read returning decoded values, aligned to `keys`. **Lossy** —
740    /// like single-key [`get`](Self::get), a decode failure yields `None`.
741    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    /// Batch read-modify-write over decoded values. See [`crate::ConstTree::update_many`].
750    ///
751    /// **Strict on codec failures**, consistent with single-key [`update`](Self::update):
752    /// a codec decode of an old value or encode of a new value that fails aborts the
753    /// batch with that `DbError` (the applied prefix stays committed, as for any
754    /// mid-batch error). Codec errors are never silently collapsed into
755    /// `Keep`/`Kept`. (Read-only [`get_many`](Self::get_many) stays lossy, mirroring
756    /// single-key [`get`](Self::get).)
757    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        // The inner (raw-bytes) closure cannot return a `Result`, so a codec
765        // failure is captured here and surfaced after the batch unwinds. Once an
766        // error is recorded, remaining items short-circuit to `Keep` so the batch
767        // stops mutating; the recorded error is then returned.
768        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        // On the success path every old value already decoded inside the closure
802        // and every `new` was just encoded here, so these decodes succeed; a
803        // failure would be genuine corruption and is propagated, not swallowed.
804        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    // -- Migration ------------------------------------------------------------
823
824    /// Iterate all entries and optionally mutate them. Call once at startup.
825    ///
826    /// The callback receives each `(key, &T)` and returns `MigrateAction`:
827    /// - `Keep` — no change
828    /// - `Update(new_value)` — replace value (re-encoded via codec)
829    /// - `Delete` — remove entry
830    ///
831    /// **Strict on codec failures**, like the underlying
832    /// [`VarTree::migrate`](crate::VarTree::migrate) value read: an entry that
833    /// fails to decode — or a migrated value that fails to re-encode — aborts the
834    /// migration with that `DbError`. A migration must not report success while
835    /// corrupt rows remain untouched on disk: the caller
836    /// ([`run_migration`](crate::armour::Db) path) would commit a version bump
837    /// over unmigrated data.
838    ///
839    /// **Failure is not clean.** Entries are rewritten as the scan walks, so an
840    /// `Err` leaves the collection *partially* migrated with the version
841    /// un-bumped; the next open re-runs the whole step, which is why migration
842    /// callbacks must be idempotent (the standing `run_migration` contract). A
843    /// single undecodable entry therefore blocks the collection's open on
844    /// **every** subsequent start until it is repaired out-of-band — deliberate:
845    /// silently carrying corrupt rows through a version bump is worse.
846    ///
847    /// Returns the number of mutated entries.
848    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        // The inner (raw-bytes) closure cannot return a `Result`, so a codec
861        // failure is captured here and surfaced after the scan unwinds — same
862        // shape as `update_many`. Once an error is recorded the remaining
863        // entries short-circuit to `Keep`, so the scan stops mutating and the
864        // outcome matches an abort at the failing entry.
865        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, &current) {
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    /// Replay `on_init` for every live entry. Used by Db when no migration ran.
903    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
989// ---------------------------------------------------------------------------
990// VarTypedIter
991// ---------------------------------------------------------------------------
992
993/// Iterator over entries in a [`VarTypedTree`]. Returned by `iter()`, `range()`,
994/// `range_bounds()`, and `prefix_iter()`. Wraps [`VarIter`] and decodes each
995/// value via the codec. Entries with decode errors are skipped (see `tracing::debug!`).
996pub 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
1042/// Direction-aware range iterator over a [`VarTypedTree`].
1043pub(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/// Strict iterator over entries in a [`VarTypedTree`]. Wraps [`TryVarIter`] and
1077/// decodes each value via the codec, surfacing decode errors.
1078#[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
1121// ---------------------------------------------------------------------------
1122// VarTypedShard
1123// ---------------------------------------------------------------------------
1124
1125/// Handle for atomic multi-key operations within a single shard.
1126/// Obtained via [`VarTypedTree::atomic`]. The shard lock is held for the
1127/// lifetime of this struct — keep the closure short.
1128pub 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    // Type-erased raw pointer to a `VarShard<'_, K, VarTypedHookAdapter<K, T, C, H>>`.
1137    // Invariance of the inner `'_` lifetime would otherwise force it to `'static`
1138    // when stored. Only dereferenced inside the enclosing `atomic()` closure.
1139    inner: *mut (),
1140    _marker: PhantomData<&'tree mut ()>,
1141}
1142
1143// SAFETY: raw pointer dereferenced only while the enclosing `atomic` closure
1144// is active; `VarShard` itself is `Send` and guards a mutex. `tree` is `Send+Sync`.
1145unsafe 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        // SAFETY: pointer set in `VarTypedTree::atomic` from a valid `&mut VarShard`,
1155        // only dereferenced while the closure is live.
1156        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        // SAFETY: see `inner_mut`.
1161        unsafe { &*(self.inner as *const VarShard<'_, K, VarTypedHookAdapter<K, T, C, H>>) }
1162    }
1163
1164    /// Insert or overwrite `key`. Returns `true` if the key already existed (overwrite),
1165    /// `false` for a fresh insert.
1166    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    /// Atomically read-modify-write within the shard. Returns `Some(new_value)`
1199    /// if the key existed, `None` if absent.
1200    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; // &'tree VarTypedTree — Copy, avoids borrowing self in closure
1203        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(&current);
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    /// Like [`update`](Self::update), but returns `Some(old_value)` instead of
1223    /// the new one.
1224    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; // Copy
1227        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(&current);
1233                let mut buf = Vec::new();
1234                tree.codec.encode_to(&new_val, &mut buf)?;
1235                out.set(Some(current)); // capture the OLD value
1236                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    /// Decide-and-write within the shard. The closure must be short — shard lock is held.
1247    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    /// Read path — no shard write lock: `try_get` on the raw byte tree is a
1437    /// strict, lock-free-on-hit read; unlike `get`/`try_get` on `Self` it
1438    /// returns the stored bytes directly instead of decode-then-reencode.
1439    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    /// Codec that counts decodes, so tests can assert "a hit does not decode"
1503    /// directly instead of inferring it from `Arc` identity.
1504    #[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)); // consequence of residency, not a contract
1558    }
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        // Write raw bytes that the codec cannot decode, bypassing the wrapper.
1597        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        // `T` here is deliberately not `Clone`: this test fails to compile if
1634        // `#[derive(Clone)]` ever replaces the manual `Clone for DecodedEntry<T>`.
1635        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        // Measure the on-disk weight the decoded cache actually charges (`DiskLoc::len`).
1720        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        // Control, on the probe's roomy cache: a resident entry is served without
1730        // decoding. Without it every assertion below would also hold for a cache
1731        // that never hits at all — three reads, three decodes — so this is what
1732        // makes the re-decode further down evidence of *eviction*. It cannot run
1733        // on the one-entry budget instead: there the policy does not keep even
1734        // the most recent insert resident.
1735        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        // Budget holds exactly one decoded entry; warming a second key evicts the first.
1745        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        // No assertion on decode count or Arc identity: without coalescing,
1782        // concurrent cold misses may each decode (spec, "Alternatives considered").
1783        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        // RapiraCodec encodes Vec<u8> as 4-byte length prefix + data bytes.
1828        // vec![10, 20, 30, 40] → 4 + 4 = 8 encoded bytes.
1829        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        // Default reversed=true => first() is the largest key. Put a clean entry and
1905        // a poisoned boundary entry (key 2 > key 1).
1906        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        // Result methods surface CorruptedEntry, not KeyNotFound / Ok(None).
1912        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        // Lossy first() skips the poisoned boundary and returns the next valid entry,
1930        // matching iter().next().
1931        assert_eq!(tree.first(), Some((k1, 7u64)));
1932        assert_eq!(tree.first(), tree.iter().next());
1933        // Strict try_first surfaces the boundary decode error.
1934        assert!(matches!(
1935            tree.try_first(),
1936            Err(DbError::CorruptedEntry { .. })
1937        ));
1938
1939        // cas/compare_delete compare RAW bytes — a decodable-but-poison value is not
1940        // a read error: mismatch on a different expected, success on the exact bytes.
1941        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        // A stored value whose bytes are cursor-valid but semantically invalid
1969        // (here: a `String` field with invalid UTF-8) must surface as
1970        // Err(CorruptedEntry) on the strict read — never UB / panic / abort.
1971        // Under the old unchecked codec this constructed an invalid `String`.
1972        let dir = tempdir().unwrap();
1973        let tree = open(dir.path());
1974        let key = [0u8; 16];
1975
1976        // Msg layout: id:u64 | body:String(u32 len + utf8) | tags:Vec<String>(u32 count).
1977        // body has len=2 but bytes 0xFF 0xFE (invalid UTF-8); tags empty.
1978        let bad: &[u8] = &[
1979            0, 0, 0, 0, 0, 0, 0, 0, // id: u64 = 0
1980            2, 0, 0, 0, // body String length = 2
1981            0xFF, 0xFE, // body content — invalid UTF-8
1982            0, 0, 0, 0, // tags Vec<String> length = 0
1983        ];
1984        // NoHook (default) → the write-path adapter does not decode; raw bytes land on disk.
1985        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        // Inject a value that cannot decode as `Msg` (same layout as the
2001        // `try_get` fault test: `body` claims 2 bytes of invalid UTF-8).
2002        let bad: &[u8] = &[
2003            0, 0, 0, 0, 0, 0, 0, 0, // id: u64 = 0
2004            2, 0, 0, 0, // body String length = 2
2005            0xFF, 0xFE, // body content — invalid UTF-8
2006            0, 0, 0, 0, // tags Vec<String> length = 0
2007        ];
2008        tree.as_inner().put(&[2u8; 16], bad).unwrap();
2009
2010        // A migration that rewrites every row must surface the corrupt row as an
2011        // error, not skip it and report success (which would let `run_migration`
2012        // commit a version bump with unmigrated rows on disk).
2013        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        // A migrated value that will not re-encode is a real failure: the row
2028        // would stay at its old contents while the version moves on.
2029        let err = tree
2030            .migrate(|_k, _v| MigrateAction::Update(ENCODE_FAIL))
2031            .unwrap_err();
2032        assert!(matches!(err, DbError::CorruptedEntry { .. }), "{err:?}");
2033    }
2034
2035    /// Sentinel that [`EncodeFailCodec`] refuses to encode.
2036    const ENCODE_FAIL: u64 = 0xBAD0_BAD0_BAD0_BAD0;
2037
2038    /// Mirror of `test_faults::PoisonCodec`: decodes anything 8 bytes wide but
2039    /// fails to *encode* the [`ENCODE_FAIL`] sentinel.
2040    #[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    // ---------------------------------------------------------------------------
2123    // Tests — get_many / update_many (Task 5)
2124    // ---------------------------------------------------------------------------
2125
2126    #[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        // Reading the poisoned old value during update_many must surface the decode
2175        // error (strict), not silently collapse to Applied::Kept.
2176        let res = tree.update_many(vec![(k, 5u64)], |_k, _old, v| Upsert::Put(*v));
2177        assert!(matches!(res, Err(DbError::CorruptedEntry { .. })));
2178
2179        // The poisoned key was not overwritten by a hidden Keep path — raw bytes
2180        // remain, and a strict read still surfaces the corruption.
2181        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        // After put: k -> 100
2215        // update: 100 + 1 = 101 stored, returns Some(101) [new]
2216        // fetch_update: 101 + 1 = 102 stored, returns Some(101) [old]
2217        // missing key returns None
2218        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        // Seed the value so update finds the key.
2246        tree.put(&k, &10u64).unwrap();
2247        // Reset counter after the initial put.
2248        state.writes.store(0, AtomicOrdering::Relaxed);
2249
2250        tree.atomic(&k, |s| s.update(&k, |old| old + 5))
2251            .expect("atomic update");
2252
2253        // The hook must have fired once for the update (new = 15).
2254        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}