Skip to main content

sochdb_storage/
durable_storage.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//! Durable Storage Layer
19//!
20//! Wires together the live storage components into a durable engine:
21//!
22//! - WAL (txn_wal.rs) for crash-consistent durability + recovery
23//! - Group Commit for throughput
24//! - MVCC for isolation
25//! - LSCS for columnar efficiency
26//!
27//! Truth-in-capabilities: the live path provides crash-consistent WAL recovery,
28//! but NOT at-rest encryption, point-in-time recovery, ARIES checkpointing, or
29//! WAL fencing — those modules exist but are quarantined behind the empty,
30//! non-default `experimental` feature and are unwired. Query
31//! [`crate::durability_capabilities`] rather than relying on prose like
32//! "production-grade".
33//!
34//! ## Architecture
35//!
36//! ```text
37//! ┌─────────────────────────────────────────────────────────────────┐
38//! │                      DurableStorage                              │
39//! ├─────────────────────────────────────────────────────────────────┤
40//! │  ┌─────────────┐    ┌─────────────┐    ┌─────────────────────┐ │
41//! │  │ MvccManager │    │ GroupCommit │───▶│ TxnWal (fsync)      │ │
42//! │  │             │    │             │    └─────────────────────┘ │
43//! │  │ ┌─────────┐ │    └─────────────┘                            │
44//! │  │ │Snapshots│ │                                                │
45//! │  │ └─────────┘ │    ┌─────────────────────────────────────────┐│
46//! │  │ ┌─────────┐ │    │              MemTable                    ││
47//! │  │ │ Txn Map │ │    │  (key → (value, txn_id, version))       ││
48//! │  │ └─────────┘ │    └─────────────────────────────────────────┘│
49//! │  └─────────────┘                                                │
50//! │                      ┌─────────────────────────────────────────┐│
51//! │                      │              LSCS (SST)                  ││
52//! │                      │  Immutable columnar segments             ││
53//! │                      └─────────────────────────────────────────┘│
54//! └─────────────────────────────────────────────────────────────────┘
55//! ```
56//!
57//! ## Concurrency
58//!
59//! - Writers: Serialize through WAL, use MVCC for conflict detection
60//! - Readers: Lock-free reads at snapshot timestamp
61//! - Commits: Batched through GroupCommit for throughput
62//!
63//! ## Isolation Contract
64//!
65//! The live write path (`MvccManager`, used by `DurableStorage`) provides
66//! **Serializable Snapshot Isolation (SSI)**, which is strictly stronger than
67//! plain Snapshot Isolation (SI):
68//!
69//! - **Snapshot reads.** Every transaction reads from a consistent snapshot
70//!   fixed at `begin_transaction()` (its `snapshot_ts`). Concurrent commits are
71//!   invisible to it — see `test_snapshot_isolation`. This alone is SI and is
72//!   vulnerable to **write skew** (two transactions each read a set the other
73//!   writes, both commit, and no serial order reproduces the result).
74//! - **Write-skew prevention.** On commit, `MvccManager::validate_ssi` inspects
75//!   recently-committed concurrent transactions for rw-antidependency edges:
76//!   an inbound edge (another txn wrote a key we read, `T_other →rw→ T_me`) and
77//!   an outbound edge (we wrote a key another txn read, `T_me →rw→ T_other`).
78//!   When a transaction sits on **both** an inbound and an outbound rw-edge it
79//!   is the pivot of a potential dependency cycle (Cahill/Fekete "dangerous
80//!   structure"), so it is aborted. This is the conservative safe subset of the
81//!   SSI test: it may abort some serializable schedules (false positives) but
82//!   **never admits a non-serializable one** (no false negatives), so the
83//!   externally observable isolation level is Serializable.
84//! - **Read-only transactions** (`begin_read_only`) skip read-set tracking and
85//!   never participate in validation; they always observe a serializable
86//!   snapshot and never abort.
87//!
88//! ### MVCC garbage collection is snapshot-safe
89//!
90//! Old versions are pruned by `DurableStorage::gc()`, which feeds the
91//! **low-water-mark** `MvccManager::min_active_snapshot()` — the minimum
92//! `snapshot_ts` across all still-active transactions — into
93//! `MvccMemTable::gc()` and then `BinarySearchChain::gc_by_ts()`. The chain
94//! retains every version with `commit_ts > min_active_ts` **plus one anchor
95//! version at or below the watermark**, so any in-flight reader can still
96//! resolve the correct version for its snapshot. A version is only freed once
97//! **no active snapshot can observe it**. The watermark is recomputed on every
98//! `begin`/`commit`/`abort`, so it is monotonic with respect to the oldest live
99//! reader. See `test_gc_preserves_versions_for_active_snapshot`.
100
101use std::collections::HashSet;
102use std::path::{Path, PathBuf};
103use std::sync::Arc;
104use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
105
106use dashmap::DashMap;
107use smallvec::SmallVec;
108
109use crossbeam_skiplist::SkipMap;
110
111use crate::DurabilityCapabilities;
112use crate::deferred_index::{DeferredIndexConfig, DeferredSortedIndex};
113use crate::encryption::EncryptionKey;
114use crate::group_commit::EventDrivenGroupCommit;
115use crate::keyring;
116use crate::txn_wal::{TxnWal, TxnWalBuffer, TxnWalEntry};
117use sochdb_core::version_chain::{
118    BinarySearchChain, ChainEntry, MvccVersionChain, MvccVersionChainMut, Timestamp, TxnId,
119    VisibilityContext, WriteConflictDetection,
120};
121use sochdb_core::{Result, SochDBError};
122
123// =============================================================================
124// SSI Bloom Filter - Fast Conflict Pre-Filtering
125// =============================================================================
126
127/// Space-efficient Bloom filter for SSI conflict detection
128///
129/// Used to quickly determine if two transactions MIGHT have conflicting keys.
130/// False positives are acceptable (leads to unnecessary exact checks),
131/// but false negatives are not allowed.
132///
133/// ## Configuration
134///
135/// For 1000 keys with 1% false positive rate:
136/// - m = ~9600 bits ≈ 1.2 KB per transaction
137/// - k = 7 hash functions
138///
139/// ## Lazy Initialization
140///
141/// The bit vector is lazily initialized on first insert to avoid
142/// allocation overhead for read-only transactions.
143#[derive(Clone, Debug)]
144pub struct SsiBloomFilter {
145    /// Bit vector (each u64 holds 64 bits) - lazily initialized
146    bits: Option<Vec<u64>>,
147    /// Expected capacity (used for lazy init sizing)
148    expected_capacity: usize,
149    /// Number of hash functions to use
150    num_hashes: u32,
151}
152
153impl SsiBloomFilter {
154    /// Optimal number of bits per item for 1% false positive rate
155    /// m/n = -ln(p) / (ln(2))² ≈ 9.6 for p = 0.01
156    const BITS_PER_ITEM: f64 = 9.6;
157
158    /// Optimal number of hash functions for 1% false positive rate
159    /// k = (m/n) × ln(2) ≈ 7
160    const DEFAULT_NUM_HASHES: u32 = 7;
161
162    /// Minimum capacity to avoid tiny filters
163    const MIN_CAPACITY: usize = 64;
164
165    /// Create a new bloom filter for expected item count (lazy allocation)
166    ///
167    /// Configured for ~1% false positive rate.
168    /// The bit vector is not allocated until first insert.
169    #[inline]
170    pub fn new(expected_items: usize) -> Self {
171        Self {
172            bits: None,
173            expected_capacity: expected_items.max(Self::MIN_CAPACITY),
174            num_hashes: Self::DEFAULT_NUM_HASHES,
175        }
176    }
177
178    /// Create with specific capacity in words (for memory-constrained scenarios)
179    pub fn with_word_capacity(words: usize) -> Self {
180        Self {
181            bits: None,
182            expected_capacity: words.max(1) * 64 / 10, // Approx items from words
183            num_hashes: Self::DEFAULT_NUM_HASHES,
184        }
185    }
186
187    /// Ensure bits are allocated (lazy initialization)
188    #[inline]
189    fn ensure_allocated(&mut self) {
190        if self.bits.is_none() {
191            let num_bits = ((self.expected_capacity as f64) * Self::BITS_PER_ITEM).ceil() as usize;
192            let num_words = num_bits.div_ceil(64);
193            self.bits = Some(vec![0u64; num_words]);
194        }
195    }
196
197    /// Add a key to the filter - O(k) where k = num_hashes
198    #[inline]
199    pub fn insert(&mut self, key: &[u8]) {
200        self.ensure_allocated();
201        let bits = self.bits.as_mut().unwrap();
202        let num_bits = bits.len() * 64;
203        if num_bits == 0 {
204            return;
205        }
206
207        // Use two hash functions to simulate k hash functions
208        // h(i) = h1 + i * h2 (double hashing technique)
209        let h1 = Self::hash1(key);
210        let h2 = Self::hash2(key);
211
212        for i in 0..self.num_hashes {
213            let h = h1.wrapping_add((i as u64).wrapping_mul(h2));
214            let bit_idx = (h as usize) % num_bits;
215            let word_idx = bit_idx / 64;
216            let bit_pos = bit_idx % 64;
217            bits[word_idx] |= 1 << bit_pos;
218        }
219    }
220
221    /// Check if a key might be present - O(k)
222    ///
223    /// Returns:
224    /// - false: Key is definitely NOT in the set (or filter not initialized)
225    /// - true: Key MIGHT be in the set (needs exact check)
226    #[inline]
227    pub fn may_contain(&self, key: &[u8]) -> bool {
228        let bits = match &self.bits {
229            Some(b) => b,
230            None => return false, // Uninitialized = empty
231        };
232        let num_bits = bits.len() * 64;
233        if num_bits == 0 {
234            return false;
235        }
236
237        let h1 = Self::hash1(key);
238        let h2 = Self::hash2(key);
239
240        for i in 0..self.num_hashes {
241            let h = h1.wrapping_add((i as u64).wrapping_mul(h2));
242            let bit_idx = (h as usize) % num_bits;
243            let word_idx = bit_idx / 64;
244            let bit_pos = bit_idx % 64;
245            if bits[word_idx] & (1 << bit_pos) == 0 {
246                return false; // Definitely not present
247            }
248        }
249        true // Might be present
250    }
251
252    /// Check if this filter might intersect with another
253    ///
254    /// Fast O(m/64) check using bitwise AND of all words.
255    /// If no bits are shared, sets are definitely disjoint.
256    #[inline]
257    pub fn may_intersect(&self, other: &SsiBloomFilter) -> bool {
258        let (self_bits, other_bits) = match (&self.bits, &other.bits) {
259            (Some(s), Some(o)) => (s, o),
260            _ => return false, // Either uninitialized = no intersection
261        };
262        let min_len = self_bits.len().min(other_bits.len());
263        for i in 0..min_len {
264            if self_bits[i] & other_bits[i] != 0 {
265                return true; // Might intersect
266            }
267        }
268        false // Definitely disjoint
269    }
270
271    /// First hash function (using built-in hasher)
272    #[inline]
273    fn hash1(key: &[u8]) -> u64 {
274        use std::collections::hash_map::DefaultHasher;
275        use std::hash::{Hash, Hasher};
276        let mut hasher = DefaultHasher::new();
277        key.hash(&mut hasher);
278        hasher.finish()
279    }
280
281    /// Second hash function (using twox-hash for independence)
282    #[inline]
283    fn hash2(key: &[u8]) -> u64 {
284        twox_hash::xxh3::hash64(key)
285    }
286
287    /// Get the memory size in bytes
288    pub fn size_bytes(&self) -> usize {
289        self.bits.as_ref().map(|b| b.len() * 8).unwrap_or(0) + std::mem::size_of::<Self>()
290    }
291
292    /// Check if the filter is empty
293    pub fn is_empty(&self) -> bool {
294        match &self.bits {
295            Some(bits) => bits.iter().all(|&w| w == 0),
296            None => true,
297        }
298    }
299}
300
301/// Type alias for inline key storage - keys up to 32 bytes stored on stack
302/// This eliminates heap allocation for typical keys like "users/12345" (12 bytes)
303pub type InlineKey = SmallVec<[u8; 32]>;
304
305/// Version of a key-value pair
306#[derive(Debug, Clone)]
307pub struct Version {
308    /// The value (None = tombstone)
309    pub value: Option<Vec<u8>>,
310    /// Transaction that created this version
311    pub txn_id: u64,
312    /// Commit timestamp (0 = uncommitted)
313    pub commit_ts: u64,
314}
315
316// Rec 11: Implement ChainEntry so BinarySearchChain<Version> works
317impl ChainEntry for Version {
318    #[inline]
319    fn commit_ts(&self) -> u64 {
320        self.commit_ts
321    }
322    #[inline]
323    fn txn_id(&self) -> u64 {
324        self.txn_id
325    }
326    #[inline]
327    fn set_commit_ts(&mut self, ts: u64) {
328        self.commit_ts = ts;
329    }
330}
331
332// ============================================================================
333// Optimized VersionChain with Binary Search (Task 1: mm.md)
334// ============================================================================
335
336/// Multi-version data for a single key with O(log v) read complexity
337///
338/// ## Optimization: Binary Search with Sorted Commit Ordering
339///
340/// Separates committed versions (sorted descending by commit_ts) from
341/// uncommitted version (single optional slot per transaction).
342///
343/// **Before:** O(v) linear scan + O(v) max computation = O(v)
344/// **After:** O(1) uncommitted check + O(log v) binary search = O(log v)
345///
346/// For v=10 versions: 3.3x speedup
347/// For v=100 versions: 7x speedup
348///
349/// ## Rec 11: Consolidated
350///
351/// Delegates binary-search logic to `BinarySearchChain<Version>` from sochdb-core,
352/// eliminating duplication with `mvcc_concurrent::VersionChain`.
353#[derive(Debug, Default)]
354pub struct VersionChain {
355    /// Consolidated binary-search chain (Rec 11)
356    inner: BinarySearchChain<Version>,
357}
358
359impl VersionChain {
360    /// Create a new empty version chain
361    #[inline]
362    pub fn new() -> Self {
363        Self {
364            inner: BinarySearchChain::new(),
365        }
366    }
367
368    /// Add a new uncommitted version
369    /// If there's already an uncommitted version from this txn, update it in place
370    ///
371    /// O(1) - just updates the uncommitted slot
372    #[inline]
373    pub fn add_uncommitted(&mut self, value: Option<Vec<u8>>, txn_id: u64) {
374        match self.inner.uncommitted_mut() {
375            Some(v) if v.txn_id == txn_id => {
376                // Update in place - O(1)
377                v.value = value;
378            }
379            _ => {
380                // New or different txn — set the slot
381                self.inner.set_uncommitted(Version {
382                    value,
383                    txn_id,
384                    commit_ts: 0,
385                });
386            }
387        }
388    }
389
390    /// Commit a version - moves from uncommitted slot to sorted committed list
391    ///
392    /// O(log v) - inserts into sorted position using binary search
393    #[inline]
394    pub fn commit(&mut self, txn_id: u64, commit_ts: u64) -> bool {
395        self.inner.commit(txn_id, commit_ts)
396    }
397
398    /// Abort a version (remove uncommitted version for txn)
399    ///
400    /// O(1) - just clears the uncommitted slot if it matches
401    #[inline]
402    pub fn abort(&mut self, txn_id: u64) {
403        self.inner.abort(txn_id);
404    }
405
406    /// Read at a snapshot timestamp, optionally seeing own uncommitted writes
407    ///
408    /// ## Complexity: O(1) + O(log v) = O(log v)
409    #[inline]
410    pub fn read_at(&self, snapshot_ts: u64, current_txn_id: Option<u64>) -> Option<&Version> {
411        self.inner.read_at(snapshot_ts, current_txn_id)
412    }
413
414    /// Check if there's an uncommitted version by another transaction
415    ///
416    /// O(1) - just checks the uncommitted slot
417    #[inline]
418    pub fn has_write_conflict(&self, my_txn_id: u64) -> bool {
419        self.inner.has_write_conflict(my_txn_id)
420    }
421
422    /// Garbage collect old versions
423    pub fn gc(&mut self, min_active_ts: u64) {
424        self.inner.gc_by_ts(min_active_ts);
425    }
426
427    /// Get total version count (committed + uncommitted)
428    #[inline]
429    pub fn version_count(&self) -> usize {
430        self.inner.version_count()
431    }
432
433    // Legacy compatibility: get versions vec (for tests)
434    #[cfg(test)]
435    pub fn versions(&self) -> Vec<Version> {
436        let mut result = self.inner.committed_versions().to_vec();
437        if let Some(v) = self.inner.uncommitted() {
438            result.push(v.clone());
439        }
440        result
441    }
442}
443
444// =============================================================================
445// Rec 6: Unified Version Chain Trait Implementations
446// =============================================================================
447
448impl MvccVersionChain for VersionChain {
449    type Value = Option<Vec<u8>>;
450
451    fn get_visible(&self, ctx: &VisibilityContext) -> Option<&Self::Value> {
452        // Delegate to BinarySearchChain, then project to value field
453        self.inner
454            .read_at(ctx.snapshot_ts, Some(ctx.reader_txn_id))
455            .map(|v| &v.value)
456    }
457
458    fn get_latest(&self) -> Option<&Self::Value> {
459        self.inner.latest().map(|v| &v.value)
460    }
461
462    fn version_count(&self) -> usize {
463        self.inner.version_count()
464    }
465}
466
467impl MvccVersionChainMut for VersionChain {
468    fn add_uncommitted(&mut self, value: Self::Value, txn_id: TxnId) {
469        self.add_uncommitted(value, txn_id);
470    }
471
472    fn commit_version(&mut self, txn_id: TxnId, commit_ts: Timestamp) -> bool {
473        self.inner.commit(txn_id, commit_ts)
474    }
475
476    fn delete_version(&mut self, txn_id: TxnId, _delete_ts: Timestamp) -> bool {
477        // Insert a tombstone (None value) as uncommitted
478        self.add_uncommitted(None, txn_id);
479        true
480    }
481
482    fn gc(&mut self, min_visible_ts: Timestamp) -> (usize, usize) {
483        let before = self.inner.committed_count();
484        self.inner.gc_by_ts(min_visible_ts);
485        let removed = before - self.inner.committed_count();
486        (removed, removed * std::mem::size_of::<Version>())
487    }
488}
489
490impl WriteConflictDetection for VersionChain {
491    fn has_write_conflict(&self, txn_id: TxnId) -> bool {
492        self.has_write_conflict(txn_id)
493    }
494}
495
496// =============================================================================
497// Pre-sizing Constants to Avoid HashSet Resize Overhead
498// =============================================================================
499
500/// Default capacity for write_set HashSet
501/// Sized for typical OLTP transactions (10-50 keys)
502/// Avoids resize overhead that caused +11% regression
503const WRITE_SET_INITIAL_CAPACITY: usize = 32;
504
505/// Default capacity for read_set HashSet  
506/// Typically larger than write_set due to read-heavy patterns
507const READ_SET_INITIAL_CAPACITY: usize = 64;
508
509/// Transaction state for MVCC
510#[derive(Debug, Clone)]
511pub struct MvccTransaction {
512    /// Transaction ID
513    pub txn_id: u64,
514    /// Snapshot timestamp (reads see commits before this)
515    pub snapshot_ts: u64,
516    /// Keys written by this transaction - uses SmallVec for inline storage
517    /// Pre-sized to WRITE_SET_INITIAL_CAPACITY to avoid resize overhead
518    pub write_set: HashSet<InlineKey>,
519    /// Keys read by this transaction (for SSI validation) - uses SmallVec for inline storage
520    /// Pre-sized to READ_SET_INITIAL_CAPACITY to avoid resize overhead
521    pub read_set: HashSet<InlineKey>,
522    /// Bloom filter for write set - fast SSI pre-filtering
523    pub write_bloom: SsiBloomFilter,
524    /// Bloom filter for read set - fast SSI pre-filtering
525    pub read_bloom: SsiBloomFilter,
526    /// Transaction state
527    pub state: TxnState,
528    /// Transaction mode for SSI optimization (Recommendation 9)
529    /// ReadOnly/WriteOnly modes skip SSI tracking for 2.6x improvement
530    pub mode: TransactionMode,
531}
532
533impl MvccTransaction {
534    /// Create a new transaction with pre-sized collections
535    ///
536    /// This avoids HashSet resize overhead during the transaction
537    /// which was causing +11% regression on write_set.insert().
538    #[inline]
539    pub fn new(txn_id: u64, snapshot_ts: u64) -> Self {
540        Self::with_mode(txn_id, snapshot_ts, TransactionMode::ReadWrite)
541    }
542
543    /// Create a read-only transaction (SSI bypass - 2.6x faster)
544    ///
545    /// Read-only transactions skip all SSI tracking:
546    /// - No read_set allocation
547    /// - No read_bloom allocation  
548    /// - No commit validation
549    ///
550    /// ## Performance
551    ///
552    /// For N=100 reads: 8350ns → 3230ns (2.6× improvement)
553    #[inline]
554    pub fn read_only(txn_id: u64, snapshot_ts: u64) -> Self {
555        Self::with_mode(txn_id, snapshot_ts, TransactionMode::ReadOnly)
556    }
557
558    /// Create a write-only transaction (partial SSI bypass)
559    ///
560    /// Write-only transactions skip read tracking:
561    /// - No read_set tracking
562    /// - No read_bloom inserts
563    /// - Still needs write_set for commit
564    #[inline]
565    pub fn write_only(txn_id: u64, snapshot_ts: u64) -> Self {
566        Self::with_mode(txn_id, snapshot_ts, TransactionMode::WriteOnly)
567    }
568
569    /// Create transaction with specific mode
570    #[inline]
571    pub fn with_mode(txn_id: u64, snapshot_ts: u64, mode: TransactionMode) -> Self {
572        // Optimize allocation based on mode
573        let (write_capacity, read_capacity) = match mode {
574            TransactionMode::ReadOnly => (0, 0), // No tracking needed
575            TransactionMode::WriteOnly => (WRITE_SET_INITIAL_CAPACITY, 0),
576            TransactionMode::ReadWrite => (WRITE_SET_INITIAL_CAPACITY, READ_SET_INITIAL_CAPACITY),
577        };
578        Self::with_capacity(txn_id, snapshot_ts, write_capacity, read_capacity, mode)
579    }
580
581    /// Create with custom capacities for expected workload
582    ///
583    /// Use this when you know the transaction will write many keys
584    /// to avoid resize overhead entirely.
585    #[inline]
586    pub fn with_capacity(
587        txn_id: u64,
588        snapshot_ts: u64,
589        write_capacity: usize,
590        read_capacity: usize,
591        mode: TransactionMode,
592    ) -> Self {
593        Self {
594            txn_id,
595            snapshot_ts,
596            write_set: HashSet::with_capacity(write_capacity),
597            read_set: HashSet::with_capacity(read_capacity),
598            write_bloom: SsiBloomFilter::new(write_capacity.max(1)),
599            read_bloom: SsiBloomFilter::new(read_capacity.max(1)),
600            state: TxnState::Active,
601            mode,
602        }
603    }
604
605    /// Check if this is a read-only transaction
606    #[inline]
607    pub fn is_read_only(&self) -> bool {
608        self.write_set.is_empty()
609    }
610
611    /// Check if this is a single-key write transaction
612    #[inline]
613    pub fn is_single_key_write(&self) -> bool {
614        self.write_set.len() == 1 && self.read_set.len() <= 1
615    }
616}
617
618/// Transaction state
619#[derive(Debug, Clone, Copy, PartialEq, Eq)]
620pub enum TxnState {
621    Active,
622    Committed,
623    Aborted,
624}
625
626// =============================================================================
627// Transaction Mode for SSI Bypass (Recommendation 9)
628// =============================================================================
629
630/// Transaction mode for SSI optimization
631///
632/// By classifying transactions at begin time, we can skip expensive SSI
633/// tracking for the majority of transactions:
634///
635/// | Mode      | SSI Read Tracking | SSI Write Tracking | Commit Overhead |
636/// |-----------|-------------------|--------------------|-----------------|
637/// | ReadOnly  | None             | None               | ~10 ns          |
638/// | WriteOnly | None             | Full               | ~30 ns          |
639/// | ReadWrite | Full             | Full               | ~50 ns          |
640///
641/// ## Performance Analysis
642///
643/// For read-only transactions (typically 90% of workload):
644/// ```text
645/// Current:  T_txn = T_begin + N × (T_read + T_record) + T_commit
646///                 = 100ns + N × (32ns + 50ns) + 50ns = 150ns + 82ns × N
647///
648/// ReadOnly: T_txn = T_begin_ro + N × T_read + T_commit_ro
649///                 = 20ns + N × 32ns + 10ns = 30ns + 32ns × N
650///
651/// For N=100 reads: 8350ns → 3230ns (2.6× faster)
652/// ```
653#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
654pub enum TransactionMode {
655    /// Read-only transaction - skips ALL SSI tracking
656    /// Cannot form rw-antidependency cycles (no writes to create outgoing edges)
657    /// Safe to skip read_set, read_bloom, and commit validation entirely
658    ReadOnly,
659
660    /// Write-only transaction - skips read tracking
661    /// Cannot form incoming rw-edges (no reads from concurrent writers)
662    /// Only needs write_set and write_bloom tracking
663    WriteOnly,
664
665    /// Full read-write transaction (default) - complete SSI tracking
666    /// May form both incoming and outgoing rw-edges
667    /// Requires full validation at commit time
668    #[default]
669    ReadWrite,
670}
671
672impl TransactionMode {
673    /// Check if this mode requires read tracking
674    #[inline]
675    pub fn tracks_reads(&self) -> bool {
676        matches!(self, TransactionMode::ReadWrite)
677    }
678
679    /// Check if this mode requires write tracking
680    #[inline]
681    pub fn tracks_writes(&self) -> bool {
682        matches!(
683            self,
684            TransactionMode::WriteOnly | TransactionMode::ReadWrite
685        )
686    }
687
688    /// Check if commit needs SSI validation
689    #[inline]
690    pub fn needs_ssi_validation(&self) -> bool {
691        matches!(self, TransactionMode::ReadWrite)
692    }
693}
694
695/// SSI conflict edge type
696#[derive(Debug, Clone, Copy, PartialEq, Eq)]
697pub enum ConflictType {
698    /// Read-write conflict: T1 reads X, then T2 writes X
699    ReadWrite,
700    /// Write-read conflict: T1 writes X, then T2 reads X  
701    WriteRead,
702}
703
704/// SSI conflict edge for dangerous structure detection
705#[derive(Debug, Clone)]
706pub struct ConflictEdge {
707    /// Source transaction
708    pub from_txn: u64,
709    /// Target transaction
710    pub to_txn: u64,
711    /// Type of conflict
712    pub conflict_type: ConflictType,
713}
714
715/// MVCC Manager with SSI support
716///
717/// Uses DashMap for lock-free per-transaction access.
718/// Implements Serializable Snapshot Isolation (SSI) with
719/// dangerous structure detection for rw-antidependency cycles.
720#[allow(clippy::type_complexity)]
721pub struct MvccManager {
722    /// Active transactions (sharded for concurrent access)
723    active_txns: DashMap<u64, MvccTransaction>,
724    /// Current timestamp counter
725    ts_counter: AtomicU64,
726    /// Minimum active snapshot timestamp (for GC)
727    min_active_ts: AtomicU64,
728    /// Refcounted multiset of active snapshot timestamps, ordered.
729    ///
730    /// Maintained incrementally on txn begin/end so the GC watermark
731    /// (`min_active_ts`) is O(log N) per begin/commit instead of an O(active_txns)
732    /// full `active_txns` DashMap scan (which locked every shard on every
733    /// begin AND commit — the dominant on-CPU cost under concurrent writes).
734    /// A multiset (count per ts) is required because `begin` reads ts_counter
735    /// without incrementing, so concurrent txns can share a snapshot_ts.
736    active_snapshots: parking_lot::Mutex<std::collections::BTreeMap<u64, u32>>,
737    /// Recently committed transactions for SSI validation
738    /// Maps txn_id -> (commit_ts, read_bloom, write_bloom, read_set, write_set)
739    /// Bloom filters enable fast O(m/64) pre-filtering before O(n) exact checks
740    recent_commits: DashMap<
741        u64,
742        (
743            u64,
744            SsiBloomFilter,
745            SsiBloomFilter,
746            HashSet<InlineKey>,
747            HashSet<InlineKey>,
748        ),
749    >,
750    /// Max recent commits to track
751    max_recent_commits: usize,
752}
753
754impl Default for MvccManager {
755    fn default() -> Self {
756        Self::new()
757    }
758}
759
760impl MvccManager {
761    pub fn new() -> Self {
762        Self {
763            active_txns: DashMap::new(),
764            ts_counter: AtomicU64::new(1),
765            min_active_ts: AtomicU64::new(0),
766            active_snapshots: parking_lot::Mutex::new(std::collections::BTreeMap::new()),
767            recent_commits: DashMap::new(),
768            max_recent_commits: 1000, // Track last 1000 commits for SSI
769        }
770    }
771
772    /// Begin a new transaction with snapshot isolation
773    ///
774    /// Uses pre-sized HashSets to avoid resize overhead (+11% regression fix)
775    pub fn begin(&self, txn_id: u64) -> MvccTransaction {
776        self.begin_with_mode(txn_id, TransactionMode::ReadWrite)
777    }
778
779    /// Begin a read-only transaction (SSI bypass - 2.6x faster)
780    ///
781    /// Read-only transactions skip all SSI tracking, reducing
782    /// per-read overhead from ~82ns to ~32ns.
783    ///
784    /// ## Safety
785    ///
786    /// Caller must ensure no writes are performed. Attempting to
787    /// write in a read-only transaction will still succeed but
788    /// won't be tracked for SSI validation.
789    #[inline]
790    pub fn begin_read_only(&self, txn_id: u64) -> MvccTransaction {
791        self.begin_with_mode(txn_id, TransactionMode::ReadOnly)
792    }
793
794    /// Begin a write-only transaction (partial SSI bypass)
795    ///
796    /// Write-only transactions skip read tracking, reducing overhead
797    /// for insert-heavy workloads.
798    #[inline]
799    pub fn begin_write_only(&self, txn_id: u64) -> MvccTransaction {
800        self.begin_with_mode(txn_id, TransactionMode::WriteOnly)
801    }
802
803    /// Begin a transaction with specific mode
804    ///
805    /// This is the core transaction creation method that all other
806    /// begin_* methods delegate to.
807    pub fn begin_with_mode(&self, txn_id: u64, mode: TransactionMode) -> MvccTransaction {
808        let snapshot_ts = self.ts_counter.load(Ordering::SeqCst);
809
810        // Create transaction with mode-optimized allocations
811        let txn = MvccTransaction::with_mode(txn_id, snapshot_ts, mode);
812
813        self.active_txns.insert(txn_id, txn.clone());
814        self.track_active_begin(snapshot_ts);
815
816        txn
817    }
818
819    /// Get transaction if active (clones - use get_snapshot_ts for hot path)
820    pub fn get(&self, txn_id: u64) -> Option<MvccTransaction> {
821        self.active_txns.get(&txn_id).map(|t| t.clone())
822    }
823
824    /// Fast path: get just the snapshot timestamp without cloning
825    /// This is the hot path for reads - avoids cloning bloom filters
826    #[inline]
827    pub fn get_snapshot_ts(&self, txn_id: u64) -> Option<u64> {
828        self.active_txns.get(&txn_id).map(|t| t.snapshot_ts)
829    }
830
831    /// Record a read (for SSI) - uses inline key storage + bloom filter
832    ///
833    /// ## SSI Bypass (Recommendation 9)
834    ///
835    /// For ReadOnly mode transactions, this is a no-op (instant return).
836    /// For WriteOnly mode transactions, this is a no-op.
837    /// Only ReadWrite mode transactions track reads for SSI.
838    ///
839    /// This reduces per-read overhead from ~50ns to ~0ns for read-only txns.
840    #[inline]
841    pub fn record_read(&self, txn_id: u64, key: &[u8]) {
842        if let Some(mut txn) = self.active_txns.get_mut(&txn_id) {
843            // SSI Bypass: Skip tracking for read-only and write-only modes
844            if !txn.mode.tracks_reads() {
845                return;
846            }
847
848            // Only track reads if within reasonable bounds
849            if txn.read_set.len() < 10000 {
850                txn.read_set.insert(SmallVec::from_slice(key));
851                txn.read_bloom.insert(key);
852            }
853        }
854    }
855
856    /// Record a write - uses inline key storage + bloom filter
857    ///
858    /// Note: Even ReadOnly transactions can record writes (mode is a hint).
859    /// The mode only affects SSI tracking, not write capability.
860    pub fn record_write(&self, txn_id: u64, key: &[u8]) {
861        if let Some(mut txn) = self.active_txns.get_mut(&txn_id) {
862            txn.write_set.insert(SmallVec::from_slice(key));
863            txn.write_bloom.insert(key);
864        }
865    }
866
867    /// Allocate commit timestamp
868    pub fn alloc_commit_ts(&self) -> u64 {
869        self.ts_counter.fetch_add(1, Ordering::SeqCst)
870    }
871
872    /// Commit transaction with SSI validation
873    /// Returns (commit_ts, write_set) so the memtable can be updated efficiently
874    /// Returns None if SSI validation fails (dangerous structure detected)
875    ///
876    /// ## SSI Bypass (Recommendation 9)
877    ///
878    /// For ReadOnly mode: Skip validation entirely (~10ns commit)
879    /// For WriteOnly mode: Skip read-based validation (~30ns commit)
880    /// For ReadWrite mode: Full validation (~50ns commit)
881    pub fn commit(&self, txn_id: u64) -> Option<(u64, HashSet<InlineKey>)> {
882        // Get transaction before removing
883        let txn = self.active_txns.get(&txn_id)?.clone();
884
885        // SSI Bypass: Skip validation for ReadOnly transactions
886        // ReadOnly can never form rw-antidependency cycles
887        if txn.mode != TransactionMode::ReadWrite || !self.validate_ssi(&txn) {
888            // For ReadOnly/WriteOnly: always valid (mode check short-circuits)
889            // For ReadWrite: check SSI validation result
890            if txn.mode == TransactionMode::ReadWrite && !self.validate_ssi(&txn) {
891                // Abort on SSI violation
892                if self.active_txns.remove(&txn_id).is_some() {
893                    self.track_active_end(txn.snapshot_ts);
894                }
895                return None;
896            }
897        }
898
899        let commit_ts = self.alloc_commit_ts();
900
901        // Extract write_set and remove transaction - takes ownership
902        let (_, removed_txn) = self.active_txns.remove(&txn_id)?;
903        // Capture before `removed_txn` is partially moved below; snapshot_ts is
904        // the multiset key for the matching begin.
905        let snap = removed_txn.snapshot_ts;
906
907        // OPTIMIZATION: Only track ReadWrite transactions for SSI
908        // ReadOnly/WriteOnly can't form complete rw-antidependency cycles
909        let needs_ssi_tracking = removed_txn.mode == TransactionMode::ReadWrite
910            && !removed_txn.read_set.is_empty()
911            && !removed_txn.write_set.is_empty();
912
913        if needs_ssi_tracking {
914            // Need to clone write_set since we return it AND track it
915            let write_set_for_return = removed_txn.write_set.clone();
916
917            self.track_commit_owned(
918                txn_id,
919                commit_ts,
920                removed_txn.read_bloom,
921                removed_txn.write_bloom,
922                removed_txn.read_set,
923                removed_txn.write_set,
924            );
925
926            self.track_active_end(snap);
927            Some((commit_ts, write_set_for_return))
928        } else {
929            // Fast path: no SSI tracking needed, avoid clone entirely
930            self.track_active_end(snap);
931            Some((commit_ts, removed_txn.write_set))
932        }
933    }
934
935    /// Validate SSI constraints for a committing transaction
936    ///
937    /// ## Transaction Classification (Task 3: Optimistic MVCC)
938    ///
939    /// Transactions are classified and routed through appropriate fast paths:
940    ///
941    /// | Class      | Criteria                      | Validation Cost |
942    /// |------------|-------------------------------|-----------------|
943    /// | ReadOnly   | write_set.is_empty()          | 0 ns           |
944    /// | SingleKey  | write_set.len() == 1          | 0 ns           |
945    /// | Disjoint   | bloom filters don't intersect | ~10 ns         |
946    /// | General    | full SSI check                | ~50 ns         |
947    ///
948    /// Expected distribution: ~60% read-only, ~25% single-key, ~10% disjoint, ~5% general
949    /// Weighted average: ~8 ns vs 50 ns baseline (6x improvement)
950    ///
951    /// Detects "dangerous structures" - rw-antidependency cycles:
952    /// - T1 reads X (snapshot sees old value)
953    /// - T2 writes X (concurrent write)  
954    /// - T2 reads Y (snapshot sees old value)
955    /// - T1 writes Y (concurrent write)
956    ///
957    /// If T1 → rw → T2 → rw → T1 exists, abort T1
958    #[inline]
959    fn validate_ssi(&self, txn: &MvccTransaction) -> bool {
960        // =================================================================
961        // Fast Path 1: Read-only transactions (0 ns)
962        // =================================================================
963        // Read-only transactions can never form rw-antidependency cycles
964        // because they have no writes to create outgoing rw-edges
965        if txn.write_set.is_empty() {
966            return true;
967        }
968
969        // =================================================================
970        // Fast Path 2: No recent commits to check (0 ns)
971        // =================================================================
972        if self.recent_commits.is_empty() {
973            return true;
974        }
975
976        // =================================================================
977        // Fast Path 3: Single-key write transactions (0 ns)
978        // =================================================================
979        // A single-key write transaction cannot form a dangerous cycle:
980        // - For a cycle T1 →rw→ T2 →rw→ T1, we need T1 to read what T2 wrote
981        //   AND T2 to read what T1 wrote
982        // - With only one key in write_set, the same key would need to be
983        //   in both read_set AND write_set of both transactions
984        // - This is already prevented by our conflict detection (write-write)
985        if txn.write_set.len() == 1 && txn.read_set.len() <= 1 {
986            return true;
987        }
988
989        let my_snapshot = txn.snapshot_ts;
990
991        // =================================================================
992        // Fast Path 4: Disjoint transactions using Bloom filters (~10 ns)
993        // =================================================================
994        // Pre-filter using bloom filters: if our write_bloom doesn't intersect
995        // with any concurrent transaction's read_bloom AND vice versa,
996        // there can be no rw-antidependency
997        let mut any_may_intersect = false;
998        for entry in self.recent_commits.iter() {
999            let (_, (other_commit_ts, other_read_bloom, other_write_bloom, _, _)) = entry.pair();
1000
1001            // Only check concurrent transactions.
1002            //
1003            // A committed transaction is *concurrent* with us iff its writes are
1004            // invisible to our snapshot. Read visibility is strict
1005            // (`commit_ts < snapshot_ts`), so a transaction with
1006            // `commit_ts >= my_snapshot` is invisible and must be validated
1007            // against. Using `<` here (not `<=`) keeps this window consistent
1008            // with `read_at`; a `<=` would skip a boundary transaction whose
1009            // `commit_ts == my_snapshot` and miss a genuine write-skew.
1010            if *other_commit_ts < my_snapshot {
1011                continue;
1012            }
1013
1014            // Check bloom filter intersection (O(m/64) per filter)
1015            // If our writes may intersect their reads OR their writes may intersect our reads
1016            if txn.write_bloom.may_intersect(other_read_bloom)
1017                || other_write_bloom.may_intersect(&txn.read_bloom)
1018            {
1019                any_may_intersect = true;
1020                break;
1021            }
1022        }
1023
1024        // No bloom intersection means definitely disjoint - no SSI conflict possible
1025        if !any_may_intersect {
1026            return true;
1027        }
1028
1029        // =================================================================
1030        // Full SSI Validation (~50 ns)
1031        // =================================================================
1032        // Check for rw-conflicts with recently committed transactions
1033        // An rw-conflict exists if:
1034        // - T_other wrote to a key that T_me read (T_other →rw→ T_me)
1035        // - T_me wrote to a key that T_other read (T_me →rw→ T_other)
1036
1037        let mut in_conflict_with: Vec<u64> = Vec::new();
1038        let mut out_conflict_to: Vec<u64> = Vec::new();
1039
1040        for entry in self.recent_commits.iter() {
1041            let (
1042                other_txn_id,
1043                (
1044                    other_commit_ts,
1045                    _other_read_bloom,
1046                    other_write_bloom,
1047                    other_read_set,
1048                    other_write_set,
1049                ),
1050            ) = entry.pair();
1051
1052            // Only consider transactions concurrent with us: those whose writes
1053            // are invisible to our snapshot. Read visibility is strict
1054            // (`commit_ts < snapshot_ts`), so `commit_ts >= my_snapshot` means
1055            // concurrent. `<` (not `<=`) keeps this consistent with `read_at`.
1056            if *other_commit_ts < my_snapshot {
1057                continue;
1058            }
1059
1060            // Check: other wrote → we read (other →rw→ me)
1061            // T_other wrote a key that T_me read (rw-dependency inbound)
1062            //
1063            // Bloom-accelerated: First check bloom filter for fast rejection (O(m/64))
1064            // Only do expensive HashSet intersection if bloom says "maybe conflict"
1065            let mut has_in_conflict = false;
1066            for key in txn.read_set.iter() {
1067                if other_write_bloom.may_contain(key) {
1068                    // Bloom says maybe - do exact check
1069                    if other_write_set.contains(key) {
1070                        has_in_conflict = true;
1071                        break;
1072                    }
1073                }
1074            }
1075            if has_in_conflict {
1076                in_conflict_with.push(*other_txn_id);
1077            }
1078
1079            // Check: we wrote → other read (me →rw→ other)
1080            // T_me wrote a key that T_other read (rw-dependency outbound)
1081            //
1082            // Bloom-accelerated: Use our write_bloom against their read_set
1083            let mut has_out_conflict = false;
1084            for key in other_read_set.iter() {
1085                if txn.write_bloom.may_contain(key) {
1086                    // Bloom says maybe - do exact check
1087                    if txn.write_set.contains(key) {
1088                        has_out_conflict = true;
1089                        break;
1090                    }
1091                }
1092            }
1093            if has_out_conflict {
1094                out_conflict_to.push(*other_txn_id);
1095            }
1096        }
1097
1098        // Dangerous structure: we have both incoming AND outgoing rw-edges
1099        // This creates a potential cycle: T1 →rw→ T_me →rw→ T2
1100        //
1101        // Conservative check: if both exist, abort
1102        // A more precise check would verify the cycle path, but this is safe
1103        if !in_conflict_with.is_empty() && !out_conflict_to.is_empty() {
1104            return false; // SSI violation - abort
1105        }
1106
1107        true
1108    }
1109
1110    /// Track a committed transaction for future SSI validation
1111    ///
1112    /// Only tracks transactions that have both reads AND writes, since SSI
1113    /// only detects rw-antidependency cycles. Pure read or pure write
1114    /// transactions can't form cycles.
1115    ///
1116    /// ## Optimization: Zero-Copy Transfer
1117    ///
1118    /// Takes ownership of sets instead of cloning to avoid the +15% commit
1119    /// phase regression. The caller should use mem::take() to transfer ownership.
1120    fn track_commit_owned(
1121        &self,
1122        txn_id: u64,
1123        commit_ts: u64,
1124        read_bloom: SsiBloomFilter,
1125        write_bloom: SsiBloomFilter,
1126        read_set: HashSet<InlineKey>,
1127        write_set: HashSet<InlineKey>,
1128    ) {
1129        // Optimization: Only track mixed read-write transactions
1130        // Pure reads can't create outgoing rw-edges
1131        // Pure writes can't create incoming rw-edges
1132        if read_set.is_empty() || write_set.is_empty() {
1133            return; // Skip tracking - can't form SSI cycle
1134        }
1135
1136        // Add to recent commits with bloom filters for fast SSI pre-filtering
1137        // No cloning needed - we take ownership
1138        self.recent_commits.insert(
1139            txn_id,
1140            (commit_ts, read_bloom, write_bloom, read_set, write_set),
1141        );
1142
1143        // Lazy pruning: only prune when we're significantly over capacity
1144        // Avoids pruning overhead on every commit
1145        if self.recent_commits.len() > self.max_recent_commits * 2 {
1146            // Remove entries with lowest commit_ts
1147            let min_active = self.min_active_ts.load(Ordering::Relaxed);
1148            self.recent_commits
1149                .retain(|_, (ts, _, _, _, _)| *ts >= min_active);
1150        }
1151    }
1152
1153    /// Legacy track_commit that clones - kept for compatibility
1154    #[allow(dead_code)]
1155    fn track_commit(
1156        &self,
1157        txn_id: u64,
1158        commit_ts: u64,
1159        read_bloom: SsiBloomFilter,
1160        write_bloom: SsiBloomFilter,
1161        read_set: &HashSet<InlineKey>,
1162        write_set: &HashSet<InlineKey>,
1163    ) {
1164        if read_set.is_empty() || write_set.is_empty() {
1165            return;
1166        }
1167        self.recent_commits.insert(
1168            txn_id,
1169            (
1170                commit_ts,
1171                read_bloom,
1172                write_bloom,
1173                read_set.clone(),
1174                write_set.clone(),
1175            ),
1176        );
1177    }
1178
1179    /// Abort transaction
1180    pub fn abort(&self, txn_id: u64) {
1181        if let Some((_, t)) = self.active_txns.remove(&txn_id) {
1182            self.track_active_end(t.snapshot_ts);
1183        }
1184    }
1185
1186    /// Get minimum active snapshot timestamp
1187    pub fn min_active_snapshot(&self) -> u64 {
1188        self.min_active_ts.load(Ordering::SeqCst)
1189    }
1190
1191    /// Get count of active transactions
1192    pub fn active_transaction_count(&self) -> usize {
1193        self.active_txns.len()
1194    }
1195
1196    /// Record a transaction entering the active set (its snapshot_ts) and
1197    /// refresh `min_active_ts`. O(log N) — replaces the old O(active_txns) scan.
1198    fn track_active_begin(&self, snapshot_ts: u64) {
1199        let mut snaps = self.active_snapshots.lock();
1200        *snaps.entry(snapshot_ts).or_insert(0) += 1;
1201        // The set is non-empty (we just inserted); the smallest key is the min.
1202        let min = *snaps.keys().next().expect("non-empty after insert");
1203        self.min_active_ts.store(min, Ordering::SeqCst);
1204    }
1205
1206    /// Record a transaction leaving the active set and refresh `min_active_ts`.
1207    /// O(log N). `snapshot_ts` MUST be the value used at the matching begin.
1208    fn track_active_end(&self, snapshot_ts: u64) {
1209        let mut snaps = self.active_snapshots.lock();
1210        if let Some(c) = snaps.get_mut(&snapshot_ts) {
1211            *c -= 1;
1212            if *c == 0 {
1213                snaps.remove(&snapshot_ts);
1214            }
1215        }
1216        // When no txn is active the watermark is the current ts_counter, exactly
1217        // as the old full-scan did via `unwrap_or_else`.
1218        let min = snaps
1219            .keys()
1220            .next()
1221            .copied()
1222            .unwrap_or_else(|| self.ts_counter.load(Ordering::SeqCst));
1223        self.min_active_ts.store(min, Ordering::SeqCst);
1224    }
1225
1226    /// Test-only SOUND consistency check: with no concurrent mutation in flight,
1227    /// the active-snapshot multiset must exactly mirror `active_txns` and yield
1228    /// the same watermark as a full scan. Must be called from a quiescent (e.g.
1229    /// single-threaded) point — it reads both structures and is not atomic.
1230    #[cfg(test)]
1231    pub(crate) fn assert_active_snapshots_consistent(&self) {
1232        use std::collections::BTreeMap;
1233        let mut from_txns: BTreeMap<u64, u32> = BTreeMap::new();
1234        for e in self.active_txns.iter() {
1235            *from_txns.entry(e.value().snapshot_ts).or_insert(0) += 1;
1236        }
1237        let snaps = self.active_snapshots.lock();
1238        assert_eq!(
1239            *snaps, from_txns,
1240            "active_snapshots multiset drifted from active_txns"
1241        );
1242        let watermark = self.min_active_ts.load(Ordering::SeqCst);
1243        match from_txns.keys().next().copied() {
1244            // Non-empty: watermark must equal the oldest active snapshot.
1245            Some(min) => assert_eq!(watermark, min, "min_active_ts watermark wrong"),
1246            // Empty: any watermark <= ts_counter is SAFE (GC just stays
1247            // conservative); the initial state is 0, post-drain it is ts_counter.
1248            None => assert!(
1249                watermark <= self.ts_counter.load(Ordering::SeqCst),
1250                "empty-state watermark {} exceeds ts_counter",
1251                watermark
1252            ),
1253        }
1254    }
1255
1256    /// Recompute `min_active_ts` from the active-snapshot multiset (O(log N)).
1257    /// Retained for any caller that needs a watermark refresh without a
1258    /// corresponding begin/end (e.g. recovery).
1259    #[allow(dead_code)]
1260    fn update_min_active_ts(&self) {
1261        let snaps = self.active_snapshots.lock();
1262        let min = snaps
1263            .keys()
1264            .next()
1265            .copied()
1266            .unwrap_or_else(|| self.ts_counter.load(Ordering::SeqCst));
1267        self.min_active_ts.store(min, Ordering::SeqCst);
1268    }
1269}
1270
1271/// Epoch-based dirty list for O(expired) GC instead of O(n)
1272///
1273/// Instead of scanning ALL version chains, we track which keys have versions
1274/// created in each epoch. GC only needs to visit keys from old epochs.
1275struct EpochDirtyList {
1276    /// Ring buffer of epoch -> dirty keys
1277    /// Index = epoch % EPOCH_RING_SIZE
1278    epochs: [parking_lot::Mutex<Vec<Vec<u8>>>; 4],
1279    /// Current epoch
1280    current_epoch: AtomicU64,
1281}
1282
1283const EPOCH_RING_SIZE: usize = 4;
1284
1285impl EpochDirtyList {
1286    fn new() -> Self {
1287        Self {
1288            epochs: [
1289                parking_lot::Mutex::new(Vec::new()),
1290                parking_lot::Mutex::new(Vec::new()),
1291                parking_lot::Mutex::new(Vec::new()),
1292                parking_lot::Mutex::new(Vec::new()),
1293            ],
1294            current_epoch: AtomicU64::new(0),
1295        }
1296    }
1297
1298    /// Record a version created in the current epoch
1299    #[inline]
1300    fn record_version(&self, key: Vec<u8>) {
1301        let epoch = self.current_epoch.load(Ordering::Relaxed);
1302        let idx = (epoch as usize) % EPOCH_RING_SIZE;
1303        self.epochs[idx].lock().push(key);
1304    }
1305
1306    /// Record multiple versions in a single lock acquisition (Rec 3: MVCC Batching)
1307    ///
1308    /// Performance: Single lock acquire vs N lock acquires for batch of N writes.
1309    /// For 100 writes: ~100x fewer mutex operations.
1310    #[inline]
1311    fn record_versions_batch(&self, keys: impl IntoIterator<Item = Vec<u8>>) {
1312        let epoch = self.current_epoch.load(Ordering::Relaxed);
1313        let idx = (epoch as usize) % EPOCH_RING_SIZE;
1314        let mut guard = self.epochs[idx].lock();
1315        guard.extend(keys);
1316    }
1317
1318    /// Advance to next epoch, returning old epoch's dirty keys
1319    fn advance_epoch(&self) -> (u64, Vec<Vec<u8>>) {
1320        let old_epoch = self.current_epoch.fetch_add(1, Ordering::SeqCst);
1321        let old_idx = (old_epoch as usize) % EPOCH_RING_SIZE;
1322
1323        // Drain the old epoch's dirty list
1324        let mut guard = self.epochs[old_idx].lock();
1325        let keys = std::mem::take(&mut *guard);
1326        (old_epoch, keys)
1327    }
1328
1329    /// Get current epoch
1330    #[allow(dead_code)]
1331    fn current(&self) -> u64 {
1332        self.current_epoch.load(Ordering::Relaxed)
1333    }
1334}
1335
1336// ============================================================================
1337// Streaming Scan Iterator
1338// ============================================================================
1339
1340/// Streaming iterator for range scans
1341///
1342/// Yields results one at a time without materializing the full result set.
1343/// This enables processing of very large result sets with O(1) memory per
1344/// iteration instead of O(N) for the entire result set.
1345struct ScanRangeIterator<'a> {
1346    memtable: &'a MvccMemTable,
1347    start: Vec<u8>,
1348    end: Vec<u8>,
1349    snapshot_ts: u64,
1350    current_txn_id: Option<u64>,
1351    use_ordered: bool,
1352    // We use Option to defer initialization
1353    ordered_iter: Option<Box<dyn Iterator<Item = (Vec<u8>, Vec<u8>)> + 'a>>,
1354    unordered_iter: Option<Box<dyn Iterator<Item = (Vec<u8>, Vec<u8>)> + 'a>>,
1355    initialized: bool,
1356}
1357
1358impl<'a> Iterator for ScanRangeIterator<'a> {
1359    type Item = (Vec<u8>, Vec<u8>);
1360
1361    fn next(&mut self) -> Option<Self::Item> {
1362        // Lazy initialization on first call
1363        if !self.initialized {
1364            self.initialized = true;
1365
1366            if self.use_ordered {
1367                // Try deferred index first (after compaction, it uses a SkipMap internally)
1368                if let Some(ref def_idx) = self.memtable.deferred_index {
1369                    let start = self.start.clone();
1370                    let end = self.end.clone();
1371                    let snapshot_ts = self.snapshot_ts;
1372                    let current_txn_id = self.current_txn_id;
1373                    let data = &self.memtable.data;
1374
1375                    // Collect keys from deferred index (already sorted after compact)
1376                    let keys: Vec<Vec<u8>> = if end.is_empty() {
1377                        def_idx.range_from(&start).collect()
1378                    } else {
1379                        def_idx.range(&start, &end).collect()
1380                    };
1381
1382                    let iter: Box<dyn Iterator<Item = (Vec<u8>, Vec<u8>)> + 'a> =
1383                        Box::new(keys.into_iter().filter_map(move |key| {
1384                            if let Some(chain) = data.get(&key)
1385                                && let Some(v) = chain.read_at(snapshot_ts, current_txn_id)
1386                                && let Some(value) = &v.value
1387                            {
1388                                Some((key, value.clone()))
1389                            } else {
1390                                None
1391                            }
1392                        }));
1393                    self.ordered_iter = Some(iter);
1394                } else if let Some(ref idx) = self.memtable.ordered_index {
1395                    let start = self.start.clone();
1396                    let end = self.end.clone();
1397                    let snapshot_ts = self.snapshot_ts;
1398                    let current_txn_id = self.current_txn_id;
1399                    let data = &self.memtable.data;
1400
1401                    let iter: Box<dyn Iterator<Item = (Vec<u8>, Vec<u8>)> + 'a> = if end.is_empty()
1402                    {
1403                        Box::new(idx.range(start..).filter_map(move |entry| {
1404                            let key = entry.key();
1405                            if let Some(chain) = data.get(key)
1406                                && let Some(v) = chain.read_at(snapshot_ts, current_txn_id)
1407                                && let Some(value) = &v.value
1408                            {
1409                                Some((key.clone(), value.clone()))
1410                            } else {
1411                                None
1412                            }
1413                        }))
1414                    } else {
1415                        Box::new(idx.range(start..end).filter_map(move |entry| {
1416                            let key = entry.key();
1417                            if let Some(chain) = data.get(key)
1418                                && let Some(v) = chain.read_at(snapshot_ts, current_txn_id)
1419                                && let Some(value) = &v.value
1420                            {
1421                                Some((key.clone(), value.clone()))
1422                            } else {
1423                                None
1424                            }
1425                        }))
1426                    };
1427                    self.ordered_iter = Some(iter);
1428                }
1429            } else {
1430                // Unordered full scan
1431                let start = self.start.clone();
1432                let end = self.end.clone();
1433                let snapshot_ts = self.snapshot_ts;
1434                let current_txn_id = self.current_txn_id;
1435
1436                let iter: Box<dyn Iterator<Item = (Vec<u8>, Vec<u8>)> + 'a> =
1437                    Box::new(self.memtable.data.iter().filter_map(move |entry| {
1438                        let key = entry.key();
1439
1440                        if key.as_slice() < start.as_slice() {
1441                            return None;
1442                        }
1443                        if !end.is_empty() && key.as_slice() >= end.as_slice() {
1444                            return None;
1445                        }
1446
1447                        if let Some(v) = entry.value().read_at(snapshot_ts, current_txn_id)
1448                            && let Some(value) = &v.value
1449                        {
1450                            Some((key.clone(), value.clone()))
1451                        } else {
1452                            None
1453                        }
1454                    }));
1455                self.unordered_iter = Some(iter);
1456            }
1457        }
1458
1459        // Get next from appropriate iterator
1460        if let Some(ref mut iter) = self.ordered_iter {
1461            iter.next()
1462        } else if let Some(ref mut iter) = self.unordered_iter {
1463            iter.next()
1464        } else {
1465            None
1466        }
1467    }
1468}
1469
1470/// MemTable with MVCC support
1471///
1472/// Uses DashMap for lock-free concurrent access per key.
1473/// This eliminates the global write lock bottleneck.
1474///
1475/// Uses epoch-based dirty tracking for O(expired) GC instead of O(n) full scan.
1476/// Maintains a deferred sorted index for efficient scans:
1477/// - Writes: O(1) append to hot buffer
1478/// - Scans: O(N log N) sort-on-demand (amortized across many writes)
1479pub struct MvccMemTable {
1480    /// Key -> VersionChain (sharded for concurrent access)
1481    data: DashMap<Vec<u8>, VersionChain>,
1482    /// Deferred sorted index for efficient prefix/range scans (optional)
1483    /// O(1) insert to hot buffer, O(N log N) sort on first scan
1484    /// When None, scan_prefix will fall back to O(N) DashMap iteration
1485    deferred_index: Option<DeferredSortedIndex>,
1486    /// Legacy SkipMap for compatibility (used when deferred=false)
1487    ordered_index: Option<SkipMap<Vec<u8>, ()>>,
1488    /// Whether to use deferred sorting (true) or immediate SkipMap (false)
1489    #[allow(dead_code)]
1490    use_deferred: bool,
1491    /// Approximate size in bytes
1492    size_bytes: AtomicU64,
1493    /// Epoch-based dirty list for efficient GC
1494    dirty_list: EpochDirtyList,
1495}
1496
1497impl Default for MvccMemTable {
1498    fn default() -> Self {
1499        Self::new()
1500    }
1501}
1502
1503impl MvccMemTable {
1504    pub fn new() -> Self {
1505        Self::with_ordered_index(true)
1506    }
1507
1508    /// Create memtable with optional ordered index
1509    ///
1510    /// When `enable_ordered_index` is false, saves ~134 ns/op on writes
1511    /// but scan_prefix becomes O(N) instead of O(log N + K)
1512    ///
1513    /// Uses deferred sorting by default for better write performance:
1514    /// - Writes: O(1) append to hot buffer
1515    /// - Scans: O(N log N) sort-on-demand
1516    pub fn with_ordered_index(enable_ordered_index: bool) -> Self {
1517        Self::with_index_mode(enable_ordered_index, true)
1518    }
1519
1520    /// Create memtable with fine-grained control over indexing
1521    ///
1522    /// # Arguments
1523    /// * `enable_ordered_index` - Whether to maintain an ordered index
1524    /// * `use_deferred` - If true, use deferred sorting (O(1) writes, sort-on-scan)
1525    ///                    If false, use SkipMap (O(log N) writes)
1526    pub fn with_index_mode(enable_ordered_index: bool, use_deferred: bool) -> Self {
1527        Self {
1528            data: DashMap::new(),
1529            deferred_index: if enable_ordered_index && use_deferred {
1530                Some(DeferredSortedIndex::with_config(DeferredIndexConfig {
1531                    max_unsorted_entries: 10_000, // Compact every 10K writes
1532                    enabled: true,
1533                }))
1534            } else {
1535                None
1536            },
1537            ordered_index: if enable_ordered_index && !use_deferred {
1538                Some(SkipMap::new())
1539            } else {
1540                None
1541            },
1542            use_deferred,
1543            size_bytes: AtomicU64::new(0),
1544            dirty_list: EpochDirtyList::new(),
1545        }
1546    }
1547
1548    /// Write a key-value pair (uncommitted)
1549    pub fn write(&self, key: Vec<u8>, value: Option<Vec<u8>>, txn_id: u64) -> Result<()> {
1550        let value_size = value.as_ref().map(|v| v.len()).unwrap_or(0);
1551        let key_len = key.len();
1552
1553        // Track this key in the current epoch's dirty list for GC
1554        self.dirty_list.record_version(key.clone());
1555
1556        // Insert into ordered index for prefix scans (if enabled)
1557        // Deferred: O(1) append to hot buffer
1558        // SkipMap: O(log N) insert
1559        if let Some(ref idx) = self.deferred_index {
1560            idx.insert(key.clone());
1561        } else if let Some(ref idx) = self.ordered_index {
1562            idx.insert(key.clone(), ());
1563        }
1564
1565        // Use entry API for atomic get-or-insert
1566        let mut entry = self.data.entry(key).or_default();
1567
1568        // Check for write-write conflict
1569        if entry.has_write_conflict(txn_id) {
1570            return Err(SochDBError::Internal(
1571                "Write-write conflict detected".into(),
1572            ));
1573        }
1574        entry.add_uncommitted(value, txn_id);
1575        self.size_bytes
1576            .fetch_add((key_len + value_size) as u64, Ordering::Relaxed);
1577
1578        Ok(())
1579    }
1580
1581    /// Write multiple key-value pairs (uncommitted) - more efficient than individual writes
1582    ///
1583    /// Optimizations applied (Rec 3: MVCC Batching):
1584    /// - Batched dirty list tracking: single lock acquire for all keys
1585    /// - Deferred index: O(1) append per key
1586    pub fn write_batch(&self, writes: &[(Vec<u8>, Option<Vec<u8>>)], txn_id: u64) -> Result<()> {
1587        let mut total_size = 0u64;
1588
1589        // Rec 3: Batch MVCC tracking - single lock acquire for all keys
1590        self.dirty_list
1591            .record_versions_batch(writes.iter().map(|(k, _)| k.clone()));
1592
1593        for (key, value) in writes {
1594            // Insert into ordered index (if enabled)
1595            // Deferred: O(1) append, SkipMap: O(log N)
1596            if let Some(ref idx) = self.deferred_index {
1597                idx.insert(key.clone());
1598            } else if let Some(ref idx) = self.ordered_index {
1599                idx.insert(key.clone(), ());
1600            }
1601
1602            let mut entry = self.data.entry(key.clone()).or_default();
1603
1604            if entry.has_write_conflict(txn_id) {
1605                return Err(SochDBError::Internal(
1606                    "Write-write conflict detected".into(),
1607                ));
1608            }
1609
1610            let value_size = value.as_ref().map(|v| v.len()).unwrap_or(0);
1611            entry.add_uncommitted(value.clone(), txn_id);
1612            total_size += (key.len() + value_size) as u64;
1613        }
1614
1615        self.size_bytes.fetch_add(total_size, Ordering::Relaxed);
1616        Ok(())
1617    }
1618
1619    /// Read at snapshot timestamp, with optional current txn to see own writes
1620    pub fn read(
1621        &self,
1622        key: &[u8],
1623        snapshot_ts: u64,
1624        current_txn_id: Option<u64>,
1625    ) -> Option<Vec<u8>> {
1626        self.data.get(key).and_then(|chain| {
1627            chain
1628                .read_at(snapshot_ts, current_txn_id)
1629                .and_then(|v| v.value.clone())
1630        })
1631    }
1632
1633    /// Commit all versions for a transaction
1634    ///
1635    /// Only updates the keys that were written by this transaction (tracked in write_set).
1636    /// Accepts InlineKey for zero-allocation MVCC tracking.
1637    pub fn commit(&self, txn_id: u64, commit_ts: u64, write_set: &HashSet<InlineKey>) {
1638        // Only iterate over keys we know were written - O(k) instead of O(n)
1639        for key in write_set {
1640            if let Some(mut chain) = self.data.get_mut(key.as_slice()) {
1641                chain.commit(txn_id, commit_ts);
1642            }
1643        }
1644    }
1645
1646    /// Legacy commit method (iterates all keys) - kept for backward compatibility
1647    #[allow(dead_code)]
1648    pub fn commit_all(&self, txn_id: u64, commit_ts: u64) {
1649        for mut entry in self.data.iter_mut() {
1650            entry.value_mut().commit(txn_id, commit_ts);
1651        }
1652    }
1653
1654    /// Abort all versions for a transaction
1655    pub fn abort(&self, txn_id: u64) {
1656        for mut entry in self.data.iter_mut() {
1657            entry.value_mut().abort(txn_id);
1658        }
1659    }
1660
1661    /// Scan keys with prefix at snapshot (without seeing uncommitted from other txns)
1662    ///
1663    /// ## Performance
1664    ///
1665    /// When ordered_index is enabled: O(log N + K) complexity
1666    /// - O(log N) to seek to the first key with prefix
1667    /// - O(K) to iterate matching keys
1668    ///
1669    /// When ordered_index is disabled: O(N) full DashMap scan (fallback)
1670    ///
1671    /// ## Optimizations Applied
1672    ///
1673    /// - Pre-allocates result vector based on expected output size
1674    /// - Uses batch-friendly iteration patterns
1675    /// - Minimizes allocations during iteration
1676    /// - Deferred index: compacts hot buffer on first scan for sorted iteration
1677    pub fn scan_prefix(
1678        &self,
1679        prefix: &[u8],
1680        snapshot_ts: u64,
1681        current_txn_id: Option<u64>,
1682    ) -> Vec<(Vec<u8>, Vec<u8>)> {
1683        // Estimate result size for pre-allocation (use 10% of total as heuristic)
1684        let estimated_size = (self.data.len() / 10).max(64);
1685        let mut results = Vec::with_capacity(estimated_size);
1686
1687        if let Some(ref idx) = self.deferred_index {
1688            // Deferred index path: sort-on-scan (compacts hot buffer if needed)
1689            for key in idx.range_from(prefix) {
1690                // Stop when we've passed the prefix range
1691                if !key.starts_with(prefix) {
1692                    break;
1693                }
1694
1695                // O(1) lookup in DashMap for version chain
1696                if let Some(chain) = self.data.get(&key)
1697                    && let Some(v) = chain.read_at(snapshot_ts, current_txn_id)
1698                    && let Some(value) = &v.value
1699                {
1700                    results.push((key, value.clone()));
1701                }
1702            }
1703        } else if let Some(ref idx) = self.ordered_index {
1704            // Fast path: O(log N) seek to first key >= prefix
1705            for entry in idx.range(prefix.to_vec()..) {
1706                let key = entry.key();
1707
1708                // Stop when we've passed the prefix range
1709                if !key.starts_with(prefix) {
1710                    break;
1711                }
1712
1713                // O(1) lookup in DashMap for version chain
1714                if let Some(chain) = self.data.get(key)
1715                    && let Some(v) = chain.read_at(snapshot_ts, current_txn_id)
1716                    && let Some(value) = &v.value
1717                {
1718                    results.push((key.clone(), value.clone()));
1719                }
1720            }
1721        } else {
1722            // Fallback: O(N) full DashMap scan when ordered_index is disabled
1723            // Optimized with batch-friendly iteration
1724            for entry in self.data.iter() {
1725                let key = entry.key();
1726                if !key.starts_with(prefix) {
1727                    continue;
1728                }
1729                if let Some(v) = entry.value().read_at(snapshot_ts, current_txn_id)
1730                    && let Some(value) = &v.value
1731                {
1732                    results.push((key.clone(), value.clone()));
1733                }
1734            }
1735        }
1736
1737        results
1738    }
1739
1740    /// Optimized full scan with batch allocation
1741    ///
1742    /// For use when scanning entire tables/namespaces.
1743    /// Pre-allocates based on actual data size.
1744    pub fn scan_all(
1745        &self,
1746        snapshot_ts: u64,
1747        current_txn_id: Option<u64>,
1748    ) -> Vec<(Vec<u8>, Vec<u8>)> {
1749        let mut results = Vec::with_capacity(self.data.len());
1750
1751        for entry in self.data.iter() {
1752            if let Some(v) = entry.value().read_at(snapshot_ts, current_txn_id)
1753                && let Some(value) = &v.value
1754            {
1755                results.push((entry.key().clone(), value.clone()));
1756            }
1757        }
1758
1759        results
1760    }
1761
1762    /// Streaming scan iterator for very large datasets
1763    ///
1764    /// Returns an iterator that yields (key, value) pairs without
1765    /// materializing the entire result set in memory.
1766    pub fn scan_prefix_iter<'a>(
1767        &'a self,
1768        prefix: &'a [u8],
1769        snapshot_ts: u64,
1770        current_txn_id: Option<u64>,
1771    ) -> impl Iterator<Item = (Vec<u8>, Vec<u8>)> + 'a {
1772        self.data.iter().filter_map(move |entry| {
1773            let key = entry.key();
1774            if !key.starts_with(prefix) {
1775                return None;
1776            }
1777            if let Some(v) = entry.value().read_at(snapshot_ts, current_txn_id)
1778                && let Some(value) = &v.value
1779            {
1780                Some((key.clone(), value.clone()))
1781            } else {
1782                None
1783            }
1784        })
1785    }
1786
1787    /// Scan range
1788    pub fn scan_range(
1789        &self,
1790        start: &[u8],
1791        end: &[u8],
1792        snapshot_ts: u64,
1793        current_txn_id: Option<u64>,
1794    ) -> Vec<(Vec<u8>, Vec<u8>)> {
1795        let mut results = Vec::new();
1796
1797        if let Some(ref idx) = self.deferred_index {
1798            // Deferred index path: sort-on-scan
1799            if end.is_empty() {
1800                for key in idx.range_from(start) {
1801                    if let Some(chain) = self.data.get(&key)
1802                        && let Some(v) = chain.read_at(snapshot_ts, current_txn_id)
1803                        && let Some(value) = &v.value
1804                    {
1805                        results.push((key, value.clone()));
1806                    }
1807                }
1808            } else {
1809                for key in idx.range(start, end) {
1810                    if let Some(chain) = self.data.get(&key)
1811                        && let Some(v) = chain.read_at(snapshot_ts, current_txn_id)
1812                        && let Some(value) = &v.value
1813                    {
1814                        results.push((key, value.clone()));
1815                    }
1816                }
1817            }
1818        } else if let Some(ref idx) = self.ordered_index {
1819            // Use range scan on SkipMap
1820            if end.is_empty() {
1821                // Unbounded end
1822                for entry in idx.range(start.to_vec()..) {
1823                    let key = entry.key();
1824                    if let Some(chain) = self.data.get(key)
1825                        && let Some(v) = chain.read_at(snapshot_ts, current_txn_id)
1826                        && let Some(value) = &v.value
1827                    {
1828                        results.push((key.clone(), value.clone()));
1829                    }
1830                }
1831            } else {
1832                for entry in idx.range(start.to_vec()..end.to_vec()) {
1833                    let key = entry.key();
1834                    if let Some(chain) = self.data.get(key)
1835                        && let Some(v) = chain.read_at(snapshot_ts, current_txn_id)
1836                        && let Some(value) = &v.value
1837                    {
1838                        results.push((key.clone(), value.clone()));
1839                    }
1840                }
1841            }
1842        } else {
1843            // Fallback to full scan if no ordered index
1844            for entry in self.data.iter() {
1845                let key = entry.key();
1846
1847                if key.as_slice() < start {
1848                    continue;
1849                }
1850                if !end.is_empty() && key.as_slice() >= end {
1851                    continue;
1852                }
1853
1854                if let Some(v) = entry.value().read_at(snapshot_ts, current_txn_id)
1855                    && let Some(value) = &v.value
1856                {
1857                    results.push((key.clone(), value.clone()));
1858                }
1859            }
1860        }
1861
1862        results
1863    }
1864
1865    /// Streaming range scan iterator for very large datasets
1866    ///
1867    /// Returns an iterator that yields (key, value) pairs without
1868    /// materializing the entire result set in memory. Uses the ordered
1869    /// index when available for O(log N + K) complexity.
1870    ///
1871    /// ## Zero-Allocation Design
1872    ///
1873    /// While the iterator itself cannot avoid allocations for returned
1874    /// values (since the caller needs ownership), it avoids:
1875    /// - Pre-materializing all results
1876    /// - Intermediate buffers
1877    /// - Repeated key comparisons for already-visited entries
1878    ///
1879    /// ## Usage
1880    ///
1881    /// ```ignore
1882    /// for (key, value) in memtable.scan_range_iter(b"start", b"end", ts, txn) {
1883    ///     // Process each result as it arrives
1884    ///     // Memory usage is O(1) per iteration, not O(N) total
1885    /// }
1886    /// ```
1887    pub fn scan_range_iter<'a>(
1888        &'a self,
1889        start: &'a [u8],
1890        end: &'a [u8],
1891        snapshot_ts: u64,
1892        current_txn_id: Option<u64>,
1893    ) -> impl Iterator<Item = (Vec<u8>, Vec<u8>)> + 'a {
1894        // Compact deferred index before scanning if needed
1895        if let Some(ref idx) = self.deferred_index {
1896            idx.compact();
1897        }
1898
1899        // Use either ordered index or full scan
1900        let use_ordered = self.ordered_index.is_some() || self.deferred_index.is_some();
1901
1902        // Create iterator based on availability of ordered index
1903        ScanRangeIterator {
1904            memtable: self,
1905            start: start.to_vec(),
1906            end: end.to_vec(),
1907            snapshot_ts,
1908            current_txn_id,
1909            use_ordered,
1910            ordered_iter: None,
1911            unordered_iter: None,
1912            initialized: false,
1913        }
1914    }
1915
1916    /// Get approximate size
1917    pub fn size(&self) -> u64 {
1918        self.size_bytes.load(Ordering::Relaxed)
1919    }
1920
1921    /// Garbage collect old versions using epoch-based dirty list
1922    ///
1923    /// O(expired_versions) instead of O(all_versions)
1924    /// Only visits keys that had versions created in the old epoch.
1925    pub fn gc(&self, min_active_ts: u64) -> usize {
1926        // Advance epoch and get the dirty keys from the old epoch
1927        let (_old_epoch, dirty_keys) = self.dirty_list.advance_epoch();
1928
1929        if dirty_keys.is_empty() {
1930            return 0;
1931        }
1932
1933        let mut gc_count = 0;
1934
1935        // Only visit keys that were modified in the old epoch
1936        // Use a HashSet to deduplicate keys that were written multiple times
1937        let unique_keys: std::collections::HashSet<_> = dirty_keys.into_iter().collect();
1938
1939        for key in unique_keys {
1940            if let Some(mut entry) = self.data.get_mut(&key) {
1941                let before = entry.value().version_count();
1942                entry.value_mut().gc(min_active_ts);
1943                gc_count += before.saturating_sub(entry.value().version_count());
1944            }
1945        }
1946
1947        gc_count
1948    }
1949
1950    /// Legacy full-scan GC (for testing or when epoch-based tracking isn't available)
1951    #[allow(dead_code)]
1952    pub fn gc_full_scan(&self, min_active_ts: u64) -> usize {
1953        let mut gc_count = 0;
1954
1955        for mut entry in self.data.iter_mut() {
1956            let before = entry.value().version_count();
1957            entry.value_mut().gc(min_active_ts);
1958            gc_count += before.saturating_sub(entry.value().version_count());
1959        }
1960
1961        gc_count
1962    }
1963}
1964
1965// =============================================================================
1966// Rec 11: Unified MvccStore Implementation for MvccMemTable
1967// =============================================================================
1968
1969impl sochdb_core::version_chain::MvccStore for MvccMemTable {
1970    fn mvcc_get(&self, key: &[u8], snapshot_ts: u64, txn_id: Option<u64>) -> Option<Vec<u8>> {
1971        self.read(key, snapshot_ts, txn_id)
1972    }
1973
1974    fn mvcc_put(
1975        &self,
1976        key: &[u8],
1977        value: Option<Vec<u8>>,
1978        txn_id: u64,
1979    ) -> std::result::Result<(), sochdb_core::version_chain::MvccStoreError> {
1980        let mut entry = self.data.entry(key.to_vec()).or_default();
1981        if entry.has_write_conflict(txn_id) {
1982            return Err(sochdb_core::version_chain::MvccStoreError::WriteConflict);
1983        }
1984        entry.add_uncommitted(value, txn_id);
1985        Ok(())
1986    }
1987
1988    fn mvcc_commit_key(&self, key: &[u8], txn_id: u64, commit_ts: u64) -> bool {
1989        if let Some(mut chain) = self.data.get_mut(key) {
1990            return chain.commit(txn_id, commit_ts);
1991        }
1992        false
1993    }
1994
1995    fn mvcc_abort_key(&self, key: &[u8], txn_id: u64) {
1996        if let Some(mut chain) = self.data.get_mut(key) {
1997            chain.abort(txn_id);
1998        }
1999    }
2000
2001    fn mvcc_has_conflict(&self, key: &[u8], txn_id: u64) -> bool {
2002        self.data
2003            .get(key)
2004            .map(|chain| chain.has_write_conflict(txn_id))
2005            .unwrap_or(false)
2006    }
2007
2008    fn mvcc_gc(&self, min_ts: u64) -> sochdb_core::version_chain::MvccGcStats {
2009        let mut stats = sochdb_core::version_chain::MvccGcStats::default();
2010        for mut entry in self.data.iter_mut() {
2011            stats.keys_scanned += 1;
2012            let before = entry.value().version_count();
2013            entry.value_mut().gc(min_ts);
2014            stats.versions_removed += before.saturating_sub(entry.value().version_count());
2015        }
2016        stats
2017    }
2018
2019    fn mvcc_key_count(&self) -> usize {
2020        self.data.len()
2021    }
2022}
2023
2024// ============================================================================
2025// ArenaMvccMemTable - Arena-Backed MVCC MemTable with Reduced Allocations
2026// ============================================================================
2027
2028use crate::key_buffer::ArenaKeyHandle;
2029
2030/// Epoch-based dirty list using ArenaKeyHandle for reduced allocations
2031struct ArenaEpochDirtyList {
2032    epochs: [parking_lot::Mutex<Vec<ArenaKeyHandle>>; 4],
2033    current_epoch: AtomicU64,
2034}
2035
2036impl ArenaEpochDirtyList {
2037    fn new() -> Self {
2038        Self {
2039            epochs: [
2040                parking_lot::Mutex::new(Vec::new()),
2041                parking_lot::Mutex::new(Vec::new()),
2042                parking_lot::Mutex::new(Vec::new()),
2043                parking_lot::Mutex::new(Vec::new()),
2044            ],
2045            current_epoch: AtomicU64::new(0),
2046        }
2047    }
2048
2049    #[inline]
2050    fn record_version(&self, key: ArenaKeyHandle) {
2051        let epoch = self.current_epoch.load(Ordering::Relaxed);
2052        let idx = (epoch as usize) % EPOCH_RING_SIZE;
2053        self.epochs[idx].lock().push(key);
2054    }
2055
2056    fn advance_epoch(&self) -> (u64, Vec<ArenaKeyHandle>) {
2057        let old_epoch = self.current_epoch.fetch_add(1, Ordering::SeqCst);
2058        let old_idx = (old_epoch as usize) % EPOCH_RING_SIZE;
2059        let mut guard = self.epochs[old_idx].lock();
2060        let keys = std::mem::take(&mut *guard);
2061        (old_epoch, keys)
2062    }
2063}
2064
2065/// Arena-backed MVCC MemTable with optimized key storage
2066///
2067/// This version uses `ArenaKeyHandle` instead of `Vec<u8>` for keys,
2068/// reducing per-write allocations from 3 to 1:
2069///
2070/// - Before: 3 × Vec<u8> clones per write (dirty_list, ordered_index, data)
2071/// - After: 1 × ArenaKeyHandle creation, 3 × O(1) copies (16 bytes each)
2072///
2073/// ## Performance
2074///
2075/// Expected improvement: 20-30% throughput increase on write-heavy workloads
2076/// by reducing:
2077/// - Heap allocations: 3 → 1 per write
2078/// - Bytes copied: 3L → L + 48 bytes (where L = key length)
2079pub struct ArenaMvccMemTable {
2080    /// Key -> VersionChain (uses ArenaKeyHandle for O(1) hash)
2081    data: DashMap<ArenaKeyHandle, VersionChain>,
2082    /// Ordered index for prefix scans
2083    ordered_index: Option<SkipMap<ArenaKeyHandle, ()>>,
2084    /// Approximate size in bytes
2085    size_bytes: AtomicU64,
2086    /// Epoch-based dirty list (arena-backed)
2087    dirty_list: ArenaEpochDirtyList,
2088}
2089
2090impl ArenaMvccMemTable {
2091    pub fn new() -> Self {
2092        Self::with_ordered_index(true)
2093    }
2094
2095    pub fn with_ordered_index(enable_ordered_index: bool) -> Self {
2096        Self {
2097            data: DashMap::new(),
2098            ordered_index: if enable_ordered_index {
2099                Some(SkipMap::new())
2100            } else {
2101                None
2102            },
2103            size_bytes: AtomicU64::new(0),
2104            dirty_list: ArenaEpochDirtyList::new(),
2105        }
2106    }
2107
2108    /// Write a key-value pair using arena key handle
2109    ///
2110    /// Only creates ONE ArenaKeyHandle, then copies it (16 bytes) to each location.
2111    /// This is much cheaper than cloning Vec<u8> which requires heap allocation.
2112    pub fn write(&self, key: &[u8], value: Option<Vec<u8>>, txn_id: u64) -> Result<()> {
2113        let value_size = value.as_ref().map(|v| v.len()).unwrap_or(0);
2114        let key_len = key.len();
2115
2116        // Create ONE ArenaKeyHandle - this is the only allocation for the key
2117        let key_handle = ArenaKeyHandle::new(key);
2118
2119        // Track in dirty list (O(1) copy of 16-byte handle)
2120        self.dirty_list.record_version(key_handle.clone());
2121
2122        // Insert into ordered index (O(1) copy of 16-byte handle)
2123        if let Some(ref idx) = self.ordered_index {
2124            idx.insert(key_handle.clone(), ());
2125        }
2126
2127        // Use entry API with the handle
2128        let mut entry = self.data.entry(key_handle).or_default();
2129
2130        if entry.has_write_conflict(txn_id) {
2131            return Err(SochDBError::Internal(
2132                "Write-write conflict detected".into(),
2133            ));
2134        }
2135        entry.add_uncommitted(value, txn_id);
2136        self.size_bytes
2137            .fetch_add((key_len + value_size) as u64, Ordering::Relaxed);
2138
2139        Ok(())
2140    }
2141
2142    /// Write batch using arena key handles
2143    pub fn write_batch(&self, writes: &[(&[u8], Option<Vec<u8>>)], txn_id: u64) -> Result<()> {
2144        let mut total_size = 0u64;
2145
2146        for (key, value) in writes {
2147            let key_handle = ArenaKeyHandle::new(key);
2148
2149            self.dirty_list.record_version(key_handle.clone());
2150
2151            if let Some(ref idx) = self.ordered_index {
2152                idx.insert(key_handle.clone(), ());
2153            }
2154
2155            let mut entry = self.data.entry(key_handle).or_default();
2156
2157            if entry.has_write_conflict(txn_id) {
2158                return Err(SochDBError::Internal(
2159                    "Write-write conflict detected".into(),
2160                ));
2161            }
2162
2163            let value_size = value.as_ref().map(|v| v.len()).unwrap_or(0);
2164            entry.add_uncommitted(value.clone(), txn_id);
2165            total_size += (key.len() + value_size) as u64;
2166        }
2167
2168        self.size_bytes.fetch_add(total_size, Ordering::Relaxed);
2169        Ok(())
2170    }
2171
2172    /// Read at snapshot timestamp
2173    pub fn read(
2174        &self,
2175        key: &[u8],
2176        snapshot_ts: u64,
2177        current_txn_id: Option<u64>,
2178    ) -> Option<Vec<u8>> {
2179        // Create temporary handle for lookup (uses pre-computed hash for O(1) lookup)
2180        let key_handle = ArenaKeyHandle::new(key);
2181        self.data.get(&key_handle).and_then(|chain| {
2182            chain
2183                .read_at(snapshot_ts, current_txn_id)
2184                .and_then(|v| v.value.clone())
2185        })
2186    }
2187
2188    /// Commit transaction
2189    pub fn commit(&self, txn_id: u64, commit_ts: u64, write_set: &HashSet<InlineKey>) {
2190        for key in write_set {
2191            let key_handle = ArenaKeyHandle::new(key.as_slice());
2192            if let Some(mut chain) = self.data.get_mut(&key_handle) {
2193                chain.commit(txn_id, commit_ts);
2194            }
2195        }
2196    }
2197
2198    /// Abort transaction
2199    pub fn abort(&self, txn_id: u64) {
2200        for mut entry in self.data.iter_mut() {
2201            entry.value_mut().abort(txn_id);
2202        }
2203    }
2204
2205    /// Scan prefix
2206    pub fn scan_prefix(
2207        &self,
2208        prefix: &[u8],
2209        snapshot_ts: u64,
2210        current_txn_id: Option<u64>,
2211    ) -> Vec<(Vec<u8>, Vec<u8>)> {
2212        let mut results = Vec::new();
2213        let prefix_handle = ArenaKeyHandle::new(prefix);
2214
2215        if let Some(ref idx) = self.ordered_index {
2216            for entry in idx.range(prefix_handle..) {
2217                let key = entry.key();
2218
2219                if !key.as_bytes().starts_with(prefix) {
2220                    break;
2221                }
2222
2223                if let Some(chain) = self.data.get(key)
2224                    && let Some(v) = chain.read_at(snapshot_ts, current_txn_id)
2225                    && let Some(value) = &v.value
2226                {
2227                    results.push((key.as_bytes().to_vec(), value.clone()));
2228                }
2229            }
2230        } else {
2231            for entry in self.data.iter() {
2232                let key = entry.key();
2233                if !key.as_bytes().starts_with(prefix) {
2234                    continue;
2235                }
2236                if let Some(v) = entry.value().read_at(snapshot_ts, current_txn_id)
2237                    && let Some(value) = &v.value
2238                {
2239                    results.push((key.as_bytes().to_vec(), value.clone()));
2240                }
2241            }
2242        }
2243
2244        results
2245    }
2246
2247    /// Get approximate size
2248    pub fn size(&self) -> u64 {
2249        self.size_bytes.load(Ordering::Relaxed)
2250    }
2251
2252    /// Garbage collect old versions
2253    pub fn gc(&self, min_active_ts: u64) -> usize {
2254        let (_old_epoch, dirty_keys) = self.dirty_list.advance_epoch();
2255
2256        if dirty_keys.is_empty() {
2257            return 0;
2258        }
2259
2260        let mut gc_count = 0;
2261        let unique_keys: std::collections::HashSet<_> = dirty_keys.into_iter().collect();
2262
2263        for key in unique_keys {
2264            if let Some(mut entry) = self.data.get_mut(&key) {
2265                let before = entry.value().version_count();
2266                entry.value_mut().gc(min_active_ts);
2267                gc_count += before.saturating_sub(entry.value().version_count());
2268            }
2269        }
2270
2271        gc_count
2272    }
2273}
2274
2275impl Default for ArenaMvccMemTable {
2276    fn default() -> Self {
2277        Self::new()
2278    }
2279}
2280
2281// ============================================================================
2282// MemTableKind - Unified MemTable Abstraction (Principal Engineer Pattern)
2283// ============================================================================
2284
2285/// Configuration for memtable type selection
2286#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2287pub enum MemTableType {
2288    /// Standard MVCC memtable with deferred sorting
2289    /// Best for: general workloads, balanced read/write
2290    Standard,
2291    /// Arena-backed memtable with reduced allocations
2292    /// Best for: write-heavy workloads, large keys
2293    Arena,
2294}
2295
2296impl Default for MemTableType {
2297    fn default() -> Self {
2298        // Default to Standard which now has deferred sorting
2299        MemTableType::Standard
2300    }
2301}
2302
2303/// Unified memtable abstraction using enum dispatch
2304///
2305/// This pattern provides:
2306/// - Zero-cost abstraction (no vtable, no dynamic dispatch)
2307/// - Type-safe switching between implementations
2308/// - Easy extensibility for future memtable types
2309///
2310/// ## Why Enum over Trait Object?
2311///
2312/// - Hot path performance: enum match is a single branch vs vtable indirection
2313/// - Cache friendliness: no pointer chasing
2314/// - Inlining: compiler can inline through enum dispatch
2315pub enum MemTableKind {
2316    Standard(MvccMemTable),
2317    Arena(ArenaMvccMemTable),
2318}
2319
2320impl MemTableKind {
2321    /// Create a new memtable of the specified type
2322    pub fn new(kind: MemTableType, enable_ordered_index: bool) -> Self {
2323        match kind {
2324            MemTableType::Standard => {
2325                MemTableKind::Standard(MvccMemTable::with_ordered_index(enable_ordered_index))
2326            }
2327            MemTableType::Arena => {
2328                MemTableKind::Arena(ArenaMvccMemTable::with_ordered_index(enable_ordered_index))
2329            }
2330        }
2331    }
2332
2333    /// Write a key-value pair
2334    #[inline]
2335    pub fn write(&self, key: Vec<u8>, value: Option<Vec<u8>>, txn_id: u64) -> Result<()> {
2336        match self {
2337            MemTableKind::Standard(m) => m.write(key, value, txn_id),
2338            MemTableKind::Arena(m) => m.write(&key, value, txn_id),
2339        }
2340    }
2341
2342    /// Write batch of key-value pairs
2343    #[inline]
2344    pub fn write_batch(&self, writes: &[(Vec<u8>, Option<Vec<u8>>)], txn_id: u64) -> Result<()> {
2345        match self {
2346            MemTableKind::Standard(m) => m.write_batch(writes, txn_id),
2347            MemTableKind::Arena(m) => {
2348                // Convert to arena-compatible format
2349                let arena_writes: Vec<(&[u8], Option<Vec<u8>>)> = writes
2350                    .iter()
2351                    .map(|(k, v)| (k.as_slice(), v.clone()))
2352                    .collect();
2353                m.write_batch(&arena_writes, txn_id)
2354            }
2355        }
2356    }
2357
2358    /// Read at snapshot timestamp
2359    #[inline]
2360    pub fn read(
2361        &self,
2362        key: &[u8],
2363        snapshot_ts: u64,
2364        current_txn_id: Option<u64>,
2365    ) -> Option<Vec<u8>> {
2366        match self {
2367            MemTableKind::Standard(m) => m.read(key, snapshot_ts, current_txn_id),
2368            MemTableKind::Arena(m) => m.read(key, snapshot_ts, current_txn_id),
2369        }
2370    }
2371
2372    /// Commit transaction
2373    #[inline]
2374    pub fn commit(&self, txn_id: u64, commit_ts: u64, write_set: &HashSet<InlineKey>) {
2375        match self {
2376            MemTableKind::Standard(m) => m.commit(txn_id, commit_ts, write_set),
2377            MemTableKind::Arena(m) => m.commit(txn_id, commit_ts, write_set),
2378        }
2379    }
2380
2381    /// Abort transaction
2382    #[inline]
2383    pub fn abort(&self, txn_id: u64) {
2384        match self {
2385            MemTableKind::Standard(m) => m.abort(txn_id),
2386            MemTableKind::Arena(m) => m.abort(txn_id),
2387        }
2388    }
2389
2390    /// Scan prefix
2391    #[inline]
2392    pub fn scan_prefix(
2393        &self,
2394        prefix: &[u8],
2395        snapshot_ts: u64,
2396        current_txn_id: Option<u64>,
2397    ) -> Vec<(Vec<u8>, Vec<u8>)> {
2398        match self {
2399            MemTableKind::Standard(m) => m.scan_prefix(prefix, snapshot_ts, current_txn_id),
2400            MemTableKind::Arena(m) => m.scan_prefix(prefix, snapshot_ts, current_txn_id),
2401        }
2402    }
2403
2404    /// Scan range
2405    #[inline]
2406    pub fn scan_range(
2407        &self,
2408        start: &[u8],
2409        end: &[u8],
2410        snapshot_ts: u64,
2411        current_txn_id: Option<u64>,
2412    ) -> Vec<(Vec<u8>, Vec<u8>)> {
2413        match self {
2414            MemTableKind::Standard(m) => m.scan_range(start, end, snapshot_ts, current_txn_id),
2415            MemTableKind::Arena(m) => {
2416                // ArenaMvccMemTable doesn't have scan_range, use scan_prefix fallback
2417                let mut results = Vec::new();
2418                if let Some(ref idx) = m.ordered_index {
2419                    let start_handle = ArenaKeyHandle::new(start);
2420                    let end_handle = ArenaKeyHandle::new(end);
2421
2422                    if end.is_empty() {
2423                        for entry in idx.range(start_handle..) {
2424                            let key = entry.key();
2425                            if let Some(chain) = m.data.get(key)
2426                                && let Some(v) = chain.read_at(snapshot_ts, current_txn_id)
2427                                && let Some(value) = &v.value
2428                            {
2429                                results.push((key.as_bytes().to_vec(), value.clone()));
2430                            }
2431                        }
2432                    } else {
2433                        for entry in idx.range(start_handle..end_handle) {
2434                            let key = entry.key();
2435                            if let Some(chain) = m.data.get(key)
2436                                && let Some(v) = chain.read_at(snapshot_ts, current_txn_id)
2437                                && let Some(value) = &v.value
2438                            {
2439                                results.push((key.as_bytes().to_vec(), value.clone()));
2440                            }
2441                        }
2442                    }
2443                } else {
2444                    for entry in m.data.iter() {
2445                        let key = entry.key();
2446                        let key_bytes = key.as_bytes();
2447                        if key_bytes < start {
2448                            continue;
2449                        }
2450                        if !end.is_empty() && key_bytes >= end {
2451                            continue;
2452                        }
2453                        if let Some(v) = entry.value().read_at(snapshot_ts, current_txn_id)
2454                            && let Some(value) = &v.value
2455                        {
2456                            results.push((key_bytes.to_vec(), value.clone()));
2457                        }
2458                    }
2459                }
2460                results
2461            }
2462        }
2463    }
2464
2465    /// Scan range iterator (returns collected results for now)
2466    #[inline]
2467    pub fn scan_range_iter<'a>(
2468        &'a self,
2469        start: &'a [u8],
2470        end: &'a [u8],
2471        snapshot_ts: u64,
2472        current_txn_id: Option<u64>,
2473    ) -> Box<dyn Iterator<Item = (Vec<u8>, Vec<u8>)> + 'a> {
2474        match self {
2475            MemTableKind::Standard(m) => {
2476                Box::new(m.scan_range_iter(start, end, snapshot_ts, current_txn_id))
2477            }
2478            MemTableKind::Arena(_) => {
2479                // Arena version returns collected results as iterator
2480                let results = self.scan_range(start, end, snapshot_ts, current_txn_id);
2481                Box::new(results.into_iter())
2482            }
2483        }
2484    }
2485
2486    /// Get approximate size
2487    #[inline]
2488    pub fn size(&self) -> u64 {
2489        match self {
2490            MemTableKind::Standard(m) => m.size(),
2491            MemTableKind::Arena(m) => m.size(),
2492        }
2493    }
2494
2495    /// Garbage collect old versions
2496    #[inline]
2497    pub fn gc(&self, min_active_ts: u64) -> usize {
2498        match self {
2499            MemTableKind::Standard(m) => m.gc(min_active_ts),
2500            MemTableKind::Arena(m) => m.gc(min_active_ts),
2501        }
2502    }
2503
2504    /// Get the kind of memtable
2505    pub fn kind(&self) -> MemTableType {
2506        match self {
2507            MemTableKind::Standard(_) => MemTableType::Standard,
2508            MemTableKind::Arena(_) => MemTableType::Arena,
2509        }
2510    }
2511}
2512
2513/// Durable storage engine with full ACID support
2514pub struct DurableStorage {
2515    /// Path to storage directory
2516    path: PathBuf,
2517    /// Write-ahead log
2518    wal: Arc<TxnWal>,
2519    /// MVCC manager
2520    mvcc: Arc<MvccManager>,
2521    /// In-memory data (unified abstraction over Standard/Arena)
2522    memtable: Arc<MemTableKind>,
2523    /// Per-transaction WAL buffers for batched writes
2524    /// Key: txn_id, Value: TxnWalBuffer that accumulates writes in memory
2525    /// At commit, buffer is flushed to WAL with single lock acquisition
2526    txn_write_buffers: DashMap<u64, TxnWalBuffer>,
2527    /// Group commit buffer (optional)
2528    group_commit: Option<Arc<EventDrivenGroupCommit>>,
2529    /// Recovery state
2530    needs_recovery: AtomicU64, // 1 = needs recovery
2531    /// Last checkpoint LSN
2532    last_checkpoint_lsn: AtomicU64,
2533    /// Synchronous mode (like SQLite's PRAGMA synchronous)
2534    /// 0 = OFF, 1 = NORMAL (periodic sync), 2 = FULL (sync every commit)
2535    sync_mode: AtomicU64,
2536    /// Commits since last sync (for NORMAL mode)
2537    commits_since_sync: AtomicU64,
2538    /// Adaptive batch sizing for NORMAL mode (Little's Law)
2539    /// Arrival rate in requests/sec × 1000 for precision
2540    arrival_rate_ema: AtomicU64,
2541    /// Last commit timestamp in microseconds
2542    last_commit_us: AtomicU64,
2543    /// Estimated fsync latency in microseconds
2544    fsync_latency_us: AtomicU64,
2545    /// Database lock for exclusive access (None = no locking)
2546    #[allow(dead_code)]
2547    db_lock: Option<crate::lock::DatabaseLock>,
2548    /// Whether at-rest encryption is active for this instance (drives the live
2549    /// per-instance durability matrix). Set from the resolved keyring at open.
2550    at_rest_encrypted: bool,
2551    /// Whether Point-in-Time Recovery is enabled for this database (Task 3B PITR
2552    /// phase 1). Derived from the presence of `wal.manifest` at open (the manifest
2553    /// is the single source of truth), or set by `enable_point_in_time_recovery`.
2554    /// When enabled, the destructive `truncate_wal()` is forbidden (segment
2555    /// sealing is the PITR-safe replacement, landing in a later phase) so the WAL
2556    /// record ordinal stays a stable, durable monotonic LSN across restarts.
2557    pitr_enabled: AtomicBool,
2558}
2559
2560/// Encryption configuration for opening a [`DurableStorage`].
2561///
2562/// `disabled()` (the default for the legacy open variants) keeps the database
2563/// plaintext and byte-compatible with pre-encryption binaries. `with_kek()`
2564/// supplies the Key-Encryption-Key — the operator secret that *wraps* a
2565/// per-database data key; it is never used verbatim as the cipher key (see
2566/// [`crate::keyring`]). A wrong/missing KEK for an encrypted database fails
2567/// closed at open (the DB will refuse to open, never silently read as plaintext).
2568pub struct StorageEncryption {
2569    /// The KEK. `None` ⇒ plaintext database.
2570    pub kek: Option<EncryptionKey>,
2571    /// Human-readable identifier for the key source (e.g. "env:SOCHDB_ENCRYPTION_KEY",
2572    /// "embedded", "kms:..."). Bound into the keyring for provenance.
2573    pub source_id: String,
2574}
2575
2576impl StorageEncryption {
2577    /// Plaintext (no encryption) — the default.
2578    pub fn disabled() -> Self {
2579        Self {
2580            kek: None,
2581            source_id: "none".to_string(),
2582        }
2583    }
2584
2585    /// Encrypt at rest under the given KEK.
2586    pub fn with_kek(kek: EncryptionKey, source_id: impl Into<String>) -> Self {
2587        Self {
2588            kek: Some(kek),
2589            source_id: source_id.into(),
2590        }
2591    }
2592
2593    /// Whether a key is configured (i.e. encryption is requested).
2594    pub fn is_enabled(&self) -> bool {
2595        self.kek.is_some()
2596    }
2597}
2598
2599impl DurableStorage {
2600    /// Open or create durable storage at path
2601    pub fn open<P: AsRef<Path>>(path: P) -> Result<Self> {
2602        Self::open_with_config(path, true)
2603    }
2604
2605    /// Open with configurable ordered index
2606    ///
2607    /// When `enable_ordered_index` is false, saves ~134 ns/op on writes
2608    /// but scan_prefix becomes O(N) instead of O(log N + K)
2609    pub fn open_with_config<P: AsRef<Path>>(path: P, enable_ordered_index: bool) -> Result<Self> {
2610        Self::open_with_full_config(path, enable_ordered_index, MemTableType::Standard)
2611    }
2612
2613    /// Open with arena-backed memtable for write-heavy workloads
2614    ///
2615    /// Uses ArenaMvccMemTable which reduces per-write allocations from 3 to 1.
2616    /// Best for workloads with:
2617    /// - High write throughput
2618    /// - Large keys (reduces allocation overhead)
2619    /// - Minimal concurrent reads during writes
2620    pub fn open_with_arena<P: AsRef<Path>>(path: P) -> Result<Self> {
2621        Self::open_with_full_config(path, true, MemTableType::Arena)
2622    }
2623
2624    /// Open with full configuration options
2625    ///
2626    /// # Arguments
2627    /// * `path` - Storage directory path
2628    /// * `enable_ordered_index` - Enable ordered index for O(log N) scans
2629    /// * `memtable_type` - Type of memtable to use (Standard or Arena)
2630    ///
2631    /// # Locking
2632    ///
2633    /// Acquires an exclusive advisory lock on the database directory.
2634    /// This prevents concurrent multi-process access which would corrupt data.
2635    /// If another process has the database open, returns `Err(DatabaseLocked)`.
2636    pub fn open_with_full_config<P: AsRef<Path>>(
2637        path: P,
2638        enable_ordered_index: bool,
2639        memtable_type: MemTableType,
2640    ) -> Result<Self> {
2641        Self::open_with_full_config_internal(
2642            path,
2643            enable_ordered_index,
2644            memtable_type,
2645            true,
2646            StorageEncryption::disabled(),
2647        )
2648    }
2649
2650    /// Open with at-rest encryption configured via [`StorageEncryption`].
2651    ///
2652    /// With `StorageEncryption::disabled()` this is identical to
2653    /// [`Self::open_with_full_config`]. With a KEK, the database is opened (or
2654    /// created) encrypted: a per-DB data key is generated and wrapped into the
2655    /// keyring on first open, and a wrong/missing key on a subsequent open fails
2656    /// closed. Reads are unaffected (the live read path is in-memory); only the
2657    /// WAL write/recovery path pays the AEAD cost.
2658    pub fn open_with_encryption<P: AsRef<Path>>(
2659        path: P,
2660        enable_ordered_index: bool,
2661        memtable_type: MemTableType,
2662        encryption: StorageEncryption,
2663    ) -> Result<Self> {
2664        Self::open_with_full_config_internal(
2665            path,
2666            enable_ordered_index,
2667            memtable_type,
2668            true,
2669            encryption,
2670        )
2671    }
2672
2673    /// The live, per-instance durability capabilities of THIS database.
2674    ///
2675    /// Unlike the build-level [`crate::durability_capabilities`] (which reports
2676    /// defaults), this reflects the actual resolved state — notably whether
2677    /// at-rest encryption is active for this opened instance.
2678    pub fn durability_capabilities(&self) -> DurabilityCapabilities {
2679        DurabilityCapabilities {
2680            crash_recovery: true,
2681            at_rest_encryption: self.at_rest_encrypted,
2682            // Point-in-time recovery is live (recover_to) when PITR is enabled:
2683            // the WAL is fully retained and can be replayed to an LSN/timestamp.
2684            point_in_time_recovery: self.pitr_enabled.load(Ordering::SeqCst),
2685            // ARIES / fencing remain unwired on the live path.
2686            aries_checkpoint: false,
2687            wal_fencing: false,
2688        }
2689    }
2690
2691    /// Whether at-rest encryption is active for this instance.
2692    pub fn is_encrypted(&self) -> bool {
2693        self.at_rest_encrypted
2694    }
2695
2696    /// Open without locking (for testing crash recovery scenarios)
2697    ///
2698    /// # Safety
2699    /// This should ONLY be used in tests that simulate crashes by forgetting
2700    /// the storage instance. In production, always use `open_with_full_config`.
2701    #[cfg(test)]
2702    pub fn open_without_lock<P: AsRef<Path>>(path: P) -> Result<Self> {
2703        Self::open_with_full_config_internal(
2704            path,
2705            true,
2706            MemTableType::Standard,
2707            false,
2708            StorageEncryption::disabled(),
2709        )
2710    }
2711
2712    /// Open an ephemeral (in-memory-like) DurableStorage backed by a temp directory.
2713    ///
2714    /// Uses the full DurableStorage engine (WAL, MVCC, SSI) but writes to a
2715    /// temporary directory that is automatically cleaned up when the
2716    /// `EphemeralHandle` is dropped. This ensures test and production code paths
2717    /// are identical — bugs found in tests are guaranteed to reproduce in production.
2718    ///
2719    /// # Returns
2720    /// An `EphemeralHandle` that owns both the storage and the temp directory.
2721    /// Access the storage via `handle.storage()` or `Deref` coercion.
2722    ///
2723    /// # Example
2724    /// ```ignore
2725    /// let handle = DurableStorage::open_ephemeral()?;
2726    /// let txn = handle.begin_transaction()?;
2727    /// handle.write(txn, b"key".to_vec(), b"value".to_vec())?;
2728    /// handle.commit(txn)?;
2729    /// // temp directory cleaned up when `handle` drops
2730    /// ```
2731    pub fn open_ephemeral() -> Result<EphemeralHandle> {
2732        let tmp = tempfile::tempdir().map_err(|e| SochDBError::Io(e))?;
2733        let storage = Self::open_with_full_config_internal(
2734            tmp.path(),
2735            true,
2736            MemTableType::Standard,
2737            false, // No lock needed for ephemeral
2738            StorageEncryption::disabled(),
2739        )?;
2740        Ok(EphemeralHandle {
2741            storage,
2742            _tmpdir: tmp,
2743        })
2744    }
2745
2746    /// Open an ephemeral DurableStorage with group commit enabled.
2747    ///
2748    /// Same as `open_ephemeral()` but with group commit for higher throughput.
2749    pub fn open_ephemeral_with_group_commit() -> Result<EphemeralHandle> {
2750        let tmp = tempfile::tempdir().map_err(|e| SochDBError::Io(e))?;
2751        let mut storage = Self::open_with_full_config_internal(
2752            tmp.path(),
2753            true,
2754            MemTableType::Standard,
2755            false,
2756            StorageEncryption::disabled(),
2757        )?;
2758
2759        let wal = storage.wal.clone();
2760        let gc = EventDrivenGroupCommit::new(move |txn_ids: &[u64]| {
2761            for &txn_id in txn_ids {
2762                let entry = TxnWalEntry::txn_commit(txn_id);
2763                wal.append_no_flush(&entry).map_err(|e| e.to_string())?;
2764            }
2765            wal.flush().map_err(|e| e.to_string())?;
2766            wal.sync().map_err(|e| e.to_string())?;
2767            Ok(std::time::SystemTime::now()
2768                .duration_since(std::time::UNIX_EPOCH)
2769                .unwrap()
2770                .as_micros() as u64)
2771        });
2772        storage.group_commit = Some(Arc::new(gc));
2773
2774        Ok(EphemeralHandle {
2775            storage,
2776            _tmpdir: tmp,
2777        })
2778    }
2779
2780    fn open_with_full_config_internal<P: AsRef<Path>>(
2781        path: P,
2782        enable_ordered_index: bool,
2783        memtable_type: MemTableType,
2784        acquire_lock: bool,
2785        encryption: StorageEncryption,
2786    ) -> Result<Self> {
2787        let path = path.as_ref().to_path_buf();
2788        std::fs::create_dir_all(&path)?;
2789
2790        // Acquire exclusive lock on database directory (unless disabled for testing)
2791        let db_lock = if acquire_lock {
2792            Some(
2793                crate::lock::DatabaseLock::acquire(&path)
2794                    .map_err(|e| SochDBError::LockError(e.to_string()))?,
2795            )
2796        } else {
2797            None
2798        };
2799
2800        let wal_path = path.join("wal.log");
2801
2802        // Resolve at-rest encryption from the keyring BEFORE touching the WAL.
2803        // - A fresh DB (no wal.log yet) with a KEK ⇒ create an encrypted keyring.
2804        // - An existing plaintext DB with a KEK ⇒ refused (must migrate explicitly).
2805        // - An encrypted DB with a wrong/missing KEK ⇒ refused fail-closed.
2806        let is_new_db = !wal_path.exists();
2807        let enc_state = keyring::load_or_init(
2808            &path,
2809            encryption.kek.as_ref(),
2810            &encryption.source_id,
2811            is_new_db,
2812        )?;
2813        let at_rest_encrypted = enc_state.is_encrypted();
2814        let wal = Arc::new(TxnWal::new_with_encryption(
2815            &wal_path,
2816            enc_state.engine(),
2817            enc_state.db_uuid(),
2818            enc_state.key_epoch(),
2819        )?);
2820
2821        // PITR anchor: the presence of wal.manifest is the single source of truth
2822        // that this DB is PITR-enabled. If present, seed last_checkpoint_lsn from
2823        // it (it is otherwise in-memory and lost on restart).
2824        let (pitr_enabled, initial_checkpoint_lsn) =
2825            if crate::wal_manifest::WalManifest::exists(&path) {
2826                let m = crate::wal_manifest::WalManifest::load(&path)?;
2827                (true, m.last_checkpoint_lsn)
2828            } else {
2829                (false, 0)
2830            };
2831
2832        let storage = Self {
2833            path,
2834            wal: wal.clone(),
2835            mvcc: Arc::new(MvccManager::new()),
2836            memtable: Arc::new(MemTableKind::new(memtable_type, enable_ordered_index)),
2837            txn_write_buffers: DashMap::new(),
2838            group_commit: None,
2839            needs_recovery: AtomicU64::new(0),
2840            last_checkpoint_lsn: AtomicU64::new(initial_checkpoint_lsn),
2841            sync_mode: AtomicU64::new(1), // Default: NORMAL (like SQLite)
2842            commits_since_sync: AtomicU64::new(0),
2843            // Adaptive batch sizing (Little's Law)
2844            arrival_rate_ema: AtomicU64::new(1_000_000), // 1000 req/s × 1000 initial
2845            last_commit_us: AtomicU64::new(0),
2846            fsync_latency_us: AtomicU64::new(5000), // 5ms default
2847            db_lock,
2848            at_rest_encrypted,
2849            pitr_enabled: AtomicBool::new(pitr_enabled),
2850        };
2851
2852        // Check if recovery needed
2853        if storage.check_recovery_needed()? {
2854            storage.needs_recovery.store(1, Ordering::SeqCst);
2855        }
2856
2857        Ok(storage)
2858    }
2859
2860    /// Open with group commit enabled
2861    pub fn open_with_group_commit<P: AsRef<Path>>(path: P) -> Result<Self> {
2862        Self::open_with_group_commit_and_config(path, true)
2863    }
2864
2865    /// Open with group commit and configurable ordered index
2866    pub fn open_with_group_commit_and_config<P: AsRef<Path>>(
2867        path: P,
2868        enable_ordered_index: bool,
2869    ) -> Result<Self> {
2870        let mut storage = Self::open_with_config(path, enable_ordered_index)?;
2871
2872        let wal = storage.wal.clone();
2873        let gc = EventDrivenGroupCommit::new(move |txn_ids: &[u64]| {
2874            // Write all commit records WITHOUT flushing (batch them)
2875            for &txn_id in txn_ids {
2876                let entry = TxnWalEntry::txn_commit(txn_id);
2877                wal.append_no_flush(&entry).map_err(|e| e.to_string())?;
2878            }
2879
2880            // Then do a SINGLE flush + fsync for the entire batch
2881            wal.flush().map_err(|e| e.to_string())?;
2882            wal.sync().map_err(|e| e.to_string())?;
2883
2884            // Return commit timestamp
2885            Ok(std::time::SystemTime::now()
2886                .duration_since(std::time::UNIX_EPOCH)
2887                .unwrap()
2888                .as_micros() as u64)
2889        });
2890
2891        storage.group_commit = Some(Arc::new(gc));
2892        Ok(storage)
2893    }
2894
2895    /// Open with IndexPolicy for automatic memtable/index configuration
2896    ///
2897    /// This is the recommended constructor for new code. The policy determines:
2898    /// - Whether to use ordered index (ScanOptimized only)
2899    /// - Whether to use arena-backed memtable (WriteOptimized, AppendOnly)
2900    /// - Default settings optimized for the workload pattern
2901    ///
2902    /// # Arguments
2903    /// * `path` - Storage directory path
2904    /// * `policy` - Index policy determining write/scan tradeoffs
2905    /// * `group_commit` - Whether to enable group commit for throughput
2906    pub fn open_with_policy<P: AsRef<Path>>(
2907        path: P,
2908        policy: crate::index_policy::IndexPolicy,
2909        group_commit: bool,
2910    ) -> Result<Self> {
2911        Self::open_with_policy_encrypted(path, policy, group_commit, StorageEncryption::disabled())
2912    }
2913
2914    /// Policy-based open with at-rest encryption configured.
2915    ///
2916    /// Identical to [`Self::open_with_policy`] but threads a [`StorageEncryption`]
2917    /// down to the keyring/WAL so the embedded `Database` kernel can open (or
2918    /// create) an encrypted database. `StorageEncryption::disabled()` is exactly
2919    /// the plaintext behaviour.
2920    pub fn open_with_policy_encrypted<P: AsRef<Path>>(
2921        path: P,
2922        policy: crate::index_policy::IndexPolicy,
2923        group_commit: bool,
2924        encryption: StorageEncryption,
2925    ) -> Result<Self> {
2926        use crate::index_policy::IndexPolicy;
2927
2928        // Derive configuration from policy
2929        let (enable_ordered_index, memtable_type) = match policy {
2930            IndexPolicy::WriteOptimized | IndexPolicy::AppendOnly => {
2931                // Write-heavy: no ordered index, use arena for reduced allocations
2932                (false, MemTableType::Arena)
2933            }
2934            IndexPolicy::Balanced => {
2935                // Mixed OLTP: deferred sorting (already implemented in Standard)
2936                (true, MemTableType::Standard)
2937            }
2938            IndexPolicy::ScanOptimized => {
2939                // Scan-heavy: maintain ordered index
2940                (true, MemTableType::Standard)
2941            }
2942        };
2943
2944        if group_commit {
2945            let mut storage =
2946                Self::open_with_encryption(path, enable_ordered_index, memtable_type, encryption)?;
2947
2948            let wal = storage.wal.clone();
2949            let gc = EventDrivenGroupCommit::new(move |txn_ids: &[u64]| {
2950                for &txn_id in txn_ids {
2951                    let entry = TxnWalEntry::txn_commit(txn_id);
2952                    wal.append_no_flush(&entry).map_err(|e| e.to_string())?;
2953                }
2954                wal.flush().map_err(|e| e.to_string())?;
2955                wal.sync().map_err(|e| e.to_string())?;
2956                Ok(std::time::SystemTime::now()
2957                    .duration_since(std::time::UNIX_EPOCH)
2958                    .unwrap()
2959                    .as_micros() as u64)
2960            });
2961            storage.group_commit = Some(Arc::new(gc));
2962            Ok(storage)
2963        } else {
2964            Self::open_with_encryption(path, enable_ordered_index, memtable_type, encryption)
2965        }
2966    }
2967
2968    /// Open storage for concurrent mode (multi-reader, single-writer)
2969    ///
2970    /// This method opens the storage WITHOUT acquiring the exclusive file lock.
2971    /// Coordination is handled by the concurrent MVCC layer instead.
2972    ///
2973    /// # Safety
2974    ///
2975    /// This must ONLY be called from `Database::open_concurrent()` which
2976    /// manages the concurrent MVCC coordination. Direct use will cause
2977    /// data corruption.
2978    pub fn open_for_concurrent<P: AsRef<Path>>(
2979        path: P,
2980        policy: crate::index_policy::IndexPolicy,
2981    ) -> Result<Self> {
2982        Self::open_for_concurrent_encrypted(path, policy, StorageEncryption::disabled())
2983    }
2984
2985    /// Concurrent-mode open with at-rest encryption configured.
2986    ///
2987    /// Identical to [`Self::open_for_concurrent`] but threads a
2988    /// [`StorageEncryption`] through, so an encrypted database can also be opened
2989    /// in concurrent (multi-reader) mode rather than failing closed for lack of a
2990    /// key channel.
2991    pub fn open_for_concurrent_encrypted<P: AsRef<Path>>(
2992        path: P,
2993        policy: crate::index_policy::IndexPolicy,
2994        encryption: StorageEncryption,
2995    ) -> Result<Self> {
2996        use crate::index_policy::IndexPolicy;
2997
2998        let (enable_ordered_index, memtable_type) = match policy {
2999            IndexPolicy::WriteOptimized | IndexPolicy::AppendOnly => (false, MemTableType::Arena),
3000            IndexPolicy::Balanced => (true, MemTableType::Standard),
3001            IndexPolicy::ScanOptimized => (true, MemTableType::Standard),
3002        };
3003
3004        // Open WITHOUT exclusive file lock (concurrent MVCC handles coordination)
3005        Self::open_with_full_config_internal(
3006            path,
3007            enable_ordered_index,
3008            memtable_type,
3009            false,
3010            encryption,
3011        )
3012    }
3013
3014    /// Get the memtable type being used
3015    pub fn memtable_type(&self) -> MemTableType {
3016        self.memtable.kind()
3017    }
3018
3019    /// Check if recovery is needed (dirty shutdown detection)
3020    ///
3021    /// Note: Recovery must ALWAYS run to rebuild the in-memory memtable from WAL.
3022    /// The clean_shutdown marker only tells us if there might be uncommitted transactions,
3023    /// but committed data still needs to be loaded from WAL into the memtable.
3024    fn check_recovery_needed(&self) -> Result<bool> {
3025        let marker_path = self.path.join(".clean_shutdown");
3026        if marker_path.exists() {
3027            // Clean shutdown - remove marker
3028            std::fs::remove_file(&marker_path)?;
3029        }
3030        // ALWAYS need recovery to rebuild memtable from WAL
3031        // The memtable is in-memory only and needs to be restored on every startup
3032        Ok(true)
3033    }
3034
3035    /// Perform crash recovery
3036    pub fn recover(&self) -> Result<RecoveryStats> {
3037        if self.needs_recovery.load(Ordering::SeqCst) == 0 {
3038            return Ok(RecoveryStats::default());
3039        }
3040
3041        let (writes, txn_count) = self.wal.replay_for_recovery()?;
3042
3043        // Apply committed writes to memtable
3044        let recovery_txn_id = self.wal.alloc_txn_id();
3045        let commit_ts = self.mvcc.alloc_commit_ts();
3046
3047        // Collect keys being written for efficient commit
3048        let mut write_set: HashSet<InlineKey> = HashSet::new();
3049        for (key, value) in &writes {
3050            write_set.insert(SmallVec::from_slice(key));
3051            self.memtable
3052                .write(key.clone(), Some(value.clone()), recovery_txn_id)?;
3053        }
3054        self.memtable.commit(recovery_txn_id, commit_ts, &write_set);
3055
3056        self.needs_recovery.store(0, Ordering::SeqCst);
3057
3058        Ok(RecoveryStats {
3059            transactions_recovered: txn_count,
3060            writes_recovered: writes.len(),
3061            commit_ts,
3062        })
3063    }
3064
3065    /// Point-in-Time Recovery: rebuild the in-memory state as of `target`.
3066    ///
3067    /// This is the PITR analogue of [`Self::recover`] — call it on a FRESH open
3068    /// INSTEAD of `recover()` (not in addition to it), to materialize the
3069    /// database as it existed at a chosen LSN or commit timestamp. Because PITR
3070    /// mode never truncates the WAL, the full history is retained and replayed up
3071    /// to (and stopping at) the target, with transaction atomicity preserved
3072    /// (a transaction is applied only if its commit is within the target).
3073    ///
3074    /// Requires PITR to be enabled (the WAL must be fully retained); returns an
3075    /// error otherwise, since a truncated WAL cannot honor an arbitrary target.
3076    pub fn recover_to(&self, target: crate::txn_wal::RecoveryTarget) -> Result<RecoveryStats> {
3077        if !self.pitr_enabled.load(Ordering::SeqCst) {
3078            return Err(SochDBError::InvalidArgument(
3079                "recover_to requires Point-in-Time Recovery to be enabled \
3080                 (the full WAL must be retained); call enable_point_in_time_recovery first"
3081                    .to_string(),
3082            ));
3083        }
3084
3085        // Single-shot recovery on a fresh open: refuse if state was already
3086        // rebuilt (by recover() or a prior recover_to()). Re-applying would layer
3087        // a stale set over the point-in-time state under a newer commit_ts and
3088        // silently corrupt it (recover()/recover_to() both clear needs_recovery).
3089        if self.needs_recovery.load(Ordering::SeqCst) == 0 {
3090            return Err(SochDBError::InvalidArgument(
3091                "recover_to must be the sole recovery on a fresh open, but state \
3092                 was already recovered; reopen the database and call recover_to first"
3093                    .to_string(),
3094            ));
3095        }
3096
3097        // Make the on-disk WAL match current_lsn() before replaying. Under the
3098        // default NORMAL sync mode the commit record(s) may still sit in the
3099        // BufWriter, while current_lsn() counts the in-memory sequence — so
3100        // without this flush+fsync a captured-LSN cut would silently drop
3101        // committed-but-unflushed records (replay reads a fresh on-disk handle).
3102        self.wal.flush()?;
3103        self.wal.sync()?;
3104
3105        let (writes, txn_count) = self.wal.replay_to_target(target)?;
3106
3107        // Apply the bounded set of committed writes to the (fresh) memtable,
3108        // mirroring recover().
3109        let recovery_txn_id = self.wal.alloc_txn_id();
3110        let commit_ts = self.mvcc.alloc_commit_ts();
3111        let mut write_set: HashSet<InlineKey> = HashSet::new();
3112        for (key, value) in &writes {
3113            write_set.insert(SmallVec::from_slice(key));
3114            self.memtable
3115                .write(key.clone(), Some(value.clone()), recovery_txn_id)?;
3116        }
3117        self.memtable.commit(recovery_txn_id, commit_ts, &write_set);
3118
3119        self.needs_recovery.store(0, Ordering::SeqCst);
3120
3121        Ok(RecoveryStats {
3122            transactions_recovered: txn_count,
3123            writes_recovered: writes.len(),
3124            commit_ts,
3125        })
3126    }
3127
3128    /// Begin a new transaction
3129    pub fn begin_transaction(&self) -> Result<u64> {
3130        let txn_id = self.wal.begin_transaction()?;
3131        self.mvcc.begin(txn_id);
3132        Ok(txn_id)
3133    }
3134
3135    /// Begin a transaction with a specific mode (ReadOnly/WriteOnly/ReadWrite)
3136    ///
3137    /// This enables mode-aware optimizations:
3138    /// - ReadOnly: Skip SSI tracking, 2.6x faster reads
3139    /// - WriteOnly: Skip read tracking, faster bulk inserts
3140    /// - ReadWrite: Full SSI for serializable isolation
3141    pub fn begin_with_mode(&self, mode: TransactionMode) -> Result<u64> {
3142        let txn_id = self.wal.begin_transaction()?;
3143        self.mvcc.begin_with_mode(txn_id, mode);
3144        Ok(txn_id)
3145    }
3146
3147    /// Begin a read-only transaction without any WAL records.
3148    ///
3149    /// This is a performance-critical optimization that eliminates two WAL
3150    /// mutex acquisitions per read (TxnBegin + TxnAbort). Since read-only
3151    /// transactions have no state to recover, WAL records are unnecessary.
3152    ///
3153    /// Callers MUST use `abort_read_only_fast()` to clean up.
3154    #[inline]
3155    pub fn begin_read_only_fast(&self) -> u64 {
3156        let txn_id = self.wal.alloc_txn_id();
3157        self.mvcc.begin_read_only(txn_id);
3158        txn_id
3159    }
3160
3161    /// Abort a fast read-only transaction.
3162    ///
3163    /// O(1) cleanup: only removes MVCC state. No WAL write, no memtable scan.
3164    #[inline]
3165    pub fn abort_read_only_fast(&self, txn_id: u64) {
3166        self.mvcc.abort(txn_id);
3167    }
3168
3169    /// Read a key WITHOUT any MVCC transaction tracking.
3170    ///
3171    /// Uses the current global timestamp to see all committed writes.
3172    /// Bypasses: begin/abort, active_txns DashMap, record_read, stats.
3173    /// Only safe for single-threaded access (no concurrent writes).
3174    #[inline]
3175    pub fn read_latest(&self, key: &[u8]) -> Option<Vec<u8>> {
3176        let snapshot_ts = self
3177            .mvcc
3178            .ts_counter
3179            .load(std::sync::atomic::Ordering::Relaxed);
3180        self.memtable.read(key, snapshot_ts, None)
3181    }
3182
3183    /// Scan keys with a prefix WITHOUT any MVCC transaction tracking.
3184    ///
3185    /// Uses the current global timestamp. Only safe for single-threaded access.
3186    #[inline]
3187    pub fn scan_latest(&self, prefix: &[u8]) -> Vec<(Vec<u8>, Vec<u8>)> {
3188        let snapshot_ts = self
3189            .mvcc
3190            .ts_counter
3191            .load(std::sync::atomic::Ordering::Relaxed);
3192        self.memtable.scan_prefix(prefix, snapshot_ts, None)
3193    }
3194
3195    /// Read a key within a transaction
3196    #[inline]
3197    pub fn read(&self, txn_id: u64, key: &[u8]) -> Result<Option<Vec<u8>>> {
3198        // Fast path: get just snapshot_ts without cloning whole transaction
3199        let snapshot_ts = self
3200            .mvcc
3201            .get_snapshot_ts(txn_id)
3202            .ok_or_else(|| SochDBError::Internal("Transaction not found".into()))?;
3203
3204        // Record read for SSI (skipped for read-only transactions)
3205        self.mvcc.record_read(txn_id, key);
3206
3207        // Read at snapshot timestamp, seeing own uncommitted writes
3208        Ok(self.memtable.read(key, snapshot_ts, Some(txn_id)))
3209    }
3210
3211    /// Write a key-value pair within a transaction
3212    ///
3213    /// Writes are buffered and only flushed to disk on commit.
3214    /// This provides ~10× better throughput for batched inserts.
3215    pub fn write(&self, txn_id: u64, key: Vec<u8>, value: Vec<u8>) -> Result<()> {
3216        // Use the zero-allocation path internally
3217        self.write_refs(txn_id, &key, &value)?;
3218
3219        Ok(())
3220    }
3221
3222    /// Write from references - zero allocation hot path
3223    ///
3224    /// Avoids cloning key/value by writing to WAL from refs directly,
3225    /// then only allocating once for memtable storage.
3226    #[inline]
3227    pub fn write_refs(&self, txn_id: u64, key: &[u8], value: &[u8]) -> Result<()> {
3228        // Record write for MVCC (uses InlineKey - zero allocation for small keys)
3229        self.mvcc.record_write(txn_id, key);
3230
3231        // Buffer writes in memory using TxnWalBuffer - NO WAL lock taken!
3232        // This reduces lock contention from O(writes) to O(1) per transaction
3233        self.txn_write_buffers
3234            .entry(txn_id)
3235            .or_insert_with(|| TxnWalBuffer::new(txn_id))
3236            .append(key, value);
3237
3238        // Write to memtable (needs owned key/value for storage)
3239        self.memtable
3240            .write(key.to_vec(), Some(value.to_vec()), txn_id)?;
3241
3242        Ok(())
3243    }
3244
3245    /// Delete a key within a transaction
3246    pub fn delete(&self, txn_id: u64, key: Vec<u8>) -> Result<()> {
3247        // Record write (uses InlineKey - zero allocation for small keys)
3248        self.mvcc.record_write(txn_id, &key);
3249
3250        // Buffer tombstone in memory - NO WAL lock taken!
3251        self.txn_write_buffers
3252            .entry(txn_id)
3253            .or_insert_with(|| TxnWalBuffer::new(txn_id))
3254            .append(&key, &[]); // Empty value = tombstone
3255
3256        // Write tombstone to memtable
3257        self.memtable.write(key, None, txn_id)?;
3258
3259        Ok(())
3260    }
3261
3262    /// Batch write multiple key-value pairs with reduced overhead
3263    ///
3264    /// This API amortizes fixed costs over the batch:
3265    /// - Single DashMap entry lookup for TxnWalBuffer
3266    /// - Single MVCC write set update
3267    /// - Batch memtable operations
3268    ///
3269    /// Performance: ~2-3x faster than individual write_refs calls
3270    /// for batches of 100+ entries.
3271    ///
3272    /// # Arguments
3273    /// * `txn_id` - Transaction ID
3274    /// * `writes` - Slice of (key, value) pairs
3275    #[inline]
3276    pub fn write_batch_refs(&self, txn_id: u64, writes: &[(&[u8], &[u8])]) -> Result<()> {
3277        if writes.is_empty() {
3278            return Ok(());
3279        }
3280
3281        // Single DashMap access for entire batch
3282        let mut buffer_entry = self
3283            .txn_write_buffers
3284            .entry(txn_id)
3285            .or_insert_with(|| TxnWalBuffer::new(txn_id));
3286
3287        // Batch operations with reduced per-row overhead
3288        for (key, value) in writes {
3289            // Record write for MVCC
3290            self.mvcc.record_write(txn_id, key);
3291
3292            // Append to WAL buffer
3293            buffer_entry.append(key, value);
3294        }
3295        drop(buffer_entry);
3296
3297        // Batch write to memtable
3298        let owned_writes: Vec<(Vec<u8>, Option<Vec<u8>>)> = writes
3299            .iter()
3300            .map(|(k, v)| (k.to_vec(), Some(v.to_vec())))
3301            .collect();
3302        self.memtable.write_batch(&owned_writes, txn_id)?;
3303
3304        Ok(())
3305    }
3306
3307    /// Commit a transaction
3308    ///
3309    /// With sync_mode:
3310    /// - 0 (OFF): No sync, risk of data loss
3311    /// - 1 (NORMAL): Adaptive sync using Little's Law: W* = √(τ/λ)
3312    /// - 2 (FULL): Sync every commit (safest, slowest)
3313    pub fn commit(&self, txn_id: u64) -> Result<u64> {
3314        // First, flush all buffered DATA writes to the WAL with a SINGLE lock
3315        // acquisition (O(1) lock instead of O(writes) locks). Flushing data
3316        // records before validation is safe: ARIES recovery only treats them as
3317        // winners if a *durable commit record* for this transaction also exists,
3318        // and that record is written below — strictly after validation succeeds.
3319        if let Some((_, buffer)) = self.txn_write_buffers.remove(&txn_id)
3320            && !buffer.is_empty()
3321        {
3322            // Flush entire buffer to WAL with one lock
3323            self.wal.flush_buffer(&buffer)?;
3324        }
3325
3326        // ====================================================================
3327        // Task 1 — Linearizable commit invariant:
3328        //
3329        //   A commit record must NEVER become durable unless the transaction
3330        //   has already passed SSI validation and its write set is final.
3331        //
3332        // We therefore VALIDATE (and freeze the write set) *before* making the
3333        // commit record durable. `mvcc.commit` performs SSI validation and
3334        // returns `None` on a dangerous structure (serialization conflict) or
3335        // if the transaction no longer exists. Writing/fsyncing the commit
3336        // record before this point would let a crash resurrect a transaction
3337        // that the live system rejected — the classic "committed-after-crash,
3338        // aborted-before-crash" non-linearizability bug.
3339        // ====================================================================
3340        let (commit_ts, write_set) = self.mvcc.commit(txn_id).ok_or_else(|| {
3341            SochDBError::Validation(
3342                "transaction aborted: SSI validation failed (serialization conflict) \
3343                 or transaction not found"
3344                    .into(),
3345            )
3346        })?;
3347
3348        // Validation passed and the write set is frozen. Only now may the
3349        // commit record become durable.
3350        if let Some(gc) = &self.group_commit {
3351            // Submit the *validated* commit intent to group commit and wait.
3352            // This batches multiple validated commits into a single fsync.
3353            gc.submit_and_wait(txn_id).map_err(SochDBError::Internal)?;
3354        } else {
3355            // Direct commit path with adaptive sync (Little's Law)
3356            let sync_mode = self.sync_mode.load(Ordering::Relaxed);
3357            let commits = self.commits_since_sync.fetch_add(1, Ordering::Relaxed);
3358
3359            // Update arrival rate for adaptive batching
3360            self.update_arrival_rate();
3361
3362            // Write commit record (no flush yet - BufWriter will buffer it)
3363            let entry = TxnWalEntry::txn_commit(txn_id);
3364            self.wal.append_no_flush(&entry)?;
3365
3366            // Determine if we should sync/flush based on mode
3367            let should_sync = match sync_mode {
3368                0 => false,                                      // OFF: never sync
3369                1 => commits >= self.adaptive_batch_threshold(), // NORMAL: adaptive
3370                _ => true,                                       // FULL: always sync
3371            };
3372
3373            if should_sync {
3374                // Measure fsync latency for adaptive tuning
3375                let start = std::time::Instant::now();
3376                self.wal.flush()?;
3377                self.wal.sync()?;
3378                let latency_us = start.elapsed().as_micros() as u64;
3379
3380                // Update fsync latency estimate (EMA with α = 0.1)
3381                let old_latency = self.fsync_latency_us.load(Ordering::Relaxed);
3382                let new_latency = (old_latency * 9 + latency_us) / 10;
3383                self.fsync_latency_us.store(new_latency, Ordering::Relaxed);
3384
3385                self.commits_since_sync.store(0, Ordering::Relaxed);
3386            }
3387        }
3388
3389        // Commit record is durable (or buffered per sync mode) for a validated
3390        // transaction — publish the writes to the memtable. (O(k), k = keys.)
3391        self.memtable.commit(txn_id, commit_ts, &write_set);
3392
3393        Ok(commit_ts)
3394    }
3395
3396    /// Update arrival rate using exponential moving average
3397    #[inline]
3398    fn update_arrival_rate(&self) {
3399        let now_us = std::time::SystemTime::now()
3400            .duration_since(std::time::UNIX_EPOCH)
3401            .unwrap()
3402            .as_micros() as u64;
3403
3404        let last = self.last_commit_us.swap(now_us, Ordering::Relaxed);
3405
3406        if last > 0 {
3407            let delta_us = now_us.saturating_sub(last);
3408            if delta_us > 0 && delta_us < 10_000_000 {
3409                // Ignore gaps > 10s
3410                // Rate = 1_000_000 / delta_us (requests/sec)
3411                // Stored as rate × 1000 for precision
3412                let instant_rate = 1_000_000_000 / delta_us;
3413
3414                // EMA with α = 0.1
3415                let old_rate = self.arrival_rate_ema.load(Ordering::Relaxed);
3416                let new_rate = (old_rate * 9 + instant_rate) / 10;
3417                self.arrival_rate_ema.store(new_rate, Ordering::Relaxed);
3418            }
3419        }
3420    }
3421
3422    /// Compute optimal batch threshold using Little's Law
3423    ///
3424    /// W* = √(τ / λ) where τ = fsync latency, λ = arrival rate
3425    /// Returns the number of commits to batch before fsync
3426    #[inline]
3427    fn adaptive_batch_threshold(&self) -> u64 {
3428        let lambda = self.arrival_rate_ema.load(Ordering::Relaxed) as f64 / 1000.0; // req/s
3429        let tau = self.fsync_latency_us.load(Ordering::Relaxed) as f64 / 1_000_000.0; // seconds
3430
3431        if lambda <= 0.0 || tau <= 0.0 {
3432            return 100; // Fallback to fixed threshold
3433        }
3434
3435        // Little's Law: W* = sqrt(2 × τ × λ)
3436        // This minimizes total time = wait_time + fsync_overhead
3437        let n_opt = (2.0 * tau * lambda).sqrt();
3438
3439        // Clamp between 1 and 1000
3440        (n_opt as u64).clamp(1, 1000)
3441    }
3442
3443    /// Set synchronous mode
3444    ///
3445    /// - 0: OFF - No fsync (risk of data loss)
3446    /// - 1: NORMAL - Periodic fsync (balanced)
3447    /// - 2: FULL - Fsync every commit (safest)
3448    pub fn set_sync_mode(&self, mode: u64) {
3449        self.sync_mode.store(mode.min(2), Ordering::Relaxed);
3450    }
3451
3452    /// Force a group commit flush (useful for benchmarking or testing)
3453    pub fn flush_group_commit(&self) {
3454        if let Some(gc) = &self.group_commit {
3455            gc.flush_batch();
3456        }
3457    }
3458
3459    /// Abort a transaction
3460    ///
3461    /// Performance: O(1) for read-only transactions (no writes to clean up).
3462    /// For write transactions, O(N) memtable scan is required to remove
3463    /// uncommitted versions.
3464    pub fn abort(&self, txn_id: u64) -> Result<()> {
3465        // Check if transaction had any buffered writes.
3466        // Read-only transactions never populate txn_write_buffers,
3467        // so this returns None — allowing us to skip the O(N) memtable scan.
3468        let had_writes = self.txn_write_buffers.remove(&txn_id).is_some();
3469
3470        if had_writes {
3471            // Write abort record to WAL (only needed if data was written)
3472            self.wal.abort_transaction(txn_id)?;
3473            // Clean up uncommitted memtable entries
3474            self.memtable.abort(txn_id);
3475        }
3476
3477        // MVCC cleanup is always O(1) — just removes from active_txns DashMap
3478        self.mvcc.abort(txn_id);
3479
3480        Ok(())
3481    }
3482
3483    /// Scan keys with prefix
3484    #[inline]
3485    pub fn scan(&self, txn_id: u64, prefix: &[u8]) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
3486        // Fast path: get just snapshot_ts without cloning whole transaction
3487        let snapshot_ts = self
3488            .mvcc
3489            .get_snapshot_ts(txn_id)
3490            .ok_or_else(|| SochDBError::Internal("Transaction not found".into()))?;
3491
3492        // Note: Scan doesn't record individual key reads for SSI (too expensive)
3493        // SSI conflicts are tracked at the prefix level if needed
3494        Ok(self.memtable.scan_prefix(prefix, snapshot_ts, Some(txn_id)))
3495    }
3496
3497    /// Scan keys in range
3498    #[inline]
3499    pub fn scan_range(
3500        &self,
3501        txn_id: u64,
3502        start: &[u8],
3503        end: &[u8],
3504    ) -> Result<Vec<(Vec<u8>, Vec<u8>)>> {
3505        let snapshot_ts = self
3506            .mvcc
3507            .get_snapshot_ts(txn_id)
3508            .ok_or_else(|| SochDBError::Internal("Transaction not found".into()))?;
3509
3510        Ok(self
3511            .memtable
3512            .scan_range(start, end, snapshot_ts, Some(txn_id)))
3513    }
3514
3515    /// Streaming scan for very large result sets
3516    ///
3517    /// Returns an iterator that yields (key, value) pairs without
3518    /// materializing the entire result set in memory.
3519    #[inline]
3520    pub fn scan_range_iter<'a>(
3521        &'a self,
3522        txn_id: u64,
3523        start: &'a [u8],
3524        end: &'a [u8],
3525    ) -> impl Iterator<Item = (Vec<u8>, Vec<u8>)> + 'a {
3526        let snapshot_ts = self.mvcc.get_snapshot_ts(txn_id).unwrap_or(0);
3527        self.memtable
3528            .scan_range_iter(start, end, snapshot_ts, Some(txn_id))
3529    }
3530
3531    /// Force fsync to disk
3532    /// Flush the WAL's in-memory buffer to the OS
3533    ///
3534    /// This ensures all buffered writes are pushed from the BufWriter
3535    /// into the OS page cache. Call this before `fsync()` to ensure
3536    /// all data is durable.
3537    pub fn flush_wal(&self) -> Result<()> {
3538        self.wal.flush()
3539    }
3540
3541    /// Force sync the WAL to disk (fsync)
3542    pub fn fsync(&self) -> Result<()> {
3543        self.wal.sync()
3544    }
3545
3546    /// Write checkpoint
3547    pub fn checkpoint(&self) -> Result<u64> {
3548        let txn_id = 0; // System transaction
3549        let entry = TxnWalEntry::checkpoint(txn_id);
3550        let lsn = self.wal.append_sync(&entry)?;
3551        self.last_checkpoint_lsn.store(lsn, Ordering::SeqCst);
3552        // PITR: persist the checkpoint LSN to the durable manifest so it survives
3553        // restart. This piggybacks the fsync that append_sync already performed;
3554        // the manifest write is itself crash-safe (temp + fsync + atomic rename).
3555        // No-op (and no manifest write) when PITR is not enabled.
3556        if self.pitr_enabled.load(Ordering::SeqCst) {
3557            self.persist_pitr_manifest(lsn)?;
3558        }
3559        Ok(lsn)
3560    }
3561
3562    /// The current durable monotonic LSN (the WAL record ordinal). In PITR mode
3563    /// the WAL is never truncated, so this is stable and monotonic across
3564    /// restarts (`recover_state` rebuilds it by re-counting on reopen).
3565    pub fn current_lsn(&self) -> u64 {
3566        self.wal.sequence()
3567    }
3568
3569    /// Whether Point-in-Time Recovery is enabled for this database.
3570    pub fn is_pitr_enabled(&self) -> bool {
3571        self.pitr_enabled.load(Ordering::SeqCst)
3572    }
3573
3574    /// Enable Point-in-Time Recovery for this database (one-way, explicit opt-in).
3575    ///
3576    /// Writes the durable `wal.manifest` whose presence marks the DB PITR-enabled
3577    /// on every future open. Once enabled, the destructive [`Self::truncate_wal`]
3578    /// is forbidden so the WAL record ordinal stays a stable durable LSN (segment
3579    /// sealing — the PITR-safe space-reclaim — lands in a later phase). Idempotent.
3580    pub fn enable_point_in_time_recovery(&self) -> Result<()> {
3581        if self.pitr_enabled.load(Ordering::SeqCst) {
3582            return Ok(()); // already enabled
3583        }
3584        // Persist the durable manifest FIRST; only flip the in-memory flag after
3585        // the durable write succeeds. Otherwise a failed manifest write would
3586        // leave `durability_capabilities()` reporting PITR (and the truncate
3587        // guard active) with no durable anchor on disk.
3588        let lsn = self.last_checkpoint_lsn.load(Ordering::SeqCst);
3589        if crate::wal_manifest::WalManifest::exists(&self.path) {
3590            self.persist_pitr_manifest(lsn)?;
3591        } else {
3592            crate::wal_manifest::WalManifest::new(lsn).write_atomic(&self.path)?;
3593        }
3594        self.pitr_enabled.store(true, Ordering::SeqCst);
3595        Ok(())
3596    }
3597
3598    /// Persist the PITR manifest with the given checkpoint LSN, preserving the
3599    /// existing db identity if a manifest is already present.
3600    fn persist_pitr_manifest(&self, lsn: u64) -> Result<()> {
3601        let manifest = match crate::wal_manifest::WalManifest::load(&self.path) {
3602            Ok(mut m) => {
3603                m.last_checkpoint_lsn = lsn;
3604                m
3605            }
3606            Err(_) => crate::wal_manifest::WalManifest::new(lsn),
3607        };
3608        manifest.write_atomic(&self.path)
3609    }
3610
3611    /// Truncate the WAL file after checkpoint.
3612    ///
3613    /// This physically truncates the WAL file to 0 bytes, reclaiming disk
3614    /// space. The in-memory memtable retains all data for the current
3615    /// session, but a crash after truncation will result in data loss
3616    /// since the WAL is the only persistence mechanism for DurableStorage.
3617    ///
3618    /// Call after `checkpoint()` when WAL durability across restarts is
3619    /// not required (e.g. desktop telemetry viewers, caches).
3620    ///
3621    /// Refused when PITR is enabled: truncation resets the WAL record ordinal,
3622    /// which would break the durable monotonic LSN that PITR anchors on. The
3623    /// PITR-safe way to reclaim space is segment sealing (a later phase).
3624    pub fn truncate_wal(&self) -> Result<()> {
3625        if self.pitr_enabled.load(Ordering::SeqCst) {
3626            return Err(SochDBError::InvalidArgument(
3627                "truncate_wal is disabled while Point-in-Time Recovery is enabled \
3628                 (it would reset the durable LSN); use segment sealing to reclaim space"
3629                    .to_string(),
3630            ));
3631        }
3632        self.wal.truncate()
3633    }
3634
3635    /// Get storage statistics
3636    pub fn stats(&self) -> StorageStats {
3637        // Get WAL size from the WAL manager
3638        let wal_size = self.wal.size_bytes();
3639
3640        // Get active transaction count from MVCC
3641        let active_txns = self.mvcc.active_transaction_count();
3642
3643        StorageStats {
3644            memtable_size_bytes: self.memtable.size(),
3645            wal_size_bytes: wal_size,
3646            active_transactions: active_txns,
3647            min_active_snapshot: self.mvcc.min_active_snapshot(),
3648            last_checkpoint_lsn: self.last_checkpoint_lsn.load(Ordering::SeqCst),
3649        }
3650    }
3651
3652    /// Garbage collect old versions
3653    pub fn gc(&self) -> usize {
3654        let min_ts = self.mvcc.min_active_snapshot();
3655        self.memtable.gc(min_ts)
3656    }
3657
3658    /// Clean shutdown
3659    pub fn shutdown(&self) -> Result<()> {
3660        // Sync WAL
3661        self.fsync()?;
3662
3663        // Write clean shutdown marker
3664        let marker_path = self.path.join(".clean_shutdown");
3665        std::fs::write(&marker_path, b"clean")?;
3666
3667        Ok(())
3668    }
3669}
3670
3671impl Drop for DurableStorage {
3672    fn drop(&mut self) {
3673        let _ = self.shutdown();
3674    }
3675}
3676
3677// =============================================================================
3678// EphemeralHandle - Temp-directory-backed DurableStorage for testing
3679// =============================================================================
3680
3681/// Owns a `DurableStorage` instance backed by a temporary directory.
3682///
3683/// The temp directory is automatically cleaned up when this handle is dropped.
3684/// Access the underlying storage via `Deref` coercion or `.storage()`.
3685///
3686/// # Why this exists
3687///
3688/// SochDB previously had two storage engines — `LscsStorage` (BTreeMap-backed,
3689/// in-memory WAL, used by tests) and `DurableStorage` (SkipMap-backed, real WAL,
3690/// used in production). This dual-engine architecture meant bugs could surface
3691/// only in production because the test path exercised different code.
3692///
3693/// `EphemeralHandle` eliminates this divergence: tests use the exact same
3694/// `DurableStorage` engine as production, just backed by a temp directory.
3695pub struct EphemeralHandle {
3696    storage: DurableStorage,
3697    _tmpdir: tempfile::TempDir,
3698}
3699
3700impl EphemeralHandle {
3701    /// Get a reference to the underlying storage
3702    pub fn storage(&self) -> &DurableStorage {
3703        &self.storage
3704    }
3705
3706    /// Get a mutable reference to the underlying storage
3707    pub fn storage_mut(&mut self) -> &mut DurableStorage {
3708        &mut self.storage
3709    }
3710
3711    /// Consume the handle and return the storage and temp directory.
3712    ///
3713    /// Useful when you need an `Arc<DurableStorage>` — the caller must keep
3714    /// the `TempDir` alive for the lifetime of the storage.
3715    pub fn into_parts(self) -> (DurableStorage, tempfile::TempDir) {
3716        (self.storage, self._tmpdir)
3717    }
3718}
3719
3720impl std::ops::Deref for EphemeralHandle {
3721    type Target = DurableStorage;
3722    fn deref(&self) -> &DurableStorage {
3723        &self.storage
3724    }
3725}
3726
3727impl std::ops::DerefMut for EphemeralHandle {
3728    fn deref_mut(&mut self) -> &mut DurableStorage {
3729        &mut self.storage
3730    }
3731}
3732
3733/// Recovery statistics
3734#[derive(Debug, Default)]
3735pub struct RecoveryStats {
3736    pub transactions_recovered: usize,
3737    pub writes_recovered: usize,
3738    pub commit_ts: u64,
3739}
3740
3741/// Storage statistics
3742#[derive(Debug, Default)]
3743pub struct StorageStats {
3744    pub memtable_size_bytes: u64,
3745    pub wal_size_bytes: u64,
3746    pub active_transactions: usize,
3747    pub min_active_snapshot: u64,
3748    pub last_checkpoint_lsn: u64,
3749}
3750
3751#[cfg(test)]
3752mod tests {
3753    use super::*;
3754    use tempfile::tempdir;
3755
3756    /// Incremental min-active-ts (multiset) must exactly track the old full-scan
3757    /// semantics across begin / commit / abort, including shared snapshot_ts
3758    /// (refcounting) and the empty -> ts_counter watermark fallback.
3759    #[test]
3760    fn test_incremental_min_active_ts() {
3761        let m = MvccManager::new();
3762        m.assert_active_snapshots_consistent(); // empty
3763
3764        // Two txns share the SAME snapshot_ts (begin reads ts_counter w/o bump).
3765        let a = m.begin(1);
3766        let b = m.begin(2);
3767        assert_eq!(a.snapshot_ts, b.snapshot_ts, "shared snapshot expected");
3768        assert_eq!(m.min_active_snapshot(), a.snapshot_ts);
3769        m.assert_active_snapshots_consistent();
3770
3771        // Advance ts via a commit, then a third txn has a HIGHER snapshot.
3772        m.commit(1); // removes a; b still holds the low snapshot
3773        assert_eq!(m.min_active_snapshot(), b.snapshot_ts);
3774        m.assert_active_snapshots_consistent();
3775
3776        let c = m.begin(3);
3777        assert!(c.snapshot_ts >= b.snapshot_ts);
3778        // min is still b's (the oldest still-active), NOT c's.
3779        assert_eq!(m.min_active_snapshot(), b.snapshot_ts);
3780        m.assert_active_snapshots_consistent();
3781
3782        // Abort the oldest -> watermark advances to c's snapshot.
3783        m.abort(2);
3784        assert_eq!(m.min_active_snapshot(), c.snapshot_ts);
3785        m.assert_active_snapshots_consistent();
3786
3787        // Drain the last one -> empty -> watermark falls back to ts_counter.
3788        m.commit(3);
3789        assert_eq!(m.min_active_snapshot(), m.ts_counter.load(Ordering::SeqCst));
3790        m.assert_active_snapshots_consistent();
3791
3792        // Double-abort / abort of unknown id must be a no-op (no underflow).
3793        m.abort(999);
3794        m.abort(3);
3795        m.assert_active_snapshots_consistent();
3796    }
3797
3798    /// Concurrent stress: many threads begin/commit/abort; after they all join
3799    /// (quiescent), the multiset must exactly mirror active_txns. This catches
3800    /// any refcount leak/underflow under real contention.
3801    #[test]
3802    fn test_incremental_min_active_ts_concurrent() {
3803        use std::sync::Arc;
3804        let m = Arc::new(MvccManager::new());
3805        let mut handles = vec![];
3806        for t in 0..8u64 {
3807            let m = Arc::clone(&m);
3808            handles.push(std::thread::spawn(move || {
3809                for i in 0..500u64 {
3810                    let id = t * 100_000 + i;
3811                    let _txn = m.begin(id);
3812                    if i % 3 == 0 {
3813                        m.abort(id);
3814                    } else {
3815                        m.commit(id);
3816                    }
3817                }
3818            }));
3819        }
3820        for h in handles {
3821            h.join().unwrap();
3822        }
3823        // All transactions retired -> multiset empty, mirrors active_txns.
3824        m.assert_active_snapshots_consistent();
3825        assert_eq!(m.active_transaction_count(), 0);
3826    }
3827
3828    #[test]
3829    fn test_basic_transaction() {
3830        let dir = tempdir().unwrap();
3831        let storage = DurableStorage::open(dir.path()).unwrap();
3832
3833        // Begin transaction
3834        let txn_id = storage.begin_transaction().unwrap();
3835
3836        // Write data
3837        storage
3838            .write(txn_id, b"key1".to_vec(), b"value1".to_vec())
3839            .unwrap();
3840        storage
3841            .write(txn_id, b"key2".to_vec(), b"value2".to_vec())
3842            .unwrap();
3843
3844        // Read back (within same transaction)
3845        let v1 = storage.read(txn_id, b"key1").unwrap();
3846        assert_eq!(v1, Some(b"value1".to_vec()));
3847
3848        // Commit
3849        let commit_ts = storage.commit(txn_id).unwrap();
3850        assert!(commit_ts > 0);
3851
3852        // Read in new transaction
3853        let txn2 = storage.begin_transaction().unwrap();
3854        let v1 = storage.read(txn2, b"key1").unwrap();
3855        assert_eq!(v1, Some(b"value1".to_vec()));
3856        storage.abort(txn2).unwrap();
3857    }
3858
3859    #[test]
3860    fn test_snapshot_isolation() {
3861        let dir = tempdir().unwrap();
3862        let storage = DurableStorage::open(dir.path()).unwrap();
3863
3864        // T1: Write initial value
3865        let t1 = storage.begin_transaction().unwrap();
3866        storage.write(t1, b"key".to_vec(), b"v1".to_vec()).unwrap();
3867        storage.commit(t1).unwrap();
3868
3869        // T2: Start reading (snapshot at this point)
3870        let t2 = storage.begin_transaction().unwrap();
3871
3872        // T3: Update the value
3873        let t3 = storage.begin_transaction().unwrap();
3874        storage.write(t3, b"key".to_vec(), b"v2".to_vec()).unwrap();
3875        storage.commit(t3).unwrap();
3876
3877        // T2 should still see v1 (snapshot isolation)
3878        let v = storage.read(t2, b"key").unwrap();
3879        assert_eq!(v, Some(b"v1".to_vec()));
3880
3881        // New transaction should see v2
3882        let t4 = storage.begin_transaction().unwrap();
3883        let v = storage.read(t4, b"key").unwrap();
3884        assert_eq!(v, Some(b"v2".to_vec()));
3885
3886        storage.abort(t2).unwrap();
3887        storage.abort(t4).unwrap();
3888    }
3889
3890    #[test]
3891    fn test_gc_preserves_versions_for_active_snapshot() {
3892        // GC must not free a version that an in-flight reader's snapshot can
3893        // still observe. The low-water-mark (min_active_snapshot) is the oldest
3894        // live reader; any version visible to it must survive GC.
3895        let dir = tempdir().unwrap();
3896        let storage = DurableStorage::open(dir.path()).unwrap();
3897
3898        // Seed an initial committed version v1.
3899        let t1 = storage.begin_transaction().unwrap();
3900        storage.write(t1, b"k".to_vec(), b"v1".to_vec()).unwrap();
3901        storage.commit(t1).unwrap();
3902
3903        // Open a long-lived snapshot reader BEFORE any newer writes. Its
3904        // snapshot_ts pins the GC watermark so v1 cannot be reclaimed.
3905        let reader = storage.begin_transaction().unwrap();
3906        assert_eq!(
3907            storage.read(reader, b"k").unwrap(),
3908            Some(b"v1".to_vec()),
3909            "reader's snapshot must see v1"
3910        );
3911
3912        // Produce several newer committed versions while the reader is active.
3913        for v in ["v2", "v3", "v4"] {
3914            let w = storage.begin_transaction().unwrap();
3915            storage
3916                .write(w, b"k".to_vec(), v.as_bytes().to_vec())
3917                .unwrap();
3918            storage.commit(w).unwrap();
3919        }
3920
3921        // Run GC. Because `reader` is still active, the watermark is pinned at
3922        // its snapshot and the version it needs (v1) must NOT be freed.
3923        let _freed = storage.gc();
3924        assert_eq!(
3925            storage.read(reader, b"k").unwrap(),
3926            Some(b"v1".to_vec()),
3927            "GC must not free a version still visible to an active snapshot"
3928        );
3929
3930        // A fresh transaction sees the latest committed version.
3931        let fresh = storage.begin_transaction().unwrap();
3932        assert_eq!(storage.read(fresh, b"k").unwrap(), Some(b"v4".to_vec()));
3933        storage.abort(fresh).unwrap();
3934
3935        // Release the old reader; the watermark can now advance.
3936        storage.abort(reader).unwrap();
3937
3938        // After the old snapshot is gone, GC may reclaim superseded versions,
3939        // and new readers still resolve the latest value correctly.
3940        let _freed2 = storage.gc();
3941        let after = storage.begin_transaction().unwrap();
3942        assert_eq!(storage.read(after, b"k").unwrap(), Some(b"v4".to_vec()));
3943        storage.abort(after).unwrap();
3944    }
3945
3946    #[test]
3947    fn test_ssi_detects_write_skew() {
3948        // Classic write-skew: two concurrent transactions each read what the
3949        // other writes (disjoint write keys). Under plain SI both would commit,
3950        // violating serializability. Under SSI the pivot transaction (with both
3951        // an inbound and an outbound rw-edge) must be aborted, so the two
3952        // commits cannot both succeed.
3953        let dir = tempdir().unwrap();
3954        let storage = DurableStorage::open(dir.path()).unwrap();
3955
3956        // Seed two keys.
3957        let seed = storage.begin_transaction().unwrap();
3958        storage.write(seed, b"x".to_vec(), b"0".to_vec()).unwrap();
3959        storage.write(seed, b"y".to_vec(), b"0".to_vec()).unwrap();
3960        storage.commit(seed).unwrap();
3961
3962        // T1 reads x and y, then writes x.
3963        let t1 = storage.begin_transaction().unwrap();
3964        let _ = storage.read(t1, b"x").unwrap();
3965        let _ = storage.read(t1, b"y").unwrap();
3966
3967        // T2 reads x and y, then writes y. (Concurrent with T1.)
3968        let t2 = storage.begin_transaction().unwrap();
3969        let _ = storage.read(t2, b"x").unwrap();
3970        let _ = storage.read(t2, b"y").unwrap();
3971
3972        storage.write(t1, b"x".to_vec(), b"1".to_vec()).unwrap();
3973        storage.write(t2, b"y".to_vec(), b"1".to_vec()).unwrap();
3974
3975        // First committer wins.
3976        let c1 = storage.commit(t1);
3977        assert!(c1.is_ok(), "first committer should succeed: {c1:?}");
3978
3979        // The second committer forms a dangerous rw-structure with T1 and must
3980        // be rejected to preserve serializability.
3981        let c2 = storage.commit(t2);
3982        assert!(
3983            c2.is_err(),
3984            "SSI must abort the write-skew pivot, but commit succeeded"
3985        );
3986    }
3987
3988    #[test]
3989    fn test_abort_transaction() {
3990        let dir = tempdir().unwrap();
3991        let storage = DurableStorage::open(dir.path()).unwrap();
3992
3993        // Write initial value
3994        let t1 = storage.begin_transaction().unwrap();
3995        storage.write(t1, b"key".to_vec(), b"v1".to_vec()).unwrap();
3996        storage.commit(t1).unwrap();
3997
3998        // Start transaction that will abort
3999        let t2 = storage.begin_transaction().unwrap();
4000        storage.write(t2, b"key".to_vec(), b"v2".to_vec()).unwrap();
4001        storage.abort(t2).unwrap();
4002
4003        // New transaction should see v1 (aborted changes not visible)
4004        let t3 = storage.begin_transaction().unwrap();
4005        let v = storage.read(t3, b"key").unwrap();
4006        assert_eq!(v, Some(b"v1".to_vec()));
4007        storage.abort(t3).unwrap();
4008    }
4009
4010    #[test]
4011    fn test_crash_recovery() {
4012        let dir = tempdir().unwrap();
4013
4014        // Phase 1: Write data and commit
4015        {
4016            // Use open_without_lock for crash simulation tests
4017            let storage = DurableStorage::open_without_lock(dir.path()).unwrap();
4018
4019            // Set sync mode to FULL to ensure data is synced before "crash"
4020            storage.set_sync_mode(2); // FULL: sync every commit
4021
4022            let txn = storage.begin_transaction().unwrap();
4023            storage
4024                .write(txn, b"persist".to_vec(), b"data".to_vec())
4025                .unwrap();
4026            storage.commit(txn).unwrap();
4027
4028            // Simulate crash (no clean shutdown)
4029            std::mem::forget(storage);
4030        }
4031
4032        // Phase 2: Reopen and recover
4033        {
4034            let storage = DurableStorage::open_without_lock(dir.path()).unwrap();
4035            let stats = storage.recover().unwrap();
4036            assert!(stats.transactions_recovered > 0 || stats.writes_recovered > 0);
4037
4038            // Data should be recovered
4039            let txn = storage.begin_transaction().unwrap();
4040            let v = storage.read(txn, b"persist").unwrap();
4041            assert_eq!(v, Some(b"data".to_vec()));
4042            storage.abort(txn).unwrap();
4043        }
4044    }
4045
4046    /// At-rest encryption end-to-end through the live DurableStorage engine
4047    /// (Task 3B): an encrypted DB round-trips committed data, never leaks
4048    /// plaintext to the WAL file, flips the per-instance durability matrix, and
4049    /// fails CLOSED on a wrong or missing key (never a silent plaintext/empty
4050    /// open).
4051    #[test]
4052    fn test_at_rest_encryption_end_to_end() {
4053        let dir = tempdir().unwrap();
4054        let kek = || EncryptionKey::new([0xABu8; 32]);
4055
4056        // Phase 1: create an encrypted DB, write committed data, clean close.
4057        {
4058            let storage = DurableStorage::open_with_encryption(
4059                dir.path(),
4060                true,
4061                MemTableType::Standard,
4062                StorageEncryption::with_kek(kek(), "test"),
4063            )
4064            .unwrap();
4065            assert!(storage.is_encrypted());
4066            assert!(storage.durability_capabilities().at_rest_encryption);
4067            storage.set_sync_mode(2);
4068            let t = storage.begin_transaction().unwrap();
4069            storage
4070                .write(t, b"secret-key".to_vec(), b"secret-value".to_vec())
4071                .unwrap();
4072            storage.commit(t).unwrap();
4073        } // clean drop releases the file lock
4074
4075        // The keyring exists and the WAL bytes do not leak the plaintext record.
4076        assert!(dir.path().join("keyring.json").exists());
4077        let raw = std::fs::read(dir.path().join("wal.log")).unwrap();
4078        assert!(!contains(&raw, b"secret-value"), "value leaked to WAL");
4079        assert!(!contains(&raw, b"secret-key"), "key leaked to WAL");
4080
4081        // Phase 2: reopen with the CORRECT key, recover, read back.
4082        {
4083            let storage = DurableStorage::open_with_encryption(
4084                dir.path(),
4085                true,
4086                MemTableType::Standard,
4087                StorageEncryption::with_kek(kek(), "test"),
4088            )
4089            .unwrap();
4090            storage.recover().unwrap();
4091            let t = storage.begin_transaction().unwrap();
4092            assert_eq!(
4093                storage.read(t, b"secret-key").unwrap(),
4094                Some(b"secret-value".to_vec()),
4095                "committed encrypted data must round-trip"
4096            );
4097            storage.abort(t).unwrap();
4098        }
4099
4100        // Phase 3: WRONG key must fail closed (keyring canary), not open empty.
4101        {
4102            let wrong = DurableStorage::open_with_encryption(
4103                dir.path(),
4104                true,
4105                MemTableType::Standard,
4106                StorageEncryption::with_kek(EncryptionKey::new([0x00u8; 32]), "test"),
4107            );
4108            assert!(wrong.is_err(), "wrong KEK must fail closed");
4109        }
4110
4111        // Phase 4: opening an encrypted DB with NO key must fail closed.
4112        {
4113            let no_key = DurableStorage::open_with_encryption(
4114                dir.path(),
4115                true,
4116                MemTableType::Standard,
4117                StorageEncryption::disabled(),
4118            );
4119            assert!(
4120                no_key.is_err(),
4121                "encrypted DB opened without key must fail closed"
4122            );
4123        }
4124    }
4125
4126    /// A plaintext DB reports at_rest_encryption=false on the live matrix.
4127    #[test]
4128    fn test_plaintext_db_reports_unencrypted() {
4129        let dir = tempdir().unwrap();
4130        let storage = DurableStorage::open_without_lock(dir.path()).unwrap();
4131        assert!(!storage.is_encrypted());
4132        assert!(!storage.durability_capabilities().at_rest_encryption);
4133        assert!(storage.durability_capabilities().crash_recovery);
4134        assert!(!dir.path().join("keyring.json").exists());
4135    }
4136
4137    fn contains(haystack: &[u8], needle: &[u8]) -> bool {
4138        haystack.windows(needle.len()).any(|w| w == needle)
4139    }
4140
4141    /// PITR phase 1 — durable monotonic LSN anchor.
4142    ///
4143    /// Enabling PITR writes the durable manifest; the WAL record ordinal (LSN)
4144    /// and the last-checkpoint LSN then survive a reopen, and the destructive
4145    /// truncate is refused so the anchor can never reset. A non-PITR DB is
4146    /// completely unaffected (no manifest, truncate works).
4147    #[test]
4148    fn test_pitr_durable_lsn_and_truncate_guard() {
4149        let dir = tempdir().unwrap();
4150
4151        // Default DB: not PITR, no manifest, truncate allowed.
4152        {
4153            let s = DurableStorage::open_without_lock(dir.path()).unwrap();
4154            assert!(!s.is_pitr_enabled());
4155            assert!(!s.durability_capabilities().point_in_time_recovery);
4156            assert!(s.truncate_wal().is_ok());
4157        }
4158        assert!(!dir.path().join("wal.manifest").exists());
4159
4160        // Enable PITR, write+checkpoint, capture the LSN.
4161        let lsn_before;
4162        let ckpt_lsn;
4163        {
4164            let s = DurableStorage::open_without_lock(dir.path()).unwrap();
4165            s.enable_point_in_time_recovery().unwrap();
4166            assert!(s.is_pitr_enabled());
4167
4168            let t = s.begin_transaction().unwrap();
4169            s.write(t, b"k1".to_vec(), b"v1".to_vec()).unwrap();
4170            s.commit(t).unwrap();
4171            ckpt_lsn = s.checkpoint().unwrap();
4172            lsn_before = s.current_lsn();
4173            assert!(lsn_before > 0);
4174
4175            // truncate is refused while PITR is on.
4176            assert!(
4177                s.truncate_wal().is_err(),
4178                "truncate must be refused in PITR mode"
4179            );
4180        }
4181        assert!(dir.path().join("wal.manifest").exists());
4182
4183        // Reopen: PITR auto-detected from the manifest; LSN did NOT reset.
4184        {
4185            let s = DurableStorage::open_without_lock(dir.path()).unwrap();
4186            assert!(
4187                s.is_pitr_enabled(),
4188                "PITR must be auto-detected from manifest"
4189            );
4190            s.recover().unwrap();
4191            assert_eq!(
4192                s.current_lsn(),
4193                lsn_before,
4194                "durable LSN must survive reopen (not reset to 0/record-recount drift)"
4195            );
4196            assert_eq!(
4197                s.stats().last_checkpoint_lsn,
4198                ckpt_lsn,
4199                "last_checkpoint_lsn must be restored from the manifest"
4200            );
4201            // Committed data still readable.
4202            let t = s.begin_transaction().unwrap();
4203            assert_eq!(s.read(t, b"k1").unwrap(), Some(b"v1".to_vec()));
4204            s.abort(t).unwrap();
4205        }
4206    }
4207
4208    /// PITR phase 2 — END-TO-END restore to a point in time.
4209    ///
4210    /// Enable PITR, commit two transactions, then on fresh reopens
4211    /// `recover_to(target)` materializes the exact historical state: an LSN cut
4212    /// between the two transactions sees only the first; the full LSN / a
4213    /// far-future timestamp sees both; timestamp 0 sees nothing. Transaction
4214    /// atomicity is preserved at the cut.
4215    #[test]
4216    fn test_pitr_recover_to_point_in_time() {
4217        use crate::txn_wal::RecoveryTarget;
4218
4219        let dir = tempdir().unwrap();
4220
4221        // Build history: txn1 sets k1=v1; txn2 overwrites k1=v1b and adds k2=v2.
4222        let (lsn_after_txn1, lsn_after_txn2);
4223        {
4224            let s = DurableStorage::open_without_lock(dir.path()).unwrap();
4225            s.enable_point_in_time_recovery().unwrap();
4226
4227            let t1 = s.begin_transaction().unwrap();
4228            s.write(t1, b"k1".to_vec(), b"v1".to_vec()).unwrap();
4229            s.commit(t1).unwrap();
4230            lsn_after_txn1 = s.current_lsn();
4231
4232            let t2 = s.begin_transaction().unwrap();
4233            s.write(t2, b"k1".to_vec(), b"v1b".to_vec()).unwrap();
4234            s.write(t2, b"k2".to_vec(), b"v2".to_vec()).unwrap();
4235            s.commit(t2).unwrap();
4236            lsn_after_txn2 = s.current_lsn();
4237
4238            s.checkpoint().unwrap();
4239        }
4240        assert!(lsn_after_txn2 > lsn_after_txn1);
4241
4242        // Restore to the cut between txn1 and txn2: only txn1's effect is visible.
4243        {
4244            let s = DurableStorage::open_without_lock(dir.path()).unwrap();
4245            s.recover_to(RecoveryTarget::Lsn(lsn_after_txn1)).unwrap();
4246            let t = s.begin_transaction().unwrap();
4247            assert_eq!(
4248                s.read(t, b"k1").unwrap(),
4249                Some(b"v1".to_vec()),
4250                "txn1 value"
4251            );
4252            assert_eq!(
4253                s.read(t, b"k2").unwrap(),
4254                None,
4255                "txn2 must NOT be present at the cut"
4256            );
4257            s.abort(t).unwrap();
4258        }
4259
4260        // Restore to the full LSN: both transactions visible (txn2 wins on k1).
4261        {
4262            let s = DurableStorage::open_without_lock(dir.path()).unwrap();
4263            s.recover_to(RecoveryTarget::Lsn(lsn_after_txn2)).unwrap();
4264            let t = s.begin_transaction().unwrap();
4265            assert_eq!(s.read(t, b"k1").unwrap(), Some(b"v1b".to_vec()));
4266            assert_eq!(s.read(t, b"k2").unwrap(), Some(b"v2".to_vec()));
4267            s.abort(t).unwrap();
4268        }
4269
4270        // Timestamp bounds: MAX => everything; 0 => nothing.
4271        {
4272            let s = DurableStorage::open_without_lock(dir.path()).unwrap();
4273            s.recover_to(RecoveryTarget::Timestamp(u64::MAX)).unwrap();
4274            let t = s.begin_transaction().unwrap();
4275            assert_eq!(s.read(t, b"k1").unwrap(), Some(b"v1b".to_vec()));
4276            assert_eq!(s.read(t, b"k2").unwrap(), Some(b"v2".to_vec()));
4277            s.abort(t).unwrap();
4278        }
4279        {
4280            let s = DurableStorage::open_without_lock(dir.path()).unwrap();
4281            s.recover_to(RecoveryTarget::Timestamp(0)).unwrap();
4282            let t = s.begin_transaction().unwrap();
4283            assert_eq!(s.read(t, b"k1").unwrap(), None, "no commit is <= ts 0");
4284            s.abort(t).unwrap();
4285        }
4286
4287        // The capability matrix now reports PITR live for this DB.
4288        {
4289            let s = DurableStorage::open_without_lock(dir.path()).unwrap();
4290            assert!(s.durability_capabilities().point_in_time_recovery);
4291        }
4292    }
4293
4294    /// recover_to is refused on a non-PITR database (the WAL may be truncated, so
4295    /// an arbitrary target cannot be honored).
4296    #[test]
4297    fn test_recover_to_refused_without_pitr() {
4298        use crate::txn_wal::RecoveryTarget;
4299        let dir = tempdir().unwrap();
4300        let s = DurableStorage::open_without_lock(dir.path()).unwrap();
4301        assert!(s.recover_to(RecoveryTarget::Lsn(1)).is_err());
4302        assert!(!s.durability_capabilities().point_in_time_recovery);
4303    }
4304
4305    /// Regression (review HIGH): under the DEFAULT NORMAL sync mode a commit
4306    /// record may sit unflushed in the BufWriter while current_lsn() counts it.
4307    /// recover_to MUST flush+fsync before replaying, or a same-process restore to
4308    /// the captured LSN silently drops the committed-but-unflushed tail.
4309    #[test]
4310    fn test_recover_to_flushes_before_replay() {
4311        use crate::txn_wal::RecoveryTarget;
4312        let dir = tempdir().unwrap();
4313        let s = DurableStorage::open_without_lock(dir.path()).unwrap(); // NORMAL sync
4314        s.enable_point_in_time_recovery().unwrap();
4315        let t = s.begin_transaction().unwrap();
4316        s.write(t, b"k".to_vec(), b"v".to_vec()).unwrap();
4317        s.commit(t).unwrap(); // commit record likely unflushed under NORMAL
4318        let lsn = s.current_lsn();
4319        // No checkpoint, SAME process: replay reads a fresh on-disk handle.
4320        let stats = s.recover_to(RecoveryTarget::Lsn(lsn)).unwrap();
4321        assert!(
4322            stats.writes_recovered >= 1,
4323            "committed-but-unflushed data must be recovered (flush before replay)"
4324        );
4325    }
4326
4327    /// Regression (review MEDIUM): recover_to must be the SOLE recovery on a
4328    /// fresh open. After recover() (or a prior recover_to) it must refuse, rather
4329    /// than silently layer a stale set over the point-in-time state.
4330    #[test]
4331    fn test_recover_to_refuses_after_recovery() {
4332        use crate::txn_wal::RecoveryTarget;
4333        let dir = tempdir().unwrap();
4334        {
4335            let s = DurableStorage::open_without_lock(dir.path()).unwrap();
4336            s.enable_point_in_time_recovery().unwrap();
4337            let t = s.begin_transaction().unwrap();
4338            s.write(t, b"k".to_vec(), b"v".to_vec()).unwrap();
4339            s.commit(t).unwrap();
4340            s.checkpoint().unwrap();
4341        }
4342        let s = DurableStorage::open_without_lock(dir.path()).unwrap();
4343        s.recover().unwrap(); // full recovery first
4344        assert!(
4345            s.recover_to(RecoveryTarget::Lsn(1)).is_err(),
4346            "recover_to after recover() must refuse (would double-apply)"
4347        );
4348        // And a second recover_to after a first also refuses.
4349        let s2 = DurableStorage::open_without_lock(dir.path()).unwrap();
4350        s2.recover_to(RecoveryTarget::Lsn(u64::MAX)).unwrap();
4351        assert!(s2.recover_to(RecoveryTarget::Lsn(1)).is_err());
4352    }
4353
4354    /// recover_to over an ENCRYPTED WAL: the bounded replay decrypts correctly
4355    /// (review LOW: the encrypted bounded path was previously untested).
4356    #[test]
4357    fn test_pitr_recover_to_encrypted() {
4358        use crate::encryption::EncryptionKey;
4359        use crate::txn_wal::RecoveryTarget;
4360
4361        let dir = tempdir().unwrap();
4362        let kek = [0x9Fu8; 32];
4363        let mk = || StorageEncryption::with_kek(EncryptionKey::new(kek), "test");
4364
4365        let lsn_after_txn1;
4366        {
4367            let s = DurableStorage::open_with_encryption(
4368                dir.path(),
4369                true,
4370                MemTableType::Standard,
4371                mk(),
4372            )
4373            .unwrap();
4374            s.enable_point_in_time_recovery().unwrap();
4375            let t1 = s.begin_transaction().unwrap();
4376            s.write(t1, b"k1".to_vec(), b"v1".to_vec()).unwrap();
4377            s.commit(t1).unwrap();
4378            lsn_after_txn1 = s.current_lsn();
4379            let t2 = s.begin_transaction().unwrap();
4380            s.write(t2, b"k2".to_vec(), b"v2".to_vec()).unwrap();
4381            s.commit(t2).unwrap();
4382            s.checkpoint().unwrap();
4383            s.shutdown().ok();
4384        }
4385
4386        // Reopen encrypted, restore to the cut between the two txns.
4387        let s =
4388            DurableStorage::open_with_encryption(dir.path(), true, MemTableType::Standard, mk())
4389                .unwrap();
4390        s.recover_to(RecoveryTarget::Lsn(lsn_after_txn1)).unwrap();
4391        let t = s.begin_transaction().unwrap();
4392        assert_eq!(
4393            s.read(t, b"k1").unwrap(),
4394            Some(b"v1".to_vec()),
4395            "encrypted bounded replay must decrypt the in-window record"
4396        );
4397        assert_eq!(s.read(t, b"k2").unwrap(), None, "txn2 is past the cut");
4398        s.abort(t).unwrap();
4399    }
4400
4401    /// PITR composes with at-rest encryption (the manifest anchor is independent
4402    /// of the keyring; an encrypted PITR DB round-trips and stays fail-closed).
4403    #[test]
4404    fn test_pitr_with_encryption() {
4405        use crate::encryption::EncryptionKey;
4406
4407        let dir = tempdir().unwrap();
4408        let kek = [0x2Bu8; 32];
4409
4410        {
4411            let s = DurableStorage::open_with_encryption(
4412                dir.path(),
4413                true,
4414                MemTableType::Standard,
4415                StorageEncryption::with_kek(EncryptionKey::new(kek), "test"),
4416            )
4417            .unwrap();
4418            s.enable_point_in_time_recovery().unwrap();
4419            let t = s.begin_transaction().unwrap();
4420            s.write(t, b"ek".to_vec(), b"ev".to_vec()).unwrap();
4421            s.commit(t).unwrap();
4422            s.checkpoint().unwrap();
4423            s.shutdown().ok();
4424        }
4425        assert!(dir.path().join("keyring.json").exists());
4426        assert!(dir.path().join("wal.manifest").exists());
4427
4428        // Reopen encrypted + PITR.
4429        let s = DurableStorage::open_with_encryption(
4430            dir.path(),
4431            true,
4432            MemTableType::Standard,
4433            StorageEncryption::with_kek(EncryptionKey::new(kek), "test"),
4434        )
4435        .unwrap();
4436        assert!(s.is_pitr_enabled());
4437        assert!(s.is_encrypted());
4438        s.recover().unwrap();
4439        let t = s.begin_transaction().unwrap();
4440        assert_eq!(s.read(t, b"ek").unwrap(), Some(b"ev".to_vec()));
4441        s.abort(t).unwrap();
4442    }
4443
4444    /// Crash-atomicity on an ENCRYPTED database: a simulated crash (forget) on an
4445    /// encrypted WAL must, on reopen with the correct key, replay committed data
4446    /// (decrypted via the crypto-aware recovery path) while NOT resurrecting
4447    /// aborted/in-flight writes — and a wrong key after the crash fails closed.
4448    /// Combines the at-rest-encryption + crash-atomicity guarantees.
4449    #[test]
4450    fn test_encrypted_crash_recovery_atomicity() {
4451        use crate::encryption::EncryptionKey;
4452        let dir = tempdir().unwrap();
4453        let kek = || StorageEncryption::with_kek(EncryptionKey::new([0xC1u8; 32]), "test");
4454        // open encrypted WITHOUT the file lock so a forget()+reopen works in-test.
4455        let open = |enc: StorageEncryption| {
4456            DurableStorage::open_with_full_config_internal(
4457                dir.path(),
4458                true,
4459                MemTableType::Standard,
4460                false,
4461                enc,
4462            )
4463        };
4464
4465        {
4466            let storage = open(kek()).unwrap();
4467            assert!(storage.is_encrypted());
4468            storage.set_sync_mode(2); // FULL: fsync each commit before the "crash"
4469            let t1 = storage.begin_transaction().unwrap();
4470            storage
4471                .write(t1, b"committed".to_vec(), b"durable".to_vec())
4472                .unwrap();
4473            storage.commit(t1).unwrap();
4474            let t2 = storage.begin_transaction().unwrap();
4475            storage
4476                .write(t2, b"aborted".to_vec(), b"x".to_vec())
4477                .unwrap();
4478            storage.abort(t2).unwrap();
4479            let t3 = storage.begin_transaction().unwrap();
4480            storage
4481                .write(t3, b"inflight".to_vec(), b"y".to_vec())
4482                .unwrap();
4483            std::mem::forget(storage); // crash: skip clean shutdown
4484        }
4485
4486        // Reopen with the CORRECT key: committed survives, others do not.
4487        {
4488            let storage = open(kek()).unwrap();
4489            storage.recover().unwrap();
4490            let t = storage.begin_transaction().unwrap();
4491            assert_eq!(
4492                storage.read(t, b"committed").unwrap(),
4493                Some(b"durable".to_vec()),
4494                "committed encrypted write must survive the crash"
4495            );
4496            assert_eq!(storage.read(t, b"aborted").unwrap(), None);
4497            assert_eq!(storage.read(t, b"inflight").unwrap(), None);
4498            storage.abort(t).unwrap();
4499        }
4500
4501        // Wrong key after the crash must fail closed (keyring canary).
4502        assert!(
4503            open(StorageEncryption::with_kek(
4504                EncryptionKey::new([0u8; 32]),
4505                "test"
4506            ))
4507            .is_err(),
4508            "wrong key after crash must fail closed"
4509        );
4510    }
4511
4512    /// Crash-atomicity invariant (Task 4 — completes the Task 1 single-writer
4513    /// contract). `test_crash_recovery` proves committed data survives a crash;
4514    /// this proves the other half: recovery must NOT resurrect aborted or
4515    /// in-flight (never-committed) writes. Together they assert the live
4516    /// single-writer engine's commit is atomic AND durable across a crash.
4517    #[test]
4518    fn test_crash_recovery_atomicity() {
4519        let dir = tempdir().unwrap();
4520
4521        // Phase 1: one committed write, one aborted, one in-flight at crash.
4522        {
4523            let storage = DurableStorage::open_without_lock(dir.path()).unwrap();
4524            storage.set_sync_mode(2); // FULL: fsync every commit before the "crash"
4525
4526            // Committed — must survive.
4527            let t1 = storage.begin_transaction().unwrap();
4528            storage
4529                .write(t1, b"committed".to_vec(), b"durable".to_vec())
4530                .unwrap();
4531            storage.commit(t1).unwrap();
4532
4533            // Aborted — must NOT be resurrected.
4534            let t2 = storage.begin_transaction().unwrap();
4535            storage
4536                .write(t2, b"aborted".to_vec(), b"rolledback".to_vec())
4537                .unwrap();
4538            storage.abort(t2).unwrap();
4539
4540            // In-flight (never committed) at crash time — must NOT be resurrected.
4541            let t3 = storage.begin_transaction().unwrap();
4542            storage
4543                .write(t3, b"inflight".to_vec(), b"lost".to_vec())
4544                .unwrap();
4545
4546            // Simulate a crash: skip Drop / clean shutdown.
4547            std::mem::forget(storage);
4548        }
4549
4550        // Phase 2: reopen + recover; assert atomicity.
4551        {
4552            let storage = DurableStorage::open_without_lock(dir.path()).unwrap();
4553            storage.recover().unwrap();
4554
4555            let t = storage.begin_transaction().unwrap();
4556            assert_eq!(
4557                storage.read(t, b"committed").unwrap(),
4558                Some(b"durable".to_vec()),
4559                "committed write must survive the crash"
4560            );
4561            assert_eq!(
4562                storage.read(t, b"aborted").unwrap(),
4563                None,
4564                "aborted write must not be resurrected by recovery"
4565            );
4566            assert_eq!(
4567                storage.read(t, b"inflight").unwrap(),
4568                None,
4569                "uncommitted in-flight write must not be resurrected by recovery"
4570            );
4571            storage.abort(t).unwrap();
4572        }
4573    }
4574
4575    #[test]
4576    fn test_scan_prefix() {
4577        let dir = tempdir().unwrap();
4578        let storage = DurableStorage::open(dir.path()).unwrap();
4579
4580        let txn = storage.begin_transaction().unwrap();
4581        storage
4582            .write(txn, b"user:1".to_vec(), b"alice".to_vec())
4583            .unwrap();
4584        storage
4585            .write(txn, b"user:2".to_vec(), b"bob".to_vec())
4586            .unwrap();
4587        storage
4588            .write(txn, b"order:1".to_vec(), b"order1".to_vec())
4589            .unwrap();
4590        storage.commit(txn).unwrap();
4591
4592        let txn2 = storage.begin_transaction().unwrap();
4593        let users = storage.scan(txn2, b"user:").unwrap();
4594        assert_eq!(users.len(), 2);
4595        storage.abort(txn2).unwrap();
4596    }
4597
4598    #[test]
4599    fn test_delete() {
4600        let dir = tempdir().unwrap();
4601        let storage = DurableStorage::open(dir.path()).unwrap();
4602
4603        // Insert
4604        let t1 = storage.begin_transaction().unwrap();
4605        storage
4606            .write(t1, b"key".to_vec(), b"value".to_vec())
4607            .unwrap();
4608        storage.commit(t1).unwrap();
4609
4610        // Verify exists
4611        let t2 = storage.begin_transaction().unwrap();
4612        assert!(storage.read(t2, b"key").unwrap().is_some());
4613        storage.abort(t2).unwrap();
4614
4615        // Delete
4616        let t3 = storage.begin_transaction().unwrap();
4617        storage.delete(t3, b"key".to_vec()).unwrap();
4618        storage.commit(t3).unwrap();
4619
4620        // Verify deleted
4621        let t4 = storage.begin_transaction().unwrap();
4622        assert!(storage.read(t4, b"key").unwrap().is_none());
4623        storage.abort(t4).unwrap();
4624    }
4625
4626    #[test]
4627    fn test_gc() {
4628        let dir = tempdir().unwrap();
4629        let storage = DurableStorage::open(dir.path()).unwrap();
4630
4631        // Create multiple versions
4632        for i in 0..10 {
4633            let txn = storage.begin_transaction().unwrap();
4634            storage
4635                .write(txn, b"key".to_vec(), format!("v{}", i).into_bytes())
4636                .unwrap();
4637            storage.commit(txn).unwrap();
4638        }
4639
4640        // GC should reclaim old versions
4641        let gc_count = storage.gc();
4642        // At least some versions should be collected
4643        // (exact count depends on implementation)
4644        let _ = gc_count; // gc_count is usize, always >= 0
4645    }
4646
4647    #[test]
4648    fn test_group_commit() {
4649        use std::sync::Arc;
4650        use std::thread;
4651
4652        let dir = tempdir().unwrap();
4653        let storage = Arc::new(DurableStorage::open_with_group_commit(dir.path()).unwrap());
4654
4655        // Spawn multiple threads to commit concurrently
4656        let mut handles = vec![];
4657        for i in 0..4 {
4658            let storage = Arc::clone(&storage);
4659            handles.push(thread::spawn(move || {
4660                let txn = storage.begin_transaction().unwrap();
4661                storage
4662                    .write(
4663                        txn,
4664                        format!("key{}", i).into_bytes(),
4665                        format!("val{}", i).into_bytes(),
4666                    )
4667                    .unwrap();
4668                storage.commit(txn).unwrap()
4669            }));
4670        }
4671
4672        // Wait for all commits
4673        let mut commit_times = vec![];
4674        for h in handles {
4675            commit_times.push(h.join().unwrap());
4676        }
4677
4678        // All commits should succeed
4679        assert!(commit_times.iter().all(|&ts| ts > 0));
4680
4681        // Verify data persisted
4682        let txn = storage.begin_transaction().unwrap();
4683        for i in 0..4 {
4684            let val = storage.read(txn, format!("key{}", i).as_bytes()).unwrap();
4685            assert_eq!(val, Some(format!("val{}", i).into_bytes()));
4686        }
4687        storage.abort(txn).unwrap();
4688    }
4689
4690    // ==================== ArenaMvccMemTable Tests ====================
4691
4692    #[test]
4693    fn test_arena_memtable_basic_write_read() {
4694        let memtable = ArenaMvccMemTable::new();
4695
4696        // Write some values
4697        memtable
4698            .write(b"key1", Some(b"value1".to_vec()), 1)
4699            .unwrap();
4700        memtable
4701            .write(b"key2", Some(b"value2".to_vec()), 1)
4702            .unwrap();
4703
4704        // Read them back (uncommitted, so need txn_id match)
4705        assert_eq!(memtable.read(b"key1", 0, Some(1)), Some(b"value1".to_vec()));
4706        assert_eq!(memtable.read(b"key2", 0, Some(1)), Some(b"value2".to_vec()));
4707        assert_eq!(memtable.read(b"key3", 0, Some(1)), None);
4708    }
4709
4710    #[test]
4711    fn test_arena_memtable_update() {
4712        let memtable = ArenaMvccMemTable::new();
4713
4714        memtable.write(b"key", Some(b"v1".to_vec()), 1).unwrap();
4715        memtable.write(b"key", Some(b"v2".to_vec()), 1).unwrap();
4716
4717        assert_eq!(memtable.read(b"key", 0, Some(1)), Some(b"v2".to_vec()));
4718    }
4719
4720    #[test]
4721    fn test_arena_memtable_delete() {
4722        let memtable = ArenaMvccMemTable::new();
4723
4724        memtable.write(b"key", Some(b"value".to_vec()), 1).unwrap();
4725        memtable.write(b"key", None, 1).unwrap(); // Delete = None value
4726
4727        assert_eq!(memtable.read(b"key", 0, Some(1)), None);
4728    }
4729
4730    #[test]
4731    fn test_arena_memtable_scan_prefix() {
4732        let memtable = ArenaMvccMemTable::new();
4733
4734        memtable
4735            .write(b"user:1:name", Some(b"Alice".to_vec()), 1)
4736            .unwrap();
4737        memtable
4738            .write(b"user:1:email", Some(b"alice@test.com".to_vec()), 1)
4739            .unwrap();
4740        memtable
4741            .write(b"user:2:name", Some(b"Bob".to_vec()), 1)
4742            .unwrap();
4743        memtable
4744            .write(b"order:1", Some(b"order_data".to_vec()), 1)
4745            .unwrap();
4746
4747        // Create a write set and commit
4748        let mut write_set = HashSet::new();
4749        write_set.insert(InlineKey::from_slice(b"user:1:name"));
4750        write_set.insert(InlineKey::from_slice(b"user:1:email"));
4751        write_set.insert(InlineKey::from_slice(b"user:2:name"));
4752        write_set.insert(InlineKey::from_slice(b"order:1"));
4753        memtable.commit(1, 10, &write_set);
4754
4755        // Scan for user:1:* (snapshot_ts > commit_ts to see committed data)
4756        let results = memtable.scan_prefix(b"user:1:", 11, None);
4757        assert_eq!(results.len(), 2);
4758
4759        // Scan for all users
4760        let results = memtable.scan_prefix(b"user:", 11, None);
4761        assert_eq!(results.len(), 3);
4762    }
4763
4764    #[test]
4765    fn test_arena_memtable_write_batch() {
4766        let memtable = ArenaMvccMemTable::new();
4767
4768        let writes: Vec<(&[u8], Option<Vec<u8>>)> = vec![
4769            (b"k1", Some(b"v1".to_vec())),
4770            (b"k2", Some(b"v2".to_vec())),
4771            (b"k3", Some(b"v3".to_vec())),
4772        ];
4773
4774        memtable.write_batch(&writes, 1).unwrap();
4775
4776        assert_eq!(memtable.read(b"k1", 0, Some(1)), Some(b"v1".to_vec()));
4777        assert_eq!(memtable.read(b"k2", 0, Some(1)), Some(b"v2".to_vec()));
4778        assert_eq!(memtable.read(b"k3", 0, Some(1)), Some(b"v3".to_vec()));
4779    }
4780
4781    #[test]
4782    fn test_arena_memtable_gc() {
4783        let memtable = ArenaMvccMemTable::new();
4784
4785        // Write multiple versions
4786        for i in 0..10 {
4787            memtable
4788                .write(b"key", Some(format!("v{}", i).into_bytes()), i + 1)
4789                .unwrap();
4790
4791            let mut write_set = HashSet::new();
4792            write_set.insert(InlineKey::from_slice(b"key"));
4793            memtable.commit(i + 1, (i + 1) * 10, &write_set);
4794        }
4795
4796        // GC old versions
4797        let gc_count = memtable.gc(90);
4798        let _ = gc_count; // gc_count is usize, always >= 0
4799    }
4800
4801    #[test]
4802    fn test_arena_memtable_size_tracking() {
4803        let memtable = ArenaMvccMemTable::new();
4804
4805        assert_eq!(memtable.size(), 0);
4806
4807        memtable.write(b"key", Some(b"value".to_vec()), 1).unwrap();
4808
4809        assert!(memtable.size() > 0);
4810    }
4811
4812    #[test]
4813    fn test_arena_memtable_abort() {
4814        let memtable = ArenaMvccMemTable::new();
4815
4816        memtable
4817            .write(b"key", Some(b"uncommitted".to_vec()), 1)
4818            .unwrap();
4819
4820        // Visible to same txn
4821        assert_eq!(
4822            memtable.read(b"key", 0, Some(1)),
4823            Some(b"uncommitted".to_vec())
4824        );
4825
4826        // Not visible to other txns
4827        assert_eq!(memtable.read(b"key", 0, Some(2)), None);
4828
4829        // Abort
4830        memtable.abort(1);
4831
4832        // No longer visible
4833        assert_eq!(memtable.read(b"key", 0, Some(1)), None);
4834    }
4835
4836    // ========================================================================
4837    // MemTableKind Tests - Unified Abstraction
4838    // ========================================================================
4839
4840    #[test]
4841    fn test_memtable_kind_standard() {
4842        let memtable = MemTableKind::new(MemTableType::Standard, true);
4843        assert_eq!(memtable.kind(), MemTableType::Standard);
4844
4845        // Write and read
4846        memtable
4847            .write(b"key1".to_vec(), Some(b"value1".to_vec()), 1)
4848            .unwrap();
4849
4850        // Commit transaction at ts=100
4851        let write_set = std::iter::once(InlineKey::from_slice(b"key1")).collect();
4852        memtable.commit(1, 100, &write_set);
4853
4854        // Read after commit - snapshot_ts must be > commit_ts for visibility
4855        let v = memtable.read(b"key1", 101, None);
4856        assert_eq!(v, Some(b"value1".to_vec()));
4857    }
4858
4859    #[test]
4860    fn test_memtable_kind_arena() {
4861        let memtable = MemTableKind::new(MemTableType::Arena, true);
4862        assert_eq!(memtable.kind(), MemTableType::Arena);
4863
4864        // Write and read
4865        memtable
4866            .write(b"key1".to_vec(), Some(b"value1".to_vec()), 1)
4867            .unwrap();
4868
4869        // Commit at ts=100
4870        let write_set = std::iter::once(InlineKey::from_slice(b"key1")).collect();
4871        memtable.commit(1, 100, &write_set);
4872
4873        // Read after commit - snapshot_ts > commit_ts
4874        let v = memtable.read(b"key1", 101, None);
4875        assert_eq!(v, Some(b"value1".to_vec()));
4876    }
4877
4878    #[test]
4879    fn test_memtable_kind_scan_range() {
4880        // Test both implementations have consistent behavior
4881        for kind in [MemTableType::Standard, MemTableType::Arena] {
4882            let memtable = MemTableKind::new(kind, true);
4883
4884            // Write some data
4885            for i in 0..5 {
4886                let key = format!("key{}", i);
4887                let value = format!("value{}", i);
4888                memtable
4889                    .write(key.into_bytes(), Some(value.into_bytes()), 1)
4890                    .unwrap();
4891            }
4892
4893            // Commit all at ts=100
4894            let write_set: HashSet<InlineKey> = (0..5)
4895                .map(|i| InlineKey::from_slice(format!("key{}", i).as_bytes()))
4896                .collect();
4897            memtable.commit(1, 100, &write_set);
4898
4899            // Scan range with snapshot_ts > commit_ts
4900            let results = memtable.scan_range(b"key1", b"key4", 101, None);
4901            assert_eq!(
4902                results.len(),
4903                3,
4904                "kind={:?} should have 3 results (key1, key2, key3)",
4905                kind
4906            );
4907        }
4908    }
4909
4910    #[test]
4911    fn test_durable_storage_arena() {
4912        let dir = tempdir().unwrap();
4913        let storage = DurableStorage::open_with_arena(dir.path()).unwrap();
4914
4915        assert_eq!(storage.memtable_type(), MemTableType::Arena);
4916
4917        // Basic transaction should work the same
4918        let txn_id = storage.begin_transaction().unwrap();
4919        storage
4920            .write(txn_id, b"key1".to_vec(), b"value1".to_vec())
4921            .unwrap();
4922        storage.commit(txn_id).unwrap();
4923
4924        let txn2 = storage.begin_transaction().unwrap();
4925        let v = storage.read(txn2, b"key1").unwrap();
4926        assert_eq!(v, Some(b"value1".to_vec()));
4927        storage.abort(txn2).unwrap();
4928    }
4929
4930    #[test]
4931    fn test_durable_storage_full_config() {
4932        let dir = tempdir().unwrap();
4933
4934        // Test with Arena and ordered index enabled
4935        let storage =
4936            DurableStorage::open_with_full_config(dir.path(), true, MemTableType::Arena).unwrap();
4937
4938        assert_eq!(storage.memtable_type(), MemTableType::Arena);
4939
4940        // Write multiple keys
4941        let txn = storage.begin_transaction().unwrap();
4942        for i in 0..10 {
4943            let key = format!("key{:02}", i);
4944            let value = format!("value{}", i);
4945            storage
4946                .write(txn, key.into_bytes(), value.into_bytes())
4947                .unwrap();
4948        }
4949        storage.commit(txn).unwrap();
4950
4951        // Scan should work (uses scan method for prefix)
4952        let txn2 = storage.begin_transaction().unwrap();
4953        let results = storage.scan(txn2, b"key0").unwrap();
4954        assert_eq!(results.len(), 10); // key00 through key09
4955        storage.abort(txn2).unwrap();
4956    }
4957}