aeternusdb 1.0.0

An embeddable, persistent key-value store built on an LSM-tree architecture.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
//! Edge-case tests: empty keys/values, scan boundaries, stats correctness,
//! close semantics, large/binary keys, and misc corner cases.
//!
//! This module exercises non-happy-path scenarios that a production database must
//! handle gracefully. It covers input validation (empty keys/values are rejected),
//! scan boundary semantics (start-inclusive / end-exclusive, inverted ranges),
//! stats counter transitions during freeze/flush, engine behavior after `close()`,
//! recovery of very large and binary keys through SSTables, and operations on
//! an empty database. These tests ensure the engine is robust against unusual
//! but valid (or intentionally invalid) usage patterns.
//!
//! ## Layer coverage
//! - `memtable__*`: memtable-only edge cases (validation, boundaries, empty DB)
//! - `memtable_sstable__*`: edge cases involving SSTable flush, recovery, and
//!   engine lifecycle (close, reopen, stats)
//!
//! ## See also
//! - [`tests_hardening`] — concurrency, extreme configs, orphan cleanup
//! - [`tests_scan`] — standard scan correctness tests

#[cfg(test)]
#[allow(non_snake_case)]
mod tests {
    use crate::engine::tests::helpers::*;
    use crate::engine::{Engine, EngineConfig};
    use tempfile::TempDir;

    // ================================================================
    // Empty key / empty value
    // ================================================================

    /// # Scenario
    /// Attempt to insert a key with an empty byte vector.
    ///
    /// # Starting environment
    /// Fresh engine with memtable-only config — no data.
    ///
    /// # Actions
    /// 1. Call `put(vec![], b"value")` with an empty key.
    ///
    /// # Expected behavior
    /// The engine returns an error — empty keys are rejected at the API level.
    #[test]
    fn memtable__empty_key_is_rejected() {
        let dir = TempDir::new().unwrap();
        let engine = Engine::open(dir.path(), memtable_only_config()).unwrap();

        let result = engine.put(vec![], b"value".to_vec());
        assert!(result.is_err(), "empty key should be rejected");
    }

    /// # Scenario
    /// Attempt to insert a value with an empty byte vector.
    ///
    /// # Starting environment
    /// Fresh engine with memtable-only config — no data.
    ///
    /// # Actions
    /// 1. Call `put(b"key", vec![])` with an empty value.
    ///
    /// # Expected behavior
    /// The engine returns an error — empty values are rejected at the API level.
    #[test]
    fn memtable__empty_value_is_rejected() {
        let dir = TempDir::new().unwrap();
        let engine = Engine::open(dir.path(), memtable_only_config()).unwrap();

        let result = engine.put(b"key".to_vec(), vec![]);
        assert!(result.is_err(), "empty value should be rejected");
    }

    /// # Scenario
    /// Attempt to insert both an empty key and an empty value.
    ///
    /// # Starting environment
    /// Fresh engine with memtable-only config — no data.
    ///
    /// # Actions
    /// 1. Call `put(vec![], vec![])` with both key and value empty.
    ///
    /// # Expected behavior
    /// The engine returns an error — at least the empty key triggers rejection.
    #[test]
    fn memtable__empty_key_and_value_rejected() {
        let dir = TempDir::new().unwrap();
        let engine = Engine::open(dir.path(), memtable_only_config()).unwrap();

        let result = engine.put(vec![], vec![]);
        assert!(result.is_err(), "empty key+value should be rejected");
    }

    // ================================================================
    // Scan boundary edge cases
    // ================================================================

    /// # Scenario
    /// Scan where start key equals end key (zero-width range).
    ///
    /// # Starting environment
    /// Engine with one key `"aaa"` inserted.
    ///
    /// # Actions
    /// 1. Scan with `start = "aaa"` and `end = "aaa"`.
    ///
    /// # Expected behavior
    /// Returns an empty result — a zero-width range `[x, x)` contains no keys
    /// because the end key is exclusive.
    #[test]
    fn memtable__scan_start_equals_end_empty() {
        let dir = TempDir::new().unwrap();
        let engine = Engine::open(dir.path(), memtable_only_config()).unwrap();
        engine.put(b"aaa".to_vec(), b"v".to_vec()).unwrap();

        let results = collect_scan(&engine, b"aaa", b"aaa");
        assert!(results.is_empty(), "scan(x, x) should return nothing");
    }

    /// # Scenario
    /// Scan where start key is greater than end key (inverted range).
    ///
    /// # Starting environment
    /// Engine with two keys `"aaa"` and `"zzz"`.
    ///
    /// # Actions
    /// 1. Scan with `start = "zzz"` and `end = "aaa"` (start > end).
    ///
    /// # Expected behavior
    /// Returns an empty result and does not panic — inverted ranges are
    /// treated as empty.
    #[test]
    fn memtable__scan_start_gt_end_empty() {
        let dir = TempDir::new().unwrap();
        let engine = Engine::open(dir.path(), memtable_only_config()).unwrap();
        engine.put(b"aaa".to_vec(), b"v1".to_vec()).unwrap();
        engine.put(b"zzz".to_vec(), b"v2".to_vec()).unwrap();

        let results = collect_scan(&engine, b"zzz", b"aaa");
        assert!(
            results.is_empty(),
            "scan(high, low) should return nothing (or at least not panic)"
        );
    }

    /// # Scenario
    /// Verify the exact boundary semantics of scan: start-inclusive, end-exclusive.
    ///
    /// # Starting environment
    /// Engine with 10 two-byte keys `[b'k', 0]` through `[b'k', 9]`.
    ///
    /// # Actions
    /// 1. Scan with `start = [k, 3]` and `end = [k, 7]`.
    ///
    /// # Expected behavior
    /// Result includes keys `[k,3]`, `[k,4]`, `[k,5]`, `[k,6]` but NOT `[k,7]`
    /// (end is exclusive) and NOT `[k,2]` (before start).
    #[test]
    fn memtable__scan_exact_boundary_inclusivity() {
        // Verify: start key is INCLUSIVE, end key is EXCLUSIVE.
        let dir = TempDir::new().unwrap();
        let engine = Engine::open(dir.path(), memtable_only_config()).unwrap();
        for i in 0..10u8 {
            engine
                .put(vec![b'k', i], format!("v{}", i).into_bytes())
                .unwrap();
        }

        // scan([k, 3], [k, 7]) should include k3, k4, k5, k6 but NOT k7
        let results = collect_scan(&engine, &[b'k', 3], &[b'k', 7]);
        let keys: Vec<Vec<u8>> = results.iter().map(|(k, _)| k.clone()).collect();

        assert!(keys.contains(&vec![b'k', 3]), "start key must be inclusive");
        assert!(
            keys.contains(&vec![b'k', 6]),
            "key just before end must be included"
        );
        assert!(!keys.contains(&vec![b'k', 7]), "end key must be exclusive");
        assert!(
            !keys.contains(&vec![b'k', 2]),
            "key before start must be excluded"
        );
    }

    /// # Scenario
    /// Full-keyspace scan using the widest possible byte range.
    ///
    /// # Starting environment
    /// Engine with 20 keys (`key_0000`..`key_0019`).
    ///
    /// # Actions
    /// 1. Scan with `start = "\x00"` and `end = "\xff"`.
    ///
    /// # Expected behavior
    /// All 20 keys are returned — the scan range covers the entire keyspace.
    #[test]
    fn memtable__scan_full_keyspace() {
        let dir = TempDir::new().unwrap();
        let engine = Engine::open(dir.path(), memtable_only_config()).unwrap();
        for i in 0..20u32 {
            engine
                .put(
                    format!("key_{:04}", i).into_bytes(),
                    format!("val_{:04}", i).into_bytes(),
                )
                .unwrap();
        }

        let results = collect_scan(&engine, b"\x00", b"\xff");
        assert_eq!(
            results.len(),
            20,
            "full keyspace scan should return all keys"
        );
    }

    // ================================================================
    // Stats correctness
    // ================================================================

    /// # Scenario
    /// Track the `frozen_count` and `sstables_count` stat counters as
    /// writes transition data through the freeze/flush lifecycle.
    ///
    /// # Starting environment
    /// Engine with small buffer config (128 bytes) — no data yet.
    /// Initial stats: frozen_count = 0, sstables_count = 0.
    ///
    /// # Actions
    /// 1. Write keys in a loop until `frozen_count > 0` (a memtable was frozen).
    /// 2. Write one more key (`"trigger"`) to flush the frozen memtable.
    /// 3. Check stats again.
    ///
    /// # Expected behavior
    /// After the trigger put, `sstables_count > 0` — the frozen memtable has
    /// been flushed to an SSTable.
    #[test]
    fn memtable_sstable__stats_frozen_count_transitions() {
        let dir = TempDir::new().unwrap();
        let engine = Engine::open(dir.path(), small_buffer_config()).unwrap();

        // Initially: no frozen, no SSTables
        let s = engine.stats().unwrap();
        assert_eq!(s.frozen_count, 0);
        assert_eq!(s.sstables_count, 0);

        // Write until we get a frozen memtable
        let mut i = 0u32;
        loop {
            engine
                .put(
                    format!("k_{:04}", i).into_bytes(),
                    format!("v_{:04}", i).into_bytes(),
                )
                .unwrap();
            let s = engine.stats().unwrap();
            if s.frozen_count > 0 {
                // Frozen memtable exists; may or may not have SSTables yet
                break;
            }
            i += 1;
        }

        let before_frozen = engine.stats().unwrap().frozen_count;
        assert!(before_frozen >= 1, "Should have at least 1 frozen");

        // Explicitly flush frozen memtables → SSTables
        engine.flush_all_frozen().unwrap();
        let after = engine.stats().unwrap();
        // The frozen that existed should now be an SSTable
        assert!(
            after.sstables_count > 0,
            "Frozen should have been flushed to SSTable"
        );
    }

    // ================================================================
    // Close semantics
    // ================================================================

    /// # Scenario
    /// Verify that the engine remains usable after `close()` is called.
    ///
    /// # Starting environment
    /// Engine with 4 KB buffer; one key `"k"` = `"v"` inserted.
    ///
    /// # Actions
    /// 1. Call `close()` (flushes frozen memtables and checkpoints).
    /// 2. Perform a `get("k")` after close.
    /// 3. Perform a `put("k2", "v2")` and `get("k2")` after close.
    /// 4. Perform a scan after close.
    ///
    /// # Expected behavior
    /// All operations succeed — `close()` checkpoints state but the engine
    /// struct remains usable for subsequent reads and writes.
    #[test]
    fn memtable_sstable__operations_after_close_work() {
        // close() flushes frozen and checkpoints, but the engine struct
        // remains usable. Verify reads still work.
        let dir = TempDir::new().unwrap();
        let engine = Engine::open(dir.path(), default_config()).unwrap();
        engine.put(b"k".to_vec(), b"v".to_vec()).unwrap();
        engine.close().unwrap();

        // Reads after close should still see data
        assert_eq!(
            engine.get(b"k".to_vec()).unwrap(),
            Some(b"v".to_vec()),
            "get after close should still return data"
        );

        // Writes after close
        engine.put(b"k2".to_vec(), b"v2".to_vec()).unwrap();
        assert_eq!(
            engine.get(b"k2".to_vec()).unwrap(),
            Some(b"v2".to_vec()),
            "put after close should succeed"
        );

        // Scan after close
        let results = collect_scan(&engine, b"k", b"k\xff");
        assert!(
            !results.is_empty(),
            "scan after close should return results"
        );
    }

    /// # Scenario
    /// Calling `close()` multiple times in a row does not panic or corrupt data.
    ///
    /// # Starting environment
    /// Engine with one key inserted.
    ///
    /// # Actions
    /// 1. Call `close()` (first close — flushes and checkpoints).
    /// 2. Call `close()` again (second close — should be a no-op).
    /// 3. Reopen the engine and get the key.
    ///
    /// # Expected behavior
    /// No error or panic on the second close, and the data is intact after reopen.
    #[test]
    fn memtable_sstable__multiple_close_calls_safe() {
        let dir = TempDir::new().unwrap();
        let engine = Engine::open(dir.path(), default_config()).unwrap();
        engine.put(b"k".to_vec(), b"v".to_vec()).unwrap();

        engine.close().unwrap();
        engine.close().unwrap(); // Second close should not panic or error

        let engine = reopen(dir.path());
        assert_eq!(engine.get(b"k".to_vec()).unwrap(), Some(b"v".to_vec()),);
    }

    /// # Scenario
    /// Open → write → close → reopen → close (no writes) → reopen → verify.
    ///
    /// # Starting environment
    /// Temporary directory with no prior database files.
    ///
    /// # Actions
    /// 1. Open engine, put `"k"` = `"v"`, close.
    /// 2. Reopen engine, close immediately (no new writes).
    /// 3. Reopen engine, get `"k"`.
    ///
    /// # Expected behavior
    /// `get("k")` returns `Some("v")` — a close-with-no-writes cycle must not
    /// lose previously persisted data.
    #[test]
    fn memtable_sstable__close_no_writes_reopen() {
        // open → write → close → reopen → close (no writes) → reopen
        let dir = TempDir::new().unwrap();

        let engine = Engine::open(dir.path(), default_config()).unwrap();
        engine.put(b"k".to_vec(), b"v".to_vec()).unwrap();
        engine.close().unwrap();

        let engine = reopen(dir.path());
        engine.close().unwrap(); // No new writes, just close

        let engine = reopen(dir.path());
        assert_eq!(
            engine.get(b"k".to_vec()).unwrap(),
            Some(b"v".to_vec()),
            "data must survive close-with-no-writes cycle"
        );
    }

    // ================================================================
    // Reopen after only deletes
    // ================================================================

    /// # Scenario
    /// A session that performs only deletes (no puts) followed by a reopen.
    ///
    /// # Starting environment
    /// Session 1: three keys `"a"`, `"b"`, `"c"` inserted and engine closed.
    ///
    /// # Actions
    /// 1. Session 2: reopen, delete `"b"` (point), range-delete `["c", "d")`, close.
    /// 2. Session 3: reopen, get `"a"`, `"b"`, `"c"`.
    ///
    /// # Expected behavior
    /// `"a"` survives, `"b"` is `None` (point-deleted), `"c"` is `None`
    /// (range-deleted). Delete-only sessions are correctly persisted.
    #[test]
    fn memtable_sstable__reopen_after_only_deletes() {
        let dir = TempDir::new().unwrap();

        // Session 1: put some keys
        let engine = Engine::open(dir.path(), default_config()).unwrap();
        engine.put(b"a".to_vec(), b"1".to_vec()).unwrap();
        engine.put(b"b".to_vec(), b"2".to_vec()).unwrap();
        engine.put(b"c".to_vec(), b"3".to_vec()).unwrap();
        engine.close().unwrap();

        // Session 2: only deletes, no puts
        let engine = reopen(dir.path());
        engine.delete(b"b".to_vec()).unwrap();
        engine.delete_range(b"c".to_vec(), b"d".to_vec()).unwrap();
        engine.close().unwrap();

        // Session 3: verify
        let engine = reopen(dir.path());
        assert_eq!(
            engine.get(b"a".to_vec()).unwrap(),
            Some(b"1".to_vec()),
            "a should survive"
        );
        assert_eq!(engine.get(b"b".to_vec()).unwrap(), None, "b was deleted");
        assert_eq!(
            engine.get(b"c".to_vec()).unwrap(),
            None,
            "c was range-deleted"
        );
    }

    // ================================================================
    // Very large keys
    // ================================================================

    /// # Scenario
    /// Store and recover an 8 KB key through SSTable flush and engine reopen.
    ///
    /// # Starting environment
    /// Engine with 16 KB write buffer (custom config so the 8 KB key fits in
    /// one memtable).
    ///
    /// # Actions
    /// 1. Put an 8192-byte key (`0xAB` repeated) with a small value.
    /// 2. Write 600 padding keys to exceed the 16 KB buffer and force SSTable
    ///    flush.
    /// 3. Verify the large key is readable before close.
    /// 4. Close and reopen the engine.
    /// 5. Get the large key again.
    ///
    /// # Expected behavior
    /// The 8 KB key is correctly stored in the SSTable and survives reopen —
    /// the engine handles very large keys without truncation or corruption.
    #[test]
    fn memtable_sstable__very_large_key_recovery() {
        let dir = TempDir::new().unwrap();

        let big_key = vec![0xAB; 8192]; // 8 KB key
        let value = b"big_key_value".to_vec();

        // Use a 16 KB buffer so the single 8 KB key fits in one memtable
        let config = EngineConfig {
            write_buffer_size: 16 * 1024,
            compaction_strategy: crate::compaction::CompactionStrategyType::Stcs,
            bucket_low: 0.5,
            bucket_high: 1.5,
            min_sstable_size: 1024,
            min_threshold: 4,
            max_threshold: 32,
            tombstone_ratio_threshold: 0.2,
            tombstone_compaction_interval: 3600,
            tombstone_bloom_fallback: false,
            tombstone_range_drop: false,
            thread_pool_size: 2,
        };

        let engine = Engine::open(dir.path(), config).unwrap();
        engine.put(big_key.clone(), value.clone()).unwrap();
        // Write enough padding to exceed the 16 KB buffer → SSTable flush
        for i in 0..600u32 {
            engine
                .put(
                    format!("pad_{:04}", i).into_bytes(),
                    format!("padding_value_with_extra_bytes_{:04}", i).into_bytes(),
                )
                .unwrap();
        }
        engine.flush_all_frozen().unwrap();
        let stats = engine.stats().unwrap();
        assert!(stats.sstables_count > 0, "Expected SSTables");
        assert_eq!(
            engine.get(big_key.clone()).unwrap(),
            Some(value.clone()),
            "large key readable before close"
        );
        engine.close().unwrap();

        // Verify after reopen
        let engine = reopen(dir.path());
        assert_eq!(
            engine.get(big_key).unwrap(),
            Some(value),
            "8 KB key must survive SSTable flush + reopen"
        );
    }

    // ================================================================
    // Binary keys (0x00/0xFF) through SSTable and recovery
    // ================================================================

    /// # Scenario
    /// Store and recover keys composed entirely of `0x00`, `0xFF`, and mixed
    /// binary bytes through SSTable flush and reopen.
    ///
    /// # Starting environment
    /// Engine with 4 KB buffer.
    ///
    /// # Actions
    /// 1. Put three binary keys: 32×`0x00`, 32×`0xFF`, and a mixed
    ///    `[0x00, 0xFF, 0x00, 0xFF, 0x01, 0xFE]` sequence.
    /// 2. Write 200 padding keys to force SSTable flush.
    /// 3. Verify all three keys before close.
    /// 4. Close and reopen the engine.
    /// 5. Get all three binary keys.
    ///
    /// # Expected behavior
    /// All binary keys are correctly stored, flushed to SSTable, and recovered
    /// after reopen — the engine correctly handles boundary byte values.
    #[test]
    fn memtable_sstable__binary_keys_recovery() {
        let dir = TempDir::new().unwrap();

        let key_zeros = vec![0x00; 32];
        let key_ones = vec![0xFF; 32];
        let key_mixed = vec![0x00, 0xFF, 0x00, 0xFF, 0x01, 0xFE];

        let engine = Engine::open(dir.path(), default_config()).unwrap();
        engine.put(key_zeros.clone(), b"zeros".to_vec()).unwrap();
        engine.put(key_ones.clone(), b"ones".to_vec()).unwrap();
        engine.put(key_mixed.clone(), b"mixed".to_vec()).unwrap();

        // Force SSTable flush
        for i in 0..200u32 {
            engine
                .put(
                    format!("pad_{:04}", i).into_bytes(),
                    format!("pval_{:04}", i).into_bytes(),
                )
                .unwrap();
        }
        engine.flush_all_frozen().unwrap();
        let stats = engine.stats().unwrap();
        assert!(stats.sstables_count > 0, "Expected SSTables");

        // Verify before close
        assert_eq!(
            engine.get(key_zeros.clone()).unwrap(),
            Some(b"zeros".to_vec())
        );
        assert_eq!(
            engine.get(key_ones.clone()).unwrap(),
            Some(b"ones".to_vec())
        );
        assert_eq!(
            engine.get(key_mixed.clone()).unwrap(),
            Some(b"mixed".to_vec())
        );
        engine.close().unwrap();

        // Verify after reopen
        let engine = reopen(dir.path());
        assert_eq!(
            engine.get(key_zeros).unwrap(),
            Some(b"zeros".to_vec()),
            "0x00 key must survive SSTable + reopen"
        );
        assert_eq!(
            engine.get(key_ones).unwrap(),
            Some(b"ones".to_vec()),
            "0xFF key must survive SSTable + reopen"
        );
        assert_eq!(
            engine.get(key_mixed).unwrap(),
            Some(b"mixed".to_vec()),
            "mixed binary key must survive SSTable + reopen"
        );
    }

    // ================================================================
    // Range-delete / delete on empty database
    // ================================================================

    /// # Scenario
    /// Issue a range delete on a completely empty database.
    ///
    /// # Starting environment
    /// Fresh engine with memtable-only config — no data whatsoever.
    ///
    /// # Actions
    /// 1. Call `delete_range("aaa", "zzz")` on the empty engine.
    /// 2. Get `"anything"` and scan the full keyspace.
    ///
    /// # Expected behavior
    /// No error or panic; get returns `None` and scan returns empty.
    /// Range-deleting on an empty database is a safe no-op.
    #[test]
    fn memtable__range_delete_on_empty_db() {
        let dir = TempDir::new().unwrap();
        let engine = Engine::open(dir.path(), memtable_only_config()).unwrap();

        // Should not panic or error
        engine
            .delete_range(b"aaa".to_vec(), b"zzz".to_vec())
            .unwrap();
        assert_eq!(
            engine.get(b"anything".to_vec()).unwrap(),
            None,
            "empty DB returns None"
        );

        // Scan should be empty
        let results = collect_scan(&engine, b"\x00", b"\xff");
        assert!(results.is_empty(), "empty DB scan should return nothing");
    }

    /// # Scenario
    /// Issue a point delete on a completely empty database.
    ///
    /// # Starting environment
    /// Fresh engine with memtable-only config — no data.
    ///
    /// # Actions
    /// 1. Delete `"nonexistent"` (never inserted).
    /// 2. Get `"nonexistent"`.
    ///
    /// # Expected behavior
    /// No error; get returns `None`. Point-deleting on an empty database is safe.
    #[test]
    fn memtable__delete_on_empty_db() {
        let dir = TempDir::new().unwrap();
        let engine = Engine::open(dir.path(), memtable_only_config()).unwrap();

        engine.delete(b"nonexistent".to_vec()).unwrap();
        assert_eq!(engine.get(b"nonexistent".to_vec()).unwrap(), None);
    }

    // ================================================================
    // Scan where all keys in range are deleted
    // ================================================================

    /// # Scenario
    /// Scan a range where every key has been deleted via range-delete.
    ///
    /// # Starting environment
    /// Engine with 10 keys (`key_0000`..`key_0009`) inserted.
    ///
    /// # Actions
    /// 1. Range-delete `["key_0000", "key_9999")` — covers all 10 keys.
    /// 2. Scan `["key_", "key_\xff")`.
    ///
    /// # Expected behavior
    /// Scan returns an empty result — all keys in the scan range have been
    /// range-deleted, and tombstones correctly suppress them.
    #[test]
    fn memtable__scan_all_keys_deleted() {
        let dir = TempDir::new().unwrap();
        let engine = Engine::open(dir.path(), memtable_only_config()).unwrap();

        for i in 0..10u32 {
            engine
                .put(
                    format!("key_{:04}", i).into_bytes(),
                    format!("val_{:04}", i).into_bytes(),
                )
                .unwrap();
        }
        // Delete everything via range
        engine
            .delete_range(b"key_0000".to_vec(), b"key_9999".to_vec())
            .unwrap();

        let results = collect_scan(&engine, b"key_", b"key_\xff");
        assert!(
            results.is_empty(),
            "scan should return empty when all keys in range are deleted"
        );
    }
}