tsink 0.10.2

A lightweight embedded time-series database with a straightforward API
Documentation
use tempfile::TempDir;
use tsink::{DataPoint, Label, Row, StorageBuilder};

#[test]
fn test_select_all_with_multiple_labels() {
    let temp_dir = TempDir::new().unwrap();
    let storage = StorageBuilder::new()
        .with_data_path(temp_dir.path())
        .build()
        .unwrap();

    let rows1 = vec![
        Row::with_labels(
            "cpu_usage",
            vec![
                Label::new("host", "server1"),
                Label::new("region", "us-west"),
            ],
            DataPoint::new(1000, 10.0),
        ),
        Row::with_labels(
            "cpu_usage",
            vec![
                Label::new("host", "server1"),
                Label::new("region", "us-west"),
            ],
            DataPoint::new(2000, 20.0),
        ),
    ];
    storage.insert_rows(&rows1).unwrap();

    let rows2 = vec![
        Row::with_labels(
            "cpu_usage",
            vec![
                Label::new("host", "server2"),
                Label::new("region", "us-east"),
            ],
            DataPoint::new(1500, 15.0),
        ),
        Row::with_labels(
            "cpu_usage",
            vec![
                Label::new("host", "server2"),
                Label::new("region", "us-east"),
            ],
            DataPoint::new(2500, 25.0),
        ),
    ];
    storage.insert_rows(&rows2).unwrap();

    let rows3 = vec![Row::with_labels(
        "cpu_usage",
        vec![Label::new("host", "server3")],
        DataPoint::new(1200, 12.0),
    )];
    storage.insert_rows(&rows3).unwrap();

    let all_results = storage.select_all("cpu_usage", 0, 3000).unwrap();

    assert_eq!(
        all_results.len(),
        3,
        "Should have 3 different label combinations"
    );

    let total_points: usize = all_results.iter().map(|(_, points)| points.len()).sum();
    assert_eq!(total_points, 5, "Should have 5 total data points");

    let mut found_server1 = false;
    let mut found_server2 = false;
    let mut found_server3 = false;

    for (labels, points) in &all_results {
        if labels
            .iter()
            .any(|l| l.name == "host" && l.value == "server1")
        {
            found_server1 = true;
            assert_eq!(points.len(), 2);
            assert!(labels
                .iter()
                .any(|l| l.name == "region" && l.value == "us-west"));
        } else if labels
            .iter()
            .any(|l| l.name == "host" && l.value == "server2")
        {
            found_server2 = true;
            assert_eq!(points.len(), 2);
            assert!(labels
                .iter()
                .any(|l| l.name == "region" && l.value == "us-east"));
        } else if labels
            .iter()
            .any(|l| l.name == "host" && l.value == "server3")
        {
            found_server3 = true;
            assert_eq!(points.len(), 1);
            assert_eq!(labels.len(), 1);
        }
    }

    assert!(found_server1, "Should find server1 data");
    assert!(found_server2, "Should find server2 data");
    assert!(found_server3, "Should find server3 data");
}

#[test]
fn test_select_all_no_labels() {
    let temp_dir = TempDir::new().unwrap();
    let storage = StorageBuilder::new()
        .with_data_path(temp_dir.path())
        .build()
        .unwrap();

    let rows = vec![
        Row::new("temperature", DataPoint::new(1000, 20.0)),
        Row::new("temperature", DataPoint::new(2000, 22.0)),
        Row::new("temperature", DataPoint::new(3000, 21.0)),
    ];
    storage.insert_rows(&rows).unwrap();

    let all_results = storage.select_all("temperature", 0, 4000).unwrap();

    assert_eq!(
        all_results.len(),
        1,
        "Should have 1 label combination (no labels)"
    );

    let (labels, points) = &all_results[0];
    assert!(labels.is_empty(), "Should have no labels");
    assert_eq!(points.len(), 3, "Should have 3 data points");
}

#[test]
fn test_select_all_mixed_labels_and_no_labels() {
    let temp_dir = TempDir::new().unwrap();
    let storage = StorageBuilder::new()
        .with_data_path(temp_dir.path())
        .build()
        .unwrap();

    let rows1 = vec![
        Row::new("requests", DataPoint::new(1000, 100.0)),
        Row::new("requests", DataPoint::new(2000, 110.0)),
    ];
    storage.insert_rows(&rows1).unwrap();

    let rows2 = vec![Row::with_labels(
        "requests",
        vec![Label::new("endpoint", "/api/core")],
        DataPoint::new(1500, 150.0),
    )];
    storage.insert_rows(&rows2).unwrap();

    let all_results = storage.select_all("requests", 0, 3000).unwrap();

    assert_eq!(
        all_results.len(),
        2,
        "Should have 2 different label combinations"
    );

    let mut found_unlabeled = false;
    let mut found_labeled = false;

    for (labels, points) in &all_results {
        if labels.is_empty() {
            found_unlabeled = true;
            assert_eq!(points.len(), 2);
        } else {
            found_labeled = true;
            assert_eq!(points.len(), 1);
            assert_eq!(labels[0].name, "endpoint");
            assert_eq!(labels[0].value, "/api/core");
        }
    }

    assert!(found_unlabeled, "Should find unlabeled data");
    assert!(found_labeled, "Should find labeled data");
}

#[test]
fn test_select_with_empty_labels_returns_only_unlabeled_series() {
    let temp_dir = TempDir::new().unwrap();
    let storage = StorageBuilder::new()
        .with_data_path(temp_dir.path())
        .build()
        .unwrap();

    storage
        .insert_rows(&[
            Row::new("requests", DataPoint::new(1000, 100.0)),
            Row::new("requests", DataPoint::new(2000, 110.0)),
            Row::with_labels(
                "requests",
                vec![Label::new("endpoint", "/api/core")],
                DataPoint::new(1500, 150.0),
            ),
        ])
        .unwrap();

    let unlabeled = storage.select("requests", &[], 0, 3000).unwrap();
    assert_eq!(unlabeled.len(), 2);
    assert_eq!(unlabeled[0].timestamp, 1000);
    assert_eq!(unlabeled[1].timestamp, 2000);

    let labeled = storage
        .select("requests", &[Label::new("endpoint", "/api/core")], 0, 3000)
        .unwrap();
    assert_eq!(labeled.len(), 1);
    assert_eq!(labeled[0].timestamp, 1500);
}

#[test]
fn test_select_with_empty_labels_returns_empty_when_only_labeled_series_exist() {
    let temp_dir = TempDir::new().unwrap();
    let storage = StorageBuilder::new()
        .with_data_path(temp_dir.path())
        .build()
        .unwrap();

    storage
        .insert_rows(&[
            Row::with_labels(
                "requests",
                vec![Label::new("endpoint", "/api/core")],
                DataPoint::new(1000, 100.0),
            ),
            Row::with_labels(
                "requests",
                vec![Label::new("endpoint", "/api/admin")],
                DataPoint::new(2000, 200.0),
            ),
        ])
        .unwrap();

    let unlabeled = storage.select("requests", &[], 0, 3000).unwrap();
    assert!(unlabeled.is_empty());
}

#[test]
fn test_select_all_nonexistent_metric() {
    let temp_dir = TempDir::new().unwrap();
    let storage = StorageBuilder::new()
        .with_data_path(temp_dir.path())
        .build()
        .unwrap();

    let rows = vec![Row::new("existing_metric", DataPoint::new(1000, 1.0))];
    storage.insert_rows(&rows).unwrap();

    let all_results = storage.select_all("nonexistent", 0, 2000).unwrap();

    assert!(
        all_results.is_empty(),
        "Should return empty results for non-existent metric"
    );
}

#[test]
fn test_select_all_time_range_filtering() {
    let temp_dir = TempDir::new().unwrap();
    let storage = StorageBuilder::new()
        .with_data_path(temp_dir.path())
        .build()
        .unwrap();

    let rows = vec![
        Row::with_labels(
            "metric",
            vec![Label::new("type", "A")],
            DataPoint::new(1000, 10.0),
        ),
        Row::with_labels(
            "metric",
            vec![Label::new("type", "A")],
            DataPoint::new(2000, 20.0),
        ),
        Row::with_labels(
            "metric",
            vec![Label::new("type", "A")],
            DataPoint::new(3000, 30.0),
        ),
        Row::with_labels(
            "metric",
            vec![Label::new("type", "B")],
            DataPoint::new(1500, 15.0),
        ),
        Row::with_labels(
            "metric",
            vec![Label::new("type", "B")],
            DataPoint::new(2500, 25.0),
        ),
    ];
    storage.insert_rows(&rows).unwrap();

    let all_results = storage.select_all("metric", 1200, 2700).unwrap();

    assert_eq!(all_results.len(), 2, "Should have both label sets");

    for (labels, points) in &all_results {
        if labels.iter().any(|l| l.value == "A") {
            assert_eq!(points.len(), 1);
            assert_eq!(points[0].timestamp, 2000);
        } else if labels.iter().any(|l| l.value == "B") {
            assert_eq!(points.len(), 2);
            assert_eq!(points[0].timestamp, 1500);
            assert_eq!(points[1].timestamp, 2500);
        }
    }
}