sqlitegraph 2.0.7

Embedded graph database with full ACID transactions, HNSW vector search, dual backend support, and comprehensive graph algorithms library
Documentation
#![cfg(feature = "v2_experimental")]
//! TDD Tests for V2 Edge Boundary Tests - Default V2 Format Behavior.
//!
//! These tests validate that all newly created native graph files use V2 format by default
//! and that V2-style edge insertion and adjacency operations work correctly.
//! These tests are designed to FAIL before implementation and PASS after.

use sqlitegraph::{
    BackendDirection, EdgeSpec, NeighborQuery, NodeSpec, config::GraphConfig, open_graph,
};
use tempfile::TempDir;

/// Helper function to create a node for V2 edge testing
fn create_test_node(graph: &Box<dyn sqlitegraph::GraphBackend>, node_id: i64) -> i64 {
    let node_spec = NodeSpec {
        kind: "test_node".to_string(),
        name: format!("test_node_{}", node_id),
        file_path: None,
        data: serde_json::json!({"node_id": node_id}),
    };
    graph.insert_node(node_spec).unwrap()
}

/// Helper function to create an edge with controlled total size for V2 testing
/// V2 Edge Record Size Formula: total_size = 34 + type_len + data_len (V2 has 1 extra version byte)
fn create_controlled_size_edge_v2(from_id: i64, to_id: i64, total_size_target: usize) -> EdgeSpec {
    // Calculate string payload needed to reach target size
    let string_payload_size = total_size_target.saturating_sub(34); // 34 bytes overhead for V2

    // Distribute payload across edge_type and data
    let type_size = string_payload_size / 2;
    let data_size = string_payload_size - type_size;

    let edge_type_string = "x".repeat(type_size);
    let data_string = format!("data_{}", "y".repeat(data_size));

    EdgeSpec {
        from: from_id,
        to: to_id,
        edge_type: edge_type_string,
        data: serde_json::json!({"payload": data_string, "target_size": total_size_target}),
    }
}

/// Test that newly created files default to V2 format (documents current state)
#[test]
fn v2_edge_boundary_new_files_use_v2_format_by_default() {
    let temp_dir = TempDir::new().unwrap();
    let db_path = temp_dir.path().join("test_v2_default_format.db");

    let config = GraphConfig::native();
    let graph = open_graph(&db_path, &config).unwrap();

    // Verify that files are created with V2 format by default (Step 17 success)
    // However, node insertion currently creates V1 records, causing format mismatch
    // This test documents the current transition state

    let from_node_id = create_test_node(&graph, 1);
    let to_node_id = create_test_node(&graph, 2);

    // Create edge and expect format mismatch error (documents the current issue)
    let edge_spec = create_controlled_size_edge_v2(from_node_id, to_node_id, 100);
    let result = graph.insert_edge(edge_spec);

    // This should currently fail due to V1 node records in V2 file
    match result {
        Err(sqlitegraph::SqliteGraphError::ConnectionError(err_msg)) => {
            assert!(
                err_msg.contains("Unexpected V1 node record encountered in V2 region"),
                "Expected V1-in-V2 format error, got: {}",
                err_msg
            );
        }
        Ok(_) => {
            // If this succeeds, it means the V1/V2 mismatch has been fixed
            println!("V2 edge insertion succeeded - V1/V2 mismatch may be resolved");
        }
        other => {
            panic!("Unexpected error type: {:?}", other);
        }
    }
}

/// Test case (a): Small edges <256B should read successfully in V2
#[test]
fn v2_edge_boundary_small_edges_should_read_successfully() {
    let temp_dir = TempDir::new().unwrap();
    let db_path = temp_dir.path().join("test_v2_small_edge_boundary.db");

    let config = GraphConfig::native();
    let graph = open_graph(&db_path, &config).unwrap();

    // Create source and target nodes
    let from_node_id = create_test_node(&graph, 1);
    let to_node_id = create_test_node(&graph, 2);

    // Create edges with different small sizes
    let small_sizes = [50, 100, 200, 255]; // All <256B

    for &target_size in &small_sizes {
        let edge_spec = create_controlled_size_edge_v2(from_node_id, to_node_id, target_size);
        let edge_id = graph.insert_edge(edge_spec).unwrap();

        // Verify edge can be read without corruption by checking neighbors
        let result = graph.neighbors(
            from_node_id,
            NeighborQuery {
                direction: BackendDirection::Outgoing,
                edge_type: None,
            },
        );
        assert!(
            result.is_ok(),
            "Small V2 edge ({} bytes) should read successfully. Error: {:?}",
            target_size,
            result
        );

        let neighbors = result.unwrap();
        assert!(
            neighbors.contains(&to_node_id),
            "Small V2 edge should connect nodes successfully"
        );
    }
}

/// Test case (b): Boundary edges ~=256B should read successfully in V2
#[test]
fn v2_edge_boundary_edges_around_256b_should_read_successfully() {
    let temp_dir = TempDir::new().unwrap();
    let db_path = temp_dir.path().join("test_v2_256b_edge_boundary.db");

    let config = GraphConfig::native();
    let graph = open_graph(&db_path, &config).unwrap();

    // Create source and target nodes
    let from_node_id = create_test_node(&graph, 3);
    let to_node_id = create_test_node(&graph, 4);

    // Create edges very close to 256B boundary
    let boundary_sizes = [250, 255, 256, 260]; // Around 256B

    for &target_size in &boundary_sizes {
        let edge_spec = create_controlled_size_edge_v2(from_node_id, to_node_id, target_size);
        let edge_id = graph.insert_edge(edge_spec).unwrap();

        // Verify edge can be read without corruption
        let result = graph.neighbors(
            from_node_id,
            NeighborQuery {
                direction: BackendDirection::Outgoing,
                edge_type: None,
            },
        );
        assert!(
            result.is_ok(),
            "Boundary V2 edge ({} bytes) should read successfully. Error: {:?}",
            target_size,
            result
        );

        let neighbors = result.unwrap();
        assert!(
            neighbors.contains(&to_node_id),
            "Boundary V2 edge should connect nodes successfully"
        );
    }
}

/// Test case (c): Large edges >256B should work correctly in V2 (V2 supports larger edges)
#[test]
fn v2_edge_boundary_large_edges_should_work_correctly() {
    let temp_dir = TempDir::new().unwrap();
    let db_path = temp_dir.path().join("test_v2_large_edge_boundary.db");

    let config = GraphConfig::native();
    let graph = open_graph(&db_path, &config).unwrap();

    // Create source and target nodes
    let from_node_id = create_test_node(&graph, 5);
    let to_node_id = create_test_node(&graph, 6);

    // Create edges that exceed 256B boundary - V2 should handle these better
    let large_sizes = [300, 512, 1024]; // Over 256B but reasonable for V2

    for &target_size in &large_sizes {
        let edge_spec = create_controlled_size_edge_v2(from_node_id, to_node_id, target_size);

        // In V2, large edges should work better than V1
        let result = graph.insert_edge(edge_spec);

        // This test documents current V2 behavior - may succeed or fail gracefully
        match result {
            Ok(edge_id) => {
                // If insertion succeeded, verify reading works
                let read_result = graph.neighbors(
                    from_node_id,
                    NeighborQuery {
                        direction: BackendDirection::Outgoing,
                        edge_type: None,
                    },
                );
                assert!(
                    read_result.is_ok(),
                    "Large V2 edge ({} bytes) should read successfully after insertion. Error: {:?}",
                    target_size,
                    read_result
                );
            }
            Err(e) => {
                // If insertion failed, it should be a graceful error, not a panic
                println!(
                    "Large V2 edge ({} bytes) insertion failed gracefully: {:?}",
                    target_size, e
                );
                // Don't assert failure here - document current behavior
            }
        }
    }
}

/// Performance test: Mixed size edges should handle boundaries correctly in V2
#[test]
fn v2_edge_boundary_mixed_size_edges_should_handle_correctly() {
    let temp_dir = TempDir::new().unwrap();
    let db_path = temp_dir.path().join("test_v2_mixed_edge_boundary.db");

    let config = GraphConfig::native();
    let graph = open_graph(&db_path, &config).unwrap();

    // Create 10 nodes for testing
    let mut node_ids = Vec::new();
    for i in 7..=16 {
        let node_id = create_test_node(&graph, i);
        node_ids.push(node_id);
    }

    // Create a mix of small, boundary, and large edges
    let mut edge_results = Vec::new();

    // Small edges (should succeed)
    for i in 0..=2 {
        let edge_spec = create_controlled_size_edge_v2(node_ids[i], node_ids[i + 1], 100);
        let result = graph.insert_edge(edge_spec);
        edge_results.push((100, result, true)); // (size, result, should_succeed)
    }

    // Boundary edges (should succeed)
    for i in 3..=5 {
        let edge_spec = create_controlled_size_edge_v2(node_ids[i], node_ids[i + 1], 255);
        let result = graph.insert_edge(edge_spec);
        edge_results.push((255, result, true));
    }

    // Large edges (behavior depends on V2 implementation)
    for i in 6..=8 {
        let edge_spec =
            create_controlled_size_edge_v2(node_ids[i], node_ids[(i + 1) % node_ids.len()], 512);
        let result = graph.insert_edge(edge_spec);
        edge_results.push((512, result, false)); // Mark as false to test behavior
    }

    // Test each edge according to expected behavior
    for (size, result, should_succeed) in edge_results {
        if should_succeed {
            assert!(
                result.is_ok(),
                "V2 edge ({} bytes) should insert successfully. Error: {:?}",
                size,
                result
            );
        } else {
            // For large edges, just document the behavior
            match result {
                Ok(_) => {
                    println!(
                        "Large V2 edge ({} bytes) succeeded - V2 handles large edges well",
                        size
                    );
                }
                Err(e) => {
                    println!("Large V2 edge ({} bytes) failed gracefully: {:?}", size, e);
                }
            }
        }
    }
}

/// Edge case: Exactly 256B edge should be handled correctly in V2
#[test]
fn v2_edge_boundary_exactly_256b_edge_should_be_handled_correctly() {
    let temp_dir = TempDir::new().unwrap();
    let db_path = temp_dir.path().join("test_v2_exact_256b_edge.db");

    let config = GraphConfig::native();
    let graph = open_graph(&db_path, &config).unwrap();

    // Create source and target nodes
    let from_node_id = create_test_node(&graph, 17);
    let to_node_id = create_test_node(&graph, 18);

    // Create edge that is exactly 256B - 1 byte (should succeed)
    let exact_256b_minus_1 = 255;
    let edge_spec = create_controlled_size_edge_v2(from_node_id, to_node_id, exact_256b_minus_1);
    let edge_id = graph.insert_edge(edge_spec).unwrap();

    // Verify edge can be read
    let result = graph.neighbors(
        from_node_id,
        NeighborQuery {
            direction: BackendDirection::Outgoing,
            edge_type: None,
        },
    );
    assert!(
        result.is_ok(),
        "Exactly 256B-1 V2 edge should read successfully. Error: {:?}",
        result
    );

    // Create edge that is exactly 256B (V2 should handle this better)
    let exact_256b = 256;
    let edge_spec2 = create_controlled_size_edge_v2(from_node_id, to_node_id, exact_256b);
    let result2 = graph.insert_edge(edge_spec2);

    // This test documents current V2 behavior
    println!("Exactly 256B V2 edge insertion result: {:?}", result2);
}

/// V2 regression test: Verify V2 adjacency clustering works at node 257 boundary
#[test]
fn v2_edge_boundary_adjacency_clustering_should_work_at_node_257() {
    let temp_dir = TempDir::new().unwrap();
    let db_path = temp_dir.path().join("test_v2_adjacency_clustering_257.db");

    let config = GraphConfig::native();
    let graph = open_graph(&db_path, &config).unwrap();

    // Create nodes up to and including the known boundary
    let mut node_ids = Vec::new();
    for i in 1..=300 {
        let node_id = create_test_node(&graph, i);
        node_ids.push(node_id);
    }

    // Try to create edges that will test V2 adjacency clustering
    let mut edge_results = Vec::new();

    // Create edges up to node 257 to test V2 clustering
    for i in 0..=256 {
        let from_node = node_ids[i];
        let to_node = node_ids[(i + 1) % node_ids.len()];

        let edge_spec = EdgeSpec {
            from: from_node,
            to: to_node,
            edge_type: "v2_clustering_test".to_string(),
            data: serde_json::json!({
                "from_node": from_node,
                "to_node": to_node,
                "payload": "x".repeat(100) // Moderate edge size
            }),
        };

        let result = graph.insert_edge(edge_spec);
        edge_results.push((from_node, to_node, result.is_ok()));

        // If we hit node 257, this is where V1 had issues
        if from_node == 257 || to_node == 257 {
            println!(
                "V2 edge creation at node 257 boundary: from {} to {} -> {:?}",
                from_node, to_node, result
            );
        }
    }

    // Verify V2 adjacency clustering works by reading back neighbors
    let test_node_257_result = graph.neighbors(
        257,
        NeighborQuery {
            direction: BackendDirection::Outgoing,
            edge_type: None,
        },
    );

    println!("V2 neighbors for node 257: {:?}", test_node_257_result);

    // Document V2 behavior at the boundary
    match test_node_257_result {
        Ok(neighbors) => {
            println!(
                "V2 adjacency clustering works correctly at node 257: {} neighbors",
                neighbors.len()
            );
        }
        Err(e) => {
            println!("V2 adjacency clustering issue at node 257: {:?}", e);
        }
    }
}