lsm_tree/pinnable_slice.rs
1// Copyright (c) 2024-present, fjall-rs
2// This source code is licensed under both the Apache 2.0 and MIT License
3// (found in the LICENSE-* files in the repository)
4
5//! Zero-copy value reference that keeps the decompressed block buffer alive.
6//!
7//! [`PinnableSlice`] is inspired by `RocksDB`'s `PinnableSlice`
8//! (`include/rocksdb/slice.h:179-263`). It wraps a value that was read from
9//! the LSM tree and indicates whether the underlying data shares the
10//! decompressed block buffer or is independently owned (e.g. from a memtable
11//! or merge result).
12//!
13//! When the value comes from an on-disk data block, holding a
14//! `PinnableSlice::Pinned` keeps the block's decompressed buffer alive
15//! (via the refcounted [`Slice`] / `ByteView` backing) for the duration of
16//! the reference. The value bytes are a sub-slice of that buffer — no copy
17//! is performed. Note: this does **not** prevent the block cache from
18//! evicting its entry; it only ensures the backing memory remains valid.
19//!
20//! Memtable and blob-resolved values use the `Owned` variant.
21
22use crate::{Slice, UserValue, table::Block};
23
24/// A value reference that may share the decompressed block buffer.
25///
26/// Use [`PinnableSlice::as_ref`] to access the raw bytes regardless of variant.
27///
28/// # Lifetime
29///
30/// The `Pinned` variant holds a [`Block`] clone whose `data` field is a
31/// refcounted [`Slice`]. As long as the `PinnableSlice` is alive, the
32/// decompressed block buffer remains valid. Dropping it releases the
33/// reference count on the underlying `ByteView` allocation.
34#[derive(Clone)]
35pub enum PinnableSlice {
36 /// Value sharing the decompressed block buffer — zero copy.
37 ///
38 /// The [`Block`] keeps the decompressed data alive via refcounted
39 /// `Slice` / `ByteView`. `value` is a sub-slice created via
40 /// [`Slice::slice`], sharing the same backing allocation.
41 Pinned {
42 /// Keeps the decompressed block buffer alive via refcount.
43 _block: Block,
44 /// Zero-copy sub-slice into the block's decompressed data.
45 value: Slice,
46 },
47
48 /// Value owned independently (memtable, blob, merge result).
49 Owned(UserValue),
50}
51
52impl PinnableSlice {
53 /// Creates a pinned value sharing the decompressed block buffer.
54 #[must_use]
55 pub fn pinned(block: Block, value: Slice) -> Self {
56 Self::Pinned {
57 _block: block,
58 value,
59 }
60 }
61
62 /// Creates an owned value (not sharing any block buffer).
63 #[must_use]
64 pub fn owned(value: UserValue) -> Self {
65 Self::Owned(value)
66 }
67
68 /// Returns `true` if this value shares the decompressed block buffer.
69 #[must_use]
70 pub fn is_pinned(&self) -> bool {
71 matches!(self, Self::Pinned { .. })
72 }
73
74 /// Returns the raw value bytes.
75 #[must_use]
76 pub fn value(&self) -> &[u8] {
77 self.as_ref()
78 }
79
80 /// Returns the length of the value in bytes.
81 #[must_use]
82 pub fn len(&self) -> usize {
83 self.as_ref().len()
84 }
85
86 /// Returns `true` if the value is empty.
87 #[must_use]
88 pub fn is_empty(&self) -> bool {
89 self.as_ref().is_empty()
90 }
91
92 /// Converts this `PinnableSlice` into an owned `UserValue`.
93 ///
94 /// For the `Pinned` variant, the `Block` is dropped but the returned
95 /// `Slice` still shares the same `ByteView` backing allocation.
96 /// For the `Owned` variant, the value is returned directly.
97 #[must_use]
98 pub fn into_value(self) -> UserValue {
99 match self {
100 Self::Pinned { value, .. } => value,
101 Self::Owned(v) => v,
102 }
103 }
104}
105
106impl std::fmt::Debug for PinnableSlice {
107 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
108 match self {
109 Self::Pinned { value, .. } => {
110 f.debug_struct("Pinned").field("len", &value.len()).finish()
111 }
112 Self::Owned(v) => f.debug_tuple("Owned").field(&v.len()).finish(),
113 }
114 }
115}
116
117impl AsRef<[u8]> for PinnableSlice {
118 fn as_ref(&self) -> &[u8] {
119 match self {
120 Self::Pinned { value, .. } => value.as_ref(),
121 Self::Owned(v) => v.as_ref(),
122 }
123 }
124}
125
126impl PartialEq<[u8]> for PinnableSlice {
127 fn eq(&self, other: &[u8]) -> bool {
128 self.as_ref() == other
129 }
130}
131
132impl PartialEq<&[u8]> for PinnableSlice {
133 fn eq(&self, other: &&[u8]) -> bool {
134 self.as_ref() == *other
135 }
136}
137
138impl From<PinnableSlice> for UserValue {
139 fn from(ps: PinnableSlice) -> Self {
140 ps.into_value()
141 }
142}