Skip to main content

noxu_dbi/
database_impl.rs

1//! Internal database implementation.
2//!
3
4use byteorder::{BigEndian, ReadBytesExt, WriteBytesExt};
5use noxu_tree::{KeyComparatorFn, Tree};
6use std::sync::atomic::{AtomicBool, AtomicI64, AtomicU64, Ordering};
7use std::sync::{Arc, RwLock};
8
9use crate::dup_key_data;
10use crate::throughput_stats::ThroughputStats;
11
12use crate::{DatabaseConfig, DatabaseId, DbType};
13
14/// Deletion processing states.
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16enum DeleteState {
17    NotDeleted,
18    DeletedCleanupInListHarvest,
19    DeletedCleanupLogHarvest,
20    Deleted,
21}
22
23/// Flag bits for persistent database properties.
24const DUPS_ENABLED: u8 = 0x01;
25const TEMPORARY_BIT: u8 = 0x02;
26const IS_REPLICATED_BIT: u8 = 0x04;
27const NOT_REPLICATED_BIT: u8 = 0x08;
28const PREFIXING_ENABLED: u8 = 0x10;
29
30/// The underlying object for a given database.
31///
32///
33pub struct DatabaseImpl {
34    /// Unique database ID.
35    id: DatabaseId,
36    /// Database name (user databases) or internal type name.
37    name: String,
38    /// Database type.
39    db_type: DbType,
40    /// Persistent flag bits.
41    flags: u8,
42    /// Delete processing state.
43    delete_state: DeleteState,
44    /// Whether this database is dirty (needs to be written to log).
45    dirty: AtomicBool,
46    /// Maximum number of entries in a B-tree node.
47    max_tree_entries_per_node: i32,
48    /// Number of open database handles (user handles referencing this db).
49    reference_count: AtomicI64,
50    /// Persistent B-tree root metadata (root LSN, serialized with the database
51    /// record in the ID database).  Populated from the log during recovery.
52    tree: Option<DatabaseTree>,
53    /// The in-memory B+tree backing cursor traversal (search, insert, delete).
54    ///
55    /// `None` only for read-only or freshly created databases before the first
56    /// write; otherwise always `Some`.  Populated either from recovery via
57    /// `set_recovered_tree()` or lazily on first write.
58    /// Wrapped in `Arc<RwLock<Tree>>` so the cleaner can share the same tree
59    /// instance for secondary-database LN liveness checks (X-7 fix).  All
60    /// cursor operations take a read guard; only setup calls need a write guard.
61    real_tree: Option<Arc<RwLock<Tree>>>,
62    /// Whether writes are deferred (not WAL-logged immediately).
63    ///
64    ///
65    /// When true, `log_ln_write()` skips WAL logging and returns NULL_LSN;
66    /// data is flushed to disk only at eviction or checkpoint.
67    deferred_write: bool,
68    /// Per-database entry count.
69    ///
70    /// Incremented on every new insert, decremented on every delete.
71    /// Shared (Arc) so that CursorImpl can update it without holding the
72    /// `DatabaseImpl` write lock — reads and writes are both O(1) atomics.
73    ///
74    /// `DatabaseImpl.count` (AtomicLong, updated in
75    /// `BIN.insertEntry` / `BIN.deleteEntry`).
76    entry_count: Arc<AtomicU64>,
77    /// Per-database operation throughput counters.
78    ///
79    /// Shared with every CursorImpl opened on this database so that insert,
80    /// search, update, delete and position operations can be counted on the
81    /// hot path without acquiring any mutex.
82    pub throughput: Arc<ThroughputStats>,
83}
84
85/// Persistent B-tree root metadata stored alongside the database record.
86///
87/// Holds the root LSN so that recovery can locate the tree root on disk.
88/// The live in-memory tree is `DatabaseImpl::real_tree`.
89///
90/// (the persistent `Tree` object stored as part
91/// of the database record).
92#[derive(Debug)]
93pub struct DatabaseTree {
94    /// Root LSN of the tree.
95    root_lsn: u64,
96}
97
98impl Default for DatabaseTree {
99    fn default() -> Self {
100        Self::new()
101    }
102}
103
104impl DatabaseTree {
105    pub fn new() -> Self {
106        DatabaseTree { root_lsn: noxu_util::NULL_LSN.as_u64() }
107    }
108    pub fn get_root_lsn(&self) -> u64 {
109        self.root_lsn
110    }
111    pub fn set_root_lsn(&mut self, lsn: u64) {
112        self.root_lsn = lsn;
113    }
114}
115
116impl DatabaseImpl {
117    /// Creates a new DatabaseImpl.
118    pub fn new(
119        id: DatabaseId,
120        name: String,
121        db_type: DbType,
122        config: &DatabaseConfig,
123    ) -> Self {
124        let mut flags = 0u8;
125        if config.sorted_duplicates {
126            flags |= DUPS_ENABLED;
127        }
128        if config.temporary {
129            flags |= TEMPORARY_BIT;
130        }
131        if config.key_prefixing {
132            flags |= PREFIXING_ENABLED;
133        }
134
135        let max_entries = config.node_max_entries as usize;
136        let real_tree = if config.sorted_duplicates {
137            // Sorted-dup databases store (key, data) as two-part composite keys.
138            // A custom comparator is required: pure lexicographic ordering fails
139            // when a shorter primary key is a byte-prefix of a longer key's data.
140            let dup_cmp: KeyComparatorFn = Arc::new(|a: &[u8], b: &[u8]| {
141                dup_key_data::cmp_two_part_keys(
142                    a,
143                    b,
144                    |x, y| x.cmp(y),
145                    |x, y| x.cmp(y),
146                )
147            });
148            Tree::new_with_comparator(id.id() as u64, max_entries, dup_cmp)
149        } else {
150            Tree::new(id.id() as u64, max_entries)
151        };
152        // Wire the DatabaseConfig.key_prefixing flag into the tree so the
153        // BIN prefix-compression path honours it (JE DatabaseImpl.getKeyPrefixing
154        // -> IN.computeKeyPrefix). Sorted-dup DBs use a custom comparator and
155        // bypass prefix compression regardless; for the default-comparator case
156        // this enables/disables prefixing per the config.
157        let mut real_tree = real_tree;
158        real_tree.set_key_prefixing(config.key_prefixing);
159        DatabaseImpl {
160            id,
161            name,
162            db_type,
163            flags,
164            delete_state: DeleteState::NotDeleted,
165            dirty: AtomicBool::new(false),
166            max_tree_entries_per_node: config.node_max_entries,
167            reference_count: AtomicI64::new(0),
168            tree: Some(DatabaseTree::new()),
169            real_tree: Some(Arc::new(RwLock::new(real_tree))),
170            deferred_write: config.deferred_write,
171            entry_count: Arc::new(AtomicU64::new(0)),
172            throughput: ThroughputStats::new(),
173        }
174    }
175
176    // Getters
177    pub fn get_id(&self) -> DatabaseId {
178        self.id
179    }
180    pub fn get_name(&self) -> &str {
181        &self.name
182    }
183    pub fn get_db_type(&self) -> DbType {
184        self.db_type
185    }
186
187    /// Returns true if this database uses deferred write mode.
188    ///
189    ///
190    pub fn is_deferred_write(&self) -> bool {
191        self.deferred_write
192    }
193
194    // Flag methods
195    pub fn get_sorted_duplicates(&self) -> bool {
196        self.flags & DUPS_ENABLED != 0
197    }
198
199    /// Whether all LNs in this DB are "immediately obsolete" — counted
200    /// obsolete at log-write time and ignorable by the cleaner (DBI-17).
201    ///
202    /// JE `DatabaseImpl.isLNImmediatelyObsolete`:
203    /// `sortedDuplicates && !btreePartialComparator &&
204    /// !duplicatePartialComparator`.  Noxu has no partial comparators, so
205    /// this reduces to `sortedDuplicates` (duplicate DBs store zero-length
206    /// LN data).  The predicate is implemented in full to match JE so the
207    /// comparator clauses can be added later without re-deriving the rule.
208    pub fn is_ln_immediately_obsolete(&self) -> bool {
209        self.get_sorted_duplicates()
210        // && !btree_partial_comparator && !duplicate_partial_comparator
211        // (always true: Noxu has no partial comparators)
212    }
213    pub fn is_temporary(&self) -> bool {
214        self.flags & TEMPORARY_BIT != 0
215    }
216    pub fn get_key_prefixing(&self) -> bool {
217        self.flags & PREFIXING_ENABLED != 0
218    }
219    pub fn is_replicated(&self) -> bool {
220        self.flags & IS_REPLICATED_BIT != 0
221    }
222
223    // Delete state
224    pub fn is_deleted(&self) -> bool {
225        self.delete_state == DeleteState::Deleted
226    }
227    pub fn is_deleting(&self) -> bool {
228        self.delete_state != DeleteState::NotDeleted
229    }
230    pub fn start_delete(&mut self) {
231        self.delete_state = DeleteState::DeletedCleanupInListHarvest;
232    }
233    pub fn finish_delete(&mut self) {
234        self.delete_state = DeleteState::Deleted;
235    }
236
237    // Dirty tracking
238    pub fn is_dirty(&self) -> bool {
239        self.dirty.load(Ordering::Relaxed)
240    }
241    pub fn set_dirty(&self) {
242        self.dirty.store(true, Ordering::Relaxed);
243    }
244    pub fn clear_dirty(&self) {
245        self.dirty.store(false, Ordering::Relaxed);
246    }
247
248    // Reference counting (for open handles)
249    pub fn increment_reference_count(&self) {
250        self.reference_count.fetch_add(1, Ordering::Relaxed);
251    }
252    pub fn decrement_reference_count(&self) {
253        self.reference_count.fetch_sub(1, Ordering::Relaxed);
254    }
255    pub fn reference_count(&self) -> i64 {
256        self.reference_count.load(Ordering::Relaxed)
257    }
258
259    // Entry count (O(1) atomic counter)
260    /// Returns the current entry count.
261    ///
262    /// In — reads an AtomicLong.
263    pub fn entry_count(&self) -> u64 {
264        self.entry_count.load(Ordering::Relaxed)
265    }
266
267    /// Increments the entry count by 1 (on new insert).
268    pub fn increment_entry_count(&self) {
269        self.entry_count.fetch_add(1, Ordering::Relaxed);
270    }
271
272    /// Decrements the entry count by 1 (on delete), saturating at zero.
273    pub fn decrement_entry_count(&self) {
274        // Use a compare-and-swap loop to avoid underflow.
275        loop {
276            let cur = self.entry_count.load(Ordering::Relaxed);
277            if cur == 0 {
278                break;
279            }
280            if self
281                .entry_count
282                .compare_exchange_weak(
283                    cur,
284                    cur - 1,
285                    Ordering::Relaxed,
286                    Ordering::Relaxed,
287                )
288                .is_ok()
289            {
290                break;
291            }
292        }
293    }
294
295    // Tree access (stub for LSN tracking)
296    pub fn get_tree(&self) -> Option<&DatabaseTree> {
297        self.tree.as_ref()
298    }
299    pub fn get_tree_mut(&mut self) -> Option<&mut DatabaseTree> {
300        self.tree.as_mut()
301    }
302
303    // Real B+tree access for cursor traversal and data operations.
304    /// Returns a read guard over the real B+tree.
305    ///
306    /// Returns `Option<RwLockReadGuard<'_, Tree>>` — the guard `Deref`s to
307    /// `&Tree`, so all existing cursor-code patterns (`tree.search(key)`,
308    /// `Self::get_data_from_tree(tree, key)`, etc.) continue to work without
309    /// modification through auto-deref coercion.
310    ///
311    /// Returns `None` if no tree is present or if the lock is poisoned.
312    ///
313    /// # X-7 fix
314    /// Use `get_real_tree_arc()` (below) to obtain the `Arc<RwLock<Tree>>`
315    /// for sharing with the cleaner's db-tree registry.
316    pub fn get_real_tree(
317        &self,
318    ) -> Option<std::sync::RwLockReadGuard<'_, Tree>> {
319        self.real_tree.as_ref()?.read().ok()
320    }
321
322    /// Returns a clone of the `Arc<RwLock<Tree>>` for sharing with the
323    /// cleaner's per-database tree registry (X-7 fix).
324    pub fn get_real_tree_arc(&self) -> Option<Arc<RwLock<Tree>>> {
325        self.real_tree.clone()
326    }
327
328    /// Sets the expiration time (absolute hours since Unix epoch) for the
329    /// BIN slot holding `key`.
330    ///
331    /// Returns `true` if the key was found and updated.
332    /// Delegates to `Tree::update_key_expiration()`.
333    pub fn update_key_expiration(
334        &self,
335        key: &[u8],
336        expiration_hours: u32,
337    ) -> bool {
338        self.real_tree
339            .as_ref()
340            .and_then(|arc| arc.read().ok())
341            .map(|t| t.update_key_expiration(key, expiration_hours))
342            .unwrap_or(false)
343    }
344
345    /// Collects structural B-tree statistics.
346    ///
347    /// Walks the full tree (O(n) in node count) and returns node counts
348    /// and maximum depth.  Implements `DatabaseImpl.getDbStats(fast=false)`.
349    ///
350    /// Returns `None` if this DatabaseImpl has no real tree (e.g. internal
351    /// metadata databases).
352    pub fn collect_btree_stats(&self) -> Option<noxu_tree::TreeStats> {
353        self.real_tree
354            .as_ref()
355            .and_then(|arc| arc.read().ok())
356            .map(|t| t.collect_stats())
357    }
358
359    /// Replace the real B+tree with a tree recovered from the log.
360    ///
361    /// Called by `EnvironmentImpl::open_database()` when a matching
362    /// `recovered_trees` entry exists (Approach B of P1b wiring).
363    pub fn set_recovered_tree(&mut self, mut tree: Tree) {
364        // Synchronise the in-memory entry_count counter from the recovered
365        // tree so that Database::count() returns the correct value after reopen.
366        let count = tree.count_entries();
367        self.entry_count.store(count, std::sync::atomic::Ordering::Relaxed);
368        // Transfer the key comparator from the current tree (if any) to the
369        // recovered tree — RecoveryManager builds trees without db-level config.
370        if let Some(ref current_arc) = self.real_tree
371            && let Ok(mut current) = current_arc.write()
372            && let Some(cmp) = current.take_comparator()
373        {
374            tree.set_comparator(cmp);
375        }
376        // Re-apply the key-prefixing flag to the recovered tree.  The
377        // recovered Tree is built by RecoveryManager with key_prefixing=false
378        // (JE default); without this the flag set in `new()` is lost on reopen
379        // and a key_prefixing=true DB silently disables prefix compression for
380        // all post-recovery inserts. (JE DatabaseImpl.getKeyPrefixing is read
381        // from persistent DB metadata, so it survives recovery.)
382        tree.set_key_prefixing(self.flags & PREFIXING_ENABLED != 0);
383        self.real_tree = Some(Arc::new(RwLock::new(tree)));
384    }
385
386    /// Wires the environment's shared memory-usage counter into this database's
387    /// tree so that BIN insertions/deletions update the Arbiter's budget.
388    ///
389    /// Must be called after `new()` in `EnvironmentImpl::open_database()`.
390    /// Also forwards the counter to the recovered tree (if any) so that
391    /// databases opened after recovery also track memory.
392    pub fn set_memory_counter(
393        &mut self,
394        counter: std::sync::Arc<std::sync::atomic::AtomicI64>,
395    ) {
396        if let Some(tree_arc) = self.real_tree.as_ref()
397            && let Ok(mut tree) = tree_arc.write()
398        {
399            tree.set_memory_counter(counter);
400        }
401    }
402
403    // Configuration
404    pub fn max_tree_entries_per_node(&self) -> i32 {
405        self.max_tree_entries_per_node
406    }
407
408    /// Serialization.
409    ///
410    pub fn log_size(&self) -> usize {
411        8 + // id
412        4 + self.name.len() + // name (length-prefixed)
413        1 + // flags
414        4 + // max entries
415        8 // root LSN
416    }
417
418    pub fn write_to_log(&self, buf: &mut Vec<u8>) -> std::io::Result<()> {
419        buf.write_i64::<BigEndian>(self.id.id())?;
420        buf.write_u32::<BigEndian>(self.name.len() as u32)?;
421        buf.extend_from_slice(self.name.as_bytes());
422        buf.write_u8(self.flags)?;
423        buf.write_i32::<BigEndian>(self.max_tree_entries_per_node)?;
424        let root_lsn = self
425            .tree
426            .as_ref()
427            .map_or(noxu_util::NULL_LSN.as_u64(), |t| t.root_lsn);
428        buf.write_u64::<BigEndian>(root_lsn)?;
429        Ok(())
430    }
431
432    pub fn read_from_log(buf: &[u8]) -> std::io::Result<Self> {
433        // Helper:
434        fn type_for_db_name(name: &str) -> DbType {
435            match name {
436                "_jeIdMap" | "_noxuIdMap" => DbType::Id,
437                "_jeNameMap" | "_noxuNameMap" => DbType::Name,
438                "_jeUtilization" | "_noxuUtilization" => DbType::Utilization,
439                _ => DbType::User,
440            }
441        }
442        use std::io::Cursor;
443
444        let mut cursor = Cursor::new(buf);
445        let id = cursor.read_i64::<BigEndian>()?;
446        let name_len = cursor.read_u32::<BigEndian>()? as usize;
447
448        // Read name bytes
449        let name_start = cursor.position() as usize;
450        let name_end = name_start + name_len;
451        if name_end > buf.len() {
452            return Err(std::io::Error::new(
453                std::io::ErrorKind::UnexpectedEof,
454                "Buffer too short for name",
455            ));
456        }
457        let name = String::from_utf8(buf[name_start..name_end].to_vec())
458            .map_err(|e| {
459                std::io::Error::new(std::io::ErrorKind::InvalidData, e)
460            })?;
461        cursor.set_position(name_end as u64);
462
463        let flags = cursor.read_u8()?;
464        let max_entries = cursor.read_i32::<BigEndian>()?;
465        let root_lsn = cursor.read_u64::<BigEndian>()?;
466
467        let db_type = type_for_db_name(&name);
468
469        let mut tree = DatabaseTree::new();
470        tree.root_lsn = root_lsn;
471
472        let real_tree = Tree::new(id as u64, max_entries as usize);
473        Ok(DatabaseImpl {
474            id: DatabaseId::new(id),
475            name,
476            db_type,
477            flags,
478            delete_state: DeleteState::NotDeleted,
479            dirty: AtomicBool::new(false),
480            max_tree_entries_per_node: max_entries,
481            reference_count: AtomicI64::new(0),
482            tree: Some(tree),
483            real_tree: Some(Arc::new(RwLock::new(real_tree))),
484            deferred_write: false, // not persisted in log record; set after open if needed
485            entry_count: Arc::new(AtomicU64::new(0)),
486            throughput: ThroughputStats::new(),
487        })
488    }
489}
490
491impl std::fmt::Debug for DatabaseImpl {
492    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
493        f.debug_struct("DatabaseImpl")
494            .field("id", &self.id)
495            .field("name", &self.name)
496            .field("db_type", &self.db_type)
497            .field("flags", &self.flags)
498            .field("delete_state", &self.delete_state)
499            .finish()
500    }
501}
502
503#[cfg(test)]
504#[expect(clippy::field_reassign_with_default)]
505mod tests {
506    use super::*;
507
508    fn make_config() -> DatabaseConfig {
509        DatabaseConfig::default()
510    }
511
512    #[test]
513    fn test_new_database() {
514        let config = make_config();
515        let db = DatabaseImpl::new(
516            DatabaseId::new(100),
517            "test_db".to_string(),
518            DbType::User,
519            &config,
520        );
521
522        assert_eq!(db.get_id(), DatabaseId::new(100));
523        assert_eq!(db.get_name(), "test_db");
524        assert_eq!(db.get_db_type(), DbType::User);
525        assert!(!db.is_deleted());
526        assert!(!db.is_deleting());
527        assert_eq!(db.reference_count(), 0);
528    }
529
530    #[test]
531    fn test_sorted_duplicates_flag() {
532        let mut config = DatabaseConfig::default();
533        config.sorted_duplicates = false;
534        let db1 = DatabaseImpl::new(
535            DatabaseId::new(1),
536            "db1".to_string(),
537            DbType::User,
538            &config,
539        );
540        assert!(!db1.get_sorted_duplicates());
541
542        config.sorted_duplicates = true;
543        let db2 = DatabaseImpl::new(
544            DatabaseId::new(2),
545            "db2".to_string(),
546            DbType::User,
547            &config,
548        );
549        assert!(db2.get_sorted_duplicates());
550    }
551
552    #[test]
553    fn test_temporary_flag() {
554        let mut config = DatabaseConfig::default();
555        config.temporary = false;
556        let db1 = DatabaseImpl::new(
557            DatabaseId::new(1),
558            "db1".to_string(),
559            DbType::User,
560            &config,
561        );
562        assert!(!db1.is_temporary());
563
564        config.temporary = true;
565        let db2 = DatabaseImpl::new(
566            DatabaseId::new(2),
567            "db2".to_string(),
568            DbType::User,
569            &config,
570        );
571        assert!(db2.is_temporary());
572    }
573
574    #[test]
575    fn test_key_prefixing_flag() {
576        let mut config = DatabaseConfig::default();
577        config.key_prefixing = false;
578        let db1 = DatabaseImpl::new(
579            DatabaseId::new(1),
580            "db1".to_string(),
581            DbType::User,
582            &config,
583        );
584        assert!(!db1.get_key_prefixing());
585
586        config.key_prefixing = true;
587        let db2 = DatabaseImpl::new(
588            DatabaseId::new(2),
589            "db2".to_string(),
590            DbType::User,
591            &config,
592        );
593        assert!(db2.get_key_prefixing());
594    }
595
596    #[test]
597    fn test_set_recovered_tree_preserves_key_prefixing() {
598        // GAP-5 regression: set_recovered_tree (the reopen/recovery path)
599        // must re-apply the key_prefixing flag to the recovered tree, which
600        // RecoveryManager builds with key_prefixing=false. Without this, a
601        // key_prefixing=true DB silently disables prefix compression after
602        // every crash/reopen.
603        let mut config = DatabaseConfig::default();
604        config.key_prefixing = true;
605        let mut db = DatabaseImpl::new(
606            DatabaseId::new(7),
607            "kp_recover".to_string(),
608            DbType::User,
609            &config,
610        );
611        // A freshly-recovered tree defaults to key_prefixing=false.
612        let recovered = Tree::new(7, 256);
613        assert!(!recovered.key_prefixing, "recovered tree starts false");
614        db.set_recovered_tree(recovered);
615        // After set_recovered_tree, the tree must honour the DB's flag.
616        let t = db.get_real_tree_arc().expect("real tree");
617        assert!(
618            t.read().unwrap().key_prefixing,
619            "GAP-5: set_recovered_tree must preserve key_prefixing=true"
620        );
621    }
622
623    #[test]
624    fn test_delete_state_transitions() {
625        let config = make_config();
626        let mut db = DatabaseImpl::new(
627            DatabaseId::new(1),
628            "db".to_string(),
629            DbType::User,
630            &config,
631        );
632
633        assert!(!db.is_deleted());
634        assert!(!db.is_deleting());
635
636        db.start_delete();
637        assert!(!db.is_deleted());
638        assert!(db.is_deleting());
639
640        db.finish_delete();
641        assert!(db.is_deleted());
642        assert!(db.is_deleting());
643    }
644
645    #[test]
646    fn test_dirty_tracking() {
647        let config = make_config();
648        let db = DatabaseImpl::new(
649            DatabaseId::new(1),
650            "db".to_string(),
651            DbType::User,
652            &config,
653        );
654
655        assert!(!db.is_dirty());
656
657        db.set_dirty();
658        assert!(db.is_dirty());
659
660        db.clear_dirty();
661        assert!(!db.is_dirty());
662    }
663
664    #[test]
665    fn test_reference_counting() {
666        let config = make_config();
667        let db = DatabaseImpl::new(
668            DatabaseId::new(1),
669            "db".to_string(),
670            DbType::User,
671            &config,
672        );
673
674        assert_eq!(db.reference_count(), 0);
675
676        db.increment_reference_count();
677        assert_eq!(db.reference_count(), 1);
678
679        db.increment_reference_count();
680        assert_eq!(db.reference_count(), 2);
681
682        db.decrement_reference_count();
683        assert_eq!(db.reference_count(), 1);
684
685        db.decrement_reference_count();
686        assert_eq!(db.reference_count(), 0);
687    }
688
689    #[test]
690    fn test_serialization_round_trip() {
691        let mut config = DatabaseConfig::default();
692        config.sorted_duplicates = true;
693        config.key_prefixing = true;
694        config.node_max_entries = 256;
695
696        let db = DatabaseImpl::new(
697            DatabaseId::new(42),
698            "my_database".to_string(),
699            DbType::User,
700            &config,
701        );
702
703        let mut buf = Vec::new();
704        db.write_to_log(&mut buf).unwrap();
705
706        let db2 = DatabaseImpl::read_from_log(&buf).unwrap();
707
708        assert_eq!(db2.get_id(), DatabaseId::new(42));
709        assert_eq!(db2.get_name(), "my_database");
710        assert!(db2.get_sorted_duplicates());
711        assert!(db2.get_key_prefixing());
712        assert_eq!(db2.max_tree_entries_per_node(), 256);
713    }
714
715    #[test]
716    fn test_tree_access() {
717        let config = make_config();
718        let mut db = DatabaseImpl::new(
719            DatabaseId::new(1),
720            "db".to_string(),
721            DbType::User,
722            &config,
723        );
724
725        // Default tree has NULL_LSN
726        {
727            let tree = db.get_tree().unwrap();
728            assert_eq!(tree.get_root_lsn(), noxu_util::NULL_LSN.as_u64());
729        }
730
731        // Set root LSN
732        {
733            let tree = db.get_tree_mut().unwrap();
734            tree.set_root_lsn(12345);
735        }
736
737        // Verify it was set
738        {
739            let tree = db.get_tree().unwrap();
740            assert_eq!(tree.get_root_lsn(), 12345);
741        }
742    }
743
744    #[test]
745    fn test_log_size() {
746        let config = make_config();
747        let db = DatabaseImpl::new(
748            DatabaseId::new(1),
749            "test".to_string(),
750            DbType::User,
751            &config,
752        );
753
754        let expected_size = 8 + 4 + 4 + 1 + 4 + 8; // id + name_len + "test" + flags + max_entries + root_lsn
755        assert_eq!(db.log_size(), expected_size);
756    }
757}