#![cfg(all(test, feature = "persistence"))]
#![allow(deprecated)]
use crate::distance::DistanceMetric;
use crate::point::Point;
use crate::velesql::Parser;
use crate::Collection;
use std::collections::HashMap;
use std::path::PathBuf;
fn create_test_collection() -> (Collection, tempfile::TempDir) {
let temp_dir = tempfile::tempdir().unwrap();
let path = PathBuf::from(temp_dir.path());
let collection = Collection::create(path, 4, DistanceMetric::Cosine).unwrap();
(collection, temp_dir)
}
#[test]
fn test_parser_having_simple() {
let query =
Parser::parse("SELECT category, COUNT(*) FROM items GROUP BY category HAVING COUNT(*) > 5")
.unwrap();
assert!(query.select.group_by.is_some());
assert!(query.select.having.is_some());
let having = query.select.having.as_ref().unwrap();
assert!(!having.conditions.is_empty());
}
#[test]
fn test_parser_having_with_avg() {
let query = Parser::parse(
"SELECT category, AVG(price) FROM items GROUP BY category HAVING AVG(price) > 100",
)
.unwrap();
assert!(query.select.having.is_some());
}
#[test]
fn test_parser_having_multiple_conditions() {
let query = Parser::parse(
"SELECT category, COUNT(*), SUM(price) FROM items GROUP BY category HAVING COUNT(*) > 2 AND SUM(price) > 500"
).unwrap();
assert!(query.select.having.is_some());
let having = query.select.having.as_ref().unwrap();
assert!(having.conditions.len() >= 2 || matches!(having.conditions[0], _));
}
#[test]
fn test_executor_having_count_filter() {
let (collection, _tmp) = create_test_collection();
let points = vec![
Point {
id: 1,
vector: vec![0.1; 4],
payload: Some(serde_json::json!({"category": "tech"})),
sparse_vectors: None,
},
Point {
id: 2,
vector: vec![0.1; 4],
payload: Some(serde_json::json!({"category": "tech"})),
sparse_vectors: None,
},
Point {
id: 3,
vector: vec![0.1; 4],
payload: Some(serde_json::json!({"category": "tech"})),
sparse_vectors: None,
},
Point {
id: 4,
vector: vec![0.1; 4],
payload: Some(serde_json::json!({"category": "science"})),
sparse_vectors: None,
},
Point {
id: 5,
vector: vec![0.1; 4],
payload: Some(serde_json::json!({"category": "science"})),
sparse_vectors: None,
},
Point {
id: 6,
vector: vec![0.1; 4],
payload: Some(serde_json::json!({"category": "history"})),
sparse_vectors: None,
},
];
collection.upsert(points).unwrap();
let query =
Parser::parse("SELECT category, COUNT(*) FROM items GROUP BY category HAVING COUNT(*) > 1")
.unwrap();
let params = HashMap::new();
let result = collection.execute_aggregate(&query, ¶ms).unwrap();
let groups = result.as_array().expect("Result should be array");
assert_eq!(groups.len(), 2);
let has_history = groups
.iter()
.any(|g| g.get("category") == Some(&serde_json::json!("history")));
assert!(!has_history, "history should be filtered by HAVING");
}
#[test]
fn test_executor_having_avg_filter() {
let (collection, _tmp) = create_test_collection();
let points = vec![
Point {
id: 1,
vector: vec![0.1; 4],
payload: Some(serde_json::json!({"category": "A", "price": 200})),
sparse_vectors: None,
},
Point {
id: 2,
vector: vec![0.1; 4],
payload: Some(serde_json::json!({"category": "A", "price": 300})),
sparse_vectors: None,
},
Point {
id: 3,
vector: vec![0.1; 4],
payload: Some(serde_json::json!({"category": "B", "price": 50})),
sparse_vectors: None,
},
Point {
id: 4,
vector: vec![0.1; 4],
payload: Some(serde_json::json!({"category": "B", "price": 50})),
sparse_vectors: None,
},
];
collection.upsert(points).unwrap();
let query = Parser::parse(
"SELECT category, AVG(price) FROM items GROUP BY category HAVING AVG(price) > 100",
)
.unwrap();
let params = HashMap::new();
let result = collection.execute_aggregate(&query, ¶ms).unwrap();
let groups = result.as_array().expect("Result should be array");
assert_eq!(groups.len(), 1);
assert_eq!(groups[0].get("category"), Some(&serde_json::json!("A")));
}
#[test]
fn test_executor_having_sum_filter() {
let (collection, _tmp) = create_test_collection();
let points = vec![
Point {
id: 1,
vector: vec![0.1; 4],
payload: Some(serde_json::json!({"category": "X", "amount": 100})),
sparse_vectors: None,
},
Point {
id: 2,
vector: vec![0.1; 4],
payload: Some(serde_json::json!({"category": "X", "amount": 200})),
sparse_vectors: None,
},
Point {
id: 3,
vector: vec![0.1; 4],
payload: Some(serde_json::json!({"category": "Y", "amount": 50})),
sparse_vectors: None,
},
];
collection.upsert(points).unwrap();
let query = Parser::parse(
"SELECT category, SUM(amount) FROM items GROUP BY category HAVING SUM(amount) >= 300",
)
.unwrap();
let params = HashMap::new();
let result = collection.execute_aggregate(&query, ¶ms).unwrap();
let groups = result.as_array().expect("Result should be array");
assert_eq!(groups.len(), 1);
assert_eq!(groups[0].get("category"), Some(&serde_json::json!("X")));
}
#[test]
fn test_executor_having_without_groupby_returns_error() {
let (collection, _tmp) = create_test_collection();
let points = vec![Point {
id: 1,
vector: vec![0.1; 4],
payload: Some(serde_json::json!({"value": 10})),
sparse_vectors: None,
}];
collection.upsert(points).unwrap();
let query = Parser::parse("SELECT COUNT(*) FROM items HAVING COUNT(*) > 10").unwrap();
let params = HashMap::new();
let result = collection.execute_aggregate(&query, ¶ms);
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(
err_msg.contains("HAVING") && err_msg.contains("GROUP BY"),
"Error should mention HAVING requires GROUP BY, got: {err_msg}"
);
}