1use crate::algebra::Variable;
43use anyhow::{anyhow, Result};
44use regex::Regex;
45#[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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
56pub struct QueryHints {
57 pub join_hints: Vec<JoinHint>,
59 pub index_hints: Vec<IndexHint>,
61 pub cardinality_hints: Vec<CardinalityHint>,
63 pub parallelism_hints: Option<ParallelismHint>,
65 pub materialization_hints: Vec<MaterializationHint>,
67 pub timeout_hint: Option<Duration>,
69 pub memory_hint: Option<MemoryHint>,
71 pub cache_hints: Option<CacheHint>,
73 pub join_order_hint: Option<JoinOrderHint>,
75 pub filter_hints: Vec<FilterHint>,
77 pub custom_directives: HashMap<String, String>,
79}
80
81impl QueryHints {
82 pub fn new() -> Self {
84 Self::default()
85 }
86
87 pub fn builder() -> QueryHintsBuilder {
89 QueryHintsBuilder::new()
90 }
91
92 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 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 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 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 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 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#[derive(Debug, Default)]
187pub struct QueryHintsBuilder {
188 hints: QueryHints,
189}
190
191impl QueryHintsBuilder {
192 pub fn new() -> Self {
194 Self::default()
195 }
196
197 pub fn with_join_hint(mut self, hint: JoinHint) -> Self {
199 self.hints.join_hints.push(hint);
200 self
201 }
202
203 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 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 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 pub fn with_index_hint(mut self, hint: IndexHint) -> Self {
229 self.hints.index_hints.push(hint);
230 self
231 }
232
233 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 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 pub fn with_cardinality_hint(mut self, hint: CardinalityHint) -> Self {
245 self.hints.cardinality_hints.push(hint);
246 self
247 }
248
249 pub fn cardinality(self, variable: &str, cardinality: u64) -> Self {
251 self.with_cardinality_hint(CardinalityHint::new(variable, cardinality))
252 }
253
254 pub fn with_parallelism(mut self, hint: ParallelismHint) -> Self {
256 self.hints.parallelism_hints = Some(hint);
257 self
258 }
259
260 pub fn parallel(self, threads: usize) -> Self {
262 self.with_parallelism(ParallelismHint::new(threads))
263 }
264
265 pub fn no_parallel(self) -> Self {
267 self.with_parallelism(ParallelismHint::disabled())
268 }
269
270 pub fn with_materialization_hint(mut self, hint: MaterializationHint) -> Self {
272 self.hints.materialization_hints.push(hint);
273 self
274 }
275
276 pub fn timeout(mut self, duration: Duration) -> Self {
278 self.hints.timeout_hint = Some(duration);
279 self
280 }
281
282 pub fn timeout_secs(self, secs: u64) -> Self {
284 self.timeout(Duration::from_secs(secs))
285 }
286
287 pub fn with_memory_hint(mut self, hint: MemoryHint) -> Self {
289 self.hints.memory_hint = Some(hint);
290 self
291 }
292
293 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 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 pub fn with_cache_hint(mut self, hint: CacheHint) -> Self {
313 self.hints.cache_hints = Some(hint);
314 self
315 }
316
317 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 pub fn with_join_order(mut self, hint: JoinOrderHint) -> Self {
328 self.hints.join_order_hint = Some(hint);
329 self
330 }
331
332 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 pub fn with_filter_hint(mut self, hint: FilterHint) -> Self {
342 self.hints.filter_hints.push(hint);
343 self
344 }
345
346 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 pub fn build(self) -> QueryHints {
356 self.hints
357 }
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize)]
362pub struct JoinHint {
363 pub variables: Vec<String>,
365 pub algorithm: JoinAlgorithmHint,
367 pub build_side: Option<JoinBuildSide>,
369 pub priority: u8,
371}
372
373impl JoinHint {
374 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 pub fn with_build_side(mut self, side: JoinBuildSide) -> Self {
386 self.build_side = Some(side);
387 self
388 }
389
390 pub fn with_priority(mut self, priority: u8) -> Self {
392 self.priority = priority;
393 self
394 }
395}
396
397#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
399pub enum JoinAlgorithmHint {
400 HashJoin,
402 MergeJoin,
404 NestedLoop,
406 IndexJoin,
408 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
426pub enum JoinBuildSide {
427 Left,
429 Right,
431 Auto,
433}
434
435#[derive(Debug, Clone, Serialize, Deserialize)]
437pub struct IndexHint {
438 pub pattern_id: String,
440 pub directive: IndexDirective,
442 pub index_names: Vec<String>,
444}
445
446impl IndexHint {
447 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 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 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 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
486pub enum IndexDirective {
487 Use,
489 Ignore,
491 Force,
493 NoIndex,
495}
496
497#[derive(Debug, Clone, Serialize, Deserialize)]
499pub struct CardinalityHint {
500 pub variable: String,
502 pub cardinality: u64,
504 pub confidence: f64,
506}
507
508impl CardinalityHint {
509 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 pub fn with_confidence(mut self, confidence: f64) -> Self {
520 self.confidence = confidence.clamp(0.0, 1.0);
521 self
522 }
523}
524
525#[derive(Debug, Clone, Serialize, Deserialize)]
527pub struct ParallelismHint {
528 pub enabled: bool,
530 pub threads: Option<usize>,
532 pub min_batch_size: usize,
534 pub work_stealing: bool,
536}
537
538impl ParallelismHint {
539 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
572pub struct MaterializationHint {
573 pub target: String,
575 pub strategy: MaterializationStrategy,
577}
578
579impl MaterializationHint {
580 pub fn materialize(target: &str) -> Self {
582 Self {
583 target: target.to_string(),
584 strategy: MaterializationStrategy::Eager,
585 }
586 }
587
588 pub fn lazy(target: &str) -> Self {
590 Self {
591 target: target.to_string(),
592 strategy: MaterializationStrategy::Lazy,
593 }
594 }
595
596 pub fn streaming(target: &str) -> Self {
598 Self {
599 target: target.to_string(),
600 strategy: MaterializationStrategy::Streaming,
601 }
602 }
603}
604
605#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
607pub enum MaterializationStrategy {
608 Eager,
610 Lazy,
612 Streaming,
614 Partial,
616}
617
618#[derive(Debug, Clone, Serialize, Deserialize)]
620pub struct MemoryHint {
621 pub max_memory: usize,
623 pub prefer_streaming: bool,
625 pub spill_to_disk: bool,
627}
628
629#[derive(Debug, Clone, Serialize, Deserialize)]
631pub struct CacheHint {
632 pub use_cache: bool,
634 pub update_cache: bool,
636 pub cache_ttl: Option<Duration>,
638}
639
640#[derive(Debug, Clone, Serialize, Deserialize)]
642pub struct JoinOrderHint {
643 pub strategy: JoinOrderStrategy,
645 pub order: Vec<String>,
647}
648
649#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
651pub enum JoinOrderStrategy {
652 Auto,
654 Fixed,
656 LeftToRight,
658 SmallestFirst,
660 MostSelectiveFirst,
662}
663
664#[derive(Debug, Clone, Serialize, Deserialize)]
666pub struct FilterHint {
667 pub filter_id: String,
669 pub directive: FilterPushdownDirective,
671 pub target_pattern: Option<String>,
673}
674
675#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
677pub enum FilterPushdownDirective {
678 Push,
680 NoPush,
682 PushTo,
684}
685
686pub struct HintParser {
688 hints_parsed: Arc<AtomicU64>,
690 parse_timer: Arc<AtomicU64>,
692}
693
694impl Default for HintParser {
695 fn default() -> Self {
696 Self::new()
697 }
698}
699
700impl HintParser {
701 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 pub fn parse(query: &str) -> Result<QueryHints> {
711 let parser = Self::new();
712 parser.parse_query(query)
713 }
714
715 pub fn parse_query(&self, query: &str) -> Result<QueryHints> {
717 let start = std::time::Instant::now();
718 let mut hints = QueryHints::new();
719
720 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 if let Some(m) = cap.get(0) {
732 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 let hint_pattern = regex();
746
747 for cap in hint_pattern.captures_iter(query) {
748 if let Some(m) = cap.get(0) {
749 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 fn parse_hint_block(&self, text: &str) -> Result<QueryHints> {
770 let mut hints = QueryHints::new();
771
772 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 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 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 if !current.is_empty() {
952 tokens.push(current.clone());
953 current.clear();
954 }
955 paren_depth += 1;
956 current.push(ch);
957 }
958 '(' => {
959 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 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 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 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 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 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 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 let val: u64 = inner
1064 .parse()
1065 .map_err(|_| anyhow!("Invalid duration: {}", text))?;
1066 Ok(Duration::from_millis(val))
1067 }
1068
1069 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 inner
1094 .parse()
1095 .map_err(|_| anyhow!("Invalid memory size: {}", text))
1096 }
1097
1098 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 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#[derive(Debug, Clone)]
1123pub struct HintParserStats {
1124 pub hints_parsed: u64,
1126 pub total_parse_time_ns: u64,
1128}
1129
1130fn regex() -> &'static Regex {
1132 static HINT_REGEX: OnceLock<Regex> = OnceLock::new();
1133 HINT_REGEX.get_or_init(|| {
1134 Regex::new(r"/\*\+\s*(.+?)\s*\*/").expect("Invalid regex")
1137 })
1138}
1139
1140fn line_hint_regex() -> &'static Regex {
1142 static LINE_HINT_REGEX: OnceLock<Regex> = OnceLock::new();
1143 LINE_HINT_REGEX.get_or_init(|| {
1144 Regex::new(r"#\s*/\*\+\s*(.+?)\s*\*/").expect("Invalid regex")
1146 })
1147}
1148
1149#[derive(Debug, Clone)]
1151pub struct HintApplicationResult {
1152 pub applied: Vec<String>,
1154 pub ignored: Vec<String>,
1156 pub conflicts: Vec<String>,
1158}
1159
1160impl HintApplicationResult {
1161 pub fn new() -> Self {
1163 Self {
1164 applied: Vec::new(),
1165 ignored: Vec::new(),
1166 conflicts: Vec::new(),
1167 }
1168 }
1169
1170 pub fn applied(&mut self, hint: &str) {
1172 self.applied.push(hint.to_string());
1173 }
1174
1175 pub fn ignored(&mut self, hint: &str) {
1177 self.ignored.push(hint.to_string());
1178 }
1179
1180 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
1192pub struct HintValidator;
1194
1195impl HintValidator {
1196 pub fn validate(hints: &QueryHints) -> Vec<HintValidationWarning> {
1198 let mut warnings = Vec::new();
1199
1200 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 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 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 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#[derive(Debug, Clone)]
1282pub struct HintValidationWarning {
1283 pub severity: WarningSeverity,
1285 pub message: String,
1287}
1288
1289#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1291pub enum WarningSeverity {
1292 Info,
1294 Warning,
1296 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"]) .timeout(Duration::from_millis(50)) .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}