#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum QueryFeature {
TriplePattern,
OptionalClause,
FilterExpression,
SubQuery,
Aggregation,
ServiceCall,
PropertyPath,
GroupBy,
OrderBy,
Distinct,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ComplexityLevel {
Trivial,
Simple,
Moderate,
Complex,
VeryComplex,
}
impl std::fmt::Display for ComplexityLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let label = match self {
ComplexityLevel::Trivial => "Trivial",
ComplexityLevel::Simple => "Simple",
ComplexityLevel::Moderate => "Moderate",
ComplexityLevel::Complex => "Complex",
ComplexityLevel::VeryComplex => "VeryComplex",
};
write!(f, "{label}")
}
}
#[derive(Debug, Clone)]
pub struct QueryProfile {
pub query_text: String,
pub features: Vec<QueryFeature>,
pub estimated_cost: f64,
pub complexity: ComplexityLevel,
pub triple_pattern_count: usize,
pub optional_count: usize,
pub filter_count: usize,
pub subquery_depth: usize,
pub warnings: Vec<String>,
}
pub struct ProfileCommand;
const COST_TRIPLE: f64 = 1.0;
const COST_OPTIONAL: f64 = 3.0;
const COST_FILTER: f64 = 0.5;
const COST_SUBQUERY: f64 = 10.0;
const COST_AGGREGATION: f64 = 5.0;
const COST_SERVICE: f64 = 20.0;
const COST_PROPERTY_PATH: f64 = 4.0;
const COST_GROUP_BY: f64 = 2.0;
const COST_ORDER_BY: f64 = 1.0;
const COST_DISTINCT: f64 = 1.5;
impl ProfileCommand {
pub fn new() -> Self {
Self
}
pub fn profile(query: &str) -> QueryProfile {
let upper = query.to_uppercase();
let triple_count = Self::count_triple_patterns(query);
let optional_count = Self::count_keyword_occurrences(&upper, "OPTIONAL");
let filter_count = Self::count_keyword_occurrences(&upper, "FILTER");
let service_count = Self::count_keyword_occurrences(&upper, "SERVICE");
let subquery_depth = Self::measure_subquery_depth(query);
let has_aggregation = Self::has_aggregation(&upper);
let has_group_by = upper.contains("GROUP BY");
let has_order_by = upper.contains("ORDER BY");
let has_distinct = upper.contains("DISTINCT");
let has_property_path = Self::has_property_path(query);
let mut features: Vec<QueryFeature> = Vec::new();
if triple_count > 0 {
features.push(QueryFeature::TriplePattern);
}
if optional_count > 0 {
features.push(QueryFeature::OptionalClause);
}
if filter_count > 0 {
features.push(QueryFeature::FilterExpression);
}
if subquery_depth > 0 {
features.push(QueryFeature::SubQuery);
}
if has_aggregation {
features.push(QueryFeature::Aggregation);
}
if service_count > 0 {
features.push(QueryFeature::ServiceCall);
}
if has_property_path {
features.push(QueryFeature::PropertyPath);
}
if has_group_by {
features.push(QueryFeature::GroupBy);
}
if has_order_by {
features.push(QueryFeature::OrderBy);
}
if has_distinct {
features.push(QueryFeature::Distinct);
}
let mut cost = 0.0_f64;
cost += triple_count as f64 * COST_TRIPLE;
cost += optional_count as f64 * COST_OPTIONAL;
cost += filter_count as f64 * COST_FILTER;
cost += subquery_depth as f64 * COST_SUBQUERY;
if has_aggregation {
cost += COST_AGGREGATION;
}
cost += service_count as f64 * COST_SERVICE;
if has_property_path {
cost += COST_PROPERTY_PATH;
}
if has_group_by {
cost += COST_GROUP_BY;
}
if has_order_by {
cost += COST_ORDER_BY;
}
if has_distinct {
cost += COST_DISTINCT;
}
let complexity = Self::classify_complexity(cost);
let mut profile = QueryProfile {
query_text: query.to_string(),
features,
estimated_cost: cost,
complexity,
triple_pattern_count: triple_count,
optional_count,
filter_count,
subquery_depth,
warnings: Vec::new(),
};
profile.warnings = Self::suggest_optimizations(&profile);
profile
}
pub fn estimate_cost(profile: &QueryProfile) -> f64 {
profile.estimated_cost
}
pub fn classify_complexity(cost: f64) -> ComplexityLevel {
if cost < 2.0 {
ComplexityLevel::Trivial
} else if cost < 5.0 {
ComplexityLevel::Simple
} else if cost < 15.0 {
ComplexityLevel::Moderate
} else if cost < 50.0 {
ComplexityLevel::Complex
} else {
ComplexityLevel::VeryComplex
}
}
pub fn suggest_optimizations(profile: &QueryProfile) -> Vec<String> {
let mut hints: Vec<String> = Vec::new();
let query_upper = profile.query_text.to_uppercase();
if !query_upper.contains("LIMIT") && query_upper.contains("SELECT") {
hints.push(
"Consider adding LIMIT to cap the result set and reduce memory usage.".to_string(),
);
}
if profile.features.contains(&QueryFeature::ServiceCall) {
hints.push(
"SERVICE calls are expensive and involve network latency; \
cache results where possible."
.to_string(),
);
}
if profile.subquery_depth >= 2 {
hints.push(
"Deeply nested sub-queries impact performance; consider flattening or \
materialising intermediate results."
.to_string(),
);
} else if profile.subquery_depth == 1 {
hints.push(
"Sub-queries add overhead; ensure they are necessary and properly indexed."
.to_string(),
);
}
if profile.optional_count >= 3 {
hints.push(
"Many OPTIONAL clauses degrade join performance; \
move mandatory patterns to the main WHERE block."
.to_string(),
);
}
if profile.features.contains(&QueryFeature::PropertyPath) {
hints.push(
"Property paths with `*` or `+` can traverse unbounded graph depth; \
add explicit depth limits where possible."
.to_string(),
);
}
if profile.features.contains(&QueryFeature::Aggregation)
&& !profile.features.contains(&QueryFeature::GroupBy)
{
hints.push(
"Aggregation without GROUP BY produces a single row; \
verify this is intentional."
.to_string(),
);
}
if profile.triple_pattern_count >= 5 && profile.filter_count == 0 {
hints.push(
"Many triple patterns without FILTER may produce a Cartesian product; \
add selective FILTER conditions."
.to_string(),
);
}
hints
}
pub fn format_report(profile: &QueryProfile) -> String {
let mut lines: Vec<String> = Vec::new();
lines.push("=== SPARQL Query Profile Report ===".to_string());
lines.push(format!("Complexity : {}", profile.complexity));
lines.push(format!("Estimated Cost: {:.2}", profile.estimated_cost));
lines.push(format!(
"Triple Patterns : {}",
profile.triple_pattern_count
));
lines.push(format!("OPTIONAL Clauses: {}", profile.optional_count));
lines.push(format!("FILTER Expressions: {}", profile.filter_count));
lines.push(format!("Sub-query Depth: {}", profile.subquery_depth));
if !profile.features.is_empty() {
lines.push("Detected Features:".to_string());
for f in &profile.features {
lines.push(format!(" - {:?}", f));
}
}
if !profile.warnings.is_empty() {
lines.push("Optimisation Hints:".to_string());
for w in &profile.warnings {
lines.push(format!(" * {w}"));
}
} else {
lines.push("Optimisation Hints: none".to_string());
}
lines.join("\n")
}
fn count_keyword_occurrences(upper_query: &str, keyword: &str) -> usize {
let mut count = 0;
let mut start = 0;
while let Some(pos) = upper_query[start..].find(keyword) {
let abs = start + pos;
let before_ok = abs == 0
|| !upper_query
.as_bytes()
.get(abs - 1)
.is_some_and(|b| b.is_ascii_alphanumeric() || *b == b'_');
let after = abs + keyword.len();
let after_ok = after >= upper_query.len()
|| !upper_query
.as_bytes()
.get(after)
.is_some_and(|b| b.is_ascii_alphanumeric() || *b == b'_');
if before_ok && after_ok {
count += 1;
}
start = abs + 1;
}
count
}
fn count_triple_patterns(query: &str) -> usize {
let upper = query.to_uppercase();
let where_start = upper.find("WHERE").map(|i| i + 5).unwrap_or(0);
let where_body = &query[where_start..];
let dot_count = where_body
.chars()
.enumerate()
.filter(|(i, c)| {
if *c != '.' {
return false;
}
let bytes = where_body.as_bytes();
let prev = if *i > 0 {
bytes.get(i - 1).copied()
} else {
None
};
let next = bytes.get(i + 1).copied();
let prev_ws = prev.map_or(true, |b| {
b == b' '
|| b == b'\t'
|| b == b'\n'
|| b == b'\r'
|| b == b'}'
|| b == b'"'
|| b == b'>'
});
let next_ws = next.map_or(true, |b| {
b == b' ' || b == b'\t' || b == b'\n' || b == b'\r' || b == b'{'
});
prev_ws && next_ws
})
.count();
let base = if where_body.contains('{') { 1 } else { 0 };
(base + dot_count).max(if where_body.contains('{') { 1 } else { 0 })
}
fn measure_subquery_depth(query: &str) -> usize {
let upper = query.to_uppercase();
let main_where = match upper.find("WHERE") {
Some(i) => i + 5,
None => return 0,
};
let body = &upper[main_where..];
let mut depth = 0_usize;
let mut start = 0;
while let Some(pos) = body[start..].find("SELECT") {
let abs = start + pos;
let before_ok = abs == 0
|| !body
.as_bytes()
.get(abs - 1)
.is_some_and(|b| b.is_ascii_alphabetic());
let after = abs + 6;
let after_ok = after >= body.len()
|| !body
.as_bytes()
.get(after)
.is_some_and(|b| b.is_ascii_alphabetic());
if before_ok && after_ok {
depth += 1;
}
start = abs + 1;
}
depth
}
fn has_aggregation(upper_query: &str) -> bool {
let agg_funcs = [
"COUNT(",
"SUM(",
"AVG(",
"MIN(",
"MAX(",
"GROUP_CONCAT(",
"SAMPLE(",
];
agg_funcs.iter().any(|f| upper_query.contains(f))
}
fn has_property_path(query: &str) -> bool {
let upper = query.to_uppercase();
let where_start = upper.find("WHERE").map(|i| i + 5).unwrap_or(0);
let body = &query[where_start..];
let mut in_iri = false;
let mut in_string = false;
let chars: Vec<char> = body.chars().collect();
let n = chars.len();
let mut i = 0;
while i < n {
match chars[i] {
'<' if !in_string => {
in_iri = true;
}
'>' if in_iri => {
in_iri = false;
}
'"' if !in_iri => {
in_string = !in_string;
}
'/' | '*' | '+' | '?' if !in_iri && !in_string => {
if chars[i] == '?' {
let next = chars.get(i + 1);
if next.is_some_and(|c| c.is_ascii_alphanumeric() || *c == '_') {
i += 1;
continue;
}
}
if chars[i] == '*' {
let prev = if i > 0 { Some(chars[i - 1]) } else { None };
let next = chars.get(i + 1);
let looks_like_path = prev.is_some_and(|c| !c.is_whitespace())
|| next.is_some_and(|c| !c.is_whitespace() && *c != ' ');
if !looks_like_path {
i += 1;
continue;
}
}
return true;
}
_ => {}
}
i += 1;
}
false
}
}
impl Default for ProfileCommand {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
fn features_of(query: &str) -> Vec<QueryFeature> {
ProfileCommand::profile(query).features
}
fn has_feature(query: &str, feature: &QueryFeature) -> bool {
features_of(query).contains(feature)
}
#[test]
fn test_complexity_display_trivial() {
assert_eq!(ComplexityLevel::Trivial.to_string(), "Trivial");
}
#[test]
fn test_complexity_display_simple() {
assert_eq!(ComplexityLevel::Simple.to_string(), "Simple");
}
#[test]
fn test_complexity_display_moderate() {
assert_eq!(ComplexityLevel::Moderate.to_string(), "Moderate");
}
#[test]
fn test_complexity_display_complex() {
assert_eq!(ComplexityLevel::Complex.to_string(), "Complex");
}
#[test]
fn test_complexity_display_very_complex() {
assert_eq!(ComplexityLevel::VeryComplex.to_string(), "VeryComplex");
}
#[test]
fn test_classify_zero_cost_trivial() {
assert_eq!(
ProfileCommand::classify_complexity(0.0),
ComplexityLevel::Trivial
);
}
#[test]
fn test_classify_just_below_2_trivial() {
assert_eq!(
ProfileCommand::classify_complexity(1.9),
ComplexityLevel::Trivial
);
}
#[test]
fn test_classify_exactly_2_simple() {
assert_eq!(
ProfileCommand::classify_complexity(2.0),
ComplexityLevel::Simple
);
}
#[test]
fn test_classify_4_9_simple() {
assert_eq!(
ProfileCommand::classify_complexity(4.9),
ComplexityLevel::Simple
);
}
#[test]
fn test_classify_5_moderate() {
assert_eq!(
ProfileCommand::classify_complexity(5.0),
ComplexityLevel::Moderate
);
}
#[test]
fn test_classify_14_9_moderate() {
assert_eq!(
ProfileCommand::classify_complexity(14.9),
ComplexityLevel::Moderate
);
}
#[test]
fn test_classify_15_complex() {
assert_eq!(
ProfileCommand::classify_complexity(15.0),
ComplexityLevel::Complex
);
}
#[test]
fn test_classify_49_9_complex() {
assert_eq!(
ProfileCommand::classify_complexity(49.9),
ComplexityLevel::Complex
);
}
#[test]
fn test_classify_50_very_complex() {
assert_eq!(
ProfileCommand::classify_complexity(50.0),
ComplexityLevel::VeryComplex
);
}
#[test]
fn test_classify_100_very_complex() {
assert_eq!(
ProfileCommand::classify_complexity(100.0),
ComplexityLevel::VeryComplex
);
}
#[test]
fn test_detect_triple_pattern_basic() {
let q = "SELECT ?s ?p ?o WHERE { ?s ?p ?o }";
assert!(has_feature(q, &QueryFeature::TriplePattern));
}
#[test]
fn test_triple_pattern_count_single() {
let q = "SELECT * WHERE { ?s ?p ?o }";
let p = ProfileCommand::profile(q);
assert!(p.triple_pattern_count >= 1);
}
#[test]
fn test_triple_pattern_count_multiple_with_dots() {
let q = "SELECT * WHERE { ?s ?p ?o . ?a ?b ?c }";
let p = ProfileCommand::profile(q);
assert!(p.triple_pattern_count >= 2);
}
#[test]
fn test_detect_optional_clause() {
let q = "SELECT ?s WHERE { ?s <p> ?o OPTIONAL { ?s <q> ?r } }";
assert!(has_feature(q, &QueryFeature::OptionalClause));
}
#[test]
fn test_optional_count_two() {
let q = "SELECT ?s WHERE { ?s <p> ?o OPTIONAL { ?s <q> ?r } OPTIONAL { ?s <x> ?y } }";
let p = ProfileCommand::profile(q);
assert_eq!(p.optional_count, 2);
}
#[test]
fn test_no_optional_in_simple_query() {
let q = "SELECT ?s WHERE { ?s <p> ?o }";
assert!(!has_feature(q, &QueryFeature::OptionalClause));
}
#[test]
fn test_detect_filter_expression() {
let q = "SELECT ?s WHERE { ?s <age> ?a FILTER(?a > 18) }";
assert!(has_feature(q, &QueryFeature::FilterExpression));
}
#[test]
fn test_filter_count_two() {
let q = "SELECT ?s WHERE { ?s <a> ?x FILTER(?x > 0) FILTER(?x < 100) }";
let p = ProfileCommand::profile(q);
assert_eq!(p.filter_count, 2);
}
#[test]
fn test_no_filter_in_simple_query() {
let q = "SELECT ?s WHERE { ?s ?p ?o }";
assert!(!has_feature(q, &QueryFeature::FilterExpression));
}
#[test]
fn test_detect_subquery() {
let q = "SELECT ?s WHERE { { SELECT ?s WHERE { ?s ?p ?o } } }";
assert!(has_feature(q, &QueryFeature::SubQuery));
}
#[test]
fn test_subquery_depth_one() {
let q = "SELECT ?s WHERE { { SELECT ?s WHERE { ?s ?p ?o } } }";
let p = ProfileCommand::profile(q);
assert_eq!(p.subquery_depth, 1);
}
#[test]
fn test_no_subquery_in_flat_query() {
let q = "SELECT ?s WHERE { ?s ?p ?o }";
assert!(!has_feature(q, &QueryFeature::SubQuery));
}
#[test]
fn test_detect_count_aggregation() {
let q = "SELECT (COUNT(?s) AS ?n) WHERE { ?s ?p ?o }";
assert!(has_feature(q, &QueryFeature::Aggregation));
}
#[test]
fn test_detect_sum_aggregation() {
let q = "SELECT (SUM(?v) AS ?total) WHERE { ?s <val> ?v }";
assert!(has_feature(q, &QueryFeature::Aggregation));
}
#[test]
fn test_detect_avg_aggregation() {
let q = "SELECT (AVG(?v) AS ?avg) WHERE { ?s <v> ?v }";
assert!(has_feature(q, &QueryFeature::Aggregation));
}
#[test]
fn test_detect_min_aggregation() {
let q = "SELECT (MIN(?v) AS ?m) WHERE { ?s <v> ?v }";
assert!(has_feature(q, &QueryFeature::Aggregation));
}
#[test]
fn test_detect_max_aggregation() {
let q = "SELECT (MAX(?v) AS ?m) WHERE { ?s <v> ?v }";
assert!(has_feature(q, &QueryFeature::Aggregation));
}
#[test]
fn test_no_aggregation_in_simple_select() {
let q = "SELECT ?s ?p ?o WHERE { ?s ?p ?o }";
assert!(!has_feature(q, &QueryFeature::Aggregation));
}
#[test]
fn test_detect_service_call() {
let q = "SELECT ?s WHERE { SERVICE <http://example.org/sparql> { ?s ?p ?o } }";
assert!(has_feature(q, &QueryFeature::ServiceCall));
}
#[test]
fn test_no_service_in_local_query() {
let q = "SELECT ?s WHERE { ?s ?p ?o }";
assert!(!has_feature(q, &QueryFeature::ServiceCall));
}
#[test]
fn test_detect_property_path_slash() {
let q = "SELECT ?o WHERE { ?s <a>/<b> ?o }";
assert!(has_feature(q, &QueryFeature::PropertyPath));
}
#[test]
fn test_detect_property_path_plus() {
let q = "SELECT ?o WHERE { ?s <knows>+ ?o }";
assert!(has_feature(q, &QueryFeature::PropertyPath));
}
#[test]
fn test_no_property_path_in_simple_query() {
let q = "SELECT ?s WHERE { ?s <p> ?o }";
assert!(!has_feature(q, &QueryFeature::PropertyPath));
}
#[test]
fn test_detect_group_by() {
let q = "SELECT ?s (COUNT(?o) AS ?c) WHERE { ?s ?p ?o } GROUP BY ?s";
assert!(has_feature(q, &QueryFeature::GroupBy));
}
#[test]
fn test_no_group_by_in_simple_query() {
let q = "SELECT ?s WHERE { ?s ?p ?o }";
assert!(!has_feature(q, &QueryFeature::GroupBy));
}
#[test]
fn test_detect_order_by() {
let q = "SELECT ?s WHERE { ?s ?p ?o } ORDER BY ?s";
assert!(has_feature(q, &QueryFeature::OrderBy));
}
#[test]
fn test_no_order_by_in_simple_query() {
let q = "SELECT ?s WHERE { ?s ?p ?o }";
assert!(!has_feature(q, &QueryFeature::OrderBy));
}
#[test]
fn test_detect_distinct() {
let q = "SELECT DISTINCT ?s WHERE { ?s ?p ?o }";
assert!(has_feature(q, &QueryFeature::Distinct));
}
#[test]
fn test_no_distinct_in_simple_query() {
let q = "SELECT ?s WHERE { ?s ?p ?o }";
assert!(!has_feature(q, &QueryFeature::Distinct));
}
#[test]
fn test_estimate_cost_simple_triple() {
let q = "SELECT * WHERE { ?s ?p ?o }";
let p = ProfileCommand::profile(q);
assert!(p.estimated_cost >= 1.0);
}
#[test]
fn test_estimate_cost_with_optional() {
let q = "SELECT ?s WHERE { ?s ?p ?o OPTIONAL { ?s <q> ?r } }";
let p = ProfileCommand::profile(q);
assert!(p.estimated_cost >= 4.0);
}
#[test]
fn test_estimate_cost_with_service() {
let q = "SELECT ?s WHERE { SERVICE <http://x.org/s> { ?s ?p ?o } }";
let p = ProfileCommand::profile(q);
assert!(p.estimated_cost >= 20.0);
}
#[test]
fn test_estimate_cost_with_aggregation() {
let q = "SELECT (COUNT(?s) AS ?c) WHERE { ?s ?p ?o }";
let p = ProfileCommand::profile(q);
assert!(p.estimated_cost >= 6.0);
}
#[test]
fn test_estimate_cost_matches_profile_field() {
let q = "SELECT ?s WHERE { ?s ?p ?o }";
let p = ProfileCommand::profile(q);
assert!((ProfileCommand::estimate_cost(&p) - p.estimated_cost).abs() < f64::EPSILON);
}
#[test]
fn test_empty_query_trivial_complexity() {
let p = ProfileCommand::profile("");
assert_eq!(p.complexity, ComplexityLevel::Trivial);
}
#[test]
fn test_service_query_very_complex() {
let q = "SELECT ?s WHERE { SERVICE <http://a.org/> { ?s ?p ?o } \
SERVICE <http://b.org/> { ?s ?q ?r } \
SERVICE <http://c.org/> { ?s ?x ?y } }";
let p = ProfileCommand::profile(q);
assert_eq!(p.complexity, ComplexityLevel::VeryComplex);
}
#[test]
fn test_simple_select_moderate_or_lower() {
let q = "SELECT ?s ?p ?o WHERE { ?s ?p ?o }";
let p = ProfileCommand::profile(q);
assert!(matches!(
p.complexity,
ComplexityLevel::Trivial | ComplexityLevel::Simple | ComplexityLevel::Moderate
));
}
#[test]
fn test_suggest_limit_for_select_without_limit() {
let q = "SELECT ?s WHERE { ?s ?p ?o }";
let hints = ProfileCommand::suggest_optimizations(&ProfileCommand::profile(q));
assert!(hints.iter().any(|h| h.to_lowercase().contains("limit")));
}
#[test]
fn test_no_limit_hint_when_limit_present() {
let q = "SELECT ?s WHERE { ?s ?p ?o } LIMIT 100";
let hints = ProfileCommand::suggest_optimizations(&ProfileCommand::profile(q));
assert!(!hints.iter().any(|h| h.to_lowercase().contains("limit")));
}
#[test]
fn test_suggest_service_hint() {
let q = "SELECT ?s WHERE { SERVICE <http://x.org/> { ?s ?p ?o } }";
let hints = ProfileCommand::suggest_optimizations(&ProfileCommand::profile(q));
assert!(hints.iter().any(|h| h.to_lowercase().contains("service")));
}
#[test]
fn test_suggest_subquery_hint_for_nested() {
let q = "SELECT ?s WHERE { { SELECT ?s WHERE { ?s ?p ?o } } }";
let hints = ProfileCommand::suggest_optimizations(&ProfileCommand::profile(q));
assert!(hints.iter().any(|h| h.to_lowercase().contains("sub")));
}
#[test]
fn test_suggest_many_optionals_hint() {
let q = "SELECT ?s WHERE { \
?s <a> ?x OPTIONAL { ?s <b> ?y } OPTIONAL { ?s <c> ?z } OPTIONAL { ?s <d> ?w } \
}";
let hints = ProfileCommand::suggest_optimizations(&ProfileCommand::profile(q));
assert!(hints.iter().any(|h| h.to_lowercase().contains("optional")));
}
#[test]
fn test_suggest_aggregation_without_group_by() {
let q = "SELECT (COUNT(?s) AS ?c) WHERE { ?s ?p ?o }";
let hints = ProfileCommand::suggest_optimizations(&ProfileCommand::profile(q));
assert!(hints.iter().any(|h| h.to_lowercase().contains("group")));
}
#[test]
fn test_no_aggregation_hint_with_group_by() {
let q = "SELECT ?s (COUNT(?o) AS ?c) WHERE { ?s ?p ?o } GROUP BY ?s";
let hints = ProfileCommand::suggest_optimizations(&ProfileCommand::profile(q));
assert!(!hints
.iter()
.any(|h| h.contains("Aggregation without GROUP BY")));
}
#[test]
fn test_format_report_contains_complexity() {
let q = "SELECT ?s WHERE { ?s ?p ?o }";
let p = ProfileCommand::profile(q);
let report = ProfileCommand::format_report(&p);
assert!(report.contains("Complexity"));
}
#[test]
fn test_format_report_contains_estimated_cost() {
let q = "SELECT ?s WHERE { ?s ?p ?o }";
let p = ProfileCommand::profile(q);
let report = ProfileCommand::format_report(&p);
assert!(report.contains("Estimated Cost"));
}
#[test]
fn test_format_report_contains_triple_pattern_count() {
let q = "SELECT ?s WHERE { ?s ?p ?o }";
let p = ProfileCommand::profile(q);
let report = ProfileCommand::format_report(&p);
assert!(report.contains("Triple Patterns"));
}
#[test]
fn test_format_report_contains_optional_count() {
let q = "SELECT ?s WHERE { ?s ?p ?o }";
let p = ProfileCommand::profile(q);
let report = ProfileCommand::format_report(&p);
assert!(report.contains("OPTIONAL"));
}
#[test]
fn test_format_report_contains_filter_count() {
let q = "SELECT ?s WHERE { ?s ?p ?o }";
let p = ProfileCommand::profile(q);
let report = ProfileCommand::format_report(&p);
assert!(report.contains("FILTER"));
}
#[test]
fn test_format_report_contains_subquery_depth() {
let q = "SELECT ?s WHERE { ?s ?p ?o }";
let p = ProfileCommand::profile(q);
let report = ProfileCommand::format_report(&p);
assert!(report.contains("Sub-query"));
}
#[test]
fn test_format_report_contains_features_section() {
let q = "SELECT ?s WHERE { ?s ?p ?o }";
let p = ProfileCommand::profile(q);
let report = ProfileCommand::format_report(&p);
assert!(report.contains("Feature") || report.contains("feature"));
}
#[test]
fn test_format_report_contains_hints() {
let q = "SELECT ?s WHERE { ?s ?p ?o }";
let p = ProfileCommand::profile(q);
let report = ProfileCommand::format_report(&p);
assert!(report.contains("Optimis") || report.contains("Hint") || report.contains("hint"));
}
#[test]
fn test_format_report_header_present() {
let q = "SELECT ?s WHERE { ?s ?p ?o }";
let p = ProfileCommand::profile(q);
let report = ProfileCommand::format_report(&p);
assert!(report.contains("SPARQL Query Profile Report"));
}
#[test]
fn test_format_report_is_multiline() {
let q = "SELECT ?s WHERE { ?s ?p ?o }";
let p = ProfileCommand::profile(q);
let report = ProfileCommand::format_report(&p);
assert!(report.contains('\n'));
}
#[test]
fn test_new_returns_instance() {
let _cmd = ProfileCommand::new();
}
#[test]
fn test_default_same_as_new() {
let _cmd: ProfileCommand = Default::default();
}
#[test]
fn test_profile_query_text_preserved() {
let q = "SELECT ?s WHERE { ?s ?p ?o }";
let p = ProfileCommand::profile(q);
assert_eq!(p.query_text, q);
}
#[test]
fn test_profile_features_deduplicated() {
let q = "SELECT DISTINCT ?s WHERE { ?s ?p ?o }";
let p = ProfileCommand::profile(q);
let distinct_count = p
.features
.iter()
.filter(|f| **f == QueryFeature::Distinct)
.count();
assert_eq!(distinct_count, 1);
}
#[test]
fn test_profile_empty_string() {
let p = ProfileCommand::profile("");
assert_eq!(p.estimated_cost, 0.0);
assert_eq!(p.complexity, ComplexityLevel::Trivial);
assert!(p.features.is_empty());
}
#[test]
fn test_profile_ask_query() {
let q = "ASK { ?s ?p ?o }";
let p = ProfileCommand::profile(q);
assert!(p.triple_pattern_count >= 1 || p.estimated_cost >= 0.0);
}
#[test]
fn test_profile_construct_query() {
let q = "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }";
let p = ProfileCommand::profile(q);
assert!(p.estimated_cost >= 0.0);
}
}