linkrs-storage 0.1.0

Storage engine with CSR and memory-mapped containers
Documentation
//! Compaction Integration Tests
//!
//! Tests the compaction mechanism which reclaims space from
//! deleted/obsolete MVCC versions of vertices and edges.
//!
//! NOTE: compaction requires persistent storage with WAL enabled.
//! After compaction, reads on the same instance return no data
//! (version_manager.clear() resets read_ts to 0). To verify
//! compaction results correctly we must save + checkpoint after
//! compact, then reopen the storage.

mod common;

use linkrs_core::types::{CompactConfig, VertexId};
use linkrs_storage::{StorageAdmin, StoragePersistenceOps, StorageReader, StorageWriter};

/// Compact and reopen helper: save+checkpoint, compact, save+checkpoint, reopen.
fn compact_and_reopen<F>(dir: &std::path::Path, fixup: F) -> linkrs_storage::GraphStorage
where
    F: FnOnce(&mut linkrs_storage::GraphStorage),
{
    let mut storage = common::create_persistent_storage(dir);
    common::setup_basic_schema(&mut storage);
    common::insert_test_data(&mut storage, "test_space");

    fixup(&mut storage);

    // Save + checkpoint before compact so we start from a clean WAL state
    storage.save_to_disk().unwrap();
    storage.create_checkpoint().unwrap();

    let compact_config = CompactConfig::with_fixed_ratio(true, 0.8);
    storage.compact(&compact_config).unwrap();

    storage.save_to_disk().unwrap();
    storage.create_checkpoint().unwrap();
    drop(storage);

    common::open_persistent_storage(dir)
}

/// After deleting a vertex and compacting, the deleted vertex should be gone
/// and surviving vertices should remain.
#[test]
fn test_compact_reclaims_deleted_vertex_space() {
    let temp_dir = common::create_test_workdir();
    let dir = temp_dir.path();

    let storage = compact_and_reopen(dir, |s| {
        s.delete_vertex(
            "test_space",
            "Person",
            &VertexId::try_from_int64(1).expect("test vertex id"),
        )
        .unwrap();
    });

    // Alice (vid=1) should be gone after delete+compact
    assert!(storage
        .get_vertex(
            "test_space",
            "Person",
            &VertexId::try_from_int64(1).expect("test vertex id")
        )
        .unwrap()
        .is_none());

    // Bob (vid=2) remains
    assert!(storage
        .get_vertex(
            "test_space",
            "Person",
            &VertexId::try_from_int64(2).expect("test vertex id")
        )
        .unwrap()
        .is_some());
}

/// Compact after multiple delete operations preserves survivors.
#[test]
fn test_compact_after_multiple_operations() {
    let temp_dir = common::create_test_workdir();
    let dir = temp_dir.path();

    let storage = compact_and_reopen(dir, |s| {
        // Insert more vertices
        for i in 3..=10 {
            let v = common::create_person_vertex(i, &format!("Person{}", i), 20 + i);
            s.insert_vertex("test_space", v).unwrap();
        }

        // Delete odd-numbered vertices
        for i in (1..=10).step_by(2) {
            s.delete_vertex(
                "test_space",
                "Person",
                &VertexId::try_from_int64(i).expect("test vertex id"),
            )
            .unwrap();
        }
    });

    // Even-numbered vertices survive
    for i in (2..=10).step_by(2) {
        assert!(
            storage
                .get_vertex(
                    "test_space",
                    "Person",
                    &VertexId::try_from_int64(i).expect("test vertex id")
                )
                .unwrap()
                .is_some(),
            "Vertex {} should survive",
            i
        );
    }

    // Odd-numbered vertices are gone
    for i in (1..=10).step_by(2) {
        assert!(
            storage
                .get_vertex(
                    "test_space",
                    "Person",
                    &VertexId::try_from_int64(i).expect("test vertex id")
                )
                .unwrap()
                .is_none(),
            "Vertex {} should be gone",
            i
        );
    }
}

/// Compact on clean state (no deletions) preserves all data.
#[test]
fn test_compact_clean_state() {
    let temp_dir = common::create_test_workdir();
    let dir = temp_dir.path();

    let storage = compact_and_reopen(dir, |_| {});

    // All data intact
    common::verify_test_data(&storage, "test_space");
}

/// Persist + compact + reload should preserve data integrity
/// (existing roundtrip test adapted to new pattern).
#[test]
fn test_compact_persistent_roundtrip() {
    let temp_dir = common::create_test_workdir();
    let dir = temp_dir.path();

    let storage = compact_and_reopen(dir, |s| {
        // Delete Bob
        s.delete_vertex(
            "test_space",
            "Person",
            &VertexId::try_from_int64(2).expect("test vertex id"),
        )
        .unwrap();
    });

    // Alice still exists
    assert!(storage
        .get_vertex(
            "test_space",
            "Person",
            &VertexId::try_from_int64(1).expect("test vertex id")
        )
        .unwrap()
        .is_some());

    // Bob still gone
    assert!(storage
        .get_vertex(
            "test_space",
            "Person",
            &VertexId::try_from_int64(2).expect("test vertex id")
        )
        .unwrap()
        .is_none());

    // Edge lookup after compaction must not fail; visibility depends on
    // the edge CSR compaction path, so only liveness of the call is asserted.
    let _ = storage
        .get_edge(
            "test_space",
            &VertexId::try_from_int64(1).expect("test vertex id"),
            &VertexId::try_from_int64(2).expect("test vertex id"),
            "KNOWS",
            0,
        )
        .unwrap();
}