Skip to main content

oxirs_arq/
query_hints.rs

1//! Query Hints System for SPARQL Query Optimization
2//!
3//! This module provides a comprehensive query hints system that allows users to guide
4//! the query optimizer with specific optimization directives. Similar to SQL query hints
5//! in PostgreSQL, MySQL, and Oracle.
6//!
7//! # Features
8//!
9//! - **Join Hints**: Specify join algorithms (HASH_JOIN, MERGE_JOIN, NESTED_LOOP)
10//! - **Index Hints**: Force or avoid specific indexes
11//! - **Cardinality Hints**: Override cardinality estimates
12//! - **Parallelism Hints**: Control parallel execution
13//! - **Materialization Hints**: Control intermediate result materialization
14//! - **Timeout Hints**: Set query-specific timeouts
15//! - **Memory Hints**: Control memory allocation for query processing
16//!
17//! # Usage
18//!
19//! ```sparql
20//! # Hint comments are embedded in SPARQL queries
21//! # /*+ HASH_JOIN(?s, ?o) PARALLEL(4) TIMEOUT(30s) */
22//! SELECT ?s ?p ?o WHERE { ?s ?p ?o }
23//! ```
24//!
25//! # Example
26//!
27//! ```rust
28//! use oxirs_arq::query_hints::{QueryHints, JoinHint, HintParser};
29//!
30//! let query = r#"
31//!     # /*+ HASH_JOIN(?person, ?name) CARDINALITY(?person, 1000) */
32//!     SELECT ?person ?name WHERE {
33//!         ?person <http://xmlns.com/foaf/0.1/name> ?name .
34//!     }
35//! "#;
36//!
37//! let hints = HintParser::parse(query)?;
38//! println!("Parsed hints: {:?}", hints);
39//! # Ok::<(), anyhow::Error>(())
40//! ```
41
42use crate::algebra::Variable;
43use anyhow::{anyhow, Result};
44use regex::Regex;
45// Metrics imports available for future use
46#[allow(unused_imports)]
47use scirs2_core::metrics::{Counter, Timer};
48use serde::{Deserialize, Serialize};
49use std::collections::HashMap;
50use std::sync::atomic::{AtomicU64, Ordering};
51use std::sync::{Arc, OnceLock};
52use std::time::Duration;
53
54/// Query hints for optimizer guidance
55#[derive(Debug, Clone, Default, Serialize, Deserialize)]
56pub struct QueryHints {
57    /// Join algorithm hints
58    pub join_hints: Vec<JoinHint>,
59    /// Index usage hints
60    pub index_hints: Vec<IndexHint>,
61    /// Cardinality override hints
62    pub cardinality_hints: Vec<CardinalityHint>,
63    /// Parallelism configuration hints
64    pub parallelism_hints: Option<ParallelismHint>,
65    /// Materialization strategy hints
66    pub materialization_hints: Vec<MaterializationHint>,
67    /// Query timeout hint
68    pub timeout_hint: Option<Duration>,
69    /// Memory limit hint
70    pub memory_hint: Option<MemoryHint>,
71    /// Cache control hints
72    pub cache_hints: Option<CacheHint>,
73    /// Ordering hints for join execution
74    pub join_order_hint: Option<JoinOrderHint>,
75    /// Filter pushdown hints
76    pub filter_hints: Vec<FilterHint>,
77    /// Custom optimizer directives
78    pub custom_directives: HashMap<String, String>,
79}
80
81impl QueryHints {
82    /// Create new empty hints
83    pub fn new() -> Self {
84        Self::default()
85    }
86
87    /// Create hints builder
88    pub fn builder() -> QueryHintsBuilder {
89        QueryHintsBuilder::new()
90    }
91
92    /// Check if any hints are specified
93    pub fn is_empty(&self) -> bool {
94        self.join_hints.is_empty()
95            && self.index_hints.is_empty()
96            && self.cardinality_hints.is_empty()
97            && self.parallelism_hints.is_none()
98            && self.materialization_hints.is_empty()
99            && self.timeout_hint.is_none()
100            && self.memory_hint.is_none()
101            && self.cache_hints.is_none()
102            && self.join_order_hint.is_none()
103            && self.filter_hints.is_empty()
104            && self.custom_directives.is_empty()
105    }
106
107    /// Get hint count
108    pub fn hint_count(&self) -> usize {
109        let mut count = self.join_hints.len()
110            + self.index_hints.len()
111            + self.cardinality_hints.len()
112            + self.materialization_hints.len()
113            + self.filter_hints.len()
114            + self.custom_directives.len();
115
116        if self.parallelism_hints.is_some() {
117            count += 1;
118        }
119        if self.timeout_hint.is_some() {
120            count += 1;
121        }
122        if self.memory_hint.is_some() {
123            count += 1;
124        }
125        if self.cache_hints.is_some() {
126            count += 1;
127        }
128        if self.join_order_hint.is_some() {
129            count += 1;
130        }
131        count
132    }
133
134    /// Merge hints from another QueryHints instance (other takes precedence)
135    pub fn merge(&mut self, other: QueryHints) {
136        self.join_hints.extend(other.join_hints);
137        self.index_hints.extend(other.index_hints);
138        self.cardinality_hints.extend(other.cardinality_hints);
139        self.materialization_hints
140            .extend(other.materialization_hints);
141        self.filter_hints.extend(other.filter_hints);
142        self.custom_directives.extend(other.custom_directives);
143
144        if other.parallelism_hints.is_some() {
145            self.parallelism_hints = other.parallelism_hints;
146        }
147        if other.timeout_hint.is_some() {
148            self.timeout_hint = other.timeout_hint;
149        }
150        if other.memory_hint.is_some() {
151            self.memory_hint = other.memory_hint;
152        }
153        if other.cache_hints.is_some() {
154            self.cache_hints = other.cache_hints;
155        }
156        if other.join_order_hint.is_some() {
157            self.join_order_hint = other.join_order_hint;
158        }
159    }
160
161    /// Get join hint for specific variables
162    pub fn get_join_hint(&self, vars: &[Variable]) -> Option<&JoinHint> {
163        self.join_hints.iter().find(|hint| {
164            vars.iter()
165                .all(|v| hint.variables.iter().any(|hv| hv == v.name()))
166        })
167    }
168
169    /// Get cardinality hint for a variable
170    pub fn get_cardinality_hint(&self, var: &Variable) -> Option<u64> {
171        self.cardinality_hints
172            .iter()
173            .find(|hint| hint.variable == var.name())
174            .map(|hint| hint.cardinality)
175    }
176
177    /// Get index hint for a pattern
178    pub fn get_index_hint(&self, pattern_id: &str) -> Option<&IndexHint> {
179        self.index_hints
180            .iter()
181            .find(|hint| hint.pattern_id == pattern_id)
182    }
183}
184
185/// Builder for QueryHints
186#[derive(Debug, Default)]
187pub struct QueryHintsBuilder {
188    hints: QueryHints,
189}
190
191impl QueryHintsBuilder {
192    /// Create new builder
193    pub fn new() -> Self {
194        Self::default()
195    }
196
197    /// Add a join hint
198    pub fn with_join_hint(mut self, hint: JoinHint) -> Self {
199        self.hints.join_hints.push(hint);
200        self
201    }
202
203    /// Add hash join hint for variables
204    pub fn hash_join(self, variables: Vec<&str>) -> Self {
205        self.with_join_hint(JoinHint::new(
206            variables.into_iter().map(String::from).collect(),
207            JoinAlgorithmHint::HashJoin,
208        ))
209    }
210
211    /// Add merge join hint for variables
212    pub fn merge_join(self, variables: Vec<&str>) -> Self {
213        self.with_join_hint(JoinHint::new(
214            variables.into_iter().map(String::from).collect(),
215            JoinAlgorithmHint::MergeJoin,
216        ))
217    }
218
219    /// Add nested loop join hint for variables
220    pub fn nested_loop_join(self, variables: Vec<&str>) -> Self {
221        self.with_join_hint(JoinHint::new(
222            variables.into_iter().map(String::from).collect(),
223            JoinAlgorithmHint::NestedLoop,
224        ))
225    }
226
227    /// Add an index hint
228    pub fn with_index_hint(mut self, hint: IndexHint) -> Self {
229        self.hints.index_hints.push(hint);
230        self
231    }
232
233    /// Force use of a specific index
234    pub fn use_index(self, pattern_id: &str, index_name: &str) -> Self {
235        self.with_index_hint(IndexHint::use_index(pattern_id, index_name))
236    }
237
238    /// Ignore a specific index
239    pub fn ignore_index(self, pattern_id: &str, index_name: &str) -> Self {
240        self.with_index_hint(IndexHint::ignore_index(pattern_id, index_name))
241    }
242
243    /// Add a cardinality hint
244    pub fn with_cardinality_hint(mut self, hint: CardinalityHint) -> Self {
245        self.hints.cardinality_hints.push(hint);
246        self
247    }
248
249    /// Override cardinality for a variable
250    pub fn cardinality(self, variable: &str, cardinality: u64) -> Self {
251        self.with_cardinality_hint(CardinalityHint::new(variable, cardinality))
252    }
253
254    /// Set parallelism hint
255    pub fn with_parallelism(mut self, hint: ParallelismHint) -> Self {
256        self.hints.parallelism_hints = Some(hint);
257        self
258    }
259
260    /// Enable parallel execution with specified threads
261    pub fn parallel(self, threads: usize) -> Self {
262        self.with_parallelism(ParallelismHint::new(threads))
263    }
264
265    /// Disable parallel execution
266    pub fn no_parallel(self) -> Self {
267        self.with_parallelism(ParallelismHint::disabled())
268    }
269
270    /// Add materialization hint
271    pub fn with_materialization_hint(mut self, hint: MaterializationHint) -> Self {
272        self.hints.materialization_hints.push(hint);
273        self
274    }
275
276    /// Set query timeout
277    pub fn timeout(mut self, duration: Duration) -> Self {
278        self.hints.timeout_hint = Some(duration);
279        self
280    }
281
282    /// Set timeout in seconds
283    pub fn timeout_secs(self, secs: u64) -> Self {
284        self.timeout(Duration::from_secs(secs))
285    }
286
287    /// Set memory hint
288    pub fn with_memory_hint(mut self, hint: MemoryHint) -> Self {
289        self.hints.memory_hint = Some(hint);
290        self
291    }
292
293    /// Set memory limit in bytes
294    pub fn memory_limit(self, bytes: usize) -> Self {
295        self.with_memory_hint(MemoryHint {
296            max_memory: bytes,
297            prefer_streaming: false,
298            spill_to_disk: true,
299        })
300    }
301
302    /// Prefer streaming execution
303    pub fn prefer_streaming(self) -> Self {
304        self.with_memory_hint(MemoryHint {
305            max_memory: usize::MAX,
306            prefer_streaming: true,
307            spill_to_disk: false,
308        })
309    }
310
311    /// Set cache hints
312    pub fn with_cache_hint(mut self, hint: CacheHint) -> Self {
313        self.hints.cache_hints = Some(hint);
314        self
315    }
316
317    /// Bypass query cache
318    pub fn no_cache(self) -> Self {
319        self.with_cache_hint(CacheHint {
320            use_cache: false,
321            update_cache: false,
322            cache_ttl: None,
323        })
324    }
325
326    /// Set join order hint
327    pub fn with_join_order(mut self, hint: JoinOrderHint) -> Self {
328        self.hints.join_order_hint = Some(hint);
329        self
330    }
331
332    /// Force specific join order
333    pub fn fixed_join_order(self, order: Vec<&str>) -> Self {
334        self.with_join_order(JoinOrderHint {
335            strategy: JoinOrderStrategy::Fixed,
336            order: order.into_iter().map(String::from).collect(),
337        })
338    }
339
340    /// Add filter hint
341    pub fn with_filter_hint(mut self, hint: FilterHint) -> Self {
342        self.hints.filter_hints.push(hint);
343        self
344    }
345
346    /// Add custom directive
347    pub fn directive(mut self, key: &str, value: &str) -> Self {
348        self.hints
349            .custom_directives
350            .insert(key.to_string(), value.to_string());
351        self
352    }
353
354    /// Build the QueryHints
355    pub fn build(self) -> QueryHints {
356        self.hints
357    }
358}
359
360/// Join algorithm hint
361#[derive(Debug, Clone, Serialize, Deserialize)]
362pub struct JoinHint {
363    /// Variables involved in the join
364    pub variables: Vec<String>,
365    /// Preferred join algorithm
366    pub algorithm: JoinAlgorithmHint,
367    /// Optional build side for hash join
368    pub build_side: Option<JoinBuildSide>,
369    /// Priority (higher = more important)
370    pub priority: u8,
371}
372
373impl JoinHint {
374    /// Create new join hint
375    pub fn new(variables: Vec<String>, algorithm: JoinAlgorithmHint) -> Self {
376        Self {
377            variables,
378            algorithm,
379            build_side: None,
380            priority: 1,
381        }
382    }
383
384    /// Set build side for hash join
385    pub fn with_build_side(mut self, side: JoinBuildSide) -> Self {
386        self.build_side = Some(side);
387        self
388    }
389
390    /// Set priority
391    pub fn with_priority(mut self, priority: u8) -> Self {
392        self.priority = priority;
393        self
394    }
395}
396
397/// Supported join algorithms for hints
398#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
399pub enum JoinAlgorithmHint {
400    /// Hash join (good for large datasets)
401    HashJoin,
402    /// Sort-merge join (good for pre-sorted data)
403    MergeJoin,
404    /// Nested loop join (good for small datasets or selective filters)
405    NestedLoop,
406    /// Index-based join
407    IndexJoin,
408    /// Let optimizer decide
409    Auto,
410}
411
412impl std::fmt::Display for JoinAlgorithmHint {
413    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
414        match self {
415            JoinAlgorithmHint::HashJoin => write!(f, "HASH_JOIN"),
416            JoinAlgorithmHint::MergeJoin => write!(f, "MERGE_JOIN"),
417            JoinAlgorithmHint::NestedLoop => write!(f, "NESTED_LOOP"),
418            JoinAlgorithmHint::IndexJoin => write!(f, "INDEX_JOIN"),
419            JoinAlgorithmHint::Auto => write!(f, "AUTO"),
420        }
421    }
422}
423
424/// Build side for hash join
425#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
426pub enum JoinBuildSide {
427    /// Use left side as build side
428    Left,
429    /// Use right side as build side
430    Right,
431    /// Let optimizer choose
432    Auto,
433}
434
435/// Index usage hint
436#[derive(Debug, Clone, Serialize, Deserialize)]
437pub struct IndexHint {
438    /// Pattern or variable this hint applies to
439    pub pattern_id: String,
440    /// Index directive
441    pub directive: IndexDirective,
442    /// Optional specific index names
443    pub index_names: Vec<String>,
444}
445
446impl IndexHint {
447    /// Create hint to use a specific index
448    pub fn use_index(pattern_id: &str, index_name: &str) -> Self {
449        Self {
450            pattern_id: pattern_id.to_string(),
451            directive: IndexDirective::Use,
452            index_names: vec![index_name.to_string()],
453        }
454    }
455
456    /// Create hint to ignore a specific index
457    pub fn ignore_index(pattern_id: &str, index_name: &str) -> Self {
458        Self {
459            pattern_id: pattern_id.to_string(),
460            directive: IndexDirective::Ignore,
461            index_names: vec![index_name.to_string()],
462        }
463    }
464
465    /// Create hint to force index scan
466    pub fn force_index(pattern_id: &str) -> Self {
467        Self {
468            pattern_id: pattern_id.to_string(),
469            directive: IndexDirective::Force,
470            index_names: Vec::new(),
471        }
472    }
473
474    /// Create hint to prevent index usage
475    pub fn no_index(pattern_id: &str) -> Self {
476        Self {
477            pattern_id: pattern_id.to_string(),
478            directive: IndexDirective::NoIndex,
479            index_names: Vec::new(),
480        }
481    }
482}
483
484/// Index directive types
485#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
486pub enum IndexDirective {
487    /// Prefer using specified indexes
488    Use,
489    /// Ignore specified indexes
490    Ignore,
491    /// Force index usage (any available)
492    Force,
493    /// Disable index usage (full scan)
494    NoIndex,
495}
496
497/// Cardinality override hint
498#[derive(Debug, Clone, Serialize, Deserialize)]
499pub struct CardinalityHint {
500    /// Variable name
501    pub variable: String,
502    /// Override cardinality value
503    pub cardinality: u64,
504    /// Confidence level (0.0 - 1.0)
505    pub confidence: f64,
506}
507
508impl CardinalityHint {
509    /// Create new cardinality hint
510    pub fn new(variable: &str, cardinality: u64) -> Self {
511        Self {
512            variable: variable.to_string(),
513            cardinality,
514            confidence: 1.0,
515        }
516    }
517
518    /// Set confidence level
519    pub fn with_confidence(mut self, confidence: f64) -> Self {
520        self.confidence = confidence.clamp(0.0, 1.0);
521        self
522    }
523}
524
525/// Parallelism configuration hint
526#[derive(Debug, Clone, Serialize, Deserialize)]
527pub struct ParallelismHint {
528    /// Enable parallel execution
529    pub enabled: bool,
530    /// Number of threads (None = auto)
531    pub threads: Option<usize>,
532    /// Minimum batch size for parallel execution
533    pub min_batch_size: usize,
534    /// Enable work stealing
535    pub work_stealing: bool,
536}
537
538impl ParallelismHint {
539    /// Create new parallelism hint with specified threads
540    pub fn new(threads: usize) -> Self {
541        Self {
542            enabled: true,
543            threads: Some(threads),
544            min_batch_size: 1000,
545            work_stealing: true,
546        }
547    }
548
549    /// Create disabled parallelism hint
550    pub fn disabled() -> Self {
551        Self {
552            enabled: false,
553            threads: None,
554            min_batch_size: 0,
555            work_stealing: false,
556        }
557    }
558
559    /// Create auto-parallelism hint
560    pub fn auto() -> Self {
561        Self {
562            enabled: true,
563            threads: None,
564            min_batch_size: 1000,
565            work_stealing: true,
566        }
567    }
568}
569
570/// Materialization strategy hint
571#[derive(Debug, Clone, Serialize, Deserialize)]
572pub struct MaterializationHint {
573    /// Pattern or subquery to materialize
574    pub target: String,
575    /// Materialization strategy
576    pub strategy: MaterializationStrategy,
577}
578
579impl MaterializationHint {
580    /// Create hint to materialize a pattern
581    pub fn materialize(target: &str) -> Self {
582        Self {
583            target: target.to_string(),
584            strategy: MaterializationStrategy::Eager,
585        }
586    }
587
588    /// Create hint for lazy evaluation
589    pub fn lazy(target: &str) -> Self {
590        Self {
591            target: target.to_string(),
592            strategy: MaterializationStrategy::Lazy,
593        }
594    }
595
596    /// Create hint for streaming execution
597    pub fn streaming(target: &str) -> Self {
598        Self {
599            target: target.to_string(),
600            strategy: MaterializationStrategy::Streaming,
601        }
602    }
603}
604
605/// Materialization strategies
606#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
607pub enum MaterializationStrategy {
608    /// Materialize immediately
609    Eager,
610    /// Lazy evaluation (materialize on demand)
611    Lazy,
612    /// Streaming (no materialization, pipeline)
613    Streaming,
614    /// Partial materialization with threshold
615    Partial,
616}
617
618/// Memory configuration hint
619#[derive(Debug, Clone, Serialize, Deserialize)]
620pub struct MemoryHint {
621    /// Maximum memory to use (bytes)
622    pub max_memory: usize,
623    /// Prefer streaming execution for memory efficiency
624    pub prefer_streaming: bool,
625    /// Allow spilling to disk
626    pub spill_to_disk: bool,
627}
628
629/// Cache control hint
630#[derive(Debug, Clone, Serialize, Deserialize)]
631pub struct CacheHint {
632    /// Use cached results if available
633    pub use_cache: bool,
634    /// Update cache with new results
635    pub update_cache: bool,
636    /// Cache TTL override
637    pub cache_ttl: Option<Duration>,
638}
639
640/// Join order hint
641#[derive(Debug, Clone, Serialize, Deserialize)]
642pub struct JoinOrderHint {
643    /// Join ordering strategy
644    pub strategy: JoinOrderStrategy,
645    /// Explicit join order (pattern/variable names)
646    pub order: Vec<String>,
647}
648
649/// Join ordering strategies
650#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
651pub enum JoinOrderStrategy {
652    /// Use optimizer's choice
653    Auto,
654    /// Fixed order as specified
655    Fixed,
656    /// Process in left-to-right order as written
657    LeftToRight,
658    /// Start with smallest estimates
659    SmallestFirst,
660    /// Start with most selective
661    MostSelectiveFirst,
662}
663
664/// Filter pushdown hint
665#[derive(Debug, Clone, Serialize, Deserialize)]
666pub struct FilterHint {
667    /// Filter expression identifier
668    pub filter_id: String,
669    /// Pushdown directive
670    pub directive: FilterPushdownDirective,
671    /// Target pattern for pushdown
672    pub target_pattern: Option<String>,
673}
674
675/// Filter pushdown directives
676#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
677pub enum FilterPushdownDirective {
678    /// Push filter down (default behavior)
679    Push,
680    /// Keep filter at current level
681    NoPush,
682    /// Push to specific pattern
683    PushTo,
684}
685
686/// Parser for query hints embedded in SPARQL comments
687pub struct HintParser {
688    /// Statistics counter
689    hints_parsed: Arc<AtomicU64>,
690    /// Parsing timer
691    parse_timer: Arc<AtomicU64>,
692}
693
694impl Default for HintParser {
695    fn default() -> Self {
696        Self::new()
697    }
698}
699
700impl HintParser {
701    /// Create new hint parser
702    pub fn new() -> Self {
703        Self {
704            hints_parsed: Arc::new(AtomicU64::new(0)),
705            parse_timer: Arc::new(AtomicU64::new(0)),
706        }
707    }
708
709    /// Parse hints from a SPARQL query string
710    pub fn parse(query: &str) -> Result<QueryHints> {
711        let parser = Self::new();
712        parser.parse_query(query)
713    }
714
715    /// Parse hints from query
716    pub fn parse_query(&self, query: &str) -> Result<QueryHints> {
717        let start = std::time::Instant::now();
718        let mut hints = QueryHints::new();
719
720        // First, find all line-comment style hints: # /*+ ... */
721        // and collect their positions to avoid double-matching
722        let line_hint_pattern = line_hint_regex();
723        let mut line_hint_positions: std::collections::HashSet<usize> =
724            std::collections::HashSet::new();
725
726        for cap in line_hint_pattern.captures_iter(query) {
727            if let Some(hint_text) = cap.get(1) {
728                let parsed = self.parse_hint_block(hint_text.as_str())?;
729                hints.merge(parsed);
730                // Record the position of the /*+ start to avoid double-matching
731                if let Some(m) = cap.get(0) {
732                    // The regular hint /*+ starts somewhere within this match
733                    // Find where /*+ starts in the original query
734                    let match_start = m.start();
735                    let match_str = m.as_str();
736                    if let Some(inner_pos) = match_str.find("/*+") {
737                        line_hint_positions.insert(match_start + inner_pos);
738                    }
739                }
740            }
741        }
742
743        // Look for regular hint comments: /*+ HINT1 HINT2 ... */
744        // Skip any that are already matched as line comments
745        let hint_pattern = regex();
746
747        for cap in hint_pattern.captures_iter(query) {
748            if let Some(m) = cap.get(0) {
749                // Skip if this was already matched as a line comment
750                if line_hint_positions.contains(&m.start()) {
751                    continue;
752                }
753            }
754            if let Some(hint_text) = cap.get(1) {
755                let parsed = self.parse_hint_block(hint_text.as_str())?;
756                hints.merge(parsed);
757            }
758        }
759
760        self.hints_parsed
761            .fetch_add(hints.hint_count() as u64, Ordering::Relaxed);
762        self.parse_timer
763            .fetch_add(start.elapsed().as_nanos() as u64, Ordering::Relaxed);
764
765        Ok(hints)
766    }
767
768    /// Parse a hint block (the text inside /*+ ... */)
769    fn parse_hint_block(&self, text: &str) -> Result<QueryHints> {
770        let mut hints = QueryHints::new();
771
772        // Split by whitespace, keeping parenthesized groups together
773        let tokens = self.tokenize_hints(text);
774        let mut i = 0;
775
776        while i < tokens.len() {
777            let token = &tokens[i];
778            let hint_upper = token.to_uppercase();
779
780            match hint_upper.as_str() {
781                "HASH_JOIN" => {
782                    if i + 1 < tokens.len() {
783                        let vars = self.parse_variable_list(&tokens[i + 1])?;
784                        hints
785                            .join_hints
786                            .push(JoinHint::new(vars, JoinAlgorithmHint::HashJoin));
787                        i += 1;
788                    }
789                }
790                "MERGE_JOIN" => {
791                    if i + 1 < tokens.len() {
792                        let vars = self.parse_variable_list(&tokens[i + 1])?;
793                        hints
794                            .join_hints
795                            .push(JoinHint::new(vars, JoinAlgorithmHint::MergeJoin));
796                        i += 1;
797                    }
798                }
799                "NESTED_LOOP" | "NL_JOIN" => {
800                    if i + 1 < tokens.len() {
801                        let vars = self.parse_variable_list(&tokens[i + 1])?;
802                        hints
803                            .join_hints
804                            .push(JoinHint::new(vars, JoinAlgorithmHint::NestedLoop));
805                        i += 1;
806                    }
807                }
808                "INDEX_JOIN" => {
809                    if i + 1 < tokens.len() {
810                        let vars = self.parse_variable_list(&tokens[i + 1])?;
811                        hints
812                            .join_hints
813                            .push(JoinHint::new(vars, JoinAlgorithmHint::IndexJoin));
814                        i += 1;
815                    }
816                }
817                "CARDINALITY" => {
818                    if i + 1 < tokens.len() {
819                        let (var, card) = self.parse_cardinality_hint(&tokens[i + 1])?;
820                        hints
821                            .cardinality_hints
822                            .push(CardinalityHint::new(&var, card));
823                        i += 1;
824                    }
825                }
826                "PARALLEL" => {
827                    if i + 1 < tokens.len() {
828                        let threads = self.parse_single_value(&tokens[i + 1])?;
829                        hints.parallelism_hints = Some(ParallelismHint::new(threads as usize));
830                        i += 1;
831                    } else {
832                        hints.parallelism_hints = Some(ParallelismHint::auto());
833                    }
834                }
835                "NO_PARALLEL" | "NOPARALLEL" => {
836                    hints.parallelism_hints = Some(ParallelismHint::disabled());
837                }
838                "TIMEOUT" => {
839                    if i + 1 < tokens.len() {
840                        let timeout = self.parse_duration(&tokens[i + 1])?;
841                        hints.timeout_hint = Some(timeout);
842                        i += 1;
843                    }
844                }
845                "MEMORY_LIMIT" | "MAX_MEMORY" => {
846                    if i + 1 < tokens.len() {
847                        let bytes = self.parse_memory_size(&tokens[i + 1])?;
848                        hints.memory_hint = Some(MemoryHint {
849                            max_memory: bytes,
850                            prefer_streaming: false,
851                            spill_to_disk: true,
852                        });
853                        i += 1;
854                    }
855                }
856                "STREAMING" => {
857                    hints.memory_hint = Some(MemoryHint {
858                        max_memory: usize::MAX,
859                        prefer_streaming: true,
860                        spill_to_disk: false,
861                    });
862                }
863                "NO_CACHE" | "NOCACHE" => {
864                    hints.cache_hints = Some(CacheHint {
865                        use_cache: false,
866                        update_cache: false,
867                        cache_ttl: None,
868                    });
869                }
870                "CACHE" => {
871                    hints.cache_hints = Some(CacheHint {
872                        use_cache: true,
873                        update_cache: true,
874                        cache_ttl: None,
875                    });
876                }
877                "USE_INDEX" | "FORCE_INDEX" => {
878                    if i + 1 < tokens.len() {
879                        let (pattern, index) = self.parse_index_hint(&tokens[i + 1])?;
880                        hints
881                            .index_hints
882                            .push(IndexHint::use_index(&pattern, &index));
883                        i += 1;
884                    }
885                }
886                "IGNORE_INDEX" | "NO_INDEX" => {
887                    if i + 1 < tokens.len() {
888                        let (pattern, index) = self.parse_index_hint(&tokens[i + 1])?;
889                        hints
890                            .index_hints
891                            .push(IndexHint::ignore_index(&pattern, &index));
892                        i += 1;
893                    }
894                }
895                "ORDERED" | "FIXED_ORDER" => {
896                    hints.join_order_hint = Some(JoinOrderHint {
897                        strategy: JoinOrderStrategy::LeftToRight,
898                        order: Vec::new(),
899                    });
900                }
901                "LEADING" => {
902                    if i + 1 < tokens.len() {
903                        let order = self.parse_variable_list(&tokens[i + 1])?;
904                        hints.join_order_hint = Some(JoinOrderHint {
905                            strategy: JoinOrderStrategy::Fixed,
906                            order,
907                        });
908                        i += 1;
909                    }
910                }
911                "MATERIALIZE" => {
912                    if i + 1 < tokens.len() {
913                        let target = self.parse_single_string(&tokens[i + 1])?;
914                        hints
915                            .materialization_hints
916                            .push(MaterializationHint::materialize(&target));
917                        i += 1;
918                    }
919                }
920                _ => {
921                    // Unknown hint - store as custom directive
922                    if i + 1 < tokens.len() && tokens[i + 1].starts_with('(') {
923                        hints.custom_directives.insert(
924                            hint_upper,
925                            tokens[i + 1]
926                                .trim_matches(|c| c == '(' || c == ')')
927                                .to_string(),
928                        );
929                        i += 1;
930                    }
931                }
932            }
933            i += 1;
934        }
935
936        Ok(hints)
937    }
938
939    /// Tokenize hint text preserving parenthesized groups
940    /// Splits hint names from their arguments: HASH_JOIN(?s, ?o) -> ["HASH_JOIN", "(?s, ?o)"]
941    fn tokenize_hints(&self, text: &str) -> Vec<String> {
942        let mut tokens = Vec::new();
943        let mut current = String::new();
944        let mut paren_depth = 0;
945
946        for ch in text.chars() {
947            match ch {
948                '(' if paren_depth == 0 => {
949                    // Starting a parenthesized group
950                    // If there's a current token (hint name), push it first
951                    if !current.is_empty() {
952                        tokens.push(current.clone());
953                        current.clear();
954                    }
955                    paren_depth += 1;
956                    current.push(ch);
957                }
958                '(' => {
959                    // Nested parens
960                    paren_depth += 1;
961                    current.push(ch);
962                }
963                ')' => {
964                    paren_depth -= 1;
965                    current.push(ch);
966                    if paren_depth == 0 && !current.is_empty() {
967                        // End of parenthesized group, push it as a token
968                        tokens.push(current.clone());
969                        current.clear();
970                    }
971                }
972                ' ' | '\t' | '\n' | '\r' if paren_depth == 0 => {
973                    if !current.is_empty() {
974                        tokens.push(current.clone());
975                        current.clear();
976                    }
977                }
978                _ => {
979                    current.push(ch);
980                }
981            }
982        }
983
984        if !current.is_empty() {
985            tokens.push(current);
986        }
987
988        tokens
989    }
990
991    /// Parse variable list from parenthesized string: (?var1, ?var2)
992    fn parse_variable_list(&self, text: &str) -> Result<Vec<String>> {
993        let inner = text.trim_matches(|c| c == '(' || c == ')');
994        let vars: Vec<String> = inner
995            .split(',')
996            .map(|s| s.trim().trim_start_matches('?').to_string())
997            .filter(|s| !s.is_empty())
998            .collect();
999
1000        if vars.is_empty() {
1001            return Err(anyhow!("Empty variable list"));
1002        }
1003        Ok(vars)
1004    }
1005
1006    /// Parse cardinality hint: (?var, 1000)
1007    fn parse_cardinality_hint(&self, text: &str) -> Result<(String, u64)> {
1008        let inner = text.trim_matches(|c| c == '(' || c == ')');
1009        let parts: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
1010
1011        if parts.len() != 2 {
1012            return Err(anyhow!(
1013                "Invalid cardinality hint format: expected (?var, number)"
1014            ));
1015        }
1016
1017        let var = parts[0].trim_start_matches('?').to_string();
1018        let card: u64 = parts[1]
1019            .parse()
1020            .map_err(|_| anyhow!("Invalid cardinality value: {}", parts[1]))?;
1021
1022        Ok((var, card))
1023    }
1024
1025    /// Parse single numeric value: (4)
1026    fn parse_single_value(&self, text: &str) -> Result<u64> {
1027        let inner = text.trim_matches(|c| c == '(' || c == ')');
1028        inner
1029            .parse()
1030            .map_err(|_| anyhow!("Invalid numeric value: {}", inner))
1031    }
1032
1033    /// Parse single string value: (name)
1034    fn parse_single_string(&self, text: &str) -> Result<String> {
1035        let inner = text.trim_matches(|c| c == '(' || c == ')');
1036        Ok(inner.trim().to_string())
1037    }
1038
1039    /// Parse duration: (30s), (5m), (1h)
1040    fn parse_duration(&self, text: &str) -> Result<Duration> {
1041        let inner = text.trim_matches(|c| c == '(' || c == ')').to_lowercase();
1042
1043        if let Some(secs) = inner.strip_suffix('s') {
1044            let val: u64 = secs
1045                .parse()
1046                .map_err(|_| anyhow!("Invalid duration: {}", text))?;
1047            return Ok(Duration::from_secs(val));
1048        }
1049        if let Some(mins) = inner.strip_suffix('m') {
1050            let val: u64 = mins
1051                .parse()
1052                .map_err(|_| anyhow!("Invalid duration: {}", text))?;
1053            return Ok(Duration::from_secs(val * 60));
1054        }
1055        if let Some(hours) = inner.strip_suffix('h') {
1056            let val: u64 = hours
1057                .parse()
1058                .map_err(|_| anyhow!("Invalid duration: {}", text))?;
1059            return Ok(Duration::from_secs(val * 3600));
1060        }
1061
1062        // Assume milliseconds if no suffix
1063        let val: u64 = inner
1064            .parse()
1065            .map_err(|_| anyhow!("Invalid duration: {}", text))?;
1066        Ok(Duration::from_millis(val))
1067    }
1068
1069    /// Parse memory size: (1GB), (512MB), (1024KB)
1070    fn parse_memory_size(&self, text: &str) -> Result<usize> {
1071        let inner = text.trim_matches(|c| c == '(' || c == ')').to_uppercase();
1072
1073        if let Some(gb) = inner.strip_suffix("GB") {
1074            let val: usize = gb
1075                .parse()
1076                .map_err(|_| anyhow!("Invalid memory size: {}", text))?;
1077            return Ok(val * 1024 * 1024 * 1024);
1078        }
1079        if let Some(mb) = inner.strip_suffix("MB") {
1080            let val: usize = mb
1081                .parse()
1082                .map_err(|_| anyhow!("Invalid memory size: {}", text))?;
1083            return Ok(val * 1024 * 1024);
1084        }
1085        if let Some(kb) = inner.strip_suffix("KB") {
1086            let val: usize = kb
1087                .parse()
1088                .map_err(|_| anyhow!("Invalid memory size: {}", text))?;
1089            return Ok(val * 1024);
1090        }
1091
1092        // Assume bytes if no suffix
1093        inner
1094            .parse()
1095            .map_err(|_| anyhow!("Invalid memory size: {}", text))
1096    }
1097
1098    /// Parse index hint: (pattern, index_name)
1099    fn parse_index_hint(&self, text: &str) -> Result<(String, String)> {
1100        let inner = text.trim_matches(|c| c == '(' || c == ')');
1101        let parts: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
1102
1103        if parts.len() != 2 {
1104            return Err(anyhow!(
1105                "Invalid index hint format: expected (pattern, index_name)"
1106            ));
1107        }
1108
1109        Ok((parts[0].to_string(), parts[1].to_string()))
1110    }
1111
1112    /// Get parsing statistics
1113    pub fn statistics(&self) -> HintParserStats {
1114        HintParserStats {
1115            hints_parsed: self.hints_parsed.load(Ordering::Relaxed),
1116            total_parse_time_ns: self.parse_timer.load(Ordering::Relaxed),
1117        }
1118    }
1119}
1120
1121/// Hint parser statistics
1122#[derive(Debug, Clone)]
1123pub struct HintParserStats {
1124    /// Total hints parsed
1125    pub hints_parsed: u64,
1126    /// Total parse time in nanoseconds
1127    pub total_parse_time_ns: u64,
1128}
1129
1130/// Regex for hint comments
1131fn regex() -> &'static Regex {
1132    static HINT_REGEX: OnceLock<Regex> = OnceLock::new();
1133    HINT_REGEX.get_or_init(|| {
1134        // Match /*+ hint content */ - capture everything between /*+ and */
1135        // Simple pattern: non-greedy match of any chars between markers
1136        Regex::new(r"/\*\+\s*(.+?)\s*\*/").expect("Invalid regex")
1137    })
1138}
1139
1140/// Regex for line hint comments
1141fn line_hint_regex() -> &'static Regex {
1142    static LINE_HINT_REGEX: OnceLock<Regex> = OnceLock::new();
1143    LINE_HINT_REGEX.get_or_init(|| {
1144        // Match # /*+ hint content */
1145        Regex::new(r"#\s*/\*\+\s*(.+?)\s*\*/").expect("Invalid regex")
1146    })
1147}
1148
1149/// Hint application result
1150#[derive(Debug, Clone)]
1151pub struct HintApplicationResult {
1152    /// Hints that were applied
1153    pub applied: Vec<String>,
1154    /// Hints that were ignored (e.g., not applicable)
1155    pub ignored: Vec<String>,
1156    /// Hints that conflicted and were resolved
1157    pub conflicts: Vec<String>,
1158}
1159
1160impl HintApplicationResult {
1161    /// Create new result
1162    pub fn new() -> Self {
1163        Self {
1164            applied: Vec::new(),
1165            ignored: Vec::new(),
1166            conflicts: Vec::new(),
1167        }
1168    }
1169
1170    /// Record an applied hint
1171    pub fn applied(&mut self, hint: &str) {
1172        self.applied.push(hint.to_string());
1173    }
1174
1175    /// Record an ignored hint
1176    pub fn ignored(&mut self, hint: &str) {
1177        self.ignored.push(hint.to_string());
1178    }
1179
1180    /// Record a conflict resolution
1181    pub fn conflict(&mut self, hint: &str) {
1182        self.conflicts.push(hint.to_string());
1183    }
1184}
1185
1186impl Default for HintApplicationResult {
1187    fn default() -> Self {
1188        Self::new()
1189    }
1190}
1191
1192/// Hint validator for checking hint consistency
1193pub struct HintValidator;
1194
1195impl HintValidator {
1196    /// Validate hints for consistency and conflicts
1197    pub fn validate(hints: &QueryHints) -> Vec<HintValidationWarning> {
1198        let mut warnings = Vec::new();
1199
1200        // Check for conflicting join hints on same variables
1201        let mut seen_vars: HashMap<String, Vec<JoinAlgorithmHint>> = HashMap::new();
1202        for hint in &hints.join_hints {
1203            for var in &hint.variables {
1204                seen_vars
1205                    .entry(var.clone())
1206                    .or_default()
1207                    .push(hint.algorithm);
1208            }
1209        }
1210        for (var, algorithms) in seen_vars {
1211            if algorithms.len() > 1 {
1212                let unique: std::collections::HashSet<_> = algorithms.iter().collect();
1213                if unique.len() > 1 {
1214                    warnings.push(HintValidationWarning {
1215                        severity: WarningSeverity::Warning,
1216                        message: format!(
1217                            "Conflicting join hints for variable '{}': {:?}",
1218                            var, algorithms
1219                        ),
1220                    });
1221                }
1222            }
1223        }
1224
1225        // Check for streaming + parallelism conflict
1226        if let Some(ref mem) = hints.memory_hint {
1227            if mem.prefer_streaming {
1228                if let Some(ref par) = hints.parallelism_hints {
1229                    if par.enabled && par.threads.is_some_and(|t| t > 1) {
1230                        warnings.push(HintValidationWarning {
1231                            severity: WarningSeverity::Info,
1232                            message:
1233                                "Streaming mode with parallelism may reduce streaming benefits"
1234                                    .to_string(),
1235                        });
1236                    }
1237                }
1238            }
1239        }
1240
1241        // Check for cardinality hints with very high values
1242        for hint in &hints.cardinality_hints {
1243            if hint.cardinality > 1_000_000_000 {
1244                warnings.push(HintValidationWarning {
1245                    severity: WarningSeverity::Warning,
1246                    message: format!(
1247                        "Very high cardinality hint for '{}': {} (may affect optimization)",
1248                        hint.variable, hint.cardinality
1249                    ),
1250                });
1251            }
1252        }
1253
1254        // Check timeout is reasonable
1255        if let Some(timeout) = hints.timeout_hint {
1256            if timeout < Duration::from_millis(100) {
1257                warnings.push(HintValidationWarning {
1258                    severity: WarningSeverity::Warning,
1259                    message: format!(
1260                        "Very short timeout: {:?} (query may timeout immediately)",
1261                        timeout
1262                    ),
1263                });
1264            }
1265            if timeout > Duration::from_secs(3600) {
1266                warnings.push(HintValidationWarning {
1267                    severity: WarningSeverity::Info,
1268                    message: format!(
1269                        "Very long timeout: {:?} (consider using async execution)",
1270                        timeout
1271                    ),
1272                });
1273            }
1274        }
1275
1276        warnings
1277    }
1278}
1279
1280/// Hint validation warning
1281#[derive(Debug, Clone)]
1282pub struct HintValidationWarning {
1283    /// Warning severity
1284    pub severity: WarningSeverity,
1285    /// Warning message
1286    pub message: String,
1287}
1288
1289/// Warning severity levels
1290#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1291pub enum WarningSeverity {
1292    /// Informational
1293    Info,
1294    /// Warning (may affect performance)
1295    Warning,
1296    /// Error (hint will be ignored)
1297    Error,
1298}
1299
1300#[cfg(test)]
1301mod tests {
1302    use super::*;
1303
1304    #[test]
1305    fn test_regex_matching() {
1306        let re = regex();
1307        let query = "/*+ HASH_JOIN(?s, ?o) */ SELECT ?s ?o WHERE { ?s ?p ?o }";
1308
1309        let caps: Vec<_> = re.captures_iter(query).collect();
1310        println!("Number of captures: {}", caps.len());
1311        for cap in &caps {
1312            println!("Full match: {:?}", cap.get(0).map(|m| m.as_str()));
1313            println!("Group 1: {:?}", cap.get(1).map(|m| m.as_str()));
1314        }
1315
1316        assert!(!caps.is_empty(), "Regex should match the hint comment");
1317    }
1318
1319    #[test]
1320    fn test_parse_hash_join_hint() {
1321        let query = "/*+ HASH_JOIN(?s, ?o) */ SELECT ?s ?o WHERE { ?s ?p ?o }";
1322        let hints = HintParser::parse(query).unwrap();
1323
1324        assert_eq!(hints.join_hints.len(), 1);
1325        assert_eq!(hints.join_hints[0].algorithm, JoinAlgorithmHint::HashJoin);
1326        assert_eq!(hints.join_hints[0].variables, vec!["s", "o"]);
1327    }
1328
1329    #[test]
1330    fn test_parse_cardinality_hint() {
1331        let query = "/*+ CARDINALITY(?person, 1000) */ SELECT ?person WHERE { ?person a <Person> }";
1332        let hints = HintParser::parse(query).unwrap();
1333
1334        assert_eq!(hints.cardinality_hints.len(), 1);
1335        assert_eq!(hints.cardinality_hints[0].variable, "person");
1336        assert_eq!(hints.cardinality_hints[0].cardinality, 1000);
1337    }
1338
1339    #[test]
1340    fn test_parse_parallel_hint() {
1341        let query = "/*+ PARALLEL(4) */ SELECT * WHERE { ?s ?p ?o }";
1342        let hints = HintParser::parse(query).unwrap();
1343
1344        assert!(hints.parallelism_hints.is_some());
1345        let par = hints.parallelism_hints.unwrap();
1346        assert!(par.enabled);
1347        assert_eq!(par.threads, Some(4));
1348    }
1349
1350    #[test]
1351    fn test_parse_no_parallel_hint() {
1352        let query = "/*+ NO_PARALLEL */ SELECT * WHERE { ?s ?p ?o }";
1353        let hints = HintParser::parse(query).unwrap();
1354
1355        assert!(hints.parallelism_hints.is_some());
1356        assert!(!hints.parallelism_hints.unwrap().enabled);
1357    }
1358
1359    #[test]
1360    fn test_parse_timeout_hint() {
1361        let query = "/*+ TIMEOUT(30s) */ SELECT * WHERE { ?s ?p ?o }";
1362        let hints = HintParser::parse(query).unwrap();
1363
1364        assert_eq!(hints.timeout_hint, Some(Duration::from_secs(30)));
1365    }
1366
1367    #[test]
1368    fn test_parse_memory_limit_hint() {
1369        let query = "/*+ MEMORY_LIMIT(1GB) */ SELECT * WHERE { ?s ?p ?o }";
1370        let hints = HintParser::parse(query).unwrap();
1371
1372        assert!(hints.memory_hint.is_some());
1373        assert_eq!(hints.memory_hint.unwrap().max_memory, 1024 * 1024 * 1024);
1374    }
1375
1376    #[test]
1377    fn test_parse_no_cache_hint() {
1378        let query = "/*+ NO_CACHE */ SELECT * WHERE { ?s ?p ?o }";
1379        let hints = HintParser::parse(query).unwrap();
1380
1381        assert!(hints.cache_hints.is_some());
1382        assert!(!hints.cache_hints.unwrap().use_cache);
1383    }
1384
1385    #[test]
1386    fn test_parse_multiple_hints() {
1387        let query =
1388            "/*+ HASH_JOIN(?s, ?o) PARALLEL(8) TIMEOUT(60s) */ SELECT ?s ?o WHERE { ?s ?p ?o }";
1389        let hints = HintParser::parse(query).unwrap();
1390
1391        assert_eq!(hints.join_hints.len(), 1);
1392        assert!(hints.parallelism_hints.is_some());
1393        assert_eq!(hints.timeout_hint, Some(Duration::from_secs(60)));
1394    }
1395
1396    #[test]
1397    fn test_parse_line_comment_hint() {
1398        let query = r#"
1399            # /*+ CARDINALITY(?x, 5000) PARALLEL(2) */
1400            SELECT ?x WHERE { ?x a <Thing> }
1401        "#;
1402        let hints = HintParser::parse(query).unwrap();
1403
1404        assert_eq!(hints.cardinality_hints.len(), 1);
1405        assert!(hints.parallelism_hints.is_some());
1406    }
1407
1408    #[test]
1409    fn test_hints_builder() {
1410        let hints = QueryHints::builder()
1411            .hash_join(vec!["s", "o"])
1412            .cardinality("person", 1000)
1413            .parallel(4)
1414            .timeout_secs(30)
1415            .no_cache()
1416            .build();
1417
1418        assert_eq!(hints.join_hints.len(), 1);
1419        assert_eq!(hints.cardinality_hints.len(), 1);
1420        assert!(hints.parallelism_hints.is_some());
1421        assert_eq!(hints.timeout_hint, Some(Duration::from_secs(30)));
1422        assert!(hints.cache_hints.is_some());
1423    }
1424
1425    #[test]
1426    fn test_hint_validator() {
1427        let hints = QueryHints::builder()
1428            .hash_join(vec!["x", "y"])
1429            .merge_join(vec!["x", "z"]) // Conflicting hint on ?x
1430            .timeout(Duration::from_millis(50)) // Very short timeout
1431            .build();
1432
1433        let warnings = HintValidator::validate(&hints);
1434
1435        assert!(warnings.iter().any(|w| w.message.contains("Conflicting")));
1436        assert!(warnings.iter().any(|w| w.message.contains("short timeout")));
1437    }
1438
1439    #[test]
1440    fn test_hint_merge() {
1441        let mut hints1 = QueryHints::builder().hash_join(vec!["a", "b"]).build();
1442
1443        let hints2 = QueryHints::builder()
1444            .cardinality("c", 500)
1445            .parallel(4)
1446            .build();
1447
1448        hints1.merge(hints2);
1449
1450        assert_eq!(hints1.join_hints.len(), 1);
1451        assert_eq!(hints1.cardinality_hints.len(), 1);
1452        assert!(hints1.parallelism_hints.is_some());
1453    }
1454
1455    #[test]
1456    fn test_empty_hints() {
1457        let hints = QueryHints::new();
1458        assert!(hints.is_empty());
1459        assert_eq!(hints.hint_count(), 0);
1460    }
1461
1462    #[test]
1463    fn test_get_join_hint() {
1464        let hints = QueryHints::builder().hash_join(vec!["s", "o"]).build();
1465
1466        let var_s = Variable::new("s").unwrap();
1467        let var_o = Variable::new("o").unwrap();
1468
1469        let hint = hints.get_join_hint(&[var_s, var_o]);
1470        assert!(hint.is_some());
1471        assert_eq!(hint.unwrap().algorithm, JoinAlgorithmHint::HashJoin);
1472    }
1473
1474    #[test]
1475    fn test_get_cardinality_hint() {
1476        let hints = QueryHints::builder().cardinality("person", 1000).build();
1477
1478        let var = Variable::new("person").unwrap();
1479        let card = hints.get_cardinality_hint(&var);
1480
1481        assert_eq!(card, Some(1000));
1482    }
1483
1484    #[test]
1485    fn test_parse_index_hint() {
1486        let query = "/*+ USE_INDEX(pattern1, idx_subject) */ SELECT * WHERE { ?s ?p ?o }";
1487        let hints = HintParser::parse(query).unwrap();
1488
1489        assert_eq!(hints.index_hints.len(), 1);
1490        assert_eq!(hints.index_hints[0].pattern_id, "pattern1");
1491        assert_eq!(hints.index_hints[0].directive, IndexDirective::Use);
1492    }
1493
1494    #[test]
1495    fn test_parse_leading_hint() {
1496        let query = "/*+ LEADING(?a, ?b, ?c) */ SELECT * WHERE { ?a ?p ?b . ?b ?q ?c }";
1497        let hints = HintParser::parse(query).unwrap();
1498
1499        assert!(hints.join_order_hint.is_some());
1500        let order = hints.join_order_hint.unwrap();
1501        assert_eq!(order.strategy, JoinOrderStrategy::Fixed);
1502        assert_eq!(order.order, vec!["a", "b", "c"]);
1503    }
1504}