nervusdb-core 1.0.3

Embeddable Graph Database Core (Rust)
Documentation
//! Database migration logic from v1.x to v2.0
//!
//! Converts .synapsedb files to redb format with:
//! - Dictionary migration
//! - Triple migration
//! - Property migration (JSON → FlexBuffers)
//! - SHA256 integrity verification

use std::path::Path;
use std::time::Instant;

use sha2::{Digest, Sha256};

use crate::migration::legacy_reader::LegacyDatabase;
use crate::{Database, Options, Result};

/// Migration statistics
#[derive(Debug, Clone)]
pub struct MigrationStats {
    pub dictionary_entries: usize,
    pub triples_migrated: usize,
    pub properties_migrated: usize,
    pub duration_secs: f64,
    pub source_sha256: String,
    pub target_sha256: String,
}

/// Migrate a legacy .synapsedb database to redb format
///
/// # Arguments
/// * `source_path` - Path to the legacy .synapsedb file
/// * `target_path` - Path for the new .redb database
/// * `verify` - Whether to perform SHA256 verification
///
/// # Returns
/// Migration statistics including checksums and counts
pub fn migrate_database<P: AsRef<Path>, Q: AsRef<Path>>(
    source_path: P,
    target_path: Q,
    verify: bool,
) -> Result<MigrationStats> {
    let start_time = Instant::now();

    // Open legacy database
    let mut legacy_db = LegacyDatabase::open(&source_path)?;

    // Calculate source checksum if verification requested
    let source_sha256 = if verify {
        calculate_file_sha256(&source_path)?
    } else {
        String::new()
    };

    // Create new redb database
    let mut new_db = Database::open(Options::new(target_path.as_ref()))?;

    // Migrate dictionary
    let dictionary = legacy_db.dictionary();
    let dictionary_entries = dictionary.len();

    for (id, value) in dictionary.iter().enumerate() {
        // Intern strings in new database
        let new_id = new_db.intern(value)?;
        // Verify ID consistency
        if new_id != id as u64 {
            return Err(crate::Error::Other(format!(
                "Dictionary ID mismatch: expected {}, got {}",
                id, new_id
            )));
        }
    }

    // Migrate triples
    let triples = legacy_db.read_triples()?;
    let triples_migrated = triples.len();

    // Use batch insert for performance
    new_db.batch_insert(&triples)?;

    // Migrate properties (if any)
    let properties_data = legacy_db.read_properties()?;
    let properties_migrated = if !properties_data.is_empty() {
        // TODO: Parse and migrate property data
        // For now, we assume properties section is empty or will be handled separately
        0
    } else {
        0
    };

    // Calculate target checksum if verification requested
    let target_sha256 = if verify {
        // Flush and close database before calculating checksum
        drop(new_db);
        calculate_file_sha256(&target_path)?
    } else {
        String::new()
    };

    let duration_secs = start_time.elapsed().as_secs_f64();

    Ok(MigrationStats {
        dictionary_entries,
        triples_migrated,
        properties_migrated,
        duration_secs,
        source_sha256,
        target_sha256,
    })
}

/// Calculate SHA256 checksum of a file
fn calculate_file_sha256<P: AsRef<Path>>(path: P) -> Result<String> {
    use std::fs::File;
    use std::io::Read;

    let mut file = File::open(path)?;
    let mut hasher = Sha256::new();
    let mut buffer = vec![0u8; 8192];

    loop {
        let n = file.read(&mut buffer)?;
        if n == 0 {
            break;
        }
        hasher.update(&buffer[..n]);
    }

    Ok(format!("{:x}", hasher.finalize()))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_sha256_calculation() {
        use std::io::Write;
        use tempfile::NamedTempFile;

        let mut file = NamedTempFile::new().unwrap();
        file.write_all(b"hello world").unwrap();
        file.flush().unwrap();

        let checksum = calculate_file_sha256(file.path()).unwrap();
        // SHA256 of "hello world"
        assert_eq!(
            checksum,
            "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
        );
    }
}