tsink 0.10.2

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

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

    let points = storage.select("nonexistent", &[], 1, 1000).unwrap();
    assert_eq!(points.len(), 0);
}

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

    let rows = vec![
        Row::new("extreme", DataPoint::new(i64::MIN + 1, 1.0)),
        Row::new("extreme", DataPoint::new(1, 2.0)),
        Row::new("extreme", DataPoint::new(i64::MAX - 1, 3.0)),
    ];
    for row in rows {
        storage.insert_rows(&[row]).unwrap();
    }

    let points = storage.select("extreme", &[], i64::MIN, i64::MAX).unwrap();
    assert_eq!(points.len(), 3);

    let points = storage
        .select("extreme", &[], i64::MIN, i64::MIN + 2)
        .unwrap();
    assert_eq!(points.len(), 1);

    let points = storage
        .select("extreme", &[], i64::MAX - 2, i64::MAX)
        .unwrap();
    assert_eq!(points.len(), 1);
}

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

    let rows = vec![
        Row::new("boundary", DataPoint::new(100, 1.0)),
        Row::new("boundary", DataPoint::new(200, 2.0)),
        Row::new("boundary", DataPoint::new(300, 3.0)),
    ];
    storage.insert_rows(&rows).unwrap();

    let points = storage.select("boundary", &[], 100, 200).unwrap();
    assert_eq!(points.len(), 1);
    assert_eq!(points[0].timestamp, 100);

    let points = storage.select("boundary", &[], 200, 301).unwrap();
    assert_eq!(points.len(), 2);

    let points = storage.select("boundary", &[], 200, 201).unwrap();
    assert_eq!(points.len(), 1);
    assert_eq!(points[0].timestamp, 200);

    let points = storage.select("boundary", &[], 201, 299).unwrap();
    assert_eq!(points.len(), 0);
}

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

    let rows = vec![
        Row::new("special", DataPoint::new(100, f64::NAN)),
        Row::new("special", DataPoint::new(200, f64::INFINITY)),
        Row::new("special", DataPoint::new(300, f64::NEG_INFINITY)),
        Row::new("special", DataPoint::new(400, 0.0)),
        Row::new("special", DataPoint::new(500, -0.0)),
    ];
    storage.insert_rows(&rows).unwrap();

    let points = storage.select("special", &[], 1, 1000).unwrap();
    assert_eq!(points.len(), 5);

    assert!(points[0].value_as_f64().unwrap_or(f64::NAN).is_nan());
    assert!(
        points[1].value_as_f64().unwrap_or(f64::NAN).is_infinite()
            && points[1].value_as_f64().unwrap_or(f64::NAN) > 0.0
    );
    assert!(
        points[2].value_as_f64().unwrap_or(f64::NAN).is_infinite()
            && points[2].value_as_f64().unwrap_or(f64::NAN) < 0.0
    );
}

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

    let rows = vec![
        Row::new("duplicates", DataPoint::new(100, 1.0)),
        Row::new("duplicates", DataPoint::new(100, 2.0)),
        Row::new("duplicates", DataPoint::new(100, 3.0)),
        Row::new("duplicates", DataPoint::new(200, 4.0)),
    ];
    storage.insert_rows(&rows).unwrap();

    let points = storage.select("duplicates", &[], 100, 201).unwrap();
    assert_eq!(points.len(), 4);

    let count_100 = points.iter().filter(|p| p.timestamp == 100).count();
    assert_eq!(count_100, 3);
}

#[test]
fn test_query_after_partition_rotation() {
    let temp_dir = TempDir::new().unwrap();
    let storage = StorageBuilder::new()
        .with_data_path(temp_dir.path())
        .with_partition_duration(std::time::Duration::from_millis(100))
        .build()
        .unwrap();

    for i in 0..50 {
        let rows = vec![Row::new(
            "rotation",
            DataPoint::new((i + 1) as i64 * 1000, i as f64),
        )];
        storage.insert_rows(&rows).unwrap();
    }

    let points = storage.select("rotation", &[], 1, 51000).unwrap();
    assert_eq!(points.len(), 50);

    for i in 0..49 {
        assert!(points[i].timestamp <= points[i + 1].timestamp);
    }
}

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

    let labels_sets = [
        vec![Label::new("key", "value with spaces")],
        vec![Label::new("key", "value/with/slashes")],
        vec![Label::new("key", "value=with=equals")],
        vec![Label::new("key", "value,with,commas")],
        vec![Label::new("key", "value\"with\"quotes")],
        vec![
            Label::new("key1", "value1"),
            Label::new("key2", "value2"),
            Label::new("key3", "value3"),
        ],
    ];

    for (i, labels) in labels_sets.iter().enumerate() {
        let rows = vec![Row::with_labels(
            "labeled",
            labels.clone(),
            DataPoint::new((i + 1) as i64, i as f64),
        )];
        storage.insert_rows(&rows).unwrap();
    }

    for (i, labels) in labels_sets.iter().enumerate() {
        let points = storage.select("labeled", labels, 1, 100).unwrap();
        assert!(
            !points.is_empty(),
            "Failed to find data for label set {}",
            i
        );
    }
}

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

    let err = storage
        .insert_rows(&[Row::with_labels(
            "invalid_label_insert",
            vec![Label::new("", "value")],
            DataPoint::new(1, 1.0),
        )])
        .unwrap_err();
    assert!(matches!(err, TsinkError::InvalidLabel(_)));

    let err = storage
        .select(
            "invalid_label_select",
            &[Label::new("", "value")],
            1,
            i64::MAX,
        )
        .unwrap_err();
    assert!(matches!(err, TsinkError::InvalidLabel(_)));

    storage
        .insert_rows(&[Row::with_labels(
            "empty_label_value",
            vec![Label::new("key", "")],
            DataPoint::new(1, 1.0),
        )])
        .unwrap();
    let points = storage
        .select("empty_label_value", &[Label::new("key", "")], 1, 2)
        .unwrap();
    assert_eq!(points, vec![DataPoint::new(1, 1.0)]);

    let oversized = Label {
        name: "key".to_string(),
        value: "x".repeat(tsink::label::MAX_LABEL_VALUE_LEN + 1),
    };
    let err = storage
        .select("invalid_label_select", &[oversized], 1, i64::MAX)
        .unwrap_err();
    assert!(matches!(err, TsinkError::InvalidLabel(_)));
}

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

    for i in 10..20 {
        let rows = vec![Row::new(
            "range_test",
            DataPoint::new(i as i64 * 100, i as f64),
        )];
        storage.insert_rows(&rows).unwrap();
    }

    let points = storage.select("range_test", &[], 1, 900).unwrap();
    assert_eq!(points.len(), 0);

    let points = storage.select("range_test", &[], 2100, 3000).unwrap();
    assert_eq!(points.len(), 0);

    let points = storage.select("range_test", &[], 1, 3000).unwrap();
    assert_eq!(points.len(), 10);
}