use super::ast::{
CompareOp, Comparison, Condition, Query, SelectColumns, SelectStatement, SimilarityCondition,
Value, VectorExpr,
};
use super::error::ParseErrorKind;
use super::validation::{QueryValidator, ValidationConfig, ValidationError, ValidationErrorKind};
#[test]
fn test_validate_multiple_similarity_with_and_passes() {
let query = create_query_with_multiple_similarity();
let result = QueryValidator::validate(&query);
assert!(result.is_ok());
}
#[test]
fn test_validate_multiple_similarity_with_or_detected() {
let query = create_query_with_multiple_similarity_or();
let result = QueryValidator::validate(&query);
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.kind, ValidationErrorKind::MultipleSimilarity);
}
#[test]
fn test_validate_single_similarity_passes() {
let query = create_query_with_single_similarity();
let result = QueryValidator::validate(&query);
assert!(result.is_ok());
}
#[test]
fn test_validate_or_with_similarity_now_passes() {
let query = create_query_with_similarity_or_metadata();
let result = QueryValidator::validate(&query);
assert!(result.is_ok());
}
#[test]
fn test_validate_and_with_similarity_passes() {
let query = create_query_with_similarity_and_metadata();
let result = QueryValidator::validate(&query);
assert!(result.is_ok());
}
#[test]
fn test_validate_not_similarity_now_passes() {
let query = create_query_with_not_similarity();
let result = QueryValidator::validate(&query);
assert!(result.is_ok());
}
#[test]
fn test_validate_not_similarity_with_limit_passes() {
let mut query = create_query_with_not_similarity();
query.select.limit = Some(100);
let result = QueryValidator::validate(&query);
assert!(result.is_ok());
}
#[test]
fn test_validate_simple_query_passes() {
let query = create_simple_query();
let result = QueryValidator::validate(&query);
assert!(result.is_ok());
}
#[test]
fn test_validate_hybrid_query_with_and_passes() {
let query = create_query_with_similarity_and_metadata();
let result = QueryValidator::validate(&query);
assert!(result.is_ok());
}
#[test]
fn test_strict_mode_allows_not_similarity() {
let query = create_query_with_not_similarity();
let config = ValidationConfig::strict();
let result = QueryValidator::validate_with_config(&query, &config);
assert!(result.is_ok());
}
#[test]
fn test_validation_error_kind_is_set() {
let query = create_query_with_multiple_similarity_or();
let result = QueryValidator::validate(&query);
let err = result.unwrap_err();
assert_eq!(err.kind, ValidationErrorKind::MultipleSimilarity);
assert!(err.position.is_none());
}
#[test]
fn test_validation_error_display_format() {
let err = ValidationError::new(
ValidationErrorKind::MultipleSimilarity,
Some(42),
"similarity(v,$v1)>0.8 AND similarity(v,$v2)>0.7",
"Use sequential queries instead",
);
let display = format!("{}", err);
assert!(display.contains("V001"));
assert!(display.contains("42"));
}
fn create_query_with_multiple_similarity() -> Query {
let sim1 = Condition::Similarity(SimilarityCondition {
field: "v".to_string(),
vector: VectorExpr::Parameter("v1".to_string()),
operator: CompareOp::Gt,
threshold: 0.8,
});
let sim2 = Condition::Similarity(SimilarityCondition {
field: "v".to_string(),
vector: VectorExpr::Parameter("v2".to_string()),
operator: CompareOp::Gt,
threshold: 0.7,
});
Query {
select: SelectStatement {
distinct: crate::velesql::DistinctMode::None,
columns: SelectColumns::All,
from: "docs".to_string(),
from_alias: vec![],
joins: vec![],
where_clause: Some(Condition::And(Box::new(sim1), Box::new(sim2))),
order_by: None,
limit: None,
offset: None,
with_clause: None,
group_by: None,
having: None,
fusion_clause: None,
},
compound: None,
match_clause: None,
dml: None,
train: None,
}
}
fn create_query_with_multiple_similarity_or() -> Query {
let sim1 = Condition::Similarity(SimilarityCondition {
field: "v".to_string(),
vector: VectorExpr::Parameter("v1".to_string()),
operator: CompareOp::Gt,
threshold: 0.8,
});
let sim2 = Condition::Similarity(SimilarityCondition {
field: "v".to_string(),
vector: VectorExpr::Parameter("v2".to_string()),
operator: CompareOp::Gt,
threshold: 0.7,
});
Query {
select: SelectStatement {
distinct: crate::velesql::DistinctMode::None,
columns: SelectColumns::All,
from: "docs".to_string(),
from_alias: vec![],
joins: vec![],
where_clause: Some(Condition::Or(Box::new(sim1), Box::new(sim2))), order_by: None,
limit: None,
offset: None,
with_clause: None,
group_by: None,
having: None,
fusion_clause: None,
},
compound: None,
match_clause: None,
dml: None,
train: None,
}
}
fn create_query_with_single_similarity() -> Query {
let sim = Condition::Similarity(SimilarityCondition {
field: "v".to_string(),
vector: VectorExpr::Parameter("v".to_string()),
operator: CompareOp::Gt,
threshold: 0.8,
});
Query {
select: SelectStatement {
distinct: crate::velesql::DistinctMode::None,
columns: SelectColumns::All,
from: "docs".to_string(),
from_alias: vec![],
joins: vec![],
where_clause: Some(sim),
order_by: None,
limit: Some(10),
offset: None,
with_clause: None,
group_by: None,
having: None,
fusion_clause: None,
},
compound: None,
match_clause: None,
dml: None,
train: None,
}
}
fn create_query_with_similarity_or_metadata() -> Query {
let sim = Condition::Similarity(SimilarityCondition {
field: "v".to_string(),
vector: VectorExpr::Parameter("v".to_string()),
operator: CompareOp::Gt,
threshold: 0.8,
});
let meta = Condition::Comparison(Comparison {
column: "category".to_string(),
operator: CompareOp::Eq,
value: Value::String("tech".to_string()),
});
Query {
select: SelectStatement {
distinct: crate::velesql::DistinctMode::None,
columns: SelectColumns::All,
from: "docs".to_string(),
from_alias: vec![],
joins: vec![],
where_clause: Some(Condition::Or(Box::new(sim), Box::new(meta))),
order_by: None,
limit: None,
offset: None,
with_clause: None,
group_by: None,
having: None,
fusion_clause: None,
},
compound: None,
match_clause: None,
dml: None,
train: None,
}
}
fn create_query_with_similarity_and_metadata() -> Query {
let sim = Condition::Similarity(SimilarityCondition {
field: "v".to_string(),
vector: VectorExpr::Parameter("v".to_string()),
operator: CompareOp::Gt,
threshold: 0.8,
});
let meta = Condition::Comparison(Comparison {
column: "category".to_string(),
operator: CompareOp::Eq,
value: Value::String("tech".to_string()),
});
Query {
select: SelectStatement {
distinct: crate::velesql::DistinctMode::None,
columns: SelectColumns::All,
from: "docs".to_string(),
from_alias: vec![],
joins: vec![],
where_clause: Some(Condition::And(Box::new(sim), Box::new(meta))),
order_by: None,
limit: Some(10),
offset: None,
with_clause: None,
group_by: None,
having: None,
fusion_clause: None,
},
compound: None,
match_clause: None,
dml: None,
train: None,
}
}
fn create_query_with_not_similarity() -> Query {
let sim = Condition::Similarity(SimilarityCondition {
field: "v".to_string(),
vector: VectorExpr::Parameter("v".to_string()),
operator: CompareOp::Gt,
threshold: 0.8,
});
Query {
select: SelectStatement {
distinct: crate::velesql::DistinctMode::None,
columns: SelectColumns::All,
from: "docs".to_string(),
from_alias: vec![],
joins: vec![],
where_clause: Some(Condition::Not(Box::new(sim))),
order_by: None,
limit: None,
offset: None,
with_clause: None,
group_by: None,
having: None,
fusion_clause: None,
},
compound: None,
match_clause: None,
dml: None,
train: None,
}
}
fn create_simple_query() -> Query {
Query {
select: SelectStatement {
distinct: crate::velesql::DistinctMode::None,
columns: SelectColumns::All,
from: "docs".to_string(),
from_alias: vec![],
joins: vec![],
where_clause: None,
order_by: None,
limit: Some(10),
offset: None,
with_clause: None,
group_by: None,
having: None,
fusion_clause: None,
},
compound: None,
match_clause: None,
dml: None,
train: None,
}
}
#[test]
fn test_validate_vector_search_near_with_or_detected() {
use crate::velesql::ast::VectorSearch;
let near1 = Condition::VectorSearch(VectorSearch {
vector: VectorExpr::Parameter("v1".to_string()),
});
let near2 = Condition::VectorSearch(VectorSearch {
vector: VectorExpr::Parameter("v2".to_string()),
});
let query = Query {
select: SelectStatement {
distinct: crate::velesql::DistinctMode::None,
columns: SelectColumns::All,
from: "docs".to_string(),
from_alias: vec![],
joins: vec![],
where_clause: Some(Condition::Or(Box::new(near1), Box::new(near2))), order_by: None,
limit: None,
offset: None,
with_clause: None,
group_by: None,
having: None,
fusion_clause: None,
},
compound: None,
match_clause: None,
dml: None,
train: None,
};
let result = QueryValidator::validate(&query);
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.kind, ValidationErrorKind::MultipleSimilarity);
}
#[test]
fn test_validate_vector_search_or_now_passes() {
use crate::velesql::ast::VectorSearch;
let near = Condition::VectorSearch(VectorSearch {
vector: VectorExpr::Parameter("v".to_string()),
});
let meta = Condition::Comparison(Comparison {
column: "category".to_string(),
operator: CompareOp::Eq,
value: Value::String("tech".to_string()),
});
let query = Query {
select: SelectStatement {
distinct: crate::velesql::DistinctMode::None,
columns: SelectColumns::All,
from: "docs".to_string(),
from_alias: vec![],
joins: vec![],
where_clause: Some(Condition::Or(Box::new(near), Box::new(meta))),
order_by: None,
limit: None,
offset: None,
with_clause: None,
group_by: None,
having: None,
fusion_clause: None,
},
compound: None,
match_clause: None,
dml: None,
train: None,
};
let result = QueryValidator::validate(&query);
assert!(result.is_ok());
}
#[test]
fn test_validate_compound_query_where_clause() {
use crate::velesql::ast::{CompoundQuery, SetOperator};
let sim1 = Condition::Similarity(SimilarityCondition {
field: "v".to_string(),
vector: VectorExpr::Parameter("v1".to_string()),
operator: CompareOp::Gt,
threshold: 0.8,
});
let sim2 = Condition::Similarity(SimilarityCondition {
field: "v".to_string(),
vector: VectorExpr::Parameter("v2".to_string()),
operator: CompareOp::Gt,
threshold: 0.7,
});
let query = Query {
select: SelectStatement {
distinct: crate::velesql::DistinctMode::None,
columns: SelectColumns::All,
from: "docs".to_string(),
from_alias: vec![],
joins: vec![],
where_clause: None,
order_by: None,
limit: Some(10),
offset: None,
with_clause: None,
group_by: None,
having: None,
fusion_clause: None,
},
compound: Some(CompoundQuery {
operator: SetOperator::Union,
right: Box::new(SelectStatement {
distinct: crate::velesql::DistinctMode::None,
columns: SelectColumns::All,
from: "docs".to_string(),
from_alias: vec![],
joins: vec![],
where_clause: Some(Condition::Or(Box::new(sim1), Box::new(sim2))), order_by: None,
limit: None,
offset: None,
with_clause: None,
group_by: None,
having: None,
fusion_clause: None,
}),
}),
match_clause: None,
dml: None,
train: None,
};
let result = QueryValidator::validate(&query);
assert!(result.is_err());
let err = result.unwrap_err();
assert_eq!(err.kind, ValidationErrorKind::MultipleSimilarity);
}
use crate::velesql::ast::VectorSearch;
fn make_query(where_clause: Option<Condition>) -> Query {
Query {
select: SelectStatement {
distinct: crate::velesql::DistinctMode::None,
columns: SelectColumns::All,
from: "test".to_string(),
from_alias: vec![],
joins: vec![],
where_clause,
order_by: None,
limit: None,
offset: None,
with_clause: None,
group_by: None,
having: None,
fusion_clause: None,
},
compound: None,
match_clause: None,
dml: None,
train: None,
}
}
fn make_comparison(col: &str, val: i64) -> Condition {
Condition::Comparison(Comparison {
column: col.to_string(),
operator: CompareOp::Eq,
value: Value::Integer(val),
})
}
fn make_similarity() -> Condition {
Condition::Similarity(SimilarityCondition {
field: "embedding".to_string(),
vector: VectorExpr::Parameter("v".to_string()),
operator: CompareOp::Gt,
threshold: 0.8,
})
}
fn make_vector_search() -> Condition {
Condition::VectorSearch(VectorSearch {
vector: VectorExpr::Parameter("v".to_string()),
})
}
#[test]
fn test_validation_error_display() {
let err = ValidationError::multiple_similarity("test");
let display = format!("{err}");
assert!(display.contains("V001"));
assert!(display.contains("Use AND instead of OR with similarity()"));
}
#[test]
fn test_validation_error_display_with_position() {
let err = ValidationError::new(
ValidationErrorKind::MultipleSimilarity,
Some(42),
"fragment",
"suggestion",
);
let display = format!("{err}");
assert!(display.contains("position 42"));
}
#[test]
fn test_validation_error_similarity_with_or() {
let err = ValidationError::similarity_with_or("test OR");
assert_eq!(err.kind, ValidationErrorKind::SimilarityWithOr);
assert!(err.suggestion.contains("AND"));
}
#[test]
fn test_validation_error_not_similarity() {
let err = ValidationError::not_similarity("NOT sim");
assert_eq!(err.kind, ValidationErrorKind::NotSimilarity);
assert!(err.suggestion.contains("LIMIT"));
}
#[test]
fn test_validation_error_kind_codes() {
assert_eq!(ValidationErrorKind::MultipleSimilarity.code(), "V001");
assert_eq!(ValidationErrorKind::SimilarityWithOr.code(), "V002");
assert_eq!(ValidationErrorKind::NotSimilarity.code(), "V003");
assert_eq!(ValidationErrorKind::ReservedKeyword.code(), "V004");
assert_eq!(ValidationErrorKind::StringEscaping.code(), "V005");
}
#[test]
fn test_validation_error_kind_messages() {
assert!(ValidationErrorKind::MultipleSimilarity
.message()
.contains("Multiple"));
assert!(ValidationErrorKind::SimilarityWithOr
.message()
.contains("OR"));
assert!(ValidationErrorKind::NotSimilarity
.message()
.contains("full scan"));
assert!(ValidationErrorKind::ReservedKeyword
.message()
.contains("escaping"));
assert!(ValidationErrorKind::StringEscaping
.message()
.contains("string"));
}
#[test]
fn test_validation_config_default() {
let config = ValidationConfig::default();
assert!(config.strict_not_similarity);
}
#[test]
fn test_validation_config_strict() {
let config = ValidationConfig::strict();
assert!(config.strict_not_similarity);
}
#[test]
fn test_validation_config_lenient() {
let config = ValidationConfig::lenient();
assert!(!config.strict_not_similarity);
}
#[test]
fn test_validate_empty_query() {
let query = make_query(None);
assert!(QueryValidator::validate(&query).is_ok());
}
#[test]
fn test_validate_simple_comparison() {
let query = make_query(Some(make_comparison("age", 25)));
assert!(QueryValidator::validate(&query).is_ok());
}
#[test]
fn test_validate_single_similarity() {
let query = make_query(Some(make_similarity()));
assert!(QueryValidator::validate(&query).is_ok());
}
#[test]
fn test_validate_single_vector_search() {
let query = make_query(Some(make_vector_search()));
assert!(QueryValidator::validate(&query).is_ok());
}
#[test]
fn test_validate_similarity_and_comparison() {
let cond = Condition::And(
Box::new(make_similarity()),
Box::new(make_comparison("category", 1)),
);
let query = make_query(Some(cond));
assert!(QueryValidator::validate(&query).is_ok());
}
#[test]
fn test_validate_multiple_similarity_in_and() {
let cond = Condition::And(Box::new(make_similarity()), Box::new(make_similarity()));
let query = make_query(Some(cond));
assert!(QueryValidator::validate(&query).is_ok());
}
#[test]
fn test_validate_multiple_similarity_in_or_rejected() {
let cond = Condition::Or(Box::new(make_similarity()), Box::new(make_similarity()));
let query = make_query(Some(cond));
let result = QueryValidator::validate(&query);
assert!(result.is_err());
assert_eq!(
result.unwrap_err().kind,
ValidationErrorKind::MultipleSimilarity
);
}
#[test]
fn test_validate_similarity_or_metadata_allowed() {
let cond = Condition::Or(
Box::new(make_similarity()),
Box::new(make_comparison("status", 1)),
);
let query = make_query(Some(cond));
assert!(QueryValidator::validate(&query).is_ok());
}
#[test]
fn test_validate_not_similarity_allowed() {
let cond = Condition::Not(Box::new(make_similarity()));
let query = make_query(Some(cond));
assert!(QueryValidator::validate(&query).is_ok());
}
#[test]
fn test_validate_grouped_condition() {
let cond = Condition::Group(Box::new(make_similarity()));
let query = make_query(Some(cond));
assert!(QueryValidator::validate(&query).is_ok());
}
#[test]
fn test_validate_nested_and_or() {
let inner = Condition::And(
Box::new(make_similarity()),
Box::new(make_comparison("a", 1)),
);
let cond = Condition::Or(Box::new(inner), Box::new(make_comparison("b", 2)));
let query = make_query(Some(cond));
assert!(QueryValidator::validate(&query).is_ok());
}
#[test]
fn test_validate_deeply_nested_multiple_sim_or() {
let inner_or = Condition::Or(Box::new(make_similarity()), Box::new(make_similarity()));
let cond = Condition::Group(Box::new(inner_or));
let query = make_query(Some(cond));
assert!(QueryValidator::validate(&query).is_err());
}
#[test]
fn test_validate_with_config_lenient() {
let query = make_query(Some(Condition::Not(Box::new(make_similarity()))));
let config = ValidationConfig::lenient();
assert!(QueryValidator::validate_with_config(&query, &config).is_ok());
}
#[test]
fn test_count_similarity_conditions_none() {
let cond = make_comparison("x", 1);
assert_eq!(QueryValidator::count_similarity_conditions(&cond), 0);
}
#[test]
fn test_count_similarity_conditions_one() {
let cond = make_similarity();
assert_eq!(QueryValidator::count_similarity_conditions(&cond), 1);
}
#[test]
fn test_count_similarity_conditions_multiple() {
let cond = Condition::And(
Box::new(make_similarity()),
Box::new(Condition::Or(
Box::new(make_vector_search()),
Box::new(make_comparison("x", 1)),
)),
);
assert_eq!(QueryValidator::count_similarity_conditions(&cond), 2);
}
#[test]
fn test_contains_similarity_true() {
let cond = Condition::And(
Box::new(make_comparison("x", 1)),
Box::new(make_similarity()),
);
assert!(QueryValidator::contains_similarity(&cond));
}
#[test]
fn test_contains_similarity_false() {
let cond = make_comparison("x", 1);
assert!(!QueryValidator::contains_similarity(&cond));
}
#[test]
fn test_has_not_similarity_true() {
let cond = Condition::Not(Box::new(make_similarity()));
assert!(QueryValidator::has_not_similarity(&cond));
}
#[test]
fn test_has_not_similarity_nested() {
let cond = Condition::And(
Box::new(make_comparison("x", 1)),
Box::new(Condition::Not(Box::new(make_similarity()))),
);
assert!(QueryValidator::has_not_similarity(&cond));
}
#[test]
fn test_has_not_similarity_false() {
let cond = make_similarity();
assert!(!QueryValidator::has_not_similarity(&cond));
}
#[test]
fn test_validation_error_is_error_trait() {
let err = ValidationError::multiple_similarity("test");
let _: &dyn std::error::Error = &err;
}
#[test]
fn test_complexity_rejects_query_length() {
let query = create_simple_query();
let cfg = ValidationConfig {
max_query_length: 8,
..ValidationConfig::default()
};
let err = QueryValidator::enforce_query_complexity(&query, "SELECT * FROM docs", &cfg)
.expect_err("must reject long query");
assert_eq!(err.kind, ParseErrorKind::ComplexityLimit);
assert!(err.message.contains("Query length exceeded"));
}
#[test]
fn test_complexity_rejects_like_budget() {
let c1 = Condition::Like(super::ast::LikeCondition {
column: "title".into(),
pattern: "%rust%".into(),
case_insensitive: false,
});
let c2 = Condition::Like(super::ast::LikeCondition {
column: "body".into(),
pattern: "%db%".into(),
case_insensitive: true,
});
let query = Query {
select: SelectStatement {
distinct: super::ast::DistinctMode::None,
columns: SelectColumns::All,
from: "docs".into(),
from_alias: vec![],
joins: vec![],
where_clause: Some(Condition::And(Box::new(c1), Box::new(c2))),
order_by: None,
limit: None,
offset: None,
with_clause: None,
group_by: None,
having: None,
fusion_clause: None,
},
compound: None,
match_clause: None,
dml: None,
train: None,
};
let cfg = ValidationConfig {
max_like_ilike_terms: 1,
..ValidationConfig::default()
};
let err = QueryValidator::enforce_query_complexity(&query, "SELECT", &cfg)
.expect_err("must reject like budget");
assert!(err.message.contains("LIKE/ILIKE budget exceeded"));
}
#[test]
fn test_complexity_rejects_graph_expansion_budget() {
let parsed = super::Parser::parse("MATCH (a)-[*1..5]->(b) RETURN a").expect("valid");
let mc = parsed
.match_clause
.as_ref()
.expect("graph query should include match clause");
assert_eq!(mc.patterns.len(), 1);
assert_eq!(mc.patterns[0].relationships.len(), 1);
assert_eq!(mc.patterns[0].relationships[0].range, Some((1, 5)));
let cfg = ValidationConfig {
max_graph_expansion: 3,
..ValidationConfig::default()
};
let err =
QueryValidator::enforce_query_complexity(&parsed, "MATCH (a)-[*1..5]->(b) RETURN a", &cfg)
.expect_err("must reject graph expansion");
assert!(err.message.contains("Graph expansion exceeded"));
}