Skip to main content

sochdb_storage/
txn_arena.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// SochDB - LLM-Optimized Embedded Database
3// Copyright (C) 2026 Sushanth Reddy Vanagala (https://github.com/sushanthpy)
4//
5// This program is free software: you can redistribute it and/or modify
6// it under the terms of the GNU Affero General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// This program is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU Affero General Public License for more details.
14//
15// You should have received a copy of the GNU Affero General Public License
16// along with this program. If not, see <https://www.gnu.org/licenses/>.
17
18//! Transaction-Scoped Arena with Zero-Copy Key/Value Plumbing
19//!
20//! This module provides a memory arena that lives for the duration of a transaction,
21//! enabling zero-copy key/value handling across multiple bookkeeping structures.
22//!
23//! ## Problem: Death by Cloning
24//!
25//! Today's write path clones `Vec<u8>` keys/values across multiple structures:
26//! - WAL buffering
27//! - MVCC write-set tracking
28//! - Ordered index maintenance
29//! - Memtable KV storage
30//! - Dirty tracking
31//!
32//! If a key participates in all 5 structures, naive cloning multiplies memory
33//! bandwidth and allocator pressure by O(#structures).
34//!
35//! ## Solution: Copy-Once, Reference Many
36//!
37//! ```text
38//! ┌────────────────────────────────────────────────────────────────┐
39//! │                    Transaction Arena                            │
40//! │  ┌────────────────────────────────────────────────────────────┐│
41//! │  │ Key/Value Bytes Storage (Vec<u8> backing store)            ││
42//! │  │ ┌──────────┬──────────┬──────────┬──────────┬────────────┐ ││
43//! │  │ │  key1    │  val1    │  key2    │  val2    │   ...      │ ││
44//! │  │ └──────────┴──────────┴──────────┴──────────┴────────────┘ ││
45//! │  └────────────────────────────────────────────────────────────┘│
46//! │                     ↑         ↑         ↑                       │
47//! │          BytesRef handles (offset, len) - 8 bytes each         │
48//! └────────────────────────────────────────────────────────────────┘
49//!                      │         │         │
50//!     ┌────────────────┼─────────┼─────────┼────────────────┐
51//!     ↓                ↓         ↓         ↓                ↓
52//! ┌──────┐        ┌──────┐  ┌──────┐  ┌──────┐        ┌──────┐
53//! │ WAL  │        │ MVCC │  │SkipM│  │DashM │        │Dirty │
54//! │Buffer│        │WriteS│  │Index│  │Memtab│        │Track │
55//! └──────┘        └──────┘  └──────┘  └──────┘        └──────┘
56//!
57//! Old cost: O(S × (|k|+|v|)) where S = number of structures
58//! New cost: O(|k|+|v| + S)   (copy once + O(1) handle copies)
59//! ```
60//!
61//! ## Performance
62//!
63//! - Bump allocation: ~3ns per key (vs ~50ns for malloc)
64//! - Handle copy: 8 bytes (vs full key copy)
65//! - Hash computation: Once per key (cached in handle)
66//! - Memory locality: All transaction data in contiguous region
67
68use std::cell::UnsafeCell;
69use std::hash::{Hash, Hasher};
70use std::sync::atomic::{AtomicU32, Ordering};
71
72/// Default arena capacity: 64KB (handles ~1000 typical writes)
73const DEFAULT_ARENA_CAPACITY: usize = 64 * 1024;
74
75/// Maximum key+value size that fits inline in BytesRef (24 bytes)
76#[allow(dead_code)]
77const INLINE_MAX_SIZE: usize = 24;
78
79// ============================================================================
80// BytesRef - Lightweight Handle to Arena-Stored Bytes
81// ============================================================================
82
83/// A lightweight reference to bytes stored in a TxnArena
84///
85/// Only 16 bytes: (offset: u32, len: u32, hash: u64)
86/// Compared to Vec<u8>: 24 bytes + heap allocation + deallocation
87///
88/// ## Usage
89///
90/// ```ignore
91/// let arena = TxnArena::new(txn_id);
92/// let key_ref = arena.alloc_key(b"users/12345/name");
93/// let val_ref = arena.alloc_value(b"Alice");
94///
95/// // Now use key_ref and val_ref in multiple structures
96/// // Each copy is just 16 bytes, not a full clone
97/// write_set.insert(key_ref.fingerprint());  // O(1) copy
98/// dirty_list.push(key_ref);                  // O(1) copy
99/// memtable.insert(key_ref, val_ref);         // O(1) copy
100/// ```
101#[derive(Clone, Copy, Debug)]
102pub struct BytesRef {
103    /// Offset into arena's backing store (or inline flag if high bit set)
104    offset_or_inline: u32,
105    /// Length of the data
106    len: u32,
107    /// Pre-computed 64-bit hash (FNV-1a) for O(1) hash lookups
108    hash: u64,
109}
110
111impl BytesRef {
112    /// Flag indicating inline storage (high bit of offset)
113    const INLINE_FLAG: u32 = 0x8000_0000;
114
115    /// Create a BytesRef from arena offset
116    #[inline]
117    pub fn from_arena(offset: u32, len: u32, hash: u64) -> Self {
118        debug_assert!(offset & Self::INLINE_FLAG == 0, "offset too large");
119        Self {
120            offset_or_inline: offset,
121            len,
122            hash,
123        }
124    }
125
126    /// Create an inline BytesRef for small keys (avoids arena allocation)
127    /// Not implemented here - see InlineBytes for that pattern
128    #[inline]
129    pub fn null() -> Self {
130        Self {
131            offset_or_inline: 0,
132            len: 0,
133            hash: 0,
134        }
135    }
136
137    /// Check if this is a null/empty reference
138    #[inline]
139    pub fn is_null(&self) -> bool {
140        self.len == 0
141    }
142
143    /// Get the length of referenced bytes
144    #[inline]
145    pub fn len(&self) -> usize {
146        self.len as usize
147    }
148
149    /// Check if empty
150    #[inline]
151    pub fn is_empty(&self) -> bool {
152        self.len == 0
153    }
154
155    /// Get the pre-computed hash
156    #[inline]
157    pub fn hash(&self) -> u64 {
158        self.hash
159    }
160
161    /// Get 128-bit fingerprint for MVCC write-set tracking
162    ///
163    /// Uses the pre-computed hash and length to create a 128-bit fingerprint.
164    /// Collision probability for 10^5 keys: ~2^-128 (astronomically small)
165    #[inline]
166    pub fn fingerprint(&self) -> u128 {
167        // Combine hash with length and a mixing constant for 128-bit fingerprint
168        let upper = self.hash;
169        let lower = (self.len as u64) ^ (self.hash.rotate_right(32));
170        ((upper as u128) << 64) | (lower as u128)
171    }
172
173    /// Get offset in arena
174    #[inline]
175    pub fn offset(&self) -> u32 {
176        self.offset_or_inline & !Self::INLINE_FLAG
177    }
178
179    /// Resolve this reference to actual bytes using the arena
180    #[inline]
181    pub fn resolve<'a>(&self, arena: &'a TxnArena) -> &'a [u8] {
182        arena.get_bytes(self.offset(), self.len as usize)
183    }
184}
185
186impl PartialEq for BytesRef {
187    #[inline]
188    fn eq(&self, other: &Self) -> bool {
189        // Fast path: compare hash and length first
190        self.hash == other.hash && self.len == other.len
191    }
192}
193
194impl Eq for BytesRef {}
195
196impl Hash for BytesRef {
197    #[inline]
198    fn hash<H: Hasher>(&self, state: &mut H) {
199        state.write_u64(self.hash);
200    }
201}
202
203impl PartialOrd for BytesRef {
204    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
205        Some(self.cmp(other))
206    }
207}
208
209impl Ord for BytesRef {
210    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
211        // For ordering, we need to compare by hash (approximate)
212        // or the actual bytes if hashes match
213        self.hash
214            .cmp(&other.hash)
215            .then_with(|| self.len.cmp(&other.len))
216    }
217}
218
219// ============================================================================
220// KeyFingerprint - 128-bit Key Identifier for MVCC Write-Set
221// ============================================================================
222
223/// 128-bit fingerprint for MVCC write-set tracking
224///
225/// Replaces `HashSet<Vec<u8>>` with `HashSet<KeyFingerprint>` for:
226/// - O(1) memory per entry (16 bytes vs 24 + heap for Vec<u8>)
227/// - No allocations in write-set operations
228/// - Fast is_disjoint validation (~2^-128 collision probability)
229///
230/// ## Collision Safety
231///
232/// For 10^5 keys, collision probability is ~10^5 × 10^5 / 2^128 ≈ 10^-29
233/// This is astronomically smaller than hardware bit-flip probability.
234#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
235pub struct KeyFingerprint(pub u128);
236
237impl KeyFingerprint {
238    /// Create from raw bytes
239    #[inline]
240    pub fn from_bytes(key: &[u8]) -> Self {
241        // Use blake3 for high-quality 128-bit hash
242        let hash = blake3::hash(key);
243        let bytes = hash.as_bytes();
244        let upper = u64::from_le_bytes([
245            bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
246        ]);
247        let lower = u64::from_le_bytes([
248            bytes[8], bytes[9], bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
249        ]);
250        Self(((upper as u128) << 64) | (lower as u128))
251    }
252
253    /// Create from BytesRef
254    #[inline]
255    pub fn from_bytes_ref(bytes_ref: &BytesRef, arena: &TxnArena) -> Self {
256        Self::from_bytes(bytes_ref.resolve(arena))
257    }
258
259    /// Get the raw fingerprint value
260    #[inline]
261    pub fn value(&self) -> u128 {
262        self.0
263    }
264}
265
266impl From<&[u8]> for KeyFingerprint {
267    fn from(bytes: &[u8]) -> Self {
268        Self::from_bytes(bytes)
269    }
270}
271
272// ============================================================================
273// TxnArena - Transaction-Scoped Memory Arena
274// ============================================================================
275
276/// Transaction-scoped memory arena for zero-copy key/value handling
277///
278/// ## Design
279///
280/// - Single contiguous allocation per transaction
281/// - Bump-pointer allocation (O(1) per key/value)
282/// - Automatic cleanup when transaction completes
283/// - Pre-computes hashes at allocation time
284///
285/// ## Thread Safety
286///
287/// TxnArena is designed for single-thread use within a transaction.
288/// Multiple threads should each have their own TxnArena.
289pub struct TxnArena {
290    /// Transaction ID this arena belongs to
291    txn_id: u64,
292    /// Backing store for all keys and values
293    data: UnsafeCell<Vec<u8>>,
294    /// Current write offset (bump pointer)
295    offset: AtomicU32,
296    /// Number of keys allocated
297    key_count: AtomicU32,
298    /// Number of values allocated
299    value_count: AtomicU32,
300}
301
302// Safety: TxnArena is Send because the UnsafeCell is only accessed
303// through &self methods with proper synchronization via AtomicU32
304unsafe impl Send for TxnArena {}
305
306impl TxnArena {
307    /// Create a new transaction arena with default capacity
308    #[inline]
309    pub fn new(txn_id: u64) -> Self {
310        Self::with_capacity(txn_id, DEFAULT_ARENA_CAPACITY)
311    }
312
313    /// Create with specific capacity
314    pub fn with_capacity(txn_id: u64, capacity: usize) -> Self {
315        Self {
316            txn_id,
317            data: UnsafeCell::new(Vec::with_capacity(capacity)),
318            offset: AtomicU32::new(0),
319            key_count: AtomicU32::new(0),
320            value_count: AtomicU32::new(0),
321        }
322    }
323
324    /// Get the transaction ID
325    #[inline]
326    pub fn txn_id(&self) -> u64 {
327        self.txn_id
328    }
329
330    /// Allocate a key in the arena and return a BytesRef handle
331    ///
332    /// The hash is computed once here and cached in the BytesRef.
333    #[inline]
334    pub fn alloc_key(&self, key: &[u8]) -> BytesRef {
335        let hash = Self::compute_hash(key);
336        let (offset, len) = self.alloc_raw(key);
337        self.key_count.fetch_add(1, Ordering::Relaxed);
338        BytesRef::from_arena(offset, len as u32, hash)
339    }
340
341    /// Allocate a value in the arena and return a BytesRef handle
342    #[inline]
343    pub fn alloc_value(&self, value: &[u8]) -> BytesRef {
344        let hash = Self::compute_hash(value);
345        let (offset, len) = self.alloc_raw(value);
346        self.value_count.fetch_add(1, Ordering::Relaxed);
347        BytesRef::from_arena(offset, len as u32, hash)
348    }
349
350    /// Allocate a key-value pair and return handles to both
351    #[inline]
352    pub fn alloc_kv(&self, key: &[u8], value: &[u8]) -> (BytesRef, BytesRef) {
353        (self.alloc_key(key), self.alloc_value(value))
354    }
355
356    /// Raw allocation into the arena (bump pointer)
357    fn alloc_raw(&self, data: &[u8]) -> (u32, usize) {
358        let len = data.len();
359        if len == 0 {
360            return (0, 0);
361        }
362
363        // Get current offset and reserve space atomically
364        let offset = self.offset.fetch_add(len as u32, Ordering::Relaxed);
365
366        // Safety: We're the only writer to this offset range
367        let vec = unsafe { &mut *self.data.get() };
368
369        // Ensure capacity
370        if vec.len() < (offset as usize + len) {
371            vec.resize(offset as usize + len, 0);
372        }
373
374        // Copy data
375        vec[offset as usize..offset as usize + len].copy_from_slice(data);
376
377        (offset, len)
378    }
379
380    /// Get bytes at the given offset and length
381    #[inline]
382    pub fn get_bytes(&self, offset: u32, len: usize) -> &[u8] {
383        let vec = unsafe { &*self.data.get() };
384        &vec[offset as usize..offset as usize + len]
385    }
386
387    /// Compute FNV-1a hash for a byte slice
388    #[inline]
389    fn compute_hash(data: &[u8]) -> u64 {
390        const FNV_OFFSET_BASIS: u64 = 0xcbf29ce484222325;
391        const FNV_PRIME: u64 = 0x00000100000001B3;
392
393        let mut h = FNV_OFFSET_BASIS;
394        for &b in data {
395            h ^= b as u64;
396            h = h.wrapping_mul(FNV_PRIME);
397        }
398        h
399    }
400
401    /// Get total bytes allocated
402    #[inline]
403    pub fn bytes_used(&self) -> usize {
404        self.offset.load(Ordering::Relaxed) as usize
405    }
406
407    /// Get number of keys allocated
408    #[inline]
409    pub fn key_count(&self) -> usize {
410        self.key_count.load(Ordering::Relaxed) as usize
411    }
412
413    /// Get number of values allocated
414    #[inline]
415    pub fn value_count(&self) -> usize {
416        self.value_count.load(Ordering::Relaxed) as usize
417    }
418
419    /// Reset the arena for reuse (O(1) operation)
420    ///
421    /// Does not deallocate memory, just resets the write pointer.
422    #[inline]
423    pub fn reset(&self) {
424        self.offset.store(0, Ordering::Relaxed);
425        self.key_count.store(0, Ordering::Relaxed);
426        self.value_count.store(0, Ordering::Relaxed);
427    }
428
429    /// Create a KeyFingerprint from a BytesRef
430    #[inline]
431    pub fn fingerprint(&self, bytes_ref: &BytesRef) -> KeyFingerprint {
432        KeyFingerprint::from_bytes(bytes_ref.resolve(self))
433    }
434}
435
436impl Drop for TxnArena {
437    fn drop(&mut self) {
438        // All memory is automatically freed when the Vec is dropped
439    }
440}
441
442// ============================================================================
443// ArenaWriteSet - MVCC Write-Set Using Fingerprints
444// ============================================================================
445
446use std::collections::HashSet;
447
448/// MVCC write-set using 128-bit fingerprints instead of Vec<u8>
449///
450/// ## Memory Comparison
451///
452/// | Structure              | Per-entry Memory | 1000 entries |
453/// |------------------------|------------------|--------------|
454/// | HashSet<Vec<u8>>       | 24 + heap (~50B) | ~74 KB       |
455/// | HashSet<KeyFingerprint>| 16 bytes         | 16 KB        |
456/// | Savings                | ~70%             | ~58 KB       |
457///
458/// ## Operations
459///
460/// - Insert: O(1) with no allocation
461/// - Contains: O(1) with pre-computed hash
462/// - is_disjoint: O(min(n,m)) where n,m are set sizes
463pub struct ArenaWriteSet {
464    /// Fingerprints of keys in the write set
465    fingerprints: HashSet<KeyFingerprint>,
466}
467
468impl ArenaWriteSet {
469    /// Create a new empty write set
470    #[inline]
471    pub fn new() -> Self {
472        Self {
473            fingerprints: HashSet::new(),
474        }
475    }
476
477    /// Create with expected capacity
478    #[inline]
479    pub fn with_capacity(capacity: usize) -> Self {
480        Self {
481            fingerprints: HashSet::with_capacity(capacity),
482        }
483    }
484
485    /// Insert a key fingerprint
486    #[inline]
487    pub fn insert(&mut self, fingerprint: KeyFingerprint) -> bool {
488        self.fingerprints.insert(fingerprint)
489    }
490
491    /// Insert from raw bytes
492    #[inline]
493    pub fn insert_bytes(&mut self, key: &[u8]) -> bool {
494        self.fingerprints.insert(KeyFingerprint::from_bytes(key))
495    }
496
497    /// Check if a key is in the write set
498    #[inline]
499    pub fn contains(&self, fingerprint: &KeyFingerprint) -> bool {
500        self.fingerprints.contains(fingerprint)
501    }
502
503    /// Check if write set contains key by bytes
504    #[inline]
505    pub fn contains_bytes(&self, key: &[u8]) -> bool {
506        self.fingerprints.contains(&KeyFingerprint::from_bytes(key))
507    }
508
509    /// Check if two write sets are disjoint (no common keys)
510    #[inline]
511    pub fn is_disjoint(&self, other: &ArenaWriteSet) -> bool {
512        self.fingerprints.is_disjoint(&other.fingerprints)
513    }
514
515    /// Get the number of keys in the write set
516    #[inline]
517    pub fn len(&self) -> usize {
518        self.fingerprints.len()
519    }
520
521    /// Check if the write set is empty
522    #[inline]
523    pub fn is_empty(&self) -> bool {
524        self.fingerprints.is_empty()
525    }
526
527    /// Iterate over fingerprints
528    #[inline]
529    pub fn iter(&self) -> impl Iterator<Item = &KeyFingerprint> {
530        self.fingerprints.iter()
531    }
532
533    /// Clear the write set
534    #[inline]
535    pub fn clear(&mut self) {
536        self.fingerprints.clear();
537    }
538}
539
540impl Default for ArenaWriteSet {
541    fn default() -> Self {
542        Self::new()
543    }
544}
545
546// ============================================================================
547// TxnWriteBuffer - Zero-Copy Transaction Write Buffer
548// ============================================================================
549
550/// A write operation in the transaction buffer
551#[derive(Clone, Copy, Debug)]
552pub struct WriteOp {
553    /// Key reference
554    pub key: BytesRef,
555    /// Value reference (null for deletes)
556    pub value: BytesRef,
557    /// Is this a delete operation?
558    pub is_delete: bool,
559}
560
561/// Transaction write buffer using arena-backed references
562///
563/// Collects all writes during a transaction with zero-copy overhead.
564/// At commit time, the buffer can be flushed to WAL and memtable
565/// while resolving references to actual bytes.
566pub struct TxnWriteBuffer {
567    /// Transaction ID
568    txn_id: u64,
569    /// Arena storing all key/value bytes
570    arena: TxnArena,
571    /// Write operations (references into arena)
572    ops: Vec<WriteOp>,
573    /// Write set for SSI validation (fingerprints)
574    write_set: ArenaWriteSet,
575    /// Read set for SSI validation (fingerprints)
576    read_set: ArenaWriteSet,
577}
578
579impl TxnWriteBuffer {
580    /// Create a new transaction write buffer
581    #[inline]
582    pub fn new(txn_id: u64) -> Self {
583        Self {
584            txn_id,
585            arena: TxnArena::new(txn_id),
586            ops: Vec::with_capacity(64),
587            write_set: ArenaWriteSet::with_capacity(64),
588            read_set: ArenaWriteSet::new(),
589        }
590    }
591
592    /// Create with expected capacity
593    pub fn with_capacity(txn_id: u64, ops_capacity: usize) -> Self {
594        Self {
595            txn_id,
596            arena: TxnArena::with_capacity(txn_id, ops_capacity * 128), // ~128 bytes per op
597            ops: Vec::with_capacity(ops_capacity),
598            write_set: ArenaWriteSet::with_capacity(ops_capacity),
599            read_set: ArenaWriteSet::new(),
600        }
601    }
602
603    /// Get transaction ID
604    #[inline]
605    pub fn txn_id(&self) -> u64 {
606        self.txn_id
607    }
608
609    /// Append a write operation
610    ///
611    /// Copies key and value into arena ONCE, then stores lightweight references.
612    #[inline]
613    pub fn put(&mut self, key: &[u8], value: &[u8]) {
614        let key_ref = self.arena.alloc_key(key);
615        let val_ref = self.arena.alloc_value(value);
616
617        // Track in write set using fingerprint
618        self.write_set.insert(KeyFingerprint::from_bytes(key));
619
620        self.ops.push(WriteOp {
621            key: key_ref,
622            value: val_ref,
623            is_delete: false,
624        });
625    }
626
627    /// Append a delete operation
628    #[inline]
629    pub fn delete(&mut self, key: &[u8]) {
630        let key_ref = self.arena.alloc_key(key);
631
632        // Track in write set using fingerprint
633        self.write_set.insert(KeyFingerprint::from_bytes(key));
634
635        self.ops.push(WriteOp {
636            key: key_ref,
637            value: BytesRef::null(),
638            is_delete: true,
639        });
640    }
641
642    /// Record a read for SSI tracking
643    #[inline]
644    pub fn record_read(&mut self, key: &[u8]) {
645        self.read_set.insert_bytes(key);
646    }
647
648    /// Get the number of write operations
649    #[inline]
650    pub fn len(&self) -> usize {
651        self.ops.len()
652    }
653
654    /// Check if buffer is empty
655    #[inline]
656    pub fn is_empty(&self) -> bool {
657        self.ops.is_empty()
658    }
659
660    /// Get the write set for SSI validation
661    #[inline]
662    pub fn write_set(&self) -> &ArenaWriteSet {
663        &self.write_set
664    }
665
666    /// Get the read set for SSI validation
667    #[inline]
668    pub fn read_set(&self) -> &ArenaWriteSet {
669        &self.read_set
670    }
671
672    /// Get bytes used by the arena
673    #[inline]
674    pub fn bytes_used(&self) -> usize {
675        self.arena.bytes_used()
676    }
677
678    /// Iterate over write operations
679    #[inline]
680    pub fn iter(&self) -> impl Iterator<Item = &WriteOp> {
681        self.ops.iter()
682    }
683
684    /// Iterate over write operations with resolved bytes
685    pub fn iter_resolved(&self) -> impl Iterator<Item = (&[u8], Option<&[u8]>, bool)> {
686        self.ops.iter().map(move |op| {
687            let key = op.key.resolve(&self.arena);
688            let value = if op.is_delete {
689                None
690            } else {
691                Some(op.value.resolve(&self.arena))
692            };
693            (key, value, op.is_delete)
694        })
695    }
696
697    /// Get the arena for resolving references
698    #[inline]
699    pub fn arena(&self) -> &TxnArena {
700        &self.arena
701    }
702
703    /// Clear the buffer for reuse
704    pub fn clear(&mut self) {
705        self.ops.clear();
706        self.write_set.clear();
707        self.read_set.clear();
708        self.arena.reset();
709    }
710}
711
712// ============================================================================
713// Tests
714// ============================================================================
715
716#[cfg(test)]
717mod tests {
718    use super::*;
719
720    #[test]
721    fn test_txn_arena_basic() {
722        let arena = TxnArena::new(1);
723
724        let key_ref = arena.alloc_key(b"users/12345");
725        let val_ref = arena.alloc_value(b"Alice");
726
727        assert_eq!(key_ref.resolve(&arena), b"users/12345");
728        assert_eq!(val_ref.resolve(&arena), b"Alice");
729        assert_eq!(arena.key_count(), 1);
730        assert_eq!(arena.value_count(), 1);
731    }
732
733    #[test]
734    fn test_bytes_ref_hash() {
735        let arena = TxnArena::new(1);
736
737        let key1 = arena.alloc_key(b"test_key");
738        let key2 = arena.alloc_key(b"test_key");
739        let key3 = arena.alloc_key(b"other_key");
740
741        assert_eq!(key1.hash(), key2.hash());
742        assert_ne!(key1.hash(), key3.hash());
743    }
744
745    #[test]
746    fn test_key_fingerprint() {
747        let fp1 = KeyFingerprint::from_bytes(b"test_key");
748        let fp2 = KeyFingerprint::from_bytes(b"test_key");
749        let fp3 = KeyFingerprint::from_bytes(b"other_key");
750
751        assert_eq!(fp1, fp2);
752        assert_ne!(fp1, fp3);
753    }
754
755    #[test]
756    fn test_arena_write_set() {
757        let mut ws1 = ArenaWriteSet::new();
758        let mut ws2 = ArenaWriteSet::new();
759
760        ws1.insert_bytes(b"key1");
761        ws1.insert_bytes(b"key2");
762
763        ws2.insert_bytes(b"key3");
764        ws2.insert_bytes(b"key4");
765
766        assert!(ws1.is_disjoint(&ws2));
767
768        ws2.insert_bytes(b"key1");
769        assert!(!ws1.is_disjoint(&ws2));
770    }
771
772    #[test]
773    fn test_txn_write_buffer() {
774        let mut buffer = TxnWriteBuffer::new(42);
775
776        buffer.put(b"key1", b"value1");
777        buffer.put(b"key2", b"value2");
778        buffer.delete(b"key3");
779
780        assert_eq!(buffer.len(), 3);
781        assert_eq!(buffer.write_set().len(), 3);
782
783        let ops: Vec<_> = buffer.iter_resolved().collect();
784        assert_eq!(
785            ops[0],
786            (b"key1".as_slice(), Some(b"value1".as_slice()), false)
787        );
788        assert_eq!(
789            ops[1],
790            (b"key2".as_slice(), Some(b"value2".as_slice()), false)
791        );
792        assert_eq!(ops[2], (b"key3".as_slice(), None, true));
793    }
794
795    #[test]
796    fn test_arena_reset() {
797        let arena = TxnArena::new(1);
798
799        for i in 0..100 {
800            let key = format!("key_{}", i);
801            arena.alloc_key(key.as_bytes());
802        }
803
804        assert_eq!(arena.key_count(), 100);
805        let used_before = arena.bytes_used();
806        assert!(used_before > 0);
807
808        arena.reset();
809
810        assert_eq!(arena.key_count(), 0);
811        assert_eq!(arena.bytes_used(), 0);
812    }
813}