#![cfg(feature = "persistence")]
#![allow(
clippy::cast_precision_loss,
clippy::uninlined_format_args,
clippy::doc_markdown
)]
use std::collections::{HashMap, HashSet};
use serde_json::json;
use tempfile::TempDir;
use velesdb_core::{velesql::Parser, Database, Point, SearchResult};
fn execute_sql(db: &Database, sql: &str) -> velesdb_core::Result<Vec<SearchResult>> {
let query = Parser::parse(sql).map_err(|e| velesdb_core::Error::Query(e.to_string()))?;
db.execute_query(&query, &HashMap::new())
}
fn execute_sql_with_params(
db: &Database,
sql: &str,
params: &HashMap<String, serde_json::Value>,
) -> velesdb_core::Result<Vec<SearchResult>> {
let query = Parser::parse(sql).map_err(|e| velesdb_core::Error::Query(e.to_string()))?;
db.execute_query(&query, params)
}
fn create_test_db() -> (TempDir, Database) {
let dir = TempDir::new().expect("test: create temp dir");
let db = Database::open(dir.path()).expect("test: open database");
(dir, db)
}
fn payload_str<'a>(result: &'a SearchResult, field: &str) -> Option<&'a str> {
result
.point
.payload
.as_ref()
.and_then(|p| p.get(field))
.and_then(serde_json::Value::as_str)
}
fn payload_f64(result: &SearchResult, field: &str) -> Option<f64> {
result
.point
.payload
.as_ref()
.and_then(|p| p.get(field))
.and_then(serde_json::Value::as_f64)
}
fn result_ids(results: &[SearchResult]) -> HashSet<u64> {
results.iter().map(|r| r.point.id).collect()
}
fn vector_param(v: &[f32]) -> HashMap<String, serde_json::Value> {
let mut params = HashMap::new();
params.insert("v".to_string(), serde_json::json!(v));
params
}
fn setup_products_and_reviews(db: &Database) {
execute_sql(
db,
"CREATE COLLECTION products (dimension = 4, metric = 'cosine');",
)
.expect("test: CREATE products");
let products = db
.get_vector_collection("products")
.expect("test: get products");
products
.upsert(vec![
Point::new(
1,
vec![1.0, 0.0, 0.0, 0.0],
Some(json!({"name": "Laptop", "category": "electronics", "price": 1200})),
),
Point::new(
2,
vec![0.0, 1.0, 0.0, 0.0],
Some(json!({"name": "Phone", "category": "electronics", "price": 800})),
),
Point::new(
3,
vec![0.0, 0.0, 1.0, 0.0],
Some(json!({"name": "Novel", "category": "books", "price": 15})),
),
Point::new(
4,
vec![0.0, 0.0, 0.0, 1.0],
Some(json!({"name": "Cookbook", "category": "books", "price": 25})),
),
Point::new(
5,
vec![0.7, 0.7, 0.0, 0.0],
Some(json!({"name": "Tablet", "category": "electronics", "price": 500})),
),
Point::new(
6,
vec![0.5, 0.0, 0.5, 0.0],
Some(json!({"name": "T-Shirt", "category": "clothing", "price": 30})),
),
])
.expect("test: upsert products");
execute_sql(db, "CREATE METADATA COLLECTION reviews;").expect("test: CREATE reviews");
let reviews = db
.get_metadata_collection("reviews")
.expect("test: get reviews");
reviews
.upsert(vec![
Point::metadata_only(1, json!({"rating": 5, "reviewer": "Alice"})),
Point::metadata_only(2, json!({"rating": 3, "reviewer": "Bob"})),
Point::metadata_only(3, json!({"rating": 4, "reviewer": "Charlie"})),
Point::metadata_only(4, json!({"rating": 2, "reviewer": "Diana"})),
Point::metadata_only(5, json!({"rating": 5, "reviewer": "Eve"})),
Point::metadata_only(6, json!({"rating": 1, "reviewer": "Frank"})),
])
.expect("test: upsert reviews");
}
#[test]
fn test_join_with_pushdown_returns_correct_results() {
let (_dir, db) = create_test_db();
setup_products_and_reviews(&db);
let sql = "SELECT * FROM products \
JOIN reviews ON products.id = reviews.id \
WHERE category = 'electronics' AND reviews.rating > 4 \
LIMIT 10";
let results = execute_sql(&db, sql).expect("test: pushdown JOIN query should succeed");
let ids = result_ids(&results);
assert_eq!(ids.len(), 2, "expected 2 results, got {}", results.len());
assert!(ids.contains(&1), "Laptop (id=1) should match");
assert!(ids.contains(&5), "Tablet (id=5) should match");
for r in &results {
assert!(
payload_str(r, "category").is_some(),
"merged payload should have 'category' from products"
);
assert!(
payload_f64(r, "rating").is_some(),
"merged payload should have 'rating' from reviews"
);
assert_eq!(
payload_str(r, "category"),
Some("electronics"),
"all results should be electronics"
);
let rating = payload_f64(r, "rating").expect("test: rating field");
assert!(rating > 4.0, "rating {} should be > 4", rating);
}
}
#[test]
fn test_join_pushdown_filters_before_join() {
let (_dir, db) = create_test_db();
setup_products_and_reviews(&db);
let sql_all = "SELECT * FROM products \
JOIN reviews ON products.id = reviews.id \
LIMIT 10";
let all_results = execute_sql(&db, sql_all).expect("test: baseline JOIN");
assert_eq!(
all_results.len(),
6,
"baseline JOIN should return all 6 rows"
);
let sql_filtered = "SELECT * FROM products \
JOIN reviews ON products.id = reviews.id \
WHERE price > 1000 \
LIMIT 10";
let filtered_results = execute_sql(&db, sql_filtered).expect("test: filtered JOIN");
assert_eq!(
filtered_results.len(),
1,
"filtered JOIN should return 1 row (Laptop)"
);
assert!(
filtered_results.len() < all_results.len(),
"pushdown filter should reduce result count"
);
assert_eq!(
payload_str(&filtered_results[0], "name"),
Some("Laptop"),
"the single result should be Laptop"
);
assert!(
payload_f64(&filtered_results[0], "rating").is_some(),
"merged payload should have rating from reviews"
);
}
#[test]
fn test_join_without_where_returns_all() {
let (_dir, db) = create_test_db();
setup_products_and_reviews(&db);
let sql = "SELECT * FROM products \
JOIN reviews ON products.id = reviews.id \
LIMIT 10";
let results = execute_sql(&db, sql).expect("test: JOIN without WHERE");
assert_eq!(results.len(), 6, "all 6 products should join with reviews");
for r in &results {
assert!(
payload_str(r, "name").is_some(),
"should have 'name' from products"
);
assert!(
payload_f64(r, "rating").is_some(),
"should have 'rating' from reviews"
);
}
}
#[test]
fn test_join_with_vector_near_and_pushdown() {
let (_dir, db) = create_test_db();
setup_products_and_reviews(&db);
let sql = "SELECT * FROM products \
JOIN reviews ON products.id = reviews.id \
WHERE vector NEAR $v AND reviews.rating > 3 \
LIMIT 5";
let params = vector_param(&[1.0, 0.0, 0.0, 0.0]);
let results = execute_sql_with_params(&db, sql, ¶ms).expect("test: NEAR + pushdown JOIN");
assert!(!results.is_empty(), "NEAR + pushdown should return results");
for r in &results {
let rating = payload_f64(r, "rating").expect("test: rating field");
assert!(rating > 3.0, "rating {} should be > 3", rating);
}
let ids = result_ids(&results);
assert!(
!ids.contains(&2),
"Phone (rating=3) should be excluded by pushdown"
);
assert!(
!ids.contains(&4),
"Cookbook (rating=2) should be excluded by pushdown"
);
assert!(
!ids.contains(&6),
"T-Shirt (rating=1) should be excluded by pushdown"
);
}
#[test]
fn test_pushdown_eliminates_all_joined_rows_returns_empty() {
let (_dir, db) = create_test_db();
setup_products_and_reviews(&db);
let sql = "SELECT * FROM products \
JOIN reviews ON products.id = reviews.id \
WHERE reviews.rating > 100 \
LIMIT 10";
let results = execute_sql(&db, sql).expect("test: pushdown eliminates all");
assert!(
results.is_empty(),
"INNER JOIN with impossible pushdown filter should return empty"
);
}
#[test]
fn test_multiple_pushdown_conditions_on_joined_table() {
let (_dir, db) = create_test_db();
setup_products_and_reviews(&db);
let sql = "SELECT * FROM products \
JOIN reviews ON products.id = reviews.id \
WHERE reviews.rating > 3 AND reviews.rating < 6 \
LIMIT 10";
let results = execute_sql(&db, sql).expect("test: multiple pushdown conditions");
let ids = result_ids(&results);
assert_eq!(ids.len(), 3, "expected 3 results, got {}", results.len());
assert!(ids.contains(&1), "Laptop (rating=5) should match");
assert!(ids.contains(&3), "Novel (rating=4) should match");
assert!(ids.contains(&5), "Tablet (rating=5) should match");
for r in &results {
let rating = payload_f64(r, "rating").expect("test: rating field");
assert!(
rating > 3.0 && rating < 6.0,
"rating {} should be between 3 and 6 exclusive",
rating
);
}
}
#[test]
fn test_base_side_and_pushdown_filters_combined() {
let (_dir, db) = create_test_db();
setup_products_and_reviews(&db);
let sql = "SELECT * FROM products \
JOIN reviews ON products.id = reviews.id \
WHERE category = 'books' AND reviews.rating > 3 \
LIMIT 10";
let results = execute_sql(&db, sql).expect("test: base + pushdown combined");
assert_eq!(results.len(), 1, "only Novel should match");
assert_eq!(
payload_str(&results[0], "name"),
Some("Novel"),
"the single result should be Novel"
);
assert_eq!(
payload_str(&results[0], "category"),
Some("books"),
"category should be 'books'"
);
let rating = payload_f64(&results[0], "rating").expect("test: rating field");
assert!(rating > 3.0, "rating {} should be > 3", rating);
}
#[test]
fn test_join_with_nonexistent_collection_returns_error() {
let (_dir, db) = create_test_db();
setup_products_and_reviews(&db);
let sql = "SELECT * FROM products \
JOIN ghost ON products.id = ghost.id \
LIMIT 10";
let err = execute_sql(&db, sql).expect_err("test: should fail for missing collection");
let msg = err.to_string();
assert!(
msg.contains("ghost") || msg.contains("not found"),
"error should mention the missing collection, got: {msg}"
);
}
#[test]
fn test_pushdown_with_limit_truncates_correctly() {
let (_dir, db) = create_test_db();
setup_products_and_reviews(&db);
let sql = "SELECT * FROM products \
JOIN reviews ON products.id = reviews.id \
WHERE reviews.rating > 3 \
LIMIT 2";
let results = execute_sql(&db, sql).expect("test: pushdown + LIMIT");
assert!(
results.len() <= 2,
"LIMIT 2 should cap results at 2, got {}",
results.len()
);
assert!(!results.is_empty(), "should return at least 1 result");
for r in &results {
let rating = payload_f64(r, "rating").expect("test: rating field");
assert!(rating > 3.0, "all results should have rating > 3");
}
}