Skip to main content

reddb_server/storage/engine/
btree.rs

1//! Native compatibility paths for the shared B-tree implementation.
2use super::pager::Pager;
3pub use reddb_engine::storage::btree::{
4    lehman_yao, BTreeError, BTreeResult, MAX_KEY_SIZE, MAX_VALUE_SIZE, MIN_FILL_FACTOR,
5};
6#[cfg(test)]
7use std::sync::Arc;
8pub type BTree = reddb_engine::storage::btree::BTree<Pager>;
9pub type BTreeCursor = reddb_engine::storage::btree::BTreeCursor<Pager>;
10#[path = "btree/value_layout.rs"]
11pub mod value_layout;
12
13#[cfg(test)]
14mod tests {
15    use super::*;
16    use std::path::PathBuf;
17
18    /// RAII temp DB path: removes the `.db` and every pager sidecar
19    /// (`.db-dwb`, `.db-hdr`, `.db-wal`, …) on drop so nothing is left in
20    /// TMPDIR for the leak guard (scripts/check-temp-residue.sh). The existing
21    /// tests only call `cleanup` at the START (a no-op for unique paths), so
22    /// without this the sidecars created during the test leaked. Derefs to
23    /// `PathBuf` so every `&path` / `cleanup(&path)` call site keeps working.
24    struct TempDbPath(PathBuf);
25    impl std::ops::Deref for TempDbPath {
26        type Target = PathBuf;
27        fn deref(&self) -> &PathBuf {
28            &self.0
29        }
30    }
31    impl AsRef<std::path::Path> for TempDbPath {
32        fn as_ref(&self) -> &std::path::Path {
33            self.0.as_ref()
34        }
35    }
36    impl Drop for TempDbPath {
37        fn drop(&mut self) {
38            cleanup(&self.0);
39        }
40    }
41
42    fn temp_db_path() -> TempDbPath {
43        use std::sync::atomic::{AtomicU64, Ordering};
44        static COUNTER: AtomicU64 = AtomicU64::new(0);
45        let id = COUNTER.fetch_add(1, Ordering::Relaxed);
46        let mut path = std::env::temp_dir();
47        path.push(format!("reddb_btree_test_{}_{}.db", std::process::id(), id));
48        TempDbPath(path)
49    }
50
51    fn cleanup(path: &PathBuf) {
52        // Remove the `.db` AND every sidecar the pager writes next to it
53        // (`.db-dwb`, `.db-hdr`, `.db-wal`, …) so nothing is left in TMPDIR for
54        // the leak guard (scripts/check-temp-residue.sh) to flag.
55        if let (Some(dir), Some(name)) = (
56            path.parent(),
57            path.file_name().and_then(|name| name.to_str()),
58        ) {
59            if let Ok(entries) = std::fs::read_dir(dir) {
60                for entry in entries.flatten() {
61                    if entry
62                        .file_name()
63                        .to_str()
64                        .is_some_and(|entry_name| entry_name.starts_with(name))
65                    {
66                        let _ = std::fs::remove_file(entry.path());
67                    }
68                }
69            }
70        }
71    }
72
73    #[test]
74    fn test_btree_empty() {
75        let path = temp_db_path();
76        cleanup(&path);
77
78        let pager = Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
79        let tree = BTree::new(pager);
80
81        assert!(tree.is_empty());
82        assert_eq!(tree.root_page_id(), 0);
83        assert_eq!(tree.get(b"key").expect("get() should succeed"), None);
84        assert_eq!(tree.count().expect("count() should succeed"), 0);
85
86        cleanup(&path);
87    }
88
89    #[test]
90    fn inline_batch_fallback_does_not_partially_update_tree() {
91        let path = temp_db_path();
92        let pager = Arc::new(Pager::open_default(&path).expect("pager"));
93        let tree = BTree::new(pager);
94        tree.insert(b"a", b"old").expect("insert");
95        let root = tree.root_page_id();
96        let mut random = 0x1234_5678_u32;
97        let large: Vec<u8> = (0..reddb_engine::storage::btree::value_layout::OVERFLOW_THRESHOLD
98            * 3)
99            .map(|_| {
100                random ^= random << 13;
101                random ^= random >> 17;
102                random ^= random << 5;
103                random as u8
104            })
105            .collect();
106        let unsupported = vec![
107            (b"a".to_vec(), b"new".to_vec()),
108            (b"missing".to_vec(), large),
109        ];
110        assert!(!tree
111            .try_upsert_inline_without_splits(&unsupported)
112            .expect("preflight"));
113        assert_eq!(tree.get(b"a").expect("read"), Some(b"old".to_vec()));
114        assert_eq!(tree.get(b"missing").expect("read"), None);
115        assert!(tree
116            .try_upsert_inline_without_splits(&[(b"missing".to_vec(), b"x".to_vec())])
117            .expect("insert"));
118        assert_eq!(tree.get(b"missing").expect("read"), Some(b"x".to_vec()));
119        assert!(tree
120            .try_upsert_inline_without_splits(&[(b"a".to_vec(), b"longer".to_vec())])
121            .expect("repack"));
122        assert_eq!(tree.get(b"a").expect("read"), Some(b"longer".to_vec()));
123        assert!(tree
124            .try_upsert_inline_without_splits(&[(b"a".to_vec(), b"new".to_vec())])
125            .expect("overwrite"));
126        assert_eq!(tree.get(b"a").expect("read"), Some(b"new".to_vec()));
127        assert_eq!(tree.root_page_id(), root);
128    }
129
130    #[test]
131    fn test_btree_single_insert() {
132        let path = temp_db_path();
133        cleanup(&path);
134
135        let pager = Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
136        let tree = BTree::new(pager);
137
138        tree.insert(b"hello", b"world")
139            .expect("insert() should succeed");
140
141        assert!(!tree.is_empty());
142        assert_eq!(
143            tree.get(b"hello").expect("get() should succeed"),
144            Some(b"world".to_vec())
145        );
146        assert_eq!(tree.get(b"other").expect("get() should succeed"), None);
147        assert_eq!(tree.count().expect("count() should succeed"), 1);
148
149        cleanup(&path);
150    }
151
152    #[test]
153    fn test_btree_multiple_inserts() {
154        let path = temp_db_path();
155        cleanup(&path);
156
157        let pager = Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
158        let tree = BTree::new(pager);
159
160        tree.insert(b"c", b"3").expect("insert() should succeed");
161        tree.insert(b"a", b"1").expect("insert() should succeed");
162        tree.insert(b"b", b"2").expect("insert() should succeed");
163
164        assert_eq!(
165            tree.get(b"a").expect("get() should succeed"),
166            Some(b"1".to_vec())
167        );
168        assert_eq!(
169            tree.get(b"b").expect("get() should succeed"),
170            Some(b"2".to_vec())
171        );
172        assert_eq!(
173            tree.get(b"c").expect("get() should succeed"),
174            Some(b"3".to_vec())
175        );
176        assert_eq!(tree.count().expect("count() should succeed"), 3);
177
178        cleanup(&path);
179    }
180
181    #[test]
182    fn test_btree_duplicate_key() {
183        let path = temp_db_path();
184        cleanup(&path);
185
186        let pager = Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
187        let tree = BTree::new(pager);
188
189        tree.insert(b"key", b"value1")
190            .expect("insert() should succeed");
191        let result = tree.insert(b"key", b"value2");
192
193        assert!(matches!(result, Err(BTreeError::DuplicateKey)));
194        assert_eq!(
195            tree.get(b"key").expect("get() should succeed"),
196            Some(b"value1".to_vec())
197        );
198
199        cleanup(&path);
200    }
201
202    #[test]
203    fn test_btree_delete() {
204        let path = temp_db_path();
205        cleanup(&path);
206
207        let pager = Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
208        let tree = BTree::new(pager);
209
210        tree.insert(b"a", b"1").expect("insert() should succeed");
211        tree.insert(b"b", b"2").expect("insert() should succeed");
212        tree.insert(b"c", b"3").expect("insert() should succeed");
213
214        assert!(tree.delete(b"b").expect("delete() should succeed"));
215        assert!(!tree.delete(b"d").expect("delete() should succeed"));
216
217        assert_eq!(
218            tree.get(b"a").expect("get() should succeed"),
219            Some(b"1".to_vec())
220        );
221        assert_eq!(tree.get(b"b").expect("get() should succeed"), None);
222        assert_eq!(
223            tree.get(b"c").expect("get() should succeed"),
224            Some(b"3".to_vec())
225        );
226        assert_eq!(tree.count().expect("count() should succeed"), 2);
227
228        cleanup(&path);
229    }
230
231    #[test]
232    fn test_btree_delete_rebalance_removes_empty_leaf() {
233        let path = temp_db_path();
234        cleanup(&path);
235
236        let pager = Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
237        let tree = BTree::new(pager.clone());
238
239        let value = vec![b'v'; 200];
240        for i in 0..300u32 {
241            let key = format!("key{:03}", i);
242            tree.insert(key.as_bytes(), &value)
243                .expect("insert() should succeed");
244        }
245
246        // Preserve small end ranges while emptying all interior leaves.
247        let keys: Vec<_> = (10..290u32)
248            .map(|id| format!("key{id:03}").into_bytes())
249            .collect();
250
251        for key in &keys {
252            tree.delete(key).expect("delete() should succeed");
253        }
254
255        let expected = 300 - keys.len();
256        assert_eq!(tree.count().expect("count() should succeed"), expected);
257
258        let mut cursor = tree.cursor_first().expect("cursor_first() should succeed");
259        let mut results = Vec::new();
260        while let Some((key, _)) = cursor.next().expect("next() should succeed") {
261            results.push(key);
262        }
263
264        assert_eq!(results.len(), expected);
265        let last_key = format!("key{:03}", 299).into_bytes();
266        assert_eq!(results.last(), Some(&last_key));
267
268        cleanup(&path);
269    }
270
271    #[test]
272    fn test_btree_cursor() {
273        let path = temp_db_path();
274        cleanup(&path);
275
276        let pager = Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
277        let tree = BTree::new(pager);
278
279        tree.insert(b"c", b"3").expect("insert() should succeed");
280        tree.insert(b"a", b"1").expect("insert() should succeed");
281        tree.insert(b"b", b"2").expect("insert() should succeed");
282
283        let mut cursor = tree.cursor_first().expect("cursor_first() should succeed");
284        let mut results: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
285
286        while let Some(entry) = cursor.next().expect("next() should succeed") {
287            results.push(entry);
288        }
289
290        assert_eq!(results.len(), 3);
291        assert_eq!(results[0], (b"a".to_vec(), b"1".to_vec()));
292        assert_eq!(results[1], (b"b".to_vec(), b"2".to_vec()));
293        assert_eq!(results[2], (b"c".to_vec(), b"3".to_vec()));
294
295        cleanup(&path);
296    }
297
298    #[test]
299    fn test_btree_range() {
300        let path = temp_db_path();
301        cleanup(&path);
302
303        let pager = Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
304        let tree = BTree::new(pager);
305
306        for i in 0..10u8 {
307            let key = format!("key{:02}", i);
308            let value = format!("val{:02}", i);
309            tree.insert(key.as_bytes(), value.as_bytes())
310                .expect("insert() should succeed");
311        }
312
313        let results = tree
314            .range(b"key03", b"key06")
315            .expect("range() should succeed");
316
317        assert_eq!(results.len(), 4);
318        assert_eq!(results[0].0, b"key03".to_vec());
319        assert_eq!(results[3].0, b"key06".to_vec());
320
321        cleanup(&path);
322    }
323
324    #[test]
325    fn test_btree_large_keys() {
326        let path = temp_db_path();
327        cleanup(&path);
328
329        let pager = Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
330        let tree = BTree::new(pager);
331
332        let key = vec![b'x'; 500];
333        let value = vec![b'y'; 500];
334
335        tree.insert(&key, &value).expect("insert() should succeed");
336        assert_eq!(tree.get(&key).expect("get() should succeed"), Some(value));
337
338        cleanup(&path);
339    }
340
341    #[test]
342    fn test_btree_key_too_large() {
343        let path = temp_db_path();
344        cleanup(&path);
345
346        let pager = Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
347        let tree = BTree::new(pager);
348
349        let key = vec![b'x'; MAX_KEY_SIZE + 1];
350        let result = tree.insert(&key, b"value");
351
352        assert!(matches!(result, Err(BTreeError::KeyTooLarge(_))));
353
354        cleanup(&path);
355    }
356
357    #[test]
358    fn test_btree_many_inserts() {
359        let path = temp_db_path();
360        cleanup(&path);
361
362        let pager = Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
363        let tree = BTree::new(pager);
364
365        // Insert enough entries to force splits
366        for i in 0..100u32 {
367            let key = format!("key{:08}", i);
368            let value = format!("value{:08}", i);
369            tree.insert(key.as_bytes(), value.as_bytes())
370                .expect("insert() should succeed");
371        }
372
373        // Verify all entries
374        for i in 0..100u32 {
375            let key = format!("key{:08}", i);
376            let expected = format!("value{:08}", i);
377            assert_eq!(
378                tree.get(key.as_bytes()).expect("get() should succeed"),
379                Some(expected.into_bytes())
380            );
381        }
382
383        assert_eq!(tree.count().expect("count() should succeed"), 100);
384
385        cleanup(&path);
386    }
387
388    #[test]
389    fn test_btree_bulk_insert_sorted_preserves_leaf_layout() {
390        let path = temp_db_path();
391        cleanup(&path);
392
393        let pager = Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
394        let tree = BTree::new(pager);
395
396        let items: Vec<(Vec<u8>, Vec<u8>)> = (0..240u32)
397            .map(|i| {
398                (
399                    format!("key{:08}", i).into_bytes(),
400                    vec![b'v'; 32 + (i as usize % 7)],
401                )
402            })
403            .collect();
404
405        tree.bulk_insert_sorted(&items)
406            .expect("bulk_insert_sorted() should succeed");
407
408        assert_eq!(tree.count().expect("count() should succeed"), items.len());
409
410        for (key, value) in &items {
411            assert_eq!(
412                tree.get(key).expect("get() should succeed"),
413                Some(value.clone())
414            );
415        }
416
417        cleanup(&path);
418    }
419
420    /// Property test for `BTree::upsert_batch_sorted` (#159).
421    ///
422    /// Generates random batches of (key, value) updates and applies them to
423    /// two independently-built trees: the baseline uses the existing
424    /// loop-of-`upsert` path, and the candidate uses `upsert_batch_sorted`
425    /// after the caller-side sort.  Both trees must end up with identical
426    /// key→value contents (verified via per-key `get` and a full cursor
427    /// scan), proving the new path is observationally equivalent for
428    /// arbitrary batches up to 200 entries.
429    mod prop_upsert_batch_sorted {
430        use super::*;
431        use proptest::prelude::*;
432        use std::collections::BTreeMap;
433
434        /// Build a tree pre-populated with `seed` and return the path so
435        /// the caller can clean it up.
436        fn populate_tree(seed: &[(Vec<u8>, Vec<u8>)]) -> (BTree, TempDbPath, Arc<Pager>) {
437            let path = temp_db_path();
438            cleanup(&path);
439            let pager =
440                Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
441            let tree = BTree::new(Arc::clone(&pager));
442            for (k, v) in seed {
443                tree.upsert(k, v).expect("upsert() should succeed");
444            }
445            (tree, path, pager)
446        }
447
448        fn key_strategy() -> impl Strategy<Value = Vec<u8>> {
449            // Short keys keep many entries per leaf so a single batch
450            // hits multiple keys in the same leaf — the path the new
451            // method optimises.  1..=4 bytes from a small alphabet.
452            prop::collection::vec(0u8..16u8, 1..=4)
453        }
454
455        fn value_strategy() -> impl Strategy<Value = Vec<u8>> {
456            // Tiny values — same-or-shrink fits the in-place overwrite
457            // fast path; growing values fall back to the generic
458            // single-key `upsert`, exercising both branches.
459            prop::collection::vec(0u8..255u8, 0..=8)
460        }
461
462        fn pair_strategy() -> impl Strategy<Value = (Vec<u8>, Vec<u8>)> {
463            (key_strategy(), value_strategy())
464        }
465
466        proptest! {
467            #![proptest_config(ProptestConfig {
468                cases: 64,
469                .. ProptestConfig::default()
470            })]
471
472            #[test]
473            fn batch_upsert_matches_loop_upsert(
474                seed in prop::collection::vec(pair_strategy(), 0..50),
475                batch in prop::collection::vec(pair_strategy(), 1..200),
476            ) {
477                // Dedup the seed by key (last write wins) so both trees
478                // start from the same well-defined state.
479                let mut seed_map: BTreeMap<Vec<u8>, Vec<u8>> = BTreeMap::new();
480                for (k, v) in seed {
481                    seed_map.insert(k, v);
482                }
483                let seed: Vec<(Vec<u8>, Vec<u8>)> = seed_map.into_iter().collect();
484
485                // Baseline: loop-of-upsert.
486                let (baseline, baseline_path, _baseline_pager) = populate_tree(&seed);
487                for (k, v) in &batch {
488                    baseline.upsert(k, v).expect("upsert() should succeed");
489                }
490
491                // Candidate: upsert_batch_sorted after caller-side sort
492                // (matches the contract documented on the method).
493                let (candidate, candidate_path, _candidate_pager) = populate_tree(&seed);
494                let mut sorted = batch.clone();
495                sorted.sort_by(|a, b| a.0.cmp(&b.0));
496                candidate.upsert_batch_sorted(&sorted).expect("upsert_batch_sorted() should succeed");
497
498                // Compute the expected end state from BTreeMap (last
499                // write wins), independent of insertion order.
500                let mut expected: BTreeMap<Vec<u8>, Vec<u8>> = BTreeMap::new();
501                for (k, v) in &seed {
502                    expected.insert(k.clone(), v.clone());
503                }
504                for (k, v) in &batch {
505                    expected.insert(k.clone(), v.clone());
506                }
507
508                // Per-key get matches expected on both trees.
509                for (k, v) in &expected {
510                    let baseline_got = baseline.get(k).expect("get() should succeed");
511                    let candidate_got = candidate.get(k).expect("get() should succeed");
512                    prop_assert_eq!(baseline_got.as_ref(), Some(v));
513                    prop_assert_eq!(candidate_got.as_ref(), Some(v));
514                }
515
516                // Full cursor scan yields identical sorted contents.
517                let collect = |t: &BTree| -> Vec<(Vec<u8>, Vec<u8>)> {
518                    let mut out = Vec::new();
519                    let mut cur = t.cursor_first().expect("cursor_first() should succeed");
520                    while let Some(entry) = cur.next().expect("next() should succeed") {
521                        out.push(entry);
522                    }
523                    out
524                };
525                let baseline_contents = collect(&baseline);
526                let candidate_contents = collect(&candidate);
527                prop_assert_eq!(&baseline_contents, &candidate_contents);
528
529                // And both equal the BTreeMap-derived expectation.
530                let expected_contents: Vec<(Vec<u8>, Vec<u8>)> =
531                    expected.into_iter().collect();
532                prop_assert_eq!(&candidate_contents, &expected_contents);
533
534                // Drop the trees and pagers BEFORE cleanup. The pager flushes
535                // its header (`.db-hdr`) on drop, so cleaning up while an
536                // `Arc<Pager>` is still alive lets the final drop re-create the
537                // sidecar AFTER removal — exactly the leak the guard
538                // (scripts/check-temp-residue.sh) flags (128 `.db-hdr` files,
539                // one per tree per proptest case). Explicit drops make cleanup
540                // the last thing to touch these paths.
541                drop(baseline);
542                drop(candidate);
543                drop(_baseline_pager);
544                drop(_candidate_pager);
545                cleanup(&baseline_path);
546                cleanup(&candidate_path);
547            }
548        }
549    }
550
551    #[test]
552    fn test_btree_sorted_iteration() {
553        let path = temp_db_path();
554        cleanup(&path);
555
556        let pager = Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
557        let tree = BTree::new(pager);
558
559        // Insert in random order
560        let keys = vec![50, 25, 75, 10, 30, 60, 80, 5, 15, 27, 35, 55, 65, 77, 90];
561        for k in &keys {
562            let key = format!("{:03}", k);
563            tree.insert(key.as_bytes(), key.as_bytes())
564                .expect("insert() should succeed");
565        }
566
567        // Should iterate in sorted order
568        let mut cursor = tree.cursor_first().expect("cursor_first() should succeed");
569        let mut prev: Option<Vec<u8>> = None;
570
571        while let Some((key, _)) = cursor.next().expect("next() should succeed") {
572            if let Some(p) = &prev {
573                assert!(p < &key, "Keys not in sorted order");
574            }
575            prev = Some(key);
576        }
577
578        cleanup(&path);
579    }
580
581    /// #2376: several threads writing one tree (the shape of concurrent
582    /// DDL emitting `red.control_events` rows) must not lose keys or tear
583    /// the tree through a racing copy-modify-write of the same leaf or a
584    /// racing split.
585    #[test]
586    fn concurrent_writers_on_one_tree_keep_every_key() {
587        const WRITER_COUNT: u32 = 4;
588        const KEY_COUNT_PER_WRITER: u32 = 2_000;
589
590        let path = temp_db_path();
591        cleanup(&path);
592        let pager = Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
593        let tree = Arc::new(BTree::new(pager));
594        let value = vec![7u8; 96];
595
596        let writers: Vec<_> = (0..WRITER_COUNT)
597            .map(|writer| {
598                let tree = Arc::clone(&tree);
599                let value = value.clone();
600                std::thread::spawn(move || {
601                    for i in 0..KEY_COUNT_PER_WRITER {
602                        // Interleave the writers' key spaces so they share leaves.
603                        let key = (i * WRITER_COUNT + writer).to_be_bytes();
604                        tree.insert(&key, &value).expect("insert() should succeed");
605                        // Churn: delete and re-insert every fourth key.
606                        if i % 4 == 0 {
607                            assert!(tree.delete(&key).expect("delete() should succeed"));
608                            tree.upsert(&key, &value).expect("upsert() should succeed");
609                        }
610                    }
611                })
612            })
613            .collect();
614        for writer in writers {
615            writer.join().expect("writer thread panicked");
616        }
617
618        let key_count = WRITER_COUNT * KEY_COUNT_PER_WRITER;
619        let mut cursor = tree.cursor_first().expect("cursor_first() should succeed");
620        let mut seen = Vec::new();
621        while let Some((key, _)) = cursor.next().expect("next() should succeed") {
622            seen.push(u32::from_be_bytes(
623                key.as_slice().try_into().expect("invariant: 4-byte keys"),
624            ));
625        }
626        let expected: Vec<u32> = (0..key_count).collect();
627        assert_eq!(seen, expected, "every key exactly once, in order");
628        for key in 0..key_count {
629            assert!(
630                tree.get(&key.to_be_bytes())
631                    .expect("get() should succeed")
632                    .is_some(),
633                "key {key} must be reachable from the root"
634            );
635        }
636
637        cleanup(&path);
638    }
639}
640
641/// Acceptance tests for slice E of PRD #662 — spill pipeline that wires
642/// the per-cell value layout (`value_layout`), the LZ4 codec
643/// (`vector_btree::value_codec`), and the overflow chain (`overflow`)
644/// into the B-tree's `insert` / `bulk_insert_sorted` / `get` paths.
645#[cfg(test)]
646mod overflow_pipeline_tests {
647    use super::value_layout::{MAX_VALUE_SIZE, OVERFLOW_THRESHOLD};
648    use super::*;
649    use std::path::PathBuf;
650
651    fn temp_db_path(tag: &str) -> PathBuf {
652        use std::sync::atomic::{AtomicU64, Ordering};
653        static COUNTER: AtomicU64 = AtomicU64::new(0);
654        let id = COUNTER.fetch_add(1, Ordering::Relaxed);
655        let mut path = std::env::temp_dir();
656        path.push(format!(
657            "reddb_btree_spill_{}_{}_{}.db",
658            tag,
659            std::process::id(),
660            id
661        ));
662        path
663    }
664
665    fn cleanup(path: &std::path::Path) {
666        let _ = std::fs::remove_file(path);
667        for sidecar in reddb_file::layout::pager_shadow_sidecar_paths(path) {
668            let _ = std::fs::remove_file(sidecar);
669        }
670    }
671
672    /// AC #1: a value at `OVERFLOW_THRESHOLD - 1` stores inline (raw or
673    /// compressed) and **no overflow pages are allocated**. We count
674    /// the file's pages before and after; the only growth should be the
675    /// leaf page itself.
676    #[test]
677    fn inline_path_allocates_no_overflow_pages() {
678        let path = temp_db_path("inline_no_overflow");
679        cleanup(&path);
680        {
681            let pager =
682                Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
683            let tree = BTree::new(pager.clone());
684            let before = pager.page_count().expect("page_count() should succeed");
685
686            let value = vec![0x5Au8; OVERFLOW_THRESHOLD - 1];
687            tree.insert(b"k", &value).expect("insert() should succeed");
688
689            let after = pager.page_count().expect("page_count() should succeed");
690            // Exactly one new page (the root leaf). An overflow path
691            // would have allocated ≥ 2 pages (leaf + chain head).
692            assert_eq!(
693                after - before,
694                1,
695                "inline value must allocate only the root leaf"
696            );
697            assert_eq!(tree.get(b"k").expect("get() should succeed"), Some(value));
698        }
699        cleanup(&path);
700    }
701
702    /// AC #2: a 44 KB highly-compressible markdown payload stores via
703    /// the compressed-inline path (or via overflow if it still exceeds
704    /// the threshold after LZ4) and `get` returns byte-identical bytes.
705    #[test]
706    fn compressible_44kb_round_trips() {
707        let path = temp_db_path("compressible_44kb");
708        cleanup(&path);
709        {
710            let pager =
711                Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
712            let tree = BTree::new(pager);
713
714            // Repeating markdown chunk — compresses far below threshold.
715            let snippet = "# heading\n- bullet line that says the same thing over and over\n";
716            let mut value = Vec::with_capacity(44 * 1024);
717            while value.len() < 44 * 1024 {
718                value.extend_from_slice(snippet.as_bytes());
719            }
720            assert!(value.len() > OVERFLOW_THRESHOLD);
721
722            tree.insert(b"doc", &value)
723                .expect("insert() should succeed");
724            assert_eq!(tree.get(b"doc").expect("get() should succeed"), Some(value));
725        }
726        cleanup(&path);
727    }
728
729    /// AC #3: a 5 MB incompressible payload spills via overflow chain
730    /// and `get` returns byte-identical bytes.
731    #[test]
732    fn incompressible_5mb_spills_and_round_trips() {
733        let path = temp_db_path("incompressible_5mb");
734        cleanup(&path);
735        {
736            let pager =
737                Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
738            let tree = BTree::new(pager.clone());
739
740            // xorshift produces incompressible bytes deterministically
741            // so the test never flakes on the codec's heuristic.
742            let mut state: u64 = 0x9E37_79B9_7F4A_7C15;
743            let value: Vec<u8> = (0..5 * 1024 * 1024)
744                .map(|_| {
745                    state ^= state << 13;
746                    state ^= state >> 7;
747                    state ^= state << 17;
748                    state as u8
749                })
750                .collect();
751
752            let before = pager.page_count().expect("page_count() should succeed");
753            tree.insert(b"blob", &value)
754                .expect("insert() should succeed");
755            let after = pager.page_count().expect("page_count() should succeed");
756            // 5 MB / overflow-page payload ≈ many pages — the leaf
757            // alone could not absorb this.
758            assert!(
759                after - before > 2,
760                "5 MB spill must allocate overflow pages (alloc'd {} pages)",
761                after - before
762            );
763
764            assert_eq!(
765                tree.get(b"blob").expect("get() should succeed").as_deref(),
766                Some(value.as_slice())
767            );
768        }
769        cleanup(&path);
770    }
771
772    /// AC #4: a value just above `MAX_VALUE_SIZE` is rejected with
773    /// `ValueTooLarge`; nothing is allocated (no LZ4 work, no overflow
774    /// pages, no leaf page).
775    #[test]
776    fn value_above_ceiling_rejected_without_allocation() {
777        let path = temp_db_path("ceiling_reject");
778        cleanup(&path);
779        {
780            let pager =
781                Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
782            let tree = BTree::new(pager.clone());
783            let before = pager.page_count().expect("page_count() should succeed");
784
785            let value = vec![0u8; MAX_VALUE_SIZE + 1];
786            let err = tree.insert(b"too_big", &value).unwrap_err();
787            assert!(matches!(err, BTreeError::ValueTooLarge(_)));
788
789            assert_eq!(
790                pager.page_count().expect("page_count() should succeed"),
791                before,
792                "rejected value must not allocate pages"
793            );
794            // Tree stays empty — no root leaf was created.
795            assert!(tree.is_empty());
796        }
797        cleanup(&path);
798    }
799
800    /// AC #5: `bulk_insert_sorted` with mixed value sizes inserts all
801    /// valid rows; an oversized row in the middle is rejected but the
802    /// rest land. The function still surfaces the violation in its
803    /// return value so callers do not silently lose data.
804    #[test]
805    fn bulk_insert_skips_oversized_row_keeps_rest() {
806        let path = temp_db_path("bulk_mixed");
807        cleanup(&path);
808        {
809            let pager =
810                Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
811            let tree = BTree::new(pager);
812
813            let mut items: Vec<(Vec<u8>, Vec<u8>)> = Vec::new();
814            // Five small rows ...
815            for i in 0..5u32 {
816                items.push((format!("k{:04}", i).into_bytes(), vec![b'a' + i as u8; 64]));
817            }
818            // ... one oversize row in the middle ...
819            items.push((b"k0005_huge".to_vec(), vec![0u8; MAX_VALUE_SIZE + 1]));
820            // ... and five more small rows after it. Keys stay sorted.
821            for i in 6..11u32 {
822                items.push((format!("k{:04}", i).into_bytes(), vec![b'z' - i as u8; 32]));
823            }
824
825            let res = tree.bulk_insert_sorted(&items);
826            assert!(matches!(res, Err(BTreeError::ValueTooLarge(_))));
827
828            // All valid rows persisted.
829            for (k, v) in items.iter().filter(|(_, v)| v.len() <= MAX_VALUE_SIZE) {
830                assert_eq!(
831                    tree.get(k).expect("get() should succeed").as_deref(),
832                    Some(v.as_slice()),
833                    "valid row {:?} must land",
834                    k
835                );
836            }
837            // The oversize row did not land.
838            assert_eq!(tree.get(b"k0005_huge").expect("get() should succeed"), None);
839        }
840        cleanup(&path);
841    }
842
843    /// AC #6: values that used to fail (≥ 1024 bytes) now persist
844    /// transparently. Walk a handful of sizes that straddle the
845    /// threshold and the compressed-inline / spill boundaries.
846    #[test]
847    fn large_values_persist_transparently() {
848        let path = temp_db_path("large_transparent");
849        cleanup(&path);
850        {
851            let pager =
852                Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
853            let tree = BTree::new(pager);
854
855            // Sizes hit: just over threshold, well over threshold,
856            // and several MB. Bytes pattern is the index — so the
857            // values differ across keys and are incompressible enough
858            // that LZ4 falls back to raw on the largest entries.
859            for (i, size) in [
860                OVERFLOW_THRESHOLD + 1,
861                OVERFLOW_THRESHOLD * 2,
862                64 * 1024,
863                1 * 1024 * 1024,
864            ]
865            .iter()
866            .enumerate()
867            {
868                let value: Vec<u8> = (0..*size).map(|n| ((n + i) & 0xff) as u8).collect();
869                let key = format!("size{:08}", size).into_bytes();
870                tree.insert(&key, &value).expect("insert() should succeed");
871                assert_eq!(
872                    tree.get(&key).expect("get() should succeed").as_deref(),
873                    Some(value.as_slice()),
874                    "size {} must round-trip",
875                    size
876                );
877            }
878        }
879        cleanup(&path);
880    }
881}
882
883/// Acceptance tests for slice F of PRD #662 — freeing overflow chains
884/// on delete and shrinking update so storage does not leak.
885#[cfg(test)]
886mod overflow_free_tests {
887    use super::value_layout::OVERFLOW_THRESHOLD;
888    use super::*;
889    use std::path::PathBuf;
890
891    fn temp_db_path(tag: &str) -> PathBuf {
892        use std::sync::atomic::{AtomicU64, Ordering};
893        static COUNTER: AtomicU64 = AtomicU64::new(0);
894        let id = COUNTER.fetch_add(1, Ordering::Relaxed);
895        let mut path = std::env::temp_dir();
896        path.push(format!(
897            "reddb_btree_free_{}_{}_{}.db",
898            tag,
899            std::process::id(),
900            id
901        ));
902        path
903    }
904
905    fn cleanup(path: &std::path::Path) {
906        let _ = std::fs::remove_file(path);
907        for sidecar in reddb_file::layout::pager_shadow_sidecar_paths(path) {
908            let _ = std::fs::remove_file(sidecar);
909        }
910    }
911
912    fn incompressible(seed: u64, len: usize) -> Vec<u8> {
913        let mut state = seed | 1;
914        (0..len)
915            .map(|_| {
916                state ^= state << 13;
917                state ^= state >> 7;
918                state ^= state << 17;
919                state as u8
920            })
921            .collect()
922    }
923
924    /// AC #1: deleting a row whose value spilled returns every overflow
925    /// page to the free list. Observable signal: page_count stays
926    /// constant when we insert a same-size row right after the delete
927    /// (no file growth on the second insert), and `tree.get` returns
928    /// the new value.
929    #[test]
930    fn delete_spilled_value_frees_overflow_chain() {
931        let path = temp_db_path("delete_frees");
932        cleanup(&path);
933        {
934            let pager =
935                Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
936            let tree = BTree::new(pager.clone());
937
938            // First spilled insert grows the file.
939            let v1 = incompressible(0xA11CE, 2 * 1024 * 1024);
940            tree.insert(b"k", &v1).expect("insert() should succeed");
941            let after_first = pager.page_count().expect("page_count() should succeed");
942
943            // Delete: every overflow page returned to the free list.
944            assert!(tree.delete(b"k").expect("delete() should succeed"));
945
946            // Re-insert the same size. With a working free list, the
947            // pager reuses the freed pages and the file does not grow.
948            let v2 = incompressible(0xB0B, 2 * 1024 * 1024);
949            tree.insert(b"k", &v2).expect("insert() should succeed");
950            let after_second = pager.page_count().expect("page_count() should succeed");
951
952            assert_eq!(
953                after_second, after_first,
954                "second spill must reuse freed pages, not grow the file"
955            );
956            assert_eq!(
957                tree.get(b"k").expect("get() should succeed").as_deref(),
958                Some(v2.as_slice())
959            );
960        }
961        cleanup(&path);
962    }
963
964    /// AC #2: updating a spilled value to a small inline value frees
965    /// the old chain in full. Same observability trick: insert another
966    /// spilled value of the original size, see that the file does not
967    /// grow beyond the original high-water mark.
968    #[test]
969    fn shrinking_update_to_inline_frees_chain() {
970        let path = temp_db_path("shrink_to_inline");
971        cleanup(&path);
972        {
973            let pager =
974                Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
975            let tree = BTree::new(pager.clone());
976
977            let big = incompressible(0xDEAD, 2 * 1024 * 1024);
978            tree.insert(b"k", &big).expect("insert() should succeed");
979            let hwm = pager.page_count().expect("page_count() should succeed");
980
981            // Shrink to inline raw (fits well under the threshold).
982            let small = b"tiny".to_vec();
983            tree.upsert(b"k", &small).expect("upsert() should succeed");
984            assert_eq!(
985                tree.get(b"k").expect("get() should succeed").as_deref(),
986                Some(small.as_slice())
987            );
988
989            // If the chain was not freed, this insert would push the
990            // page count past `hwm`. If it was, the pager reuses freed
991            // pages and we stay at the high-water mark.
992            let big2 = incompressible(0xBEEF, 2 * 1024 * 1024);
993            tree.insert(b"k2", &big2).expect("insert() should succeed");
994            assert_eq!(
995                pager.page_count().expect("page_count() should succeed"),
996                hwm,
997                "shrinking update must free the chain — replacement spill should reuse"
998            );
999            assert_eq!(
1000                tree.get(b"k2").expect("get() should succeed").as_deref(),
1001                Some(big2.as_slice())
1002            );
1003        }
1004        cleanup(&path);
1005    }
1006
1007    /// AC #3: updating a spilled value to a shorter (still spilled)
1008    /// value frees the old chain in full. The new chain is independent
1009    /// — no in-place reuse — so the freed pages must show up in the
1010    /// allocator before any new growth happens.
1011    #[test]
1012    fn shrinking_update_to_shorter_spill_frees_old_chain() {
1013        let path = temp_db_path("shrink_spill_to_spill");
1014        cleanup(&path);
1015        {
1016            let pager =
1017                Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
1018            let tree = BTree::new(pager.clone());
1019
1020            // Two spilled values, the second clearly shorter than the
1021            // first. Both encode through the pointer path because the
1022            // bytes are incompressible.
1023            let big = incompressible(0xF00D, 3 * 1024 * 1024);
1024            tree.insert(b"k", &big).expect("insert() should succeed");
1025            let hwm_after_big = pager.page_count().expect("page_count() should succeed");
1026
1027            // Per slice F's acceptance the new chain is *independent*
1028            // of the old — i.e. no in-place page reuse — so the
1029            // upsert allocates fresh pages for the smaller chain
1030            // before freeing the old one. The file may grow during
1031            // this window; what we care about is that the freed pages
1032            // re-enter the allocator. We measure that by dropping the
1033            // row entirely (which frees the new chain too) and
1034            // re-inserting at the original size: if both chains were
1035            // freed, the pager reuses freed pages and we do not exceed
1036            // the high-water mark observed during the transient.
1037            let smaller = incompressible(0xCAFE, 1 * 1024 * 1024);
1038            tree.upsert(b"k", &smaller)
1039                .expect("upsert() should succeed");
1040            assert_eq!(
1041                tree.get(b"k").expect("get() should succeed").as_deref(),
1042                Some(smaller.as_slice())
1043            );
1044            let hwm_after_upsert = pager.page_count().expect("page_count() should succeed");
1045
1046            assert!(tree.delete(b"k").expect("delete() should succeed"));
1047
1048            let again = incompressible(0x1234, 3 * 1024 * 1024);
1049            tree.insert(b"k", &again).expect("insert() should succeed");
1050            assert!(
1051                pager.page_count().expect("page_count() should succeed") <= hwm_after_upsert,
1052                "after delete+insert of original size, file must not grow past the upsert transient \
1053                 (page_count = {}, transient high-water = {}, original = {}) — the upsert must have \
1054                 freed the old chain so the re-insert reuses freed pages",
1055                pager.page_count().expect("page_count() should succeed"),
1056                hwm_after_upsert,
1057                hwm_after_big,
1058            );
1059            assert_eq!(
1060                tree.get(b"k").expect("get() should succeed").as_deref(),
1061                Some(again.as_slice())
1062            );
1063        }
1064        cleanup(&path);
1065    }
1066
1067    /// AC #5 regression: insert large → delete → insert same-size large
1068    /// → file does not grow on the second insert.
1069    #[test]
1070    fn insert_delete_reinsert_does_not_grow_file() {
1071        let path = temp_db_path("reinsert_no_grow");
1072        cleanup(&path);
1073        {
1074            let pager =
1075                Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
1076            let tree = BTree::new(pager.clone());
1077
1078            let v1 = incompressible(0x5EED, 4 * 1024 * 1024);
1079            tree.insert(b"row", &v1).expect("insert() should succeed");
1080            let pages_after_first = pager.page_count().expect("page_count() should succeed");
1081
1082            assert!(tree.delete(b"row").expect("delete() should succeed"));
1083
1084            let v2 = incompressible(0xACE, 4 * 1024 * 1024);
1085            tree.insert(b"row", &v2).expect("insert() should succeed");
1086            let pages_after_second = pager.page_count().expect("page_count() should succeed");
1087
1088            assert_eq!(
1089                pages_after_second, pages_after_first,
1090                "re-inserting at the same size must reuse the freed chain"
1091            );
1092            assert_eq!(
1093                tree.get(b"row").expect("get() should succeed").as_deref(),
1094                Some(v2.as_slice())
1095            );
1096        }
1097        cleanup(&path);
1098    }
1099
1100    /// Belt-and-braces: an in-place shrink via `upsert_batch_sorted`
1101    /// — same in-place fast path as single-key `upsert` — must also
1102    /// free the old chain. Without this hook the batch path would
1103    /// silently leak on every `bulk_update` over spilled rows.
1104    #[test]
1105    fn upsert_batch_in_place_shrink_frees_chain() {
1106        let path = temp_db_path("batch_shrink");
1107        cleanup(&path);
1108        {
1109            let pager =
1110                Arc::new(Pager::open_default(&path).expect("open_default() should succeed"));
1111            let tree = BTree::new(pager.clone());
1112
1113            // Seed two spilled rows so the batch update lands in-place
1114            // on the same leaf.
1115            let big_a = incompressible(0x1111, 2 * 1024 * 1024);
1116            let big_b = incompressible(0x2222, 2 * 1024 * 1024);
1117            tree.insert(b"a", &big_a).expect("insert() should succeed");
1118            tree.insert(b"b", &big_b).expect("insert() should succeed");
1119            let hwm = pager.page_count().expect("page_count() should succeed");
1120
1121            let updates = vec![
1122                (b"a".to_vec(), b"shrunk-a".to_vec()),
1123                (b"b".to_vec(), b"shrunk-b".to_vec()),
1124            ];
1125            tree.upsert_batch_sorted(&updates)
1126                .expect("upsert_batch_sorted() should succeed");
1127            assert_eq!(
1128                tree.get(b"a").expect("get() should succeed").as_deref(),
1129                Some(b"shrunk-a".as_slice())
1130            );
1131            assert_eq!(
1132                tree.get(b"b").expect("get() should succeed").as_deref(),
1133                Some(b"shrunk-b".as_slice())
1134            );
1135
1136            // Both chains must be freed: a fresh pair of spilled
1137            // inserts re-uses them instead of growing the file.
1138            let again_a = incompressible(0xAAAA, 2 * 1024 * 1024);
1139            let again_b = incompressible(0xBBBB, 2 * 1024 * 1024);
1140            tree.insert(b"c", &again_a)
1141                .expect("insert() should succeed");
1142            tree.insert(b"d", &again_b)
1143                .expect("insert() should succeed");
1144            assert!(
1145                pager.page_count().expect("page_count() should succeed") <= hwm,
1146                "batch in-place shrink must free chains — replacement spills should reuse"
1147            );
1148        }
1149        cleanup(&path);
1150    }
1151}