use super::*;
use crate::point::Point;
use crate::velesql::Parser;
use crate::DistanceMetric;
use tempfile::tempdir;
#[test]
fn test_execute_query_select_all_returns_inserted_points() {
let dir = tempdir().unwrap();
let db = Database::open(dir.path()).unwrap();
db.create_collection("docs", 4, DistanceMetric::Cosine)
.unwrap();
let coll = db.get_vector_collection("docs").unwrap();
coll.upsert(vec![
Point::new(
1,
vec![1.0, 0.0, 0.0, 0.0],
Some(serde_json::json!({"title": "alpha"})),
),
Point::new(
2,
vec![0.0, 1.0, 0.0, 0.0],
Some(serde_json::json!({"title": "beta"})),
),
])
.unwrap();
let query = Parser::parse("SELECT * FROM docs").unwrap();
let results = db
.execute_query(&query, &std::collections::HashMap::new())
.unwrap();
assert_eq!(results.len(), 2);
}
#[test]
fn test_execute_query_with_limit() {
let dir = tempdir().unwrap();
let db = Database::open(dir.path()).unwrap();
db.create_collection("items", 4, DistanceMetric::Cosine)
.unwrap();
let coll = db.get_vector_collection("items").unwrap();
let points: Vec<Point> = (1..=5)
.map(|i| {
#[allow(clippy::cast_precision_loss)]
let v = vec![i as f32, 0.0, 0.0, 0.0];
Point::new(i, v, Some(serde_json::json!({})))
})
.collect();
coll.upsert(points).unwrap();
let query = Parser::parse("SELECT * FROM items LIMIT 2").unwrap();
let results = db
.execute_query(&query, &std::collections::HashMap::new())
.unwrap();
assert_eq!(
results.len(),
2,
"LIMIT 2 over 5 inserted points must return exactly 2 rows"
);
}
#[test]
fn test_execute_query_nonexistent_collection_returns_error() {
let dir = tempdir().unwrap();
let db = Database::open(dir.path()).unwrap();
let query = Parser::parse("SELECT * FROM ghost").unwrap();
let err = db
.execute_query(&query, &std::collections::HashMap::new())
.unwrap_err();
assert!(matches!(err, crate::Error::CollectionNotFound(_)));
}
#[test]
fn test_execute_query_validation_error_returns_query_error() {
let dir = tempdir().unwrap();
let db = Database::open(dir.path()).unwrap();
db.create_collection_typed("t", &crate::CollectionType::MetadataOnly)
.unwrap();
let query = Parser::parse("SELECT similarity() FROM t WHERE name = 'x'").unwrap();
let err = db
.execute_query(&query, &std::collections::HashMap::new())
.unwrap_err();
assert!(matches!(err, crate::Error::Query(_)));
}
#[test]
fn test_explain_query_returns_valid_plan() {
let dir = tempdir().unwrap();
let db = Database::open(dir.path()).unwrap();
db.create_collection("plans", 4, DistanceMetric::Cosine)
.unwrap();
let query = Parser::parse("SELECT * FROM plans").unwrap();
let plan = db.explain_query(&query).unwrap();
assert_eq!(plan.cache_hit, Some(false));
assert_eq!(plan.plan_reuse_count, Some(0));
}
#[test]
fn test_explain_query_cache_hit_after_execute() {
let dir = tempdir().unwrap();
let db = Database::open(dir.path()).unwrap();
db.create_collection("cached", 4, DistanceMetric::Cosine)
.unwrap();
let query = Parser::parse("SELECT * FROM cached").unwrap();
db.execute_query(&query, &std::collections::HashMap::new())
.unwrap();
let plan = db.explain_query(&query).unwrap();
assert_eq!(plan.cache_hit, Some(true));
}
#[test]
fn test_execute_query_insert_into_metadata_collection() {
let dir = tempdir().unwrap();
let db = Database::open(dir.path()).unwrap();
db.create_collection_typed("items", &crate::CollectionType::MetadataOnly)
.unwrap();
let query =
Parser::parse("INSERT INTO items (id, tag, score) VALUES (1, 'hello', 42.0)").unwrap();
let results = db
.execute_query(&query, &std::collections::HashMap::new())
.unwrap();
assert_eq!(results.len(), 1);
assert_eq!(results[0].point.id, 1);
let payload = results[0].point.payload.as_ref().unwrap();
assert_eq!(payload["tag"], serde_json::json!("hello"));
}
#[test]
fn test_execute_query_update_modifies_payload() {
let dir = tempdir().unwrap();
let db = Database::open(dir.path()).unwrap();
db.create_collection_typed("items", &crate::CollectionType::MetadataOnly)
.unwrap();
let coll = db.get_metadata_collection("items").unwrap();
coll.upsert_metadata(vec![Point::metadata_only(
1,
serde_json::json!({"status": "draft", "count": 0}),
)])
.unwrap();
let query = Parser::parse("UPDATE items SET status = 'published' WHERE id = 1").unwrap();
let results = db
.execute_query(&query, &std::collections::HashMap::new())
.unwrap();
assert_eq!(results.len(), 1);
let updated = coll.get(&[1]).into_iter().flatten().next().unwrap();
let payload = updated.payload.unwrap();
assert_eq!(payload["status"], serde_json::json!("published"));
assert_eq!(payload["count"], serde_json::json!(0));
}
#[test]
fn test_schema_version_increments_on_create_and_delete() {
let dir = tempdir().unwrap();
let db = Database::open(dir.path()).unwrap();
let v0 = db.schema_version();
db.create_collection("a", 4, DistanceMetric::Cosine)
.unwrap();
let v1 = db.schema_version();
assert!(v1 > v0, "schema_version should increment after create");
db.delete_collection("a").unwrap();
let v2 = db.schema_version();
assert!(v2 > v1, "schema_version should increment after delete");
}
use crate::collection::search::query::pushdown::PushdownAnalysis;
use crate::collection::search::query::JOIN_ROW_CEILING;
fn select_of(sql: &str) -> crate::velesql::SelectStatement {
Parser::parse(sql).unwrap().select
}
#[test]
fn test_join_row_budget_plain_limit_uses_limit() {
let select = select_of("SELECT d.id FROM docs AS d JOIN meta AS m ON d.id = m.id LIMIT 5");
let budget = Database::join_row_budget(&select, &PushdownAnalysis::default());
assert_eq!(budget, 5);
}
#[test]
fn test_join_row_budget_group_by_uses_ceiling() {
let select = select_of(
"SELECT m.tag FROM docs AS d JOIN meta AS m ON d.id = m.id GROUP BY m.tag LIMIT 2",
);
let budget = Database::join_row_budget(&select, &PushdownAnalysis::default());
assert_eq!(budget, JOIN_ROW_CEILING);
}
#[test]
fn test_join_row_budget_distinct_uses_ceiling() {
let select =
select_of("SELECT DISTINCT m.tag FROM docs AS d JOIN meta AS m ON d.id = m.id LIMIT 2");
let budget = Database::join_row_budget(&select, &PushdownAnalysis::default());
assert_eq!(budget, JOIN_ROW_CEILING);
}
#[test]
fn test_join_row_budget_order_by_uses_ceiling() {
let select =
select_of("SELECT d.id FROM docs AS d JOIN meta AS m ON d.id = m.id ORDER BY d.id LIMIT 3");
let budget = Database::join_row_budget(&select, &PushdownAnalysis::default());
assert_eq!(budget, JOIN_ROW_CEILING);
}
#[test]
fn test_execute_aggregate_resolves_collection_via_param() {
let dir = tempdir().unwrap();
let db = Database::open(dir.path()).unwrap();
db.create_collection("orders", 2, DistanceMetric::Cosine)
.unwrap();
let coll = db.get_vector_collection("orders").unwrap();
let points: Vec<Point> = [(10, "x"), (11, "x"), (12, "y"), (13, "y")]
.into_iter()
.map(|(id, cat)| {
Point::new(
id,
vec![1.0, 0.0],
Some(serde_json::json!({ "category": cat })),
)
})
.collect();
coll.upsert(points).unwrap();
let mut query =
Parser::parse("SELECT category, COUNT(*) AS n FROM orders GROUP BY category").unwrap();
query.select.from = String::new();
let mut params = std::collections::HashMap::new();
params.insert(
"_collection".to_string(),
serde_json::Value::String("orders".to_string()),
);
let value = db.execute_aggregate(&query, ¶ms).unwrap();
let groups = value
.as_array()
.expect("GROUP BY returns an array of groups");
assert_eq!(groups.len(), 2, "two category groups (x, y); got {value:?}");
}
#[test]
fn test_execute_aggregate_unknown_collection_errors() {
let dir = tempdir().unwrap();
let db = Database::open(dir.path()).unwrap();
let query = Parser::parse("SELECT COUNT(*) AS n FROM ghost").unwrap();
let params = std::collections::HashMap::new();
let err = db.execute_aggregate(&query, ¶ms).unwrap_err();
assert!(
matches!(err, crate::Error::CollectionNotFound(_)),
"expected CollectionNotFound, got {err:?}"
);
}
struct AllowAllObserver;
impl crate::observer::DatabaseObserver for AllowAllObserver {}
#[test]
fn test_read_gate_no_observer_is_borrowed_single_pointer_check() {
let dir = tempdir().unwrap();
let db = Database::open(dir.path()).unwrap();
db.create_collection("docs", 4, DistanceMetric::Cosine)
.unwrap();
let query = Parser::parse("SELECT * FROM docs WHERE title = 'alpha'").unwrap();
let gated = db.read_gate_cow_for_test(&query).unwrap();
assert!(
matches!(gated, std::borrow::Cow::Borrowed(_)),
"no-observer read gate must return Cow::Borrowed (no query clone)"
);
assert!(
std::ptr::eq(&raw const *gated, &raw const query),
"no-observer read gate must return the caller's own query by reference"
);
}
#[test]
fn test_read_gate_allow_observer_is_borrowed_no_clone() {
let dir = tempdir().unwrap();
let observer: std::sync::Arc<dyn crate::observer::DatabaseObserver> =
std::sync::Arc::new(AllowAllObserver);
let db = Database::open_with_observer(dir.path(), observer).unwrap();
db.create_collection("docs", 4, DistanceMetric::Cosine)
.unwrap();
let query = Parser::parse("SELECT * FROM docs").unwrap();
let gated = db.read_gate_cow_for_test(&query).unwrap();
assert!(
matches!(gated, std::borrow::Cow::Borrowed(_)),
"Allow-returning observer must keep the read gate borrowed (no clone)"
);
assert!(
std::ptr::eq(&raw const *gated, &raw const query),
"Allow read path must return the caller's own query by reference"
);
}
struct DenyObserver;
impl crate::observer::DatabaseObserver for DenyObserver {
fn on_query_request(
&self,
_ctx: &crate::observer::QueryAccessContext,
) -> crate::Result<crate::observer::AccessDecision> {
Ok(crate::observer::AccessDecision::Deny(crate::Error::Config(
"read denied by policy".to_string(),
)))
}
}
fn seed_docs(db: &Database) {
db.create_collection("docs", 4, DistanceMetric::Cosine)
.unwrap();
let coll = db.get_vector_collection("docs").unwrap();
coll.upsert(vec![
Point::new(
1,
vec![1.0, 0.0, 0.0, 0.0],
Some(serde_json::json!({"title": "alpha"})),
),
Point::new(
2,
vec![0.0, 1.0, 0.0, 0.0],
Some(serde_json::json!({"title": "beta"})),
),
])
.unwrap();
}
#[test]
fn test_gated_search_deny_fails_closed_with_zero_results() {
let dir = tempdir().unwrap();
let observer: std::sync::Arc<dyn crate::observer::DatabaseObserver> =
std::sync::Arc::new(DenyObserver);
let db = Database::open_with_observer(dir.path(), observer).unwrap();
seed_docs(&db);
let q = vec![1.0, 0.0, 0.0, 0.0];
let res = db.gated_search(
"docs",
Some("mallory"),
None,
GatedRead::Dense {
query: &q,
k: 2,
ef: None,
quality: None,
filter: None,
},
);
assert!(
res.is_err(),
"Deny observer must make gated_search fail closed (no results leaked)"
);
}
#[test]
fn test_gated_search_allow_returns_results() {
let dir = tempdir().unwrap();
let observer: std::sync::Arc<dyn crate::observer::DatabaseObserver> =
std::sync::Arc::new(AllowAllObserver);
let db = Database::open_with_observer(dir.path(), observer).unwrap();
seed_docs(&db);
let q = vec![1.0, 0.0, 0.0, 0.0];
let res = db
.gated_search(
"docs",
Some("alice"),
None,
GatedRead::Dense {
query: &q,
k: 2,
ef: None,
quality: None,
filter: None,
},
)
.unwrap();
assert!(
!res.is_empty(),
"Allow observer must let the search return neighbours"
);
}
#[test]
fn test_gated_search_no_observer_runs_search() {
let dir = tempdir().unwrap();
let db = Database::open(dir.path()).unwrap();
seed_docs(&db);
let q = vec![1.0, 0.0, 0.0, 0.0];
let res = db
.gated_search(
"docs",
None,
None,
GatedRead::Dense {
query: &q,
k: 2,
ef: None,
quality: None,
filter: None,
},
)
.unwrap();
assert_eq!(res.len(), 2);
}
#[test]
fn test_explain_analyze_match_is_gated() {
fn setup(
observer: std::sync::Arc<dyn crate::observer::DatabaseObserver>,
) -> (tempfile::TempDir, Database) {
let dir = tempdir().unwrap();
let db = Database::open_with_observer(dir.path(), observer).unwrap();
db.create_collection("docs", 2, DistanceMetric::Cosine)
.unwrap();
let coll = db.get_vector_collection("docs").unwrap();
coll.upsert(vec![
Point::new(
1,
vec![1.0, 0.0],
Some(serde_json::json!({"_labels": ["Doc"], "name": "Alice"})),
),
Point::new(
2,
vec![1.0, 0.0],
Some(serde_json::json!({"_labels": ["Doc"], "name": "Bob"})),
),
])
.unwrap();
(dir, db)
}
let query = Parser::parse("MATCH (d:Doc) RETURN d.name LIMIT 5").unwrap();
let mut params = std::collections::HashMap::new();
params.insert("_collection".to_string(), serde_json::json!("docs"));
let (_d1, allow_db) = setup(std::sync::Arc::new(AllowAllObserver));
let allowed = allow_db.explain_analyze_query(&query, ¶ms);
assert!(
allowed.is_ok(),
"AllowAll observer must let EXPLAIN ANALYZE MATCH run: {allowed:?}"
);
let (_d2, deny_db) = setup(std::sync::Arc::new(DenyObserver));
let denied = deny_db.explain_analyze_query(&query, ¶ms);
assert!(
denied.is_err(),
"Deny observer must refuse EXPLAIN ANALYZE MATCH — no gate bypass"
);
}