1use anyhow::Result;
38use regex::Regex;
39use serde::{Deserialize, Serialize};
40use sha2::{Digest, Sha256};
41use std::collections::{HashMap, HashSet};
42use std::hash::{Hash, Hasher};
43use std::sync::atomic::{AtomicU64, Ordering};
44use std::sync::{Arc, OnceLock, RwLock};
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct FingerprintConfig {
49 pub include_variables: bool,
51 pub normalize_literals: bool,
53 pub normalize_numbers: bool,
55 pub normalize_iri_locals: bool,
57 pub preserve_predicates: bool,
59 pub preserve_types: bool,
61 pub hash_algorithm: HashAlgorithm,
63 pub cache_size: usize,
65}
66
67impl Default for FingerprintConfig {
68 fn default() -> Self {
69 Self {
70 include_variables: false,
71 normalize_literals: true,
72 normalize_numbers: true,
73 normalize_iri_locals: true,
74 preserve_predicates: true,
75 preserve_types: true,
76 hash_algorithm: HashAlgorithm::Sha256,
77 cache_size: 10000,
78 }
79 }
80}
81
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
84pub enum HashAlgorithm {
85 Md5,
87 Sha256,
89 Fnv1a,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
95pub struct QueryFingerprint {
96 pub hash: String,
98 pub short_hash: String,
100 pub template: String,
102 pub parameters: Vec<ParameterSlot>,
104 pub query_form: QueryForm,
106 pub features: QueryFeatures,
108 pub original_length: usize,
110}
111
112impl QueryFingerprint {
113 pub fn template_id(&self) -> &str {
115 &self.short_hash
116 }
117
118 pub fn matches_template(&self, other: &Self) -> bool {
120 self.short_hash == other.short_hash
121 }
122
123 pub fn parameter_count(&self) -> usize {
125 self.parameters.len()
126 }
127
128 pub fn is_parameterized(&self) -> bool {
130 !self.parameters.is_empty()
131 }
132}
133
134#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
136pub struct ParameterSlot {
137 pub name: String,
139 pub param_type: ParameterType,
141 pub position: usize,
143 pub original_value: Option<String>,
145}
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
149pub enum ParameterType {
150 StringLiteral,
152 NumericLiteral,
154 DateTimeLiteral,
156 Iri,
158 Boolean,
160 LangLiteral,
162 Unknown,
164}
165
166#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
168pub enum QueryForm {
169 Select,
171 Ask,
173 Construct,
175 Describe,
177 Update,
179 Unknown,
181}
182
183impl QueryForm {
184 fn from_str(s: &str) -> Self {
185 match s.to_uppercase().as_str() {
186 "SELECT" => QueryForm::Select,
187 "ASK" => QueryForm::Ask,
188 "CONSTRUCT" => QueryForm::Construct,
189 "DESCRIBE" => QueryForm::Describe,
190 "INSERT" | "DELETE" => QueryForm::Update,
191 _ => QueryForm::Unknown,
192 }
193 }
194}
195
196#[derive(Debug, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
198pub struct QueryFeatures {
199 pub triple_pattern_count: usize,
201 pub variable_count: usize,
203 pub filter_count: usize,
205 pub optional_count: usize,
207 pub union_count: usize,
209 pub subquery_count: usize,
211 pub has_group_by: bool,
213 pub has_order_by: bool,
215 pub has_limit: bool,
217 pub has_offset: bool,
219 pub has_distinct: bool,
221 pub has_service: bool,
223 pub has_values: bool,
225 pub has_property_paths: bool,
227 pub has_aggregates: bool,
229 pub has_bind: bool,
231 pub has_minus: bool,
233 pub complexity_score: u32,
235}
236
237pub struct QueryFingerprinter {
239 config: FingerprintConfig,
241 cache: Arc<RwLock<HashMap<String, QueryFingerprint>>>,
243 stats: FingerprintStats,
245}
246
247#[derive(Debug, Default)]
249struct FingerprintStats {
250 computed: AtomicU64,
252 cache_hits: AtomicU64,
254 cache_misses: AtomicU64,
256}
257
258impl QueryFingerprinter {
259 pub fn new(config: FingerprintConfig) -> Self {
261 Self {
262 config,
263 cache: Arc::new(RwLock::new(HashMap::new())),
264 stats: FingerprintStats::default(),
265 }
266 }
267
268 pub fn fingerprint(&self, query: &str) -> Result<QueryFingerprint> {
270 let query_hash = self.quick_hash(query);
272 {
273 let cache = self.cache.read().expect("read lock should not be poisoned");
274 if let Some(fp) = cache.get(&query_hash) {
275 self.stats.cache_hits.fetch_add(1, Ordering::Relaxed);
276 return Ok(fp.clone());
277 }
278 }
279
280 self.stats.cache_misses.fetch_add(1, Ordering::Relaxed);
281
282 let fingerprint = self.compute_fingerprint(query)?;
284
285 {
287 let mut cache = self
288 .cache
289 .write()
290 .expect("write lock should not be poisoned");
291 if cache.len() >= self.config.cache_size {
292 let keys_to_remove: Vec<_> = cache.keys().take(cache.len() / 2).cloned().collect();
294 for key in keys_to_remove {
295 cache.remove(&key);
296 }
297 }
298 cache.insert(query_hash, fingerprint.clone());
299 }
300
301 self.stats.computed.fetch_add(1, Ordering::Relaxed);
302 Ok(fingerprint)
303 }
304
305 fn compute_fingerprint(&self, query: &str) -> Result<QueryFingerprint> {
307 let query_form = self.extract_query_form(query);
309
310 let (normalized, parameters) = self.normalize_query(query)?;
312
313 let features = self.extract_features(query);
315
316 let hash = self.compute_hash(&normalized);
318 let short_hash = hash[..16].to_string();
319
320 Ok(QueryFingerprint {
321 hash,
322 short_hash,
323 template: normalized,
324 parameters,
325 query_form,
326 features,
327 original_length: query.len(),
328 })
329 }
330
331 fn extract_query_form(&self, query: &str) -> QueryForm {
333 let trimmed = query.trim_start();
334 let first_word = trimmed.split_whitespace().next().unwrap_or("");
335 QueryForm::from_str(first_word)
336 }
337
338 fn normalize_query(&self, query: &str) -> Result<(String, Vec<ParameterSlot>)> {
340 let mut normalized = query.to_string();
341 let mut parameters = Vec::new();
342 let mut param_index = 0;
343
344 normalized = remove_comments(&normalized);
346
347 normalized = normalize_whitespace(&normalized);
349
350 if self.config.normalize_literals {
352 let (new_query, new_params) =
353 self.normalize_string_literals(&normalized, param_index)?;
354 normalized = new_query;
355 param_index += new_params.len();
356 parameters.extend(new_params);
357 }
358
359 if self.config.normalize_numbers {
361 let (new_query, new_params) =
362 self.normalize_numeric_literals(&normalized, param_index)?;
363 normalized = new_query;
364 param_index += new_params.len();
365 parameters.extend(new_params);
366 }
367
368 if self.config.normalize_iri_locals {
370 let (new_query, new_params) = self.normalize_iri_locals(&normalized, param_index)?;
371 normalized = new_query;
372 parameters.extend(new_params);
373 }
374
375 if !self.config.include_variables {
377 normalized = self.normalize_variables(&normalized);
378 }
379
380 normalized = normalize_whitespace(&normalized);
382
383 Ok((normalized, parameters))
384 }
385
386 fn normalize_string_literals(
388 &self,
389 query: &str,
390 start_index: usize,
391 ) -> Result<(String, Vec<ParameterSlot>)> {
392 let string_pattern = string_literal_regex();
393 let mut result = query.to_string();
394 let mut params = Vec::new();
395
396 let matches: Vec<_> = string_pattern.find_iter(query).collect();
398
399 for (index, m) in (start_index..).zip(matches.into_iter().rev()) {
401 let original = m.as_str().to_string();
402 let param_name = format!("${}", index);
403
404 let param_type = if original.contains("@") {
406 ParameterType::LangLiteral
407 } else if original.contains("^^") {
408 if original.contains("dateTime") || original.contains("date") {
409 ParameterType::DateTimeLiteral
410 } else {
411 ParameterType::StringLiteral
412 }
413 } else {
414 ParameterType::StringLiteral
415 };
416
417 params.push(ParameterSlot {
418 name: param_name.clone(),
419 param_type,
420 position: m.start(),
421 original_value: Some(original),
422 });
423
424 result = format!(
425 "{}{}{}",
426 &result[..m.start()],
427 param_name,
428 &result[m.end()..]
429 );
430 }
431
432 params.reverse(); Ok((result, params))
434 }
435
436 fn normalize_numeric_literals(
438 &self,
439 query: &str,
440 start_index: usize,
441 ) -> Result<(String, Vec<ParameterSlot>)> {
442 let number_pattern = numeric_literal_regex();
443 let mut result = query.to_string();
444 let mut params = Vec::new();
445
446 let matches: Vec<_> = number_pattern.find_iter(query).collect();
447
448 for (index, m) in (start_index..).zip(matches.into_iter().rev()) {
449 let original = m.as_str().to_string();
450 let param_name = format!("${}", index);
451
452 params.push(ParameterSlot {
453 name: param_name.clone(),
454 param_type: ParameterType::NumericLiteral,
455 position: m.start(),
456 original_value: Some(original),
457 });
458
459 result = format!(
460 "{}{}{}",
461 &result[..m.start()],
462 param_name,
463 &result[m.end()..]
464 );
465 }
466
467 params.reverse();
468 Ok((result, params))
469 }
470
471 fn normalize_iri_locals(
473 &self,
474 query: &str,
475 start_index: usize,
476 ) -> Result<(String, Vec<ParameterSlot>)> {
477 let iri_pattern = iri_local_regex();
479 let mut result = query.to_string();
480 let mut params = Vec::new();
481 let mut index = start_index;
482
483 let matches: Vec<_> = iri_pattern.find_iter(query).collect();
484
485 for m in matches.into_iter().rev() {
486 let iri = m.as_str();
487
488 if self.config.preserve_predicates && is_likely_predicate(query, m.start()) {
490 continue;
491 }
492 if self.config.preserve_types && iri.contains("rdf:type") || iri.contains("#type") {
493 continue;
494 }
495
496 let param_name = format!("${}", index);
497
498 params.push(ParameterSlot {
499 name: param_name.clone(),
500 param_type: ParameterType::Iri,
501 position: m.start(),
502 original_value: Some(iri.to_string()),
503 });
504
505 if let Some(local_start) = iri.rfind('#').or_else(|| iri.rfind('/')) {
507 let prefix = &iri[..=local_start];
508 result = format!(
509 "{}{}{}{}",
510 &result[..m.start()],
511 prefix,
512 param_name,
513 &result[m.end()..]
514 );
515 }
516 index += 1;
517 }
518
519 params.reverse();
520 Ok((result, params))
521 }
522
523 fn normalize_variables(&self, query: &str) -> String {
525 let var_pattern = variable_regex();
526 let mut result = query.to_string();
527 let mut var_mapping: HashMap<String, String> = HashMap::new();
528 let mut var_index = 0;
529
530 for m in var_pattern.find_iter(query) {
532 let var_name = m.as_str().to_string();
533 if !var_mapping.contains_key(&var_name) {
534 var_mapping.insert(var_name.clone(), format!("?v{}", var_index));
535 var_index += 1;
536 }
537 }
538
539 let mut sorted_vars: Vec<_> = var_mapping.iter().collect();
541 sorted_vars.sort_by_key(|b| std::cmp::Reverse(b.0.len()));
542
543 for (original, normalized) in sorted_vars {
544 result = result.replace(original, normalized);
545 }
546
547 result
548 }
549
550 fn extract_features(&self, query: &str) -> QueryFeatures {
552 let upper_query = query.to_uppercase();
553
554 let triple_pattern_count = count_triple_patterns(query);
555 let variable_count = count_variables(query);
556 let filter_count = upper_query.matches("FILTER").count();
557 let optional_count = upper_query.matches("OPTIONAL").count();
558 let union_count = upper_query.matches("UNION").count();
559 let subquery_count = upper_query.matches("SELECT").count().saturating_sub(1);
560
561 let has_group_by = upper_query.contains("GROUP BY");
562 let has_order_by = upper_query.contains("ORDER BY");
563 let has_limit = upper_query.contains("LIMIT");
564 let has_offset = upper_query.contains("OFFSET");
565 let has_distinct = upper_query.contains("DISTINCT");
566 let has_service = upper_query.contains("SERVICE");
567 let has_values = upper_query.contains("VALUES");
568 let has_property_paths = query.contains("/")
569 || query.contains("*")
570 || query.contains("+")
571 || query.contains("|");
572 let has_aggregates = upper_query.contains("COUNT")
573 || upper_query.contains("SUM")
574 || upper_query.contains("AVG")
575 || upper_query.contains("MIN")
576 || upper_query.contains("MAX");
577 let has_bind = upper_query.contains("BIND");
578 let has_minus = upper_query.contains("MINUS");
579
580 let complexity_score = (triple_pattern_count * 10
582 + filter_count * 5
583 + optional_count * 8
584 + union_count * 6
585 + subquery_count * 15
586 + if has_group_by { 5 } else { 0 }
587 + if has_order_by { 3 } else { 0 }
588 + if has_service { 20 } else { 0 }
589 + if has_property_paths { 10 } else { 0 }
590 + if has_aggregates { 7 } else { 0 }
591 + if has_minus { 5 } else { 0 }) as u32;
592
593 QueryFeatures {
594 triple_pattern_count,
595 variable_count,
596 filter_count,
597 optional_count,
598 union_count,
599 subquery_count,
600 has_group_by,
601 has_order_by,
602 has_limit,
603 has_offset,
604 has_distinct,
605 has_service,
606 has_values,
607 has_property_paths,
608 has_aggregates,
609 has_bind,
610 has_minus,
611 complexity_score,
612 }
613 }
614
615 fn compute_hash(&self, normalized: &str) -> String {
617 match self.config.hash_algorithm {
618 HashAlgorithm::Md5 => {
619 let digest = md5::compute(normalized.as_bytes());
620 format!("{:x}", digest)
621 }
622 HashAlgorithm::Sha256 => {
623 let mut hasher = Sha256::new();
624 hasher.update(normalized.as_bytes());
625 hex::encode(hasher.finalize())
626 }
627 HashAlgorithm::Fnv1a => {
628 let mut hasher = std::collections::hash_map::DefaultHasher::new();
629 normalized.hash(&mut hasher);
630 format!("{:016x}", hasher.finish())
631 }
632 }
633 }
634
635 fn quick_hash(&self, query: &str) -> String {
637 let mut hasher = std::collections::hash_map::DefaultHasher::new();
638 query.hash(&mut hasher);
639 format!("{:016x}", hasher.finish())
640 }
641
642 pub fn similarity(&self, fp1: &QueryFingerprint, fp2: &QueryFingerprint) -> f64 {
644 if fp1.hash == fp2.hash {
646 return 1.0;
647 }
648
649 if fp1.query_form != fp2.query_form {
651 return 0.0;
652 }
653
654 let structure_sim = self.structural_similarity(&fp1.features, &fp2.features);
656
657 let template_sim = string_similarity(&fp1.template, &fp2.template);
659
660 0.6 * structure_sim + 0.4 * template_sim
662 }
663
664 fn structural_similarity(&self, f1: &QueryFeatures, f2: &QueryFeatures) -> f64 {
666 let mut matches = 0.0;
667 let mut total = 0.0;
668
669 let num_features = [
671 (f1.triple_pattern_count, f2.triple_pattern_count, 2.0),
672 (f1.variable_count, f2.variable_count, 1.0),
673 (f1.filter_count, f2.filter_count, 1.5),
674 (f1.optional_count, f2.optional_count, 1.5),
675 (f1.union_count, f2.union_count, 1.5),
676 (f1.subquery_count, f2.subquery_count, 2.0),
677 ];
678
679 for (v1, v2, weight) in num_features {
680 total += weight;
681 if v1 == v2 {
682 matches += weight;
683 } else {
684 let max_val = v1.max(v2) as f64;
685 let min_val = v1.min(v2) as f64;
686 if max_val > 0.0 {
687 matches += weight * (min_val / max_val);
688 }
689 }
690 }
691
692 let bool_features = [
694 (f1.has_group_by, f2.has_group_by),
695 (f1.has_order_by, f2.has_order_by),
696 (f1.has_limit, f2.has_limit),
697 (f1.has_distinct, f2.has_distinct),
698 (f1.has_service, f2.has_service),
699 (f1.has_values, f2.has_values),
700 (f1.has_property_paths, f2.has_property_paths),
701 (f1.has_aggregates, f2.has_aggregates),
702 (f1.has_bind, f2.has_bind),
703 (f1.has_minus, f2.has_minus),
704 ];
705
706 for (b1, b2) in bool_features {
707 total += 1.0;
708 if b1 == b2 {
709 matches += 1.0;
710 }
711 }
712
713 matches / total
714 }
715
716 pub fn find_similar(
718 &self,
719 target: &QueryFingerprint,
720 fingerprints: &[QueryFingerprint],
721 threshold: f64,
722 ) -> Vec<(usize, f64)> {
723 let mut results: Vec<(usize, f64)> = fingerprints
724 .iter()
725 .enumerate()
726 .filter_map(|(idx, fp)| {
727 let sim = self.similarity(target, fp);
728 if sim >= threshold {
729 Some((idx, sim))
730 } else {
731 None
732 }
733 })
734 .collect();
735
736 results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
737 results
738 }
739
740 pub fn cluster_fingerprints(
742 &self,
743 fingerprints: &[QueryFingerprint],
744 threshold: f64,
745 ) -> Vec<Vec<usize>> {
746 let n = fingerprints.len();
747 let mut visited = vec![false; n];
748 let mut clusters = Vec::new();
749
750 for i in 0..n {
751 if visited[i] {
752 continue;
753 }
754
755 let mut cluster = vec![i];
756 visited[i] = true;
757
758 for j in (i + 1)..n {
759 if visited[j] {
760 continue;
761 }
762
763 let sim = self.similarity(&fingerprints[i], &fingerprints[j]);
764 if sim >= threshold {
765 cluster.push(j);
766 visited[j] = true;
767 }
768 }
769
770 clusters.push(cluster);
771 }
772
773 clusters
774 }
775
776 pub fn statistics(&self) -> FingerprintingStatistics {
778 FingerprintingStatistics {
779 fingerprints_computed: self.stats.computed.load(Ordering::Relaxed),
780 cache_hits: self.stats.cache_hits.load(Ordering::Relaxed),
781 cache_misses: self.stats.cache_misses.load(Ordering::Relaxed),
782 cache_size: self
783 .cache
784 .read()
785 .expect("read lock should not be poisoned")
786 .len(),
787 }
788 }
789
790 pub fn clear_cache(&self) {
792 self.cache
793 .write()
794 .expect("write lock should not be poisoned")
795 .clear();
796 }
797}
798
799#[derive(Debug, Clone, Serialize, Deserialize)]
801pub struct FingerprintingStatistics {
802 pub fingerprints_computed: u64,
804 pub cache_hits: u64,
806 pub cache_misses: u64,
808 pub cache_size: usize,
810}
811
812impl FingerprintingStatistics {
813 pub fn cache_hit_rate(&self) -> f64 {
815 let total = self.cache_hits + self.cache_misses;
816 if total == 0 {
817 0.0
818 } else {
819 self.cache_hits as f64 / total as f64
820 }
821 }
822}
823
824fn remove_comments(query: &str) -> String {
827 let mut result = String::new();
828 let mut in_string = false;
829 let mut escape = false;
830 let mut chars = query.chars().peekable();
831
832 while let Some(c) = chars.next() {
833 if escape {
834 result.push(c);
835 escape = false;
836 continue;
837 }
838
839 if c == '\\' && in_string {
840 result.push(c);
841 escape = true;
842 continue;
843 }
844
845 if c == '"' && !in_string {
846 in_string = true;
847 result.push(c);
848 continue;
849 }
850
851 if c == '"' && in_string {
852 in_string = false;
853 result.push(c);
854 continue;
855 }
856
857 if !in_string && c == '#' {
858 while let Some(&nc) = chars.peek() {
860 if nc == '\n' {
861 break;
862 }
863 chars.next();
864 }
865 result.push(' ');
866 continue;
867 }
868
869 result.push(c);
870 }
871
872 result
873}
874
875fn normalize_whitespace(query: &str) -> String {
876 let whitespace_pattern = whitespace_regex();
877 whitespace_pattern
878 .replace_all(query, " ")
879 .trim()
880 .to_string()
881}
882
883fn count_triple_patterns(query: &str) -> usize {
884 let triple_pattern = triple_pattern_regex();
886 triple_pattern.find_iter(query).count()
887}
888
889fn count_variables(query: &str) -> usize {
890 let var_pattern = variable_regex();
891 let vars: HashSet<_> = var_pattern.find_iter(query).map(|m| m.as_str()).collect();
892 vars.len()
893}
894
895fn is_likely_predicate(query: &str, position: usize) -> bool {
896 let before = &query[..position];
899 let after = &query[position..];
900
901 let has_subject = before
903 .trim_end()
904 .ends_with(|c: char| c == '>' || c.is_alphabetic() || c == '_');
905 let has_object = after.trim_start().starts_with(['?', '<', '"']);
907
908 has_subject && has_object
909}
910
911fn string_similarity(s1: &str, s2: &str) -> f64 {
912 let tokens1: HashSet<_> = s1.split_whitespace().collect();
914 let tokens2: HashSet<_> = s2.split_whitespace().collect();
915
916 let intersection = tokens1.intersection(&tokens2).count();
917 let union = tokens1.union(&tokens2).count();
918
919 if union == 0 {
920 1.0
921 } else {
922 intersection as f64 / union as f64
923 }
924}
925
926fn string_literal_regex() -> &'static Regex {
929 static REGEX: OnceLock<Regex> = OnceLock::new();
930 REGEX.get_or_init(|| {
931 Regex::new(r#""(?:[^"\\]|\\.)*"(?:@[a-zA-Z-]+)?(?:\^\^<[^>]+>)?"#).expect("Invalid regex")
932 })
933}
934
935fn numeric_literal_regex() -> &'static Regex {
936 static REGEX: OnceLock<Regex> = OnceLock::new();
937 REGEX.get_or_init(|| {
938 Regex::new(r"\b[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?\b").expect("Invalid regex")
941 })
942}
943
944fn iri_local_regex() -> &'static Regex {
945 static REGEX: OnceLock<Regex> = OnceLock::new();
946 REGEX.get_or_init(|| Regex::new(r"<[^>]+>").expect("Invalid regex"))
947}
948
949fn variable_regex() -> &'static Regex {
950 static REGEX: OnceLock<Regex> = OnceLock::new();
951 REGEX.get_or_init(|| Regex::new(r"\?[a-zA-Z_][a-zA-Z0-9_]*").expect("Invalid regex"))
952}
953
954fn whitespace_regex() -> &'static Regex {
955 static REGEX: OnceLock<Regex> = OnceLock::new();
956 REGEX.get_or_init(|| Regex::new(r"\s+").expect("Invalid regex"))
957}
958
959fn triple_pattern_regex() -> &'static Regex {
960 static REGEX: OnceLock<Regex> = OnceLock::new();
961 REGEX.get_or_init(|| {
962 Regex::new(r#"(?:\?[a-zA-Z_]\w*|<[^>]+>)\s+(?:\?[a-zA-Z_]\w*|<[^>]+>|[a-zA-Z]+:[a-zA-Z_]\w*)\s+(?:\?[a-zA-Z_]\w*|<[^>]+>|"[^"]*"|[0-9]+)"#).expect("Invalid regex")
963 })
964}
965
966#[cfg(test)]
967mod tests {
968 use super::*;
969
970 #[test]
971 fn test_basic_fingerprint() {
972 let config = FingerprintConfig::default();
973 let fingerprinter = QueryFingerprinter::new(config);
974
975 let query = "SELECT ?s WHERE { ?s <http://example.org/name> \"Alice\" }";
976 let fp = fingerprinter.fingerprint(query).unwrap();
977
978 assert_eq!(fp.query_form, QueryForm::Select);
979 assert!(!fp.hash.is_empty());
980 assert!(!fp.template.is_empty());
981 }
982
983 #[test]
984 fn test_similar_queries_same_template() {
985 let config = FingerprintConfig::default();
986 let fingerprinter = QueryFingerprinter::new(config);
987
988 let query1 = "SELECT ?s WHERE { ?s <http://example.org/name> \"Alice\" }";
989 let query2 = "SELECT ?s WHERE { ?s <http://example.org/name> \"Bob\" }";
990
991 let fp1 = fingerprinter.fingerprint(query1).unwrap();
992 let fp2 = fingerprinter.fingerprint(query2).unwrap();
993
994 let similarity = fingerprinter.similarity(&fp1, &fp2);
996 assert!(
997 similarity > 0.7,
998 "Similarity should be high for same structure"
999 );
1000 }
1001
1002 #[test]
1003 fn test_different_queries_different_template() {
1004 let config = FingerprintConfig::default();
1005 let fingerprinter = QueryFingerprinter::new(config);
1006
1007 let query1 = "SELECT ?s WHERE { ?s <http://example.org/name> \"Alice\" }";
1008 let query2 = "ASK { ?x <http://example.org/age> ?y . ?y <http://example.org/value> ?z }";
1009
1010 let fp1 = fingerprinter.fingerprint(query1).unwrap();
1011 let fp2 = fingerprinter.fingerprint(query2).unwrap();
1012
1013 let similarity = fingerprinter.similarity(&fp1, &fp2);
1014 assert!(
1015 similarity < 0.5,
1016 "Similarity should be low for different structures"
1017 );
1018 }
1019
1020 #[test]
1021 fn test_feature_extraction() {
1022 let config = FingerprintConfig::default();
1023 let fingerprinter = QueryFingerprinter::new(config);
1024
1025 let query = r#"
1026 SELECT ?s ?name (COUNT(?o) AS ?count)
1027 WHERE {
1028 ?s <http://example.org/type> <http://example.org/Person> .
1029 ?s <http://example.org/name> ?name .
1030 OPTIONAL { ?s <http://example.org/friend> ?o }
1031 FILTER(?name != "")
1032 }
1033 GROUP BY ?s ?name
1034 ORDER BY DESC(?count)
1035 LIMIT 10
1036 "#;
1037
1038 let fp = fingerprinter.fingerprint(query).unwrap();
1039
1040 assert!(fp.features.has_group_by);
1041 assert!(fp.features.has_order_by);
1042 assert!(fp.features.has_limit);
1043 assert!(fp.features.has_aggregates);
1044 assert!(fp.features.optional_count > 0);
1045 assert!(fp.features.filter_count > 0);
1046 }
1047
1048 #[test]
1049 fn test_cache_behavior() {
1050 let config = FingerprintConfig::default();
1051 let fingerprinter = QueryFingerprinter::new(config);
1052
1053 let query = "SELECT ?s WHERE { ?s ?p ?o }";
1054
1055 let _fp1 = fingerprinter.fingerprint(query).unwrap();
1057 let stats1 = fingerprinter.statistics();
1058 assert_eq!(stats1.cache_misses, 1);
1059
1060 let _fp2 = fingerprinter.fingerprint(query).unwrap();
1062 let stats2 = fingerprinter.statistics();
1063 assert_eq!(stats2.cache_hits, 1);
1064 }
1065
1066 #[test]
1067 fn test_query_forms() {
1068 let config = FingerprintConfig::default();
1069 let fingerprinter = QueryFingerprinter::new(config);
1070
1071 let select = "SELECT ?s WHERE { ?s ?p ?o }";
1072 let ask = "ASK { ?s ?p ?o }";
1073 let construct = "CONSTRUCT { ?s ?p ?o } WHERE { ?s ?p ?o }";
1074 let describe = "DESCRIBE <http://example.org/thing>";
1075
1076 assert_eq!(
1077 fingerprinter.fingerprint(select).unwrap().query_form,
1078 QueryForm::Select
1079 );
1080 assert_eq!(
1081 fingerprinter.fingerprint(ask).unwrap().query_form,
1082 QueryForm::Ask
1083 );
1084 assert_eq!(
1085 fingerprinter.fingerprint(construct).unwrap().query_form,
1086 QueryForm::Construct
1087 );
1088 assert_eq!(
1089 fingerprinter.fingerprint(describe).unwrap().query_form,
1090 QueryForm::Describe
1091 );
1092 }
1093
1094 #[test]
1095 fn test_parameter_extraction() {
1096 let config = FingerprintConfig::default();
1097 let fingerprinter = QueryFingerprinter::new(config);
1098
1099 let query = r#"SELECT ?s WHERE { ?s <http://example.org/age> 25 . ?s <http://example.org/name> "Alice" }"#;
1100 let fp = fingerprinter.fingerprint(query).unwrap();
1101
1102 assert!(!fp.parameters.is_empty());
1103 }
1104
1105 #[test]
1106 fn test_find_similar() {
1107 let config = FingerprintConfig::default();
1108 let fingerprinter = QueryFingerprinter::new(config);
1109
1110 let queries = [
1111 "SELECT ?s WHERE { ?s <http://example.org/name> \"Alice\" }",
1112 "SELECT ?s WHERE { ?s <http://example.org/name> \"Bob\" }",
1113 "SELECT ?s WHERE { ?s <http://example.org/name> \"Charlie\" }",
1114 "ASK { ?x <http://example.org/age> ?y }",
1115 ];
1116
1117 let fingerprints: Vec<_> = queries
1118 .iter()
1119 .map(|q| fingerprinter.fingerprint(q).unwrap())
1120 .collect();
1121
1122 let target = &fingerprints[0];
1123 let similar = fingerprinter.find_similar(target, &fingerprints, 0.7);
1124
1125 assert!(similar.len() >= 2);
1127 }
1128
1129 #[test]
1130 fn test_cluster_fingerprints() {
1131 let config = FingerprintConfig::default();
1132 let fingerprinter = QueryFingerprinter::new(config);
1133
1134 let queries = [
1135 "SELECT ?s WHERE { ?s <http://example.org/name> \"Alice\" }",
1136 "SELECT ?s WHERE { ?s <http://example.org/name> \"Bob\" }",
1137 "ASK { ?x <http://example.org/age> ?y }",
1138 "ASK { ?x <http://example.org/age> ?z }",
1139 ];
1140
1141 let fingerprints: Vec<_> = queries
1142 .iter()
1143 .map(|q| fingerprinter.fingerprint(q).unwrap())
1144 .collect();
1145
1146 let clusters = fingerprinter.cluster_fingerprints(&fingerprints, 0.7);
1147
1148 assert!(clusters.len() >= 2);
1150 }
1151
1152 #[test]
1153 fn test_complexity_score() {
1154 let config = FingerprintConfig::default();
1155 let fingerprinter = QueryFingerprinter::new(config);
1156
1157 let simple = "SELECT ?s WHERE { ?s ?p ?o }";
1158 let complex = r#"
1159 SELECT ?s ?name (COUNT(?friend) AS ?friendCount)
1160 WHERE {
1161 ?s <http://example.org/type> <http://example.org/Person> .
1162 ?s <http://example.org/name> ?name .
1163 OPTIONAL { ?s <http://example.org/friend> ?friend }
1164 FILTER(?name != "")
1165 UNION {
1166 ?s <http://example.org/nickname> ?name
1167 }
1168 }
1169 GROUP BY ?s ?name
1170 ORDER BY DESC(?friendCount)
1171 LIMIT 100
1172 "#;
1173
1174 let fp_simple = fingerprinter.fingerprint(simple).unwrap();
1175 let fp_complex = fingerprinter.fingerprint(complex).unwrap();
1176
1177 assert!(fp_complex.features.complexity_score > fp_simple.features.complexity_score);
1178 }
1179
1180 #[test]
1181 fn test_hash_algorithms() {
1182 let queries = "SELECT ?s WHERE { ?s ?p ?o }";
1183
1184 let config_md5 = FingerprintConfig {
1186 hash_algorithm: HashAlgorithm::Md5,
1187 ..Default::default()
1188 };
1189 let fp_md5 = QueryFingerprinter::new(config_md5)
1190 .fingerprint(queries)
1191 .unwrap();
1192 assert_eq!(fp_md5.hash.len(), 32); let config_sha = FingerprintConfig {
1196 hash_algorithm: HashAlgorithm::Sha256,
1197 ..Default::default()
1198 };
1199 let fp_sha = QueryFingerprinter::new(config_sha)
1200 .fingerprint(queries)
1201 .unwrap();
1202 assert_eq!(fp_sha.hash.len(), 64); let config_fnv = FingerprintConfig {
1206 hash_algorithm: HashAlgorithm::Fnv1a,
1207 ..Default::default()
1208 };
1209 let fp_fnv = QueryFingerprinter::new(config_fnv)
1210 .fingerprint(queries)
1211 .unwrap();
1212 assert_eq!(fp_fnv.hash.len(), 16); }
1214
1215 #[test]
1216 fn test_statistics() {
1217 let config = FingerprintConfig::default();
1218 let fingerprinter = QueryFingerprinter::new(config);
1219
1220 let _ = fingerprinter.fingerprint("SELECT ?s WHERE { ?s ?p ?o }");
1221 let _ = fingerprinter.fingerprint("SELECT ?s WHERE { ?s ?p ?o }"); let stats = fingerprinter.statistics();
1224 assert_eq!(stats.fingerprints_computed, 1);
1225 assert_eq!(stats.cache_hits, 1);
1226 assert_eq!(stats.cache_misses, 1);
1227 assert!(stats.cache_hit_rate() > 0.0);
1228 }
1229
1230 #[test]
1231 fn test_clear_cache() {
1232 let config = FingerprintConfig::default();
1233 let fingerprinter = QueryFingerprinter::new(config);
1234
1235 let _ = fingerprinter.fingerprint("SELECT ?s WHERE { ?s ?p ?o }");
1236 assert_eq!(fingerprinter.statistics().cache_size, 1);
1237
1238 fingerprinter.clear_cache();
1239 assert_eq!(fingerprinter.statistics().cache_size, 0);
1240 }
1241}