spatio 0.3.6

A high-performance, embedded spatio-temporal database for modern applications
Documentation
//! Performance validation tests for 3D spatial queries with envelope-based pruning.
//!
//! These tests validate that the R*-tree envelope pruning optimization
//! provides significant performance improvements over linear iteration
//! for 3D spherical and cylindrical queries.

use spatio::{Point3d, Spatio};
use std::time::Instant;

#[test]
fn test_3d_sphere_query_scales_sublinearly() {
    // This test validates that spherical queries benefit from envelope pruning
    // by showing sublinear scaling behavior as dataset size increases.

    let dataset_sizes = [1000, 5000, 10000];
    let mut query_times_ms = Vec::new();

    for &size in &dataset_sizes {
        let db = Spatio::memory().unwrap();

        // Populate with distributed 3D points
        for i in 0..size {
            let lat = 40.0 + ((i % 100) as f64 * 0.001);
            let lon = -74.0 + ((i / 100) as f64 * 0.001);
            let alt = (i as f64 * 10.0) % 10000.0;
            let point = Point3d::new(lon, lat, alt);
            db.upsert(
                "aircraft",
                &format!("data_{}", i),
                point,
                serde_json::json!({}),
                None,
            )
            .unwrap();
        }

        // Query near the center of the data distribution
        let center = Point3d::new(-74.05, 40.05, 5000.0);
        let radius = 10000.0;

        let start = Instant::now();
        let results = db.query_radius("aircraft", &center, radius, 100).unwrap();
        let elapsed = start.elapsed();

        query_times_ms.push(elapsed.as_secs_f64() * 1000.0);

        // Sanity check that results were obtained
        assert!(
            !results.is_empty(),
            "Should find results for dataset size {}",
            size
        );
    }

    // With envelope pruning, query time should grow sublinearly
    // Even with 10x data (1k -> 10k), query time should be < 10x
    let ratio_10x = query_times_ms[2] / query_times_ms[0];

    println!("3D Sphere Query Performance:");
    println!("  1,000 points: {:.2}ms", query_times_ms[0]);
    println!("  5,000 points: {:.2}ms", query_times_ms[1]);
    println!(" 10,000 points: {:.2}ms", query_times_ms[2]);
    println!("  10x data ratio: {:.2}x time", ratio_10x);

    assert!(
        ratio_10x < 50.0,
        "Query time should scale sublinearly with envelope pruning (got {:.2}x for 10x data)",
        ratio_10x
    );
}

#[test]
fn test_3d_cylinder_query_altitude_pruning() {
    // This test validates that cylindrical queries efficiently prune
    // points outside the altitude range using envelope constraints.

    let db = Spatio::memory().unwrap();

    // Insert 10,000 points evenly distributed across altitudes
    for i in 0..10000 {
        let lat = 40.0 + ((i % 100) as f64 * 0.001);
        let lon = -74.0 + ((i / 100) as f64 * 0.001);
        let alt = (i as f64 / 10000.0) * 20000.0; // 0 to 20,000m
        let point = Point3d::new(lon, lat, alt);
        db.upsert(
            "aircraft",
            &format!("data_{}", i),
            point,
            serde_json::json!({}),
            None,
        )
        .unwrap();
    }

    let center_2d = spatio::Point::new(-74.0, 40.0);

    // Query 1: Narrow altitude range (2000-3000m) - should be fast
    let start1 = Instant::now();
    let narrow_results = db
        .query_within_cylinder("aircraft", center_2d, 2000.0, 3000.0, 10000.0, 1000)
        .unwrap();
    let narrow_time = start1.elapsed();

    // Query 2: Wide altitude range (0-20000m) - should take longer but still benefit from horizontal pruning
    let start2 = Instant::now();
    let wide_results = db
        .query_within_cylinder("aircraft", center_2d, 0.0, 20000.0, 10000.0, 1000)
        .unwrap();
    let wide_time = start2.elapsed();

    println!("3D Cylinder Query Altitude Pruning:");
    println!(
        "  Narrow range (2000-3000m): {} results in {:.2}ms",
        narrow_results.len(),
        narrow_time.as_secs_f64() * 1000.0
    );
    println!(
        "  Wide range (0-20000m): {} results in {:.2}ms",
        wide_results.len(),
        wide_time.as_secs_f64() * 1000.0
    );

    // If both found results, narrow range should find fewer or equal
    if !wide_results.is_empty() {
        assert!(
            narrow_results.len() <= wide_results.len(),
            "Narrow altitude range should return fewer or equal results"
        );
    }

    // All results should be within altitude bounds
    for (loc, _) in &narrow_results {
        assert!(
            loc.position.z() >= 2000.0 && loc.position.z() <= 3000.0,
            "Point altitude {} outside range [2000, 3000]",
            loc.position.z()
        );
    }
}

#[test]
fn test_3d_knn_with_large_dataset() {
    // Validate that KNN queries remain efficient with R*-tree spatial indexing

    let db = Spatio::memory().unwrap();

    // Insert 5,000 3D points
    for i in 0..5000 {
        let lat = 40.0 + ((i % 50) as f64 * 0.002);
        let lon = -74.0 + ((i / 50) as f64 * 0.002);
        let alt = (i as f64 * 5.0) % 8000.0;
        let point = Point3d::new(lon, lat, alt);
        db.upsert(
            "points",
            &format!("data_{}", i),
            point,
            serde_json::json!({}),
            None,
        )
        .unwrap();
    }

    let query_point = Point3d::new(-74.0, 40.0, 4000.0);

    // KNN should be fast even with 5k points
    let start = Instant::now();
    let neighbors = db.knn("points", &query_point, 10).unwrap();
    let elapsed = start.elapsed();

    println!(
        "3D KNN Query (k=10 from 5000 points): {:.2}ms",
        elapsed.as_secs_f64() * 1000.0
    );

    assert_eq!(neighbors.len(), 10, "Should return exactly 10 neighbors");

    // Should complete in reasonable time (< 100ms on typical hardware)
    assert!(
        elapsed.as_millis() < 100,
        "KNN query should be fast with R*-tree indexing (took {}ms)",
        elapsed.as_millis()
    );

    // Note: R*-tree uses Euclidean distance in coordinate space for KNN ordering,
    // which differs from Haversine distance. Results are ordered by R*-tree's
    // internal metric, not by the Haversine distances computed afterward.
    // This is expected behavior for geographic KNN queries.
}

#[test]
fn test_3d_sphere_query_correctness() {
    let db = Spatio::memory().unwrap();
    let test_points = [
        (-74.0, 40.0, 1000.0),
        (-74.001, 40.001, 1100.0),
        (-74.002, 40.002, 2000.0),
        (-74.01, 40.01, 5000.0),
        (-74.1, 40.1, 10000.0),
    ];

    for (i, &(lon, lat, alt)) in test_points.iter().enumerate() {
        let point = Point3d::new(lon, lat, alt);
        db.upsert(
            "test",
            &format!("point_{}", i),
            point,
            serde_json::json!({}),
            None,
        )
        .unwrap();
    }

    let center = Point3d::new(-74.0, 40.0, 1000.0);
    let radius = 2000.0;

    let results = db.query_radius("test", &center, radius, 10).unwrap();

    // Should find points 0, 1, and 2 (within ~2km including altitude)
    assert!(
        results.len() >= 2 && results.len() <= 4,
        "Should find 2-4 nearby points"
    );

    // All results should be within radius
    // Note: query_current_within_radius returns CurrentLocation, which doesn't have distance.
}