Skip to main content

qdb_rs/
storage.rs

1//! Storage backends for QDB
2
3use crate::entry::{Entry, Key, Value};
4use crate::error::{QdbError, Result};
5use dashmap::DashMap;
6use parking_lot::RwLock;
7use qft::{QftBuilder, QftFile};
8use std::collections::HashMap;
9use std::fs::{self, File};
10use std::io::{BufReader, BufWriter, Read, Write};
11use std::path::{Path, PathBuf};
12use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
13use std::sync::Arc;
14
15// =============================================================================
16// Backend Trait
17// =============================================================================
18
19/// Storage backend trait for QDB
20pub trait Backend: Send + Sync {
21    /// Get an entry by key
22    fn get(&self, key: &Key) -> Result<Option<Entry>>;
23
24    /// Put an entry
25    fn put(&self, entry: Entry) -> Result<()>;
26
27    /// Delete an entry by key
28    fn delete(&self, key: &Key) -> Result<bool>;
29
30    /// Check if a key exists
31    fn exists(&self, key: &Key) -> Result<bool>;
32
33    /// Get all keys (for iteration)
34    fn keys(&self) -> Result<Vec<Key>>;
35
36    /// Get the number of entries
37    fn len(&self) -> Result<usize>;
38
39    /// Check if empty
40    fn is_empty(&self) -> Result<bool> {
41        Ok(self.len()? == 0)
42    }
43
44    /// Flush any pending writes
45    fn flush(&self) -> Result<()>;
46
47    /// Close the backend
48    fn close(&self) -> Result<()>;
49}
50
51// =============================================================================
52// Memory Backend
53// =============================================================================
54
55/// In-memory storage backend using DashMap for concurrent access
56pub struct MemoryBackend {
57    data: DashMap<Vec<u8>, Entry>,
58    closed: AtomicBool,
59}
60
61impl MemoryBackend {
62    /// Create a new memory backend
63    pub fn new() -> Self {
64        Self {
65            data: DashMap::new(),
66            closed: AtomicBool::new(false),
67        }
68    }
69
70    fn check_closed(&self) -> Result<()> {
71        if self.closed.load(Ordering::Acquire) {
72            return Err(QdbError::Closed);
73        }
74        Ok(())
75    }
76}
77
78impl Default for MemoryBackend {
79    fn default() -> Self {
80        Self::new()
81    }
82}
83
84impl Backend for MemoryBackend {
85    fn get(&self, key: &Key) -> Result<Option<Entry>> {
86        self.check_closed()?;
87        let key_bytes = key.to_bytes();
88        Ok(self.data.get(&key_bytes).map(|e| e.value().clone()))
89    }
90
91    fn put(&self, entry: Entry) -> Result<()> {
92        self.check_closed()?;
93        let key_bytes = entry.key.to_bytes();
94        self.data.insert(key_bytes, entry);
95        Ok(())
96    }
97
98    fn delete(&self, key: &Key) -> Result<bool> {
99        self.check_closed()?;
100        let key_bytes = key.to_bytes();
101        Ok(self.data.remove(&key_bytes).is_some())
102    }
103
104    fn exists(&self, key: &Key) -> Result<bool> {
105        self.check_closed()?;
106        let key_bytes = key.to_bytes();
107        Ok(self.data.contains_key(&key_bytes))
108    }
109
110    fn keys(&self) -> Result<Vec<Key>> {
111        self.check_closed()?;
112        Ok(self.data.iter().map(|e| e.value().key.clone()).collect())
113    }
114
115    fn len(&self) -> Result<usize> {
116        self.check_closed()?;
117        Ok(self.data.len())
118    }
119
120    fn flush(&self) -> Result<()> {
121        self.check_closed()?;
122        Ok(()) // No-op for memory backend
123    }
124
125    fn close(&self) -> Result<()> {
126        self.closed.store(true, Ordering::Release);
127        self.data.clear();
128        Ok(())
129    }
130}
131
132// =============================================================================
133// Disk Backend
134// =============================================================================
135
136/// Disk-based storage backend using QFT files
137pub struct DiskBackend {
138    path: PathBuf,
139    index: RwLock<HashMap<Vec<u8>, PathBuf>>,
140    closed: AtomicBool,
141    entry_count: AtomicU64,
142}
143
144impl DiskBackend {
145    /// Create or open a disk backend at the given path
146    pub fn open(path: impl AsRef<Path>) -> Result<Self> {
147        let path = path.as_ref().to_path_buf();
148
149        // Create directory if it doesn't exist
150        fs::create_dir_all(&path)?;
151
152        let mut index = HashMap::new();
153        let mut count = 0u64;
154
155        // Scan existing files to build index
156        if let Ok(entries) = fs::read_dir(&path) {
157            for entry in entries.flatten() {
158                let file_path = entry.path();
159                if file_path.extension().map_or(false, |e| e == "qdb") {
160                    if let Ok(key_bytes) = Self::read_key_from_file(&file_path) {
161                        index.insert(key_bytes, file_path);
162                        count += 1;
163                    }
164                }
165            }
166        }
167
168        Ok(Self {
169            path,
170            index: RwLock::new(index),
171            closed: AtomicBool::new(false),
172            entry_count: AtomicU64::new(count),
173        })
174    }
175
176    fn check_closed(&self) -> Result<()> {
177        if self.closed.load(Ordering::Acquire) {
178            return Err(QdbError::Closed);
179        }
180        Ok(())
181    }
182
183    fn key_to_filename(key: &Key) -> String {
184        let hash = key.hash();
185        format!("{}.qdb", hex::encode(&hash[..16]))
186    }
187
188    fn read_key_from_file(path: &Path) -> Result<Vec<u8>> {
189        let file = File::open(path)?;
190        let mut reader = BufReader::new(file);
191
192        // Read header
193        let mut magic = [0u8; 4];
194        reader.read_exact(&mut magic)?;
195        if &magic != b"QDB\x01" {
196            return Err(QdbError::BackendError("Invalid QDB file".to_string()));
197        }
198
199        // Read key length
200        let mut key_len_bytes = [0u8; 4];
201        reader.read_exact(&mut key_len_bytes)?;
202        let key_len = u32::from_le_bytes(key_len_bytes) as usize;
203
204        // Read key
205        let mut key_bytes = vec![0u8; key_len];
206        reader.read_exact(&mut key_bytes)?;
207
208        Ok(key_bytes)
209    }
210
211    fn write_entry_to_file(path: &Path, entry: &Entry) -> Result<()> {
212        let file = File::create(path)?;
213        let mut writer = BufWriter::new(file);
214
215        // Magic number
216        writer.write_all(b"QDB\x01")?;
217
218        // Key
219        let key_bytes = entry.key.to_bytes();
220        writer.write_all(&(key_bytes.len() as u32).to_le_bytes())?;
221        writer.write_all(&key_bytes)?;
222
223        // Entry metadata
224        let metadata_json = serde_json::to_vec(&EntryMetadata {
225            entry_type: entry.entry_type,
226            created_at: entry.created_at,
227            modified_at: entry.modified_at,
228            version: entry.version,
229            metadata: entry.metadata.clone(),
230        })?;
231        writer.write_all(&(metadata_json.len() as u32).to_le_bytes())?;
232        writer.write_all(&metadata_json)?;
233
234        // Value
235        let value_bytes = entry.value.to_bytes()?;
236        writer.write_all(&(value_bytes.len() as u64).to_le_bytes())?;
237        writer.write_all(&value_bytes)?;
238
239        // Checksum
240        let mut hasher = blake3::Hasher::new();
241        hasher.update(&key_bytes);
242        hasher.update(&metadata_json);
243        hasher.update(&value_bytes);
244        let checksum = hasher.finalize();
245        writer.write_all(checksum.as_bytes())?;
246
247        writer.flush()?;
248        Ok(())
249    }
250
251    fn read_entry_from_file(path: &Path) -> Result<Entry> {
252        let file = File::open(path)?;
253        let mut reader = BufReader::new(file);
254
255        // Magic number
256        let mut magic = [0u8; 4];
257        reader.read_exact(&mut magic)?;
258        if &magic != b"QDB\x01" {
259            return Err(QdbError::BackendError("Invalid QDB file".to_string()));
260        }
261
262        // Key
263        let mut key_len_bytes = [0u8; 4];
264        reader.read_exact(&mut key_len_bytes)?;
265        let key_len = u32::from_le_bytes(key_len_bytes) as usize;
266        let mut key_bytes = vec![0u8; key_len];
267        reader.read_exact(&mut key_bytes)?;
268
269        // Entry metadata
270        let mut meta_len_bytes = [0u8; 4];
271        reader.read_exact(&mut meta_len_bytes)?;
272        let meta_len = u32::from_le_bytes(meta_len_bytes) as usize;
273        let mut meta_bytes = vec![0u8; meta_len];
274        reader.read_exact(&mut meta_bytes)?;
275
276        // Value
277        let mut value_len_bytes = [0u8; 8];
278        reader.read_exact(&mut value_len_bytes)?;
279        let value_len = u64::from_le_bytes(value_len_bytes) as usize;
280        let mut value_bytes = vec![0u8; value_len];
281        reader.read_exact(&mut value_bytes)?;
282
283        // Checksum
284        let mut stored_checksum = [0u8; 32];
285        reader.read_exact(&mut stored_checksum)?;
286
287        // Verify checksum
288        let mut hasher = blake3::Hasher::new();
289        hasher.update(&key_bytes);
290        hasher.update(&meta_bytes);
291        hasher.update(&value_bytes);
292        let computed_checksum = *hasher.finalize().as_bytes();
293
294        if stored_checksum != computed_checksum {
295            return Err(QdbError::ChecksumMismatch("File corrupted".to_string()));
296        }
297
298        // Deserialize
299        let key = deserialize_key(&key_bytes)?;
300        let metadata: EntryMetadata = serde_json::from_slice(&meta_bytes)?;
301        let value = deserialize_value(&value_bytes, metadata.entry_type)?;
302
303        Ok(Entry {
304            key,
305            value,
306            entry_type: metadata.entry_type,
307            created_at: metadata.created_at,
308            modified_at: metadata.modified_at,
309            version: metadata.version,
310            metadata: metadata.metadata,
311            checksum: Some(stored_checksum),
312        })
313    }
314}
315
316impl Backend for DiskBackend {
317    fn get(&self, key: &Key) -> Result<Option<Entry>> {
318        self.check_closed()?;
319        let key_bytes = key.to_bytes();
320        let index = self.index.read();
321
322        if let Some(path) = index.get(&key_bytes) {
323            Ok(Some(Self::read_entry_from_file(path)?))
324        } else {
325            Ok(None)
326        }
327    }
328
329    fn put(&self, entry: Entry) -> Result<()> {
330        self.check_closed()?;
331        let key_bytes = entry.key.to_bytes();
332        let filename = Self::key_to_filename(&entry.key);
333        let file_path = self.path.join(&filename);
334
335        let is_new = {
336            let index = self.index.read();
337            !index.contains_key(&key_bytes)
338        };
339
340        Self::write_entry_to_file(&file_path, &entry)?;
341
342        {
343            let mut index = self.index.write();
344            index.insert(key_bytes, file_path);
345        }
346
347        if is_new {
348            self.entry_count.fetch_add(1, Ordering::Relaxed);
349        }
350
351        Ok(())
352    }
353
354    fn delete(&self, key: &Key) -> Result<bool> {
355        self.check_closed()?;
356        let key_bytes = key.to_bytes();
357
358        let path = {
359            let mut index = self.index.write();
360            index.remove(&key_bytes)
361        };
362
363        if let Some(path) = path {
364            fs::remove_file(path)?;
365            self.entry_count.fetch_sub(1, Ordering::Relaxed);
366            Ok(true)
367        } else {
368            Ok(false)
369        }
370    }
371
372    fn exists(&self, key: &Key) -> Result<bool> {
373        self.check_closed()?;
374        let key_bytes = key.to_bytes();
375        let index = self.index.read();
376        Ok(index.contains_key(&key_bytes))
377    }
378
379    fn keys(&self) -> Result<Vec<Key>> {
380        self.check_closed()?;
381        let index = self.index.read();
382        let mut keys = Vec::with_capacity(index.len());
383
384        for path in index.values() {
385            if let Ok(entry) = Self::read_entry_from_file(path) {
386                keys.push(entry.key);
387            }
388        }
389
390        Ok(keys)
391    }
392
393    fn len(&self) -> Result<usize> {
394        self.check_closed()?;
395        Ok(self.entry_count.load(Ordering::Relaxed) as usize)
396    }
397
398    fn flush(&self) -> Result<()> {
399        self.check_closed()?;
400        Ok(()) // Files are flushed on write
401    }
402
403    fn close(&self) -> Result<()> {
404        self.closed.store(true, Ordering::Release);
405        Ok(())
406    }
407}
408
409// =============================================================================
410// QDB - Main Database Interface
411// =============================================================================
412
413/// Quantum Database - main interface
414pub struct QDB<B: Backend = MemoryBackend> {
415    backend: Arc<B>,
416    config: QdbConfig,
417    stats: QdbStats,
418}
419
420/// Radiation hardening mode for space applications
421#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
422pub enum RadiationMode {
423    /// Standard mode - no radiation hardening
424    #[default]
425    Standard,
426    /// Space hardened - SEU mitigation enabled
427    SpaceHardened,
428    /// Maximum protection - TMR + ECC + scrubbing
429    MaxProtection,
430}
431
432/// QDB configuration
433#[derive(Debug, Clone)]
434pub struct QdbConfig {
435    /// Default bond dimension for quantum states
436    pub default_bond_dimension: usize,
437    /// Enable Golay error correction
438    pub golay_enabled: bool,
439    /// Truncation threshold for MPS
440    pub truncation_threshold: f64,
441    /// Maximum entry size in bytes
442    pub max_entry_size: usize,
443    /// Read-only mode
444    pub read_only: bool,
445    /// Radiation hardening mode
446    pub radiation_mode: RadiationMode,
447    /// Enable triple modular redundancy for critical metadata
448    pub triple_redundancy: bool,
449    /// Enable ML-KEM post-quantum signatures
450    pub post_quantum_signing: bool,
451}
452
453impl Default for QdbConfig {
454    fn default() -> Self {
455        Self {
456            default_bond_dimension: 64,
457            golay_enabled: true,
458            truncation_threshold: 1e-10,
459            max_entry_size: 1024 * 1024 * 100, // 100 MB
460            read_only: false,
461            radiation_mode: RadiationMode::Standard,
462            triple_redundancy: false,
463            post_quantum_signing: false,
464        }
465    }
466}
467
468impl QdbConfig {
469    /// Create a space-hardened configuration
470    pub fn space_hardened() -> Self {
471        Self {
472            golay_enabled: true,
473            radiation_mode: RadiationMode::SpaceHardened,
474            triple_redundancy: true,
475            ..Default::default()
476        }
477    }
478
479    /// Create a maximum protection configuration
480    pub fn max_protection() -> Self {
481        Self {
482            golay_enabled: true,
483            radiation_mode: RadiationMode::MaxProtection,
484            triple_redundancy: true,
485            post_quantum_signing: true,
486            ..Default::default()
487        }
488    }
489}
490
491/// Database statistics
492#[derive(Debug, Default)]
493pub struct QdbStats {
494    pub reads: AtomicU64,
495    pub writes: AtomicU64,
496    pub deletes: AtomicU64,
497    pub cache_hits: AtomicU64,
498    pub cache_misses: AtomicU64,
499}
500
501impl<B: Backend> QDB<B> {
502    /// Create a new QDB with the given backend
503    pub fn new(backend: B) -> Self {
504        Self {
505            backend: Arc::new(backend),
506            config: QdbConfig::default(),
507            stats: QdbStats::default(),
508        }
509    }
510
511    /// Create with custom configuration
512    pub fn with_config(backend: B, config: QdbConfig) -> Self {
513        Self {
514            backend: Arc::new(backend),
515            config,
516            stats: QdbStats::default(),
517        }
518    }
519
520    /// Get the configuration
521    pub fn config(&self) -> &QdbConfig {
522        &self.config
523    }
524
525    /// Get statistics
526    pub fn stats(&self) -> &QdbStats {
527        &self.stats
528    }
529
530    /// Get a reference to the backend for direct queries
531    pub fn backend(&self) -> &B {
532        &self.backend
533    }
534
535    // =========================================================================
536    // Key-Value Operations
537    // =========================================================================
538
539    /// Get a value by key
540    pub fn get(&self, key: impl Into<Key>) -> Result<Option<Value>> {
541        self.stats.reads.fetch_add(1, Ordering::Relaxed);
542        let entry = self.backend.get(&key.into())?;
543        Ok(entry.map(|e| e.value))
544    }
545
546    /// Get a full entry by key
547    pub fn get_entry(&self, key: impl Into<Key>) -> Result<Option<Entry>> {
548        self.stats.reads.fetch_add(1, Ordering::Relaxed);
549        self.backend.get(&key.into())
550    }
551
552    /// Put a key-value pair
553    pub fn put(&self, key: impl Into<Key>, value: impl Into<Value>) -> Result<()> {
554        if self.config.read_only {
555            return Err(QdbError::ReadOnly);
556        }
557
558        self.stats.writes.fetch_add(1, Ordering::Relaxed);
559        let entry = Entry::new(key.into(), value.into());
560
561        if entry.size_bytes() > self.config.max_entry_size {
562            return Err(QdbError::CapacityExceeded(format!(
563                "Entry size {} exceeds maximum {}",
564                entry.size_bytes(),
565                self.config.max_entry_size
566            )));
567        }
568
569        self.backend.put(entry)
570    }
571
572    /// Put a full entry
573    pub fn put_entry(&self, entry: Entry) -> Result<()> {
574        if self.config.read_only {
575            return Err(QdbError::ReadOnly);
576        }
577
578        self.stats.writes.fetch_add(1, Ordering::Relaxed);
579        self.backend.put(entry)
580    }
581
582    /// Delete a key
583    pub fn delete(&self, key: impl Into<Key>) -> Result<bool> {
584        if self.config.read_only {
585            return Err(QdbError::ReadOnly);
586        }
587
588        self.stats.deletes.fetch_add(1, Ordering::Relaxed);
589        self.backend.delete(&key.into())
590    }
591
592    /// Check if a key exists
593    pub fn exists(&self, key: impl Into<Key>) -> Result<bool> {
594        self.backend.exists(&key.into())
595    }
596
597    /// Get all keys
598    pub fn keys(&self) -> Result<Vec<Key>> {
599        self.backend.keys()
600    }
601
602    /// Get the number of entries
603    pub fn len(&self) -> Result<usize> {
604        self.backend.len()
605    }
606
607    /// Check if empty
608    pub fn is_empty(&self) -> Result<bool> {
609        self.backend.is_empty()
610    }
611
612    // =========================================================================
613    // Quantum State Operations
614    // =========================================================================
615
616    /// Store a quantum state
617    pub fn put_quantum(&self, key: impl Into<Key>, qft: QftFile) -> Result<()> {
618        if self.config.read_only {
619            return Err(QdbError::ReadOnly);
620        }
621
622        self.stats.writes.fetch_add(1, Ordering::Relaxed);
623        let entry = Entry::quantum(key.into(), qft);
624        self.backend.put(entry)
625    }
626
627    /// Get a quantum state
628    pub fn get_quantum(&self, key: impl Into<Key>) -> Result<Option<QftFile>> {
629        self.stats.reads.fetch_add(1, Ordering::Relaxed);
630        let entry = self.backend.get(&key.into())?;
631
632        match entry {
633            Some(e) => match e.value {
634                Value::Quantum(qft) => Ok(Some(*qft)),
635                _ => Err(QdbError::ValueError(
636                    "Entry is not a quantum state".to_string(),
637                )),
638            },
639            None => Ok(None),
640        }
641    }
642
643    /// Create and store a new quantum state
644    pub fn create_quantum_state(
645        &self,
646        key: impl Into<Key>,
647        num_qubits: usize,
648    ) -> Result<()> {
649        let qft = QftBuilder::new(num_qubits)
650            .bond_dimension(self.config.default_bond_dimension)
651            .golay(self.config.golay_enabled)
652            .truncation_threshold(self.config.truncation_threshold)
653            .build()?;
654
655        self.put_quantum(key, qft)
656    }
657
658    // =========================================================================
659    // Batch Operations
660    // =========================================================================
661
662    /// Put multiple entries
663    pub fn put_batch(&self, entries: Vec<Entry>) -> Result<()> {
664        if self.config.read_only {
665            return Err(QdbError::ReadOnly);
666        }
667
668        for entry in entries {
669            self.backend.put(entry)?;
670            self.stats.writes.fetch_add(1, Ordering::Relaxed);
671        }
672        Ok(())
673    }
674
675    /// Get multiple values by keys
676    pub fn get_batch(&self, keys: Vec<Key>) -> Result<Vec<Option<Value>>> {
677        let mut results = Vec::with_capacity(keys.len());
678        for key in keys {
679            self.stats.reads.fetch_add(1, Ordering::Relaxed);
680            let entry = self.backend.get(&key)?;
681            results.push(entry.map(|e| e.value));
682        }
683        Ok(results)
684    }
685
686    // =========================================================================
687    // Lifecycle
688    // =========================================================================
689
690    /// Flush pending writes
691    pub fn flush(&self) -> Result<()> {
692        self.backend.flush()
693    }
694
695    /// Close the database
696    pub fn close(&self) -> Result<()> {
697        self.backend.close()
698    }
699}
700
701// =============================================================================
702// Helper Types and Functions
703// =============================================================================
704
705use serde::{Serialize, Deserialize};
706
707#[derive(Debug, Serialize, Deserialize)]
708struct EntryMetadata {
709    entry_type: crate::entry::EntryType,
710    created_at: u64,
711    modified_at: u64,
712    version: u64,
713    metadata: HashMap<String, String>,
714}
715
716fn deserialize_key(bytes: &[u8]) -> Result<Key> {
717    if bytes.is_empty() {
718        return Err(QdbError::InvalidKey("Empty key".to_string()));
719    }
720
721    match bytes[0] {
722        0 => Ok(Key::String(
723            String::from_utf8(bytes[1..].to_vec())
724                .map_err(|e| QdbError::InvalidKey(e.to_string()))?,
725        )),
726        1 => {
727            if bytes.len() < 9 {
728                return Err(QdbError::InvalidKey("Invalid integer key".to_string()));
729            }
730            let i = i64::from_le_bytes(bytes[1..9].try_into().unwrap());
731            Ok(Key::Integer(i))
732        }
733        2 => {
734            let mut parts = Vec::new();
735            let mut offset = 1;
736            while offset < bytes.len() {
737                if offset + 4 > bytes.len() {
738                    break;
739                }
740                let part_len =
741                    u32::from_le_bytes(bytes[offset..offset + 4].try_into().unwrap()) as usize;
742                offset += 4;
743                if offset + part_len > bytes.len() {
744                    break;
745                }
746                let part = deserialize_key(&bytes[offset..offset + part_len])?;
747                parts.push(part);
748                offset += part_len;
749            }
750            Ok(Key::Composite(parts))
751        }
752        3 => Ok(Key::Binary(bytes[1..].to_vec())),
753        _ => Err(QdbError::InvalidKey(format!(
754            "Unknown key type: {}",
755            bytes[0]
756        ))),
757    }
758}
759
760fn deserialize_value(bytes: &[u8], entry_type: crate::entry::EntryType) -> Result<Value> {
761    match entry_type {
762        crate::entry::EntryType::QuantumState => {
763            let qft = QftFile::from_bytes(bytes)?;
764            Ok(Value::Quantum(Box::new(qft)))
765        }
766        _ => {
767            let value: Value = serde_json::from_slice(bytes)?;
768            Ok(value)
769        }
770    }
771}
772
773mod hex {
774    pub fn encode(data: &[u8]) -> String {
775        data.iter().map(|b| format!("{:02x}", b)).collect()
776    }
777}
778
779#[cfg(test)]
780mod tests {
781    use super::*;
782
783    #[test]
784    fn test_memory_backend() {
785        let backend = MemoryBackend::new();
786        let db = QDB::new(backend);
787
788        db.put("key1", "value1").unwrap();
789        assert!(db.exists("key1").unwrap());
790
791        let value = db.get("key1").unwrap();
792        assert!(matches!(value, Some(Value::String(s)) if s == "value1"));
793
794        db.delete("key1").unwrap();
795        assert!(!db.exists("key1").unwrap());
796    }
797
798    #[test]
799    fn test_quantum_storage() {
800        let backend = MemoryBackend::new();
801        let db = QDB::new(backend);
802
803        let qft = QftFile::new(4).unwrap();
804        db.put_quantum("quantum_state", qft).unwrap();
805
806        let loaded = db.get_quantum("quantum_state").unwrap().unwrap();
807        assert_eq!(loaded.num_qubits(), 4);
808    }
809
810    #[test]
811    fn test_batch_operations() {
812        let backend = MemoryBackend::new();
813        let db = QDB::new(backend);
814
815        let entries = vec![
816            Entry::new(Key::string("k1"), Value::Integer(1)),
817            Entry::new(Key::string("k2"), Value::Integer(2)),
818            Entry::new(Key::string("k3"), Value::Integer(3)),
819        ];
820
821        db.put_batch(entries).unwrap();
822        assert_eq!(db.len().unwrap(), 3);
823
824        let keys = vec![Key::string("k1"), Key::string("k2"), Key::string("k3")];
825        let values = db.get_batch(keys).unwrap();
826        assert_eq!(values.len(), 3);
827    }
828}