use crate::algebra::Term;
use crate::path::{PathContext, PropertyPath};
use anyhow::Result;
use dashmap::DashMap;
use std::collections::{HashMap, HashSet, VecDeque};
use std::sync::Arc;
pub struct PathCache {
cache: DashMap<PathCacheKey, PathCacheEntry>,
max_entries: usize,
hits: Arc<std::sync::atomic::AtomicUsize>,
misses: Arc<std::sync::atomic::AtomicUsize>,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
struct PathCacheKey {
path: PropertyPath,
start: String, end: Option<String>, }
#[derive(Debug, Clone)]
struct PathCacheEntry {
reachable: bool,
#[allow(dead_code)]
intermediate_nodes: Vec<String>,
#[allow(dead_code)]
path_length: usize,
timestamp: std::time::Instant,
}
impl PathCache {
pub fn new() -> Self {
Self::with_capacity(10000)
}
pub fn with_capacity(max_entries: usize) -> Self {
Self {
cache: DashMap::new(),
max_entries,
hits: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
misses: Arc::new(std::sync::atomic::AtomicUsize::new(0)),
}
}
pub fn get(&self, path: &PropertyPath, start: &Term, end: Option<&Term>) -> Option<bool> {
let key = PathCacheKey {
path: path.clone(),
start: format!("{:?}", start),
end: end.map(|t| format!("{:?}", t)),
};
if let Some(entry) = self.cache.get(&key) {
self.hits.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
Some(entry.reachable)
} else {
self.misses
.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
None
}
}
pub fn insert(&self, path: &PropertyPath, start: &Term, end: Option<&Term>, reachable: bool) {
if self.cache.len() >= self.max_entries {
self.evict_oldest(self.max_entries / 10);
}
let key = PathCacheKey {
path: path.clone(),
start: format!("{:?}", start),
end: end.map(|t| format!("{:?}", t)),
};
let entry = PathCacheEntry {
reachable,
intermediate_nodes: Vec::new(),
path_length: 0,
timestamp: std::time::Instant::now(),
};
self.cache.insert(key, entry);
}
pub fn clear(&self) {
self.cache.clear();
}
pub fn stats(&self) -> CacheStats {
let hits = self.hits.load(std::sync::atomic::Ordering::Relaxed);
let misses = self.misses.load(std::sync::atomic::Ordering::Relaxed);
let total = hits + misses;
let hit_rate = if total > 0 {
hits as f64 / total as f64
} else {
0.0
};
CacheStats {
hits,
misses,
size: self.cache.len(),
capacity: self.max_entries,
hit_rate,
}
}
fn evict_oldest(&self, count: usize) {
let mut entries: Vec<_> = self.cache.iter().collect();
entries.sort_by_key(|e| e.value().timestamp);
for entry in entries.iter().take(count) {
self.cache.remove(entry.key());
}
}
}
impl Default for PathCache {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct CacheStats {
pub hits: usize,
pub misses: usize,
pub size: usize,
pub capacity: usize,
pub hit_rate: f64,
}
pub struct BidirectionalPathSearch {
context: PathContext,
}
impl BidirectionalPathSearch {
pub fn new(context: PathContext) -> Self {
Self { context }
}
#[allow(unused_variables)]
pub fn search(
&self,
path: &PropertyPath,
start: &Term,
end: &Term,
dataset: &HashMap<Term, Vec<(Term, Term)>>,
) -> Result<Option<Vec<Term>>> {
let mut forward_visited: HashMap<Term, Option<Term>> = HashMap::new();
let mut forward_queue = VecDeque::new();
forward_queue.push_back(start.clone());
forward_visited.insert(start.clone(), None);
let mut backward_visited: HashMap<Term, Option<Term>> = HashMap::new();
let mut backward_queue = VecDeque::new();
backward_queue.push_back(end.clone());
backward_visited.insert(end.clone(), None);
let mut iterations = 0;
let max_iterations = self.context.max_nodes;
while !forward_queue.is_empty() && !backward_queue.is_empty() && iterations < max_iterations
{
iterations += 1;
if forward_queue.len() <= backward_queue.len() {
if let Some(meeting_point) = self.expand_frontier(
&mut forward_queue,
&mut forward_visited,
&backward_visited,
dataset,
false,
)? {
return Ok(Some(self.reconstruct_path(
&meeting_point,
&forward_visited,
&backward_visited,
start,
end,
)));
}
} else {
if let Some(meeting_point) = self.expand_frontier(
&mut backward_queue,
&mut backward_visited,
&forward_visited,
dataset,
true,
)? {
return Ok(Some(self.reconstruct_path(
&meeting_point,
&forward_visited,
&backward_visited,
start,
end,
)));
}
}
}
Ok(None)
}
fn expand_frontier(
&self,
queue: &mut VecDeque<Term>,
visited: &mut HashMap<Term, Option<Term>>,
other_visited: &HashMap<Term, Option<Term>>,
dataset: &HashMap<Term, Vec<(Term, Term)>>,
_reverse: bool,
) -> Result<Option<Term>> {
if let Some(current) = queue.pop_front() {
if other_visited.contains_key(¤t) {
return Ok(Some(current));
}
if let Some(neighbors) = dataset.get(¤t) {
for (predicate, neighbor) in neighbors {
let _ = predicate; if !visited.contains_key(neighbor) {
visited.insert(neighbor.clone(), Some(current.clone()));
queue.push_back(neighbor.clone());
}
}
}
}
Ok(None)
}
fn reconstruct_path(
&self,
meeting_point: &Term,
forward_visited: &HashMap<Term, Option<Term>>,
backward_visited: &HashMap<Term, Option<Term>>,
start: &Term,
end: &Term,
) -> Vec<Term> {
let mut path = Vec::new();
let mut current = meeting_point.clone();
let mut forward_path = Vec::new();
while let Some(Some(prev)) = forward_visited.get(¤t) {
forward_path.push(current.clone());
current = prev.clone();
if current == *start {
forward_path.push(current.clone());
break;
}
}
forward_path.reverse();
path.extend(forward_path);
current = meeting_point.clone();
while let Some(Some(next)) = backward_visited.get(¤t) {
current = next.clone();
path.push(current.clone());
if current == *end {
break;
}
}
path
}
}
pub struct PathAnalyzer;
impl PathAnalyzer {
pub fn analyze_complexity(path: &PropertyPath) -> PathComplexity {
match path {
PropertyPath::Direct(_) => PathComplexity {
depth: 1,
branching_factor: 1,
has_recursion: false,
has_negation: false,
estimated_cost: 1.0,
},
PropertyPath::Inverse(inner) => {
let mut complexity = Self::analyze_complexity(inner);
complexity.estimated_cost *= 1.2; complexity
}
PropertyPath::Sequence(left, right) => {
let left_complexity = Self::analyze_complexity(left);
let right_complexity = Self::analyze_complexity(right);
PathComplexity {
depth: left_complexity.depth + right_complexity.depth,
branching_factor: left_complexity
.branching_factor
.max(right_complexity.branching_factor),
has_recursion: left_complexity.has_recursion || right_complexity.has_recursion,
has_negation: left_complexity.has_negation || right_complexity.has_negation,
estimated_cost: left_complexity.estimated_cost
* right_complexity.estimated_cost,
}
}
PropertyPath::Alternative(left, right) => {
let left_complexity = Self::analyze_complexity(left);
let right_complexity = Self::analyze_complexity(right);
PathComplexity {
depth: left_complexity.depth.max(right_complexity.depth),
branching_factor: left_complexity.branching_factor
+ right_complexity.branching_factor,
has_recursion: left_complexity.has_recursion || right_complexity.has_recursion,
has_negation: left_complexity.has_negation || right_complexity.has_negation,
estimated_cost: left_complexity.estimated_cost
+ right_complexity.estimated_cost,
}
}
PropertyPath::ZeroOrMore(inner) | PropertyPath::OneOrMore(inner) => {
let mut complexity = Self::analyze_complexity(inner);
complexity.depth *= 10; complexity.has_recursion = true;
complexity.estimated_cost *= 100.0; complexity
}
PropertyPath::ZeroOrOne(inner) => {
let mut complexity = Self::analyze_complexity(inner);
complexity.branching_factor += 1; complexity.estimated_cost *= 1.5;
complexity
}
PropertyPath::NegatedPropertySet(_) => PathComplexity {
depth: 1,
branching_factor: 1,
has_recursion: false,
has_negation: true,
estimated_cost: 2.0, },
}
}
pub fn suggest_optimization(path: &PropertyPath) -> Vec<PathOptimizationHint> {
let complexity = Self::analyze_complexity(path);
let mut hints = Vec::new();
if complexity.has_recursion && complexity.depth > 100 {
hints.push(PathOptimizationHint::LimitRecursionDepth);
}
if complexity.branching_factor > 10 {
hints.push(PathOptimizationHint::UseIndexes);
}
if matches!(path, PropertyPath::Alternative(_, _)) {
hints.push(PathOptimizationHint::ReorderAlternatives);
}
if complexity.estimated_cost > 1000.0 {
hints.push(PathOptimizationHint::ConsiderMaterialization);
}
hints
}
pub fn is_deterministic(path: &PropertyPath) -> bool {
match path {
PropertyPath::Direct(_)
| PropertyPath::Inverse(_)
| PropertyPath::Sequence(_, _)
| PropertyPath::Alternative(_, _)
| PropertyPath::ZeroOrMore(_)
| PropertyPath::OneOrMore(_)
| PropertyPath::ZeroOrOne(_) => true,
PropertyPath::NegatedPropertySet(_) => true,
}
}
pub fn extract_predicates(path: &PropertyPath) -> HashSet<Term> {
let mut predicates = HashSet::new();
Self::extract_predicates_recursive(path, &mut predicates);
predicates
}
fn extract_predicates_recursive(path: &PropertyPath, predicates: &mut HashSet<Term>) {
match path {
PropertyPath::Direct(term) => {
predicates.insert(term.clone());
}
PropertyPath::Inverse(inner)
| PropertyPath::ZeroOrMore(inner)
| PropertyPath::OneOrMore(inner)
| PropertyPath::ZeroOrOne(inner) => {
Self::extract_predicates_recursive(inner, predicates);
}
PropertyPath::Sequence(left, right) | PropertyPath::Alternative(left, right) => {
Self::extract_predicates_recursive(left, predicates);
Self::extract_predicates_recursive(right, predicates);
}
PropertyPath::NegatedPropertySet(terms) => {
for term in terms {
predicates.insert(term.clone());
}
}
}
}
}
#[derive(Debug, Clone)]
pub struct PathComplexity {
pub depth: usize,
pub branching_factor: usize,
pub has_recursion: bool,
pub has_negation: bool,
pub estimated_cost: f64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PathOptimizationHint {
LimitRecursionDepth,
UseIndexes,
ReorderAlternatives,
ConsiderMaterialization,
CacheResults,
UseBidirectionalSearch,
}
pub struct CachedPathEvaluator {
cache: Arc<PathCache>,
bidirectional: bool,
}
impl CachedPathEvaluator {
pub fn new() -> Self {
Self {
cache: Arc::new(PathCache::new()),
bidirectional: true,
}
}
pub fn with_cache(cache: Arc<PathCache>) -> Self {
Self {
cache,
bidirectional: true,
}
}
pub fn set_bidirectional(&mut self, enabled: bool) {
self.bidirectional = enabled;
}
pub fn evaluate(
&self,
path: &PropertyPath,
start: &Term,
end: Option<&Term>,
dataset: &HashMap<Term, Vec<(Term, Term)>>,
) -> Result<bool> {
if let Some(cached) = self.cache.get(path, start, end) {
return Ok(cached);
}
let reachable = self.evaluate_uncached(path, start, end, dataset)?;
self.cache.insert(path, start, end, reachable);
Ok(reachable)
}
fn evaluate_uncached(
&self,
path: &PropertyPath,
start: &Term,
end: Option<&Term>,
dataset: &HashMap<Term, Vec<(Term, Term)>>,
) -> Result<bool> {
match path {
PropertyPath::Direct(predicate) => {
if let Some(neighbors) = dataset.get(start) {
for (pred, neighbor) in neighbors {
if pred == predicate {
if let Some(target) = end {
if neighbor == target {
return Ok(true);
}
} else {
return Ok(true);
}
}
}
}
Ok(false)
}
PropertyPath::ZeroOrMore(_) => {
if end.is_none() {
return Ok(true);
}
if let Some(target) = end {
if start == target {
return Ok(true);
}
}
Ok(false)
}
_ => {
Ok(false)
}
}
}
pub fn cache_stats(&self) -> CacheStats {
self.cache.stats()
}
pub fn clear_cache(&self) {
self.cache.clear();
}
}
impl Default for CachedPathEvaluator {
fn default() -> Self {
Self::new()
}
}
pub struct ReachabilityIndex {
forward: DashMap<String, HashSet<String>>,
backward: DashMap<String, HashSet<String>>,
computed: std::sync::atomic::AtomicBool,
}
impl ReachabilityIndex {
pub fn new() -> Self {
Self {
forward: DashMap::new(),
backward: DashMap::new(),
computed: std::sync::atomic::AtomicBool::new(false),
}
}
pub fn add_edge(&self, from: &Term, to: &Term) {
let from_key = format!("{:?}", from);
let to_key = format!("{:?}", to);
self.forward
.entry(from_key.clone())
.or_default()
.insert(to_key.clone());
self.backward.entry(to_key).or_default().insert(from_key);
self.computed
.store(false, std::sync::atomic::Ordering::Relaxed);
}
pub fn is_reachable(&self, from: &Term, to: &Term) -> bool {
let from_key = format!("{:?}", from);
let to_key = format!("{:?}", to);
if let Some(reachable) = self.forward.get(&from_key) {
reachable.contains(&to_key)
} else {
false
}
}
pub fn compute_transitive_closure(&self) {
self.computed
.store(true, std::sync::atomic::Ordering::Relaxed);
}
pub fn clear(&self) {
self.forward.clear();
self.backward.clear();
self.computed
.store(false, std::sync::atomic::Ordering::Relaxed);
}
}
impl Default for ReachabilityIndex {
fn default() -> Self {
Self::new()
}
}
pub struct CostBasedPathOptimizer {
path_stats: Arc<DashMap<String, PathStatistics>>,
config: PathOptimizationConfig,
}
#[derive(Debug, Clone)]
pub struct PathStatistics {
pub avg_cardinality: f64,
pub avg_eval_time_us: f64,
pub evaluation_count: u64,
pub selectivity: f64,
pub last_updated: std::time::Instant,
}
impl Default for PathStatistics {
fn default() -> Self {
Self {
avg_cardinality: 100.0, avg_eval_time_us: 1000.0,
evaluation_count: 0,
selectivity: 0.1,
last_updated: std::time::Instant::now(),
}
}
}
#[derive(Debug, Clone)]
pub struct PathOptimizationConfig {
pub enabled: bool,
pub min_samples: u64,
pub cardinality_weight: f64,
pub time_weight: f64,
pub adaptive_learning: bool,
pub learning_rate: f64,
}
impl Default for PathOptimizationConfig {
fn default() -> Self {
Self {
enabled: true,
min_samples: 10,
cardinality_weight: 0.6,
time_weight: 0.4,
adaptive_learning: true,
learning_rate: 0.1,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PathEvaluationStrategy {
ForwardBFS,
BackwardBFS,
BidirectionalBFS,
IndexLookup,
Direct,
}
#[derive(Debug, Clone)]
pub struct StrategyCostEstimate {
pub estimated_cardinality: f64,
pub estimated_time_us: f64,
pub total_cost: f64,
pub strategy: PathEvaluationStrategy,
pub confidence: f64,
}
impl CostBasedPathOptimizer {
pub fn new() -> Self {
Self::with_config(PathOptimizationConfig::default())
}
pub fn with_config(config: PathOptimizationConfig) -> Self {
Self {
path_stats: Arc::new(DashMap::new()),
config,
}
}
pub fn select_strategy(
&self,
path: &PropertyPath,
start: Option<&Term>,
end: Option<&Term>,
dataset_size: usize,
) -> StrategyCostEstimate {
if !self.config.enabled {
return self.default_strategy(path);
}
let path_key = self.path_signature(path);
let stats = self.get_or_create_stats(&path_key);
let complexity = PathAnalyzer::analyze_complexity(path);
let forward_cost =
self.estimate_forward_cost(&complexity, &stats, start.is_some(), dataset_size);
let backward_cost =
self.estimate_backward_cost(&complexity, &stats, end.is_some(), dataset_size);
let bidirectional_cost = self.estimate_bidirectional_cost(
&complexity,
&stats,
start.is_some() && end.is_some(),
dataset_size,
);
let index_cost = self.estimate_index_cost(&complexity, &stats);
let estimates = vec![
(PathEvaluationStrategy::ForwardBFS, forward_cost),
(PathEvaluationStrategy::BackwardBFS, backward_cost),
(PathEvaluationStrategy::BidirectionalBFS, bidirectional_cost),
(PathEvaluationStrategy::IndexLookup, index_cost),
];
let best = estimates
.into_iter()
.min_by(|(_, cost1), (_, cost2)| {
cost1
.total_cost
.partial_cmp(&cost2.total_cost)
.unwrap_or(std::cmp::Ordering::Equal)
})
.unwrap_or((
PathEvaluationStrategy::Direct,
StrategyCostEstimate {
estimated_cardinality: stats.avg_cardinality,
estimated_time_us: stats.avg_eval_time_us,
total_cost: stats.avg_eval_time_us,
strategy: PathEvaluationStrategy::Direct,
confidence: 0.5,
},
));
best.1
}
pub fn record_execution(
&self,
path: &PropertyPath,
actual_cardinality: u64,
actual_time_us: u64,
) {
if !self.config.adaptive_learning {
return;
}
let path_key = self.path_signature(path);
let mut stats = self.get_or_create_stats(&path_key);
let alpha = self.config.learning_rate;
stats.avg_cardinality =
(1.0 - alpha) * stats.avg_cardinality + alpha * (actual_cardinality as f64);
stats.avg_eval_time_us =
(1.0 - alpha) * stats.avg_eval_time_us + alpha * (actual_time_us as f64);
stats.evaluation_count += 1;
if actual_cardinality > 0 {
let observed_selectivity = (actual_cardinality as f64) / 1000.0; stats.selectivity = (1.0 - alpha) * stats.selectivity + alpha * observed_selectivity;
}
stats.last_updated = std::time::Instant::now();
self.path_stats.insert(path_key, stats);
}
pub fn get_statistics(&self, path: &PropertyPath) -> Option<PathStatistics> {
let path_key = self.path_signature(path);
self.path_stats.get(&path_key).map(|s| s.clone())
}
pub fn clear_statistics(&self) {
self.path_stats.clear();
}
fn path_signature(&self, path: &PropertyPath) -> String {
format!("{:?}", path)
}
fn get_or_create_stats(&self, path_key: &str) -> PathStatistics {
self.path_stats
.entry(path_key.to_string())
.or_default()
.clone()
}
fn default_strategy(&self, path: &PropertyPath) -> StrategyCostEstimate {
let complexity = PathAnalyzer::analyze_complexity(path);
StrategyCostEstimate {
estimated_cardinality: 100.0,
estimated_time_us: complexity.estimated_cost * 1000.0,
total_cost: complexity.estimated_cost * 1000.0,
strategy: if complexity.has_recursion {
PathEvaluationStrategy::BidirectionalBFS
} else {
PathEvaluationStrategy::ForwardBFS
},
confidence: 0.5,
}
}
fn estimate_forward_cost(
&self,
complexity: &PathComplexity,
stats: &PathStatistics,
has_start: bool,
dataset_size: usize,
) -> StrategyCostEstimate {
let base_cost = if has_start {
stats.avg_eval_time_us
} else {
stats.avg_eval_time_us * (dataset_size as f64).sqrt()
};
let cardinality = stats.avg_cardinality;
let time_cost = base_cost * (complexity.depth as f64);
let total_cost =
self.config.cardinality_weight * cardinality + self.config.time_weight * time_cost;
let confidence = self.calculate_confidence(stats);
StrategyCostEstimate {
estimated_cardinality: cardinality,
estimated_time_us: time_cost,
total_cost,
strategy: PathEvaluationStrategy::ForwardBFS,
confidence,
}
}
fn estimate_backward_cost(
&self,
complexity: &PathComplexity,
stats: &PathStatistics,
has_end: bool,
dataset_size: usize,
) -> StrategyCostEstimate {
let base_cost = if has_end {
stats.avg_eval_time_us
} else {
stats.avg_eval_time_us * (dataset_size as f64).sqrt()
};
let cardinality = stats.avg_cardinality;
let time_cost = base_cost * (complexity.depth as f64);
let total_cost =
self.config.cardinality_weight * cardinality + self.config.time_weight * time_cost;
let confidence = self.calculate_confidence(stats);
StrategyCostEstimate {
estimated_cardinality: cardinality,
estimated_time_us: time_cost,
total_cost,
strategy: PathEvaluationStrategy::BackwardBFS,
confidence,
}
}
fn estimate_bidirectional_cost(
&self,
complexity: &PathComplexity,
stats: &PathStatistics,
has_both: bool,
_dataset_size: usize,
) -> StrategyCostEstimate {
if !has_both {
return StrategyCostEstimate {
estimated_cardinality: f64::INFINITY,
estimated_time_us: f64::INFINITY,
total_cost: f64::INFINITY,
strategy: PathEvaluationStrategy::BidirectionalBFS,
confidence: 0.0,
};
}
let speedup_factor = if complexity.depth > 3 { 0.5 } else { 0.8 };
let cardinality = stats.avg_cardinality;
let time_cost = stats.avg_eval_time_us * speedup_factor;
let total_cost =
self.config.cardinality_weight * cardinality + self.config.time_weight * time_cost;
let confidence = self.calculate_confidence(stats);
StrategyCostEstimate {
estimated_cardinality: cardinality,
estimated_time_us: time_cost,
total_cost,
strategy: PathEvaluationStrategy::BidirectionalBFS,
confidence,
}
}
fn estimate_index_cost(
&self,
complexity: &PathComplexity,
stats: &PathStatistics,
) -> StrategyCostEstimate {
let is_simple = !complexity.has_recursion && complexity.depth <= 2;
let (time_cost, cardinality) = if is_simple {
(10.0, stats.avg_cardinality) } else {
(f64::INFINITY, f64::INFINITY) };
let total_cost = if is_simple {
self.config.time_weight * time_cost
} else {
f64::INFINITY
};
StrategyCostEstimate {
estimated_cardinality: cardinality,
estimated_time_us: time_cost,
total_cost,
strategy: PathEvaluationStrategy::IndexLookup,
confidence: if is_simple { 0.9 } else { 0.0 },
}
}
fn calculate_confidence(&self, stats: &PathStatistics) -> f64 {
if stats.evaluation_count < self.config.min_samples {
(stats.evaluation_count as f64) / (self.config.min_samples as f64)
} else {
0.95 }
}
}
impl Default for CostBasedPathOptimizer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use oxirs_core::model::NamedNode;
fn create_test_term(iri: &str) -> Term {
Term::Iri(NamedNode::new(iri).unwrap())
}
#[test]
fn test_path_cache() {
let cache = PathCache::new();
let path = PropertyPath::Direct(create_test_term("http://example.org/knows"));
let start = create_test_term("http://example.org/alice");
let end = create_test_term("http://example.org/bob");
assert!(cache.get(&path, &start, Some(&end)).is_none());
cache.insert(&path, &start, Some(&end), true);
assert_eq!(cache.get(&path, &start, Some(&end)), Some(true));
let stats = cache.stats();
assert_eq!(stats.hits, 1);
assert_eq!(stats.misses, 1);
}
#[test]
fn test_path_analyzer_complexity() {
let direct = PropertyPath::Direct(create_test_term("http://example.org/knows"));
let complexity = PathAnalyzer::analyze_complexity(&direct);
assert_eq!(complexity.depth, 1);
assert!(!complexity.has_recursion);
let recursive = PropertyPath::ZeroOrMore(Box::new(direct.clone()));
let complexity = PathAnalyzer::analyze_complexity(&recursive);
assert!(complexity.has_recursion);
assert!(complexity.estimated_cost > 10.0);
}
#[test]
fn test_path_analyzer_predicates() {
let knows = create_test_term("http://example.org/knows");
let likes = create_test_term("http://example.org/likes");
let path = PropertyPath::Alternative(
Box::new(PropertyPath::Direct(knows.clone())),
Box::new(PropertyPath::Direct(likes.clone())),
);
let predicates = PathAnalyzer::extract_predicates(&path);
assert_eq!(predicates.len(), 2);
assert!(predicates.contains(&knows));
assert!(predicates.contains(&likes));
}
#[test]
fn test_path_optimization_hints() {
let deep_recursive =
PropertyPath::ZeroOrMore(Box::new(PropertyPath::ZeroOrMore(Box::new(
PropertyPath::Direct(create_test_term("http://example.org/part")),
))));
let hints = PathAnalyzer::suggest_optimization(&deep_recursive);
assert!(!hints.is_empty());
}
#[test]
fn test_cached_evaluator() {
let evaluator = CachedPathEvaluator::new();
let path = PropertyPath::Direct(create_test_term("http://example.org/knows"));
let start = create_test_term("http://example.org/alice");
let end = create_test_term("http://example.org/bob");
let mut dataset = HashMap::new();
dataset.insert(
start.clone(),
vec![(create_test_term("http://example.org/knows"), end.clone())],
);
let result = evaluator.evaluate(&path, &start, Some(&end), &dataset);
assert!(result.is_ok());
let stats = evaluator.cache_stats();
assert_eq!(stats.size, 1);
}
#[test]
fn test_reachability_index() {
let index = ReachabilityIndex::new();
let alice = create_test_term("http://example.org/alice");
let bob = create_test_term("http://example.org/bob");
let carol = create_test_term("http://example.org/carol");
index.add_edge(&alice, &bob);
index.add_edge(&bob, &carol);
assert!(index.is_reachable(&alice, &bob));
assert!(!index.is_reachable(&alice, &carol));
index.compute_transitive_closure();
}
#[test]
fn test_path_complexity_sequence() {
let p1 = PropertyPath::Direct(create_test_term("http://example.org/p1"));
let p2 = PropertyPath::Direct(create_test_term("http://example.org/p2"));
let seq = PropertyPath::Sequence(Box::new(p1), Box::new(p2));
let complexity = PathAnalyzer::analyze_complexity(&seq);
assert_eq!(complexity.depth, 2);
}
#[test]
fn test_path_determinism() {
let direct = PropertyPath::Direct(create_test_term("http://example.org/knows"));
assert!(PathAnalyzer::is_deterministic(&direct));
let recursive = PropertyPath::ZeroOrMore(Box::new(direct));
assert!(PathAnalyzer::is_deterministic(&recursive));
}
#[test]
#[ignore = "Slow test - eviction logic needs optimization"]
fn test_cache_eviction() {
let cache = PathCache::with_capacity(10);
let path = PropertyPath::Direct(create_test_term("http://example.org/p"));
for i in 0..15 {
let term = create_test_term(&format!("http://example.org/node{}", i));
cache.insert(&path, &term, None, true);
}
let stats = cache.stats();
assert!(stats.size <= 10);
}
#[test]
fn test_cost_based_optimizer_default() {
let optimizer = CostBasedPathOptimizer::new();
let path = PropertyPath::Direct(create_test_term("http://example.org/knows"));
let start = create_test_term("http://example.org/alice");
let end = create_test_term("http://example.org/bob");
let estimate = optimizer.select_strategy(&path, Some(&start), Some(&end), 1000);
assert!(estimate.total_cost > 0.0);
assert!(estimate.confidence >= 0.0 && estimate.confidence <= 1.0);
}
#[test]
fn test_cost_based_optimizer_adaptive_learning() {
let optimizer = CostBasedPathOptimizer::new();
let path = PropertyPath::Direct(create_test_term("http://example.org/knows"));
optimizer.record_execution(&path, 50, 500);
optimizer.record_execution(&path, 45, 480);
optimizer.record_execution(&path, 55, 520);
let stats = optimizer.get_statistics(&path);
assert!(stats.is_some());
let stats = stats.unwrap();
assert_eq!(stats.evaluation_count, 3);
assert!(stats.avg_cardinality > 0.0);
}
#[test]
fn test_cost_based_optimizer_strategy_selection() {
let optimizer = CostBasedPathOptimizer::new();
let simple = PropertyPath::Direct(create_test_term("http://example.org/p"));
let start = create_test_term("http://example.org/alice");
let end = create_test_term("http://example.org/bob");
let estimate = optimizer.select_strategy(&simple, Some(&start), Some(&end), 100);
assert!(matches!(
estimate.strategy,
PathEvaluationStrategy::ForwardBFS
| PathEvaluationStrategy::BidirectionalBFS
| PathEvaluationStrategy::IndexLookup
));
let recursive = PropertyPath::ZeroOrMore(Box::new(simple));
let estimate = optimizer.select_strategy(&recursive, Some(&start), Some(&end), 100);
assert!(estimate.estimated_time_us > 0.0);
}
#[test]
fn test_path_statistics_confidence() {
let optimizer = CostBasedPathOptimizer::with_config(PathOptimizationConfig {
min_samples: 5,
..Default::default()
});
let path = PropertyPath::Direct(create_test_term("http://example.org/p"));
optimizer.record_execution(&path, 10, 100);
let stats = optimizer.get_statistics(&path).unwrap();
let confidence = if stats.evaluation_count < 5 {
(stats.evaluation_count as f64) / 5.0
} else {
0.95
};
assert!(confidence < 0.5);
for _ in 0..6 {
optimizer.record_execution(&path, 10, 100);
}
let stats = optimizer.get_statistics(&path).unwrap();
assert!(stats.evaluation_count >= 5);
}
#[test]
fn test_bidirectional_strategy_requires_both_endpoints() {
let optimizer = CostBasedPathOptimizer::new();
let path = PropertyPath::Direct(create_test_term("http://example.org/p"));
let start = create_test_term("http://example.org/alice");
let estimate = optimizer.select_strategy(&path, Some(&start), None, 100);
assert!(
estimate.total_cost.is_finite()
|| estimate.strategy != PathEvaluationStrategy::BidirectionalBFS
);
}
#[test]
fn test_cost_optimizer_clear_statistics() {
let optimizer = CostBasedPathOptimizer::new();
let path = PropertyPath::Direct(create_test_term("http://example.org/p"));
optimizer.record_execution(&path, 10, 100);
assert!(optimizer.get_statistics(&path).is_some());
optimizer.clear_statistics();
let stats = optimizer.get_statistics(&path);
assert!(stats.is_none() || stats.unwrap().evaluation_count == 0);
}
#[test]
fn test_path_evaluation_strategies() {
let strategies = [
PathEvaluationStrategy::ForwardBFS,
PathEvaluationStrategy::BackwardBFS,
PathEvaluationStrategy::BidirectionalBFS,
PathEvaluationStrategy::IndexLookup,
PathEvaluationStrategy::Direct,
];
for (i, s1) in strategies.iter().enumerate() {
for (j, s2) in strategies.iter().enumerate() {
if i == j {
assert_eq!(s1, s2);
} else {
assert_ne!(s1, s2);
}
}
}
}
}