Skip to main content

oxirs_core/
interning.rs

1//! String interning system for performance optimization
2//!
3//! This module provides efficient string interning for commonly used strings
4//! like IRIs, datatype URIs, and other RDF terms. String interning reduces
5//! memory usage and improves comparison performance by ensuring that equal
6//! strings are stored only once and can be compared by pointer equality.
7//!
8//! ## Performance Monitoring
9//!
10//! The interner integrates SciRS2-core metrics for comprehensive monitoring:
11//! - Cache hit/miss rates
12//! - Intern operation timing
13//! - Memory usage tracking
14//! - Deduplication effectiveness
15
16use scirs2_core::metrics::{Counter, Histogram, Timer};
17use std::collections::HashMap;
18use std::hash::{Hash, Hasher};
19use std::sync::{Arc, RwLock, Weak};
20
21/// A thread-safe string interner that deduplicates strings with ID mapping
22///
23/// Integrates SciRS2-core metrics for production-grade performance monitoring:
24/// - Automatic cache hit/miss tracking
25/// - Intern operation timing
26/// - Memory usage histograms
27/// - Deduplication effectiveness metrics
28pub struct StringInterner {
29    /// Map from string content to weak references of interned strings
30    strings: RwLock<HashMap<String, Weak<str>>>,
31    /// Bidirectional mapping between strings and numeric IDs
32    string_to_id: RwLock<HashMap<String, u32>>,
33    /// Map from ID back to string
34    id_to_string: RwLock<HashMap<u32, Arc<str>>>,
35    /// Next available ID
36    next_id: AtomicU32,
37    /// Statistics for monitoring performance
38    stats: RwLock<InternerStats>,
39    /// SciRS2 metrics
40    cache_hit_counter: Arc<Counter>,
41    cache_miss_counter: Arc<Counter>,
42    intern_timer: Arc<Timer>,
43    string_length_histogram: Arc<Histogram>,
44    memory_usage_histogram: Arc<Histogram>,
45}
46
47/// Atomic 32-bit unsigned integer type for thread-safe ID generation
48use std::sync::atomic::AtomicU32;
49
50/// Statistics for monitoring string interner performance
51#[derive(Debug, Clone, Default)]
52pub struct InternerStats {
53    pub total_requests: usize,
54    pub cache_hits: usize,
55    pub cache_misses: usize,
56    pub total_strings_stored: usize,
57    pub memory_saved_bytes: usize,
58}
59
60/// Memory usage statistics for performance monitoring
61#[derive(Debug, Clone)]
62pub struct MemoryUsage {
63    pub interned_strings: usize,
64    pub id_mappings: usize,
65    pub estimated_memory_bytes: usize,
66    pub memory_saved_bytes: usize,
67    pub compression_ratio: f64,
68}
69
70/// SciRS2 metrics for interner performance
71#[derive(Debug, Clone)]
72pub struct InternerMetrics {
73    /// Total cache hits
74    pub cache_hits: u64,
75    /// Total cache misses
76    pub cache_misses: u64,
77    /// Total intern requests
78    pub total_requests: u64,
79    /// Cache hit ratio (0.0 to 1.0)
80    pub hit_ratio: f64,
81    /// Average intern operation time in seconds
82    pub avg_intern_time_secs: f64,
83    /// Total timing observations recorded
84    pub total_intern_observations: u64,
85    /// Average string length
86    pub avg_string_length: f64,
87    /// Total memory tracked in bytes
88    pub total_memory_tracked_bytes: u64,
89}
90
91impl InternerStats {
92    pub fn hit_ratio(&self) -> f64 {
93        if self.total_requests == 0 {
94            0.0
95        } else {
96            self.cache_hits as f64 / self.total_requests as f64
97        }
98    }
99}
100
101impl StringInterner {
102    /// Create a new string interner with ID mapping and SciRS2 metrics
103    ///
104    /// Automatically tracks:
105    /// - Cache hit/miss rates for intern operations
106    /// - Operation timing for performance analysis
107    /// - String length distribution
108    /// - Memory usage patterns
109    pub fn new() -> Self {
110        Self::with_capacity(1024) // Default capacity for typical usage
111    }
112
113    /// Create a new string interner with specified capacity
114    ///
115    /// Pre-allocates HashMaps to the given capacity to reduce reallocation overhead.
116    /// Initializes all SciRS2 metrics for comprehensive monitoring.
117    pub fn with_capacity(capacity: usize) -> Self {
118        StringInterner {
119            strings: RwLock::new(HashMap::with_capacity(capacity)),
120            string_to_id: RwLock::new(HashMap::with_capacity(capacity)),
121            id_to_string: RwLock::new(HashMap::with_capacity(capacity)),
122            next_id: AtomicU32::new(0),
123            stats: RwLock::new(InternerStats::default()),
124            cache_hit_counter: Arc::new(Counter::new("interner.cache_hits".to_string())),
125            cache_miss_counter: Arc::new(Counter::new("interner.cache_misses".to_string())),
126            intern_timer: Arc::new(Timer::new("interner.intern_time".to_string())),
127            string_length_histogram: Arc::new(Histogram::new("interner.string_length".to_string())),
128            memory_usage_histogram: Arc::new(Histogram::new("interner.memory_usage".to_string())),
129        }
130    }
131
132    /// Intern a string, returning an `Arc<str>` that can be cheaply cloned and compared
133    ///
134    /// This operation is tracked with SciRS2 metrics:
135    /// - Cache hits/misses
136    /// - Operation timing
137    /// - String length distribution
138    pub fn intern(&self, s: &str) -> Arc<str> {
139        let _guard = self.intern_timer.start();
140
141        // Track string length
142        self.string_length_histogram.observe(s.len() as f64);
143
144        // Fast path: try to get existing string with read lock
145        {
146            let strings = self
147                .strings
148                .read()
149                .unwrap_or_else(|poisoned| poisoned.into_inner());
150            if let Some(weak_ref) = strings.get(s) {
151                if let Some(arc_str) = weak_ref.upgrade() {
152                    // Update stats
153                    self.cache_hit_counter.inc();
154                    {
155                        let mut stats = self
156                            .stats
157                            .write()
158                            .unwrap_or_else(|poisoned| poisoned.into_inner());
159                        stats.total_requests += 1;
160                        stats.cache_hits += 1;
161                    }
162                    return arc_str;
163                }
164            }
165        }
166
167        // Slow path: need to create new string with write lock
168        let mut strings = self
169            .strings
170            .write()
171            .unwrap_or_else(|poisoned| poisoned.into_inner());
172
173        // Double-check in case another thread added it while we were waiting
174        if let Some(weak_ref) = strings.get(s) {
175            if let Some(arc_str) = weak_ref.upgrade() {
176                // Update stats
177                self.cache_hit_counter.inc();
178                drop(strings); // Release write lock early
179                {
180                    let mut stats = self
181                        .stats
182                        .write()
183                        .unwrap_or_else(|poisoned| poisoned.into_inner());
184                    stats.total_requests += 1;
185                    stats.cache_hits += 1;
186                }
187                return arc_str;
188            }
189        }
190
191        // Create new interned string
192        let arc_str: Arc<str> = Arc::from(s);
193        let weak_ref = Arc::downgrade(&arc_str);
194        strings.insert(s.to_string(), weak_ref);
195
196        // Update stats
197        self.cache_miss_counter.inc();
198        drop(strings); // Release write lock early
199        {
200            let mut stats = self
201                .stats
202                .write()
203                .unwrap_or_else(|poisoned| poisoned.into_inner());
204            stats.total_requests += 1;
205            stats.cache_misses += 1;
206            stats.total_strings_stored += 1;
207            stats.memory_saved_bytes += s.len(); // Approximate memory saved on subsequent hits
208        }
209
210        arc_str
211    }
212
213    /// Intern a string and return both the `Arc<str>` and its numeric ID
214    pub fn intern_with_id(&self, s: &str) -> (Arc<str>, u32) {
215        // Fast path: check if we already have this string and its ID
216        {
217            let string_to_id = self
218                .string_to_id
219                .read()
220                .unwrap_or_else(|poisoned| poisoned.into_inner());
221            if let Some(&id) = string_to_id.get(s) {
222                // We have the ID, now get the Arc<str>
223                let id_to_string = self
224                    .id_to_string
225                    .read()
226                    .unwrap_or_else(|poisoned| poisoned.into_inner());
227                if let Some(arc_str) = id_to_string.get(&id) {
228                    // Update stats
229                    {
230                        let mut stats = self
231                            .stats
232                            .write()
233                            .unwrap_or_else(|poisoned| poisoned.into_inner());
234                        stats.total_requests += 1;
235                        stats.cache_hits += 1;
236                    }
237                    return (arc_str.clone(), id);
238                }
239            }
240        }
241
242        // Slow path: need to create new entry
243        let arc_str = self.intern(s); // This will handle the string interning
244        let id = self
245            .next_id
246            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
247
248        // Update the ID mappings
249        {
250            let mut string_to_id = self
251                .string_to_id
252                .write()
253                .unwrap_or_else(|poisoned| poisoned.into_inner());
254            string_to_id.insert(s.to_string(), id);
255        }
256        {
257            let mut id_to_string = self
258                .id_to_string
259                .write()
260                .unwrap_or_else(|poisoned| poisoned.into_inner());
261            id_to_string.insert(id, arc_str.clone());
262        }
263
264        (arc_str, id)
265    }
266
267    /// Get the ID for a string if it's already interned
268    pub fn get_id(&self, s: &str) -> Option<u32> {
269        let string_to_id = self
270            .string_to_id
271            .read()
272            .unwrap_or_else(|poisoned| poisoned.into_inner());
273        string_to_id.get(s).copied()
274    }
275
276    /// Get the string for an ID if it exists
277    pub fn get_string(&self, id: u32) -> Option<Arc<str>> {
278        let id_to_string = self
279            .id_to_string
280            .read()
281            .unwrap_or_else(|poisoned| poisoned.into_inner());
282        id_to_string.get(&id).cloned()
283    }
284
285    /// Get all ID mappings (useful for serialization/debugging)
286    pub fn get_all_mappings(&self) -> Vec<(u32, Arc<str>)> {
287        let id_to_string = self
288            .id_to_string
289            .read()
290            .unwrap_or_else(|poisoned| poisoned.into_inner());
291        id_to_string
292            .iter()
293            .map(|(&id, s)| (id, s.clone()))
294            .collect()
295    }
296
297    /// Clean up expired weak references to save memory
298    ///
299    /// Returns the number of entries cleaned up
300    pub fn cleanup(&self) -> usize {
301        let mut strings = self
302            .strings
303            .write()
304            .unwrap_or_else(|poisoned| poisoned.into_inner());
305        let before = strings.len();
306        strings.retain(|_, weak_ref| weak_ref.strong_count() > 0);
307        let after = strings.len();
308        before - after
309    }
310
311    /// Get current statistics
312    pub fn stats(&self) -> InternerStats {
313        self.stats
314            .read()
315            .unwrap_or_else(|poisoned| poisoned.into_inner())
316            .clone()
317    }
318
319    /// Get the number of unique strings currently stored
320    pub fn len(&self) -> usize {
321        // Return the maximum of both counts to handle mixed usage
322        let id_count = self
323            .string_to_id
324            .read()
325            .unwrap_or_else(|poisoned| poisoned.into_inner())
326            .len();
327        let string_count = self
328            .strings
329            .read()
330            .unwrap_or_else(|poisoned| poisoned.into_inner())
331            .len();
332        std::cmp::max(id_count, string_count)
333    }
334
335    /// Get the number of strings with ID mappings
336    pub fn id_mapping_count(&self) -> usize {
337        self.string_to_id
338            .read()
339            .unwrap_or_else(|poisoned| poisoned.into_inner())
340            .len()
341    }
342
343    /// Check if the interner is empty
344    pub fn is_empty(&self) -> bool {
345        self.strings
346            .read()
347            .unwrap_or_else(|poisoned| poisoned.into_inner())
348            .is_empty()
349    }
350
351    /// Batch intern multiple strings for improved performance
352    /// Returns a Vec of `Arc<str>` in the same order as input
353    pub fn intern_batch(&self, strings: &[&str]) -> Vec<Arc<str>> {
354        let mut result = Vec::with_capacity(strings.len());
355        let mut to_create = Vec::new();
356
357        // First pass: collect existing strings with read lock
358        {
359            let string_map = self
360                .strings
361                .read()
362                .unwrap_or_else(|poisoned| poisoned.into_inner());
363            for &s in strings {
364                if let Some(weak_ref) = string_map.get(s) {
365                    if let Some(arc_str) = weak_ref.upgrade() {
366                        result.push(arc_str);
367                        continue;
368                    }
369                }
370                to_create.push((result.len(), s));
371                result.push(Arc::from("")); // Placeholder
372            }
373        }
374
375        // Second pass: create missing strings with write lock
376        if !to_create.is_empty() {
377            let mut string_map = self
378                .strings
379                .write()
380                .unwrap_or_else(|poisoned| poisoned.into_inner());
381            let mut stats = self
382                .stats
383                .write()
384                .unwrap_or_else(|poisoned| poisoned.into_inner());
385
386            for (index, s) in to_create {
387                // Double-check in case another thread added it
388                if let Some(weak_ref) = string_map.get(s) {
389                    if let Some(arc_str) = weak_ref.upgrade() {
390                        result[index] = arc_str;
391                        stats.cache_hits += 1;
392                        continue;
393                    }
394                }
395
396                // Create new interned string
397                let arc_str: Arc<str> = Arc::from(s);
398                let weak_ref = Arc::downgrade(&arc_str);
399                string_map.insert(s.to_string(), weak_ref);
400                result[index] = arc_str;
401
402                stats.cache_misses += 1;
403                stats.total_strings_stored += 1;
404                stats.memory_saved_bytes += s.len();
405            }
406
407            stats.total_requests += strings.len();
408        }
409
410        result
411    }
412
413    /// Prefetch strings into the interner cache for improved performance
414    /// This is useful when you know you'll need certain strings soon
415    pub fn prefetch(&self, strings: &[&str]) {
416        let _ = self.intern_batch(strings);
417    }
418
419    /// Get memory usage statistics for performance monitoring
420    pub fn memory_usage(&self) -> MemoryUsage {
421        let string_map_size = self
422            .strings
423            .read()
424            .unwrap_or_else(|poisoned| poisoned.into_inner())
425            .len();
426        let id_map_size = self
427            .string_to_id
428            .read()
429            .unwrap_or_else(|poisoned| poisoned.into_inner())
430            .len();
431        let stats = self
432            .stats
433            .read()
434            .unwrap_or_else(|poisoned| poisoned.into_inner());
435
436        MemoryUsage {
437            interned_strings: string_map_size,
438            id_mappings: id_map_size,
439            estimated_memory_bytes: string_map_size * 64 + id_map_size * 8, // Rough estimate
440            memory_saved_bytes: stats.memory_saved_bytes,
441            compression_ratio: if stats.memory_saved_bytes > 0 {
442                stats.memory_saved_bytes as f64
443                    / (stats.memory_saved_bytes + string_map_size * 32) as f64
444            } else {
445                0.0
446            },
447        }
448    }
449
450    /// Get comprehensive performance metrics from SciRS2
451    ///
452    /// Returns detailed statistics including:
453    /// - Cache hit/miss counts and ratios
454    /// - Operation timing statistics
455    /// - String length distribution
456    /// - Memory usage patterns
457    pub fn get_metrics(&self) -> InternerMetrics {
458        let cache_hits = self.cache_hit_counter.get();
459        let cache_misses = self.cache_miss_counter.get();
460        let total_requests = cache_hits + cache_misses;
461        let hit_ratio = if total_requests > 0 {
462            cache_hits as f64 / total_requests as f64
463        } else {
464            0.0
465        };
466
467        let timer_stats = self.intern_timer.get_stats();
468        let string_length_stats = self.string_length_histogram.get_stats();
469        let memory_stats = self.memory_usage_histogram.get_stats();
470
471        InternerMetrics {
472            cache_hits,
473            cache_misses,
474            total_requests,
475            hit_ratio,
476            avg_intern_time_secs: timer_stats.mean,
477            total_intern_observations: timer_stats.count,
478            avg_string_length: string_length_stats.mean,
479            total_memory_tracked_bytes: memory_stats.sum as u64,
480        }
481    }
482
483    /// Optimize the interner by cleaning up and compacting data structures
484    ///
485    /// Performs comprehensive optimization:
486    /// - Cleans up expired weak references
487    /// - Rehashes HashMaps with optimal capacity
488    /// - Updates memory usage statistics
489    /// - Tracks optimization impact with SciRS2 metrics
490    pub fn optimize(&self) {
491        let start = std::time::Instant::now();
492
493        // Clean up expired weak references
494        let cleaned_count = self.cleanup();
495
496        // Get current sizes
497        let current_size = {
498            let strings = self
499                .strings
500                .read()
501                .unwrap_or_else(|poisoned| poisoned.into_inner());
502            strings.len()
503        };
504
505        // Rehash with optimal capacity (1.3x current size to reduce future reallocations)
506        let optimal_capacity = ((current_size as f64 * 1.3) as usize).max(1024);
507
508        {
509            let mut strings = self
510                .strings
511                .write()
512                .unwrap_or_else(|poisoned| poisoned.into_inner());
513            let mut string_to_id = self
514                .string_to_id
515                .write()
516                .unwrap_or_else(|poisoned| poisoned.into_inner());
517            let mut id_to_string = self
518                .id_to_string
519                .write()
520                .unwrap_or_else(|poisoned| poisoned.into_inner());
521
522            // Create new maps with optimal capacity
523            let mut new_strings = HashMap::with_capacity(optimal_capacity);
524            let mut new_string_to_id = HashMap::with_capacity(optimal_capacity);
525            let mut new_id_to_string = HashMap::with_capacity(optimal_capacity);
526
527            // Move data to new maps (rehashing in the process)
528            for (key, value) in strings.drain() {
529                new_strings.insert(key, value);
530            }
531            for (key, value) in string_to_id.drain() {
532                new_string_to_id.insert(key, value);
533            }
534            for (key, value) in id_to_string.drain() {
535                new_id_to_string.insert(key, value);
536            }
537
538            // Replace with optimized maps
539            *strings = new_strings;
540            *string_to_id = new_string_to_id;
541            *id_to_string = new_id_to_string;
542        }
543
544        // Calculate and track memory usage
545        let mem_usage = self.memory_usage();
546        self.memory_usage_histogram
547            .observe(mem_usage.estimated_memory_bytes as f64);
548
549        // Update stats
550        {
551            let mut stats = self
552                .stats
553                .write()
554                .unwrap_or_else(|poisoned| poisoned.into_inner());
555            stats.total_strings_stored = current_size;
556        }
557
558        let duration = start.elapsed();
559        tracing::debug!(
560            "Interner optimized: cleaned {} entries, rehashed to capacity {}, took {:?}",
561            cleaned_count,
562            optimal_capacity,
563            duration
564        );
565    }
566}
567
568impl Default for StringInterner {
569    fn default() -> Self {
570        Self::new()
571    }
572}
573
574impl std::fmt::Debug for StringInterner {
575    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
576        f.debug_struct("StringInterner")
577            .field(
578                "strings_count",
579                &self
580                    .strings
581                    .read()
582                    .unwrap_or_else(|poisoned| poisoned.into_inner())
583                    .len(),
584            )
585            .field(
586                "id_mappings_count",
587                &self
588                    .string_to_id
589                    .read()
590                    .unwrap_or_else(|poisoned| poisoned.into_inner())
591                    .len(),
592            )
593            .field(
594                "next_id",
595                &self.next_id.load(std::sync::atomic::Ordering::Relaxed),
596            )
597            .field(
598                "stats",
599                &self
600                    .stats
601                    .read()
602                    .unwrap_or_else(|poisoned| poisoned.into_inner()),
603            )
604            .finish()
605    }
606}
607
608// Global interner instances for common string types
609/// Global interner for IRI strings
610pub static IRI_INTERNER: once_cell::sync::Lazy<StringInterner> =
611    once_cell::sync::Lazy::new(StringInterner::new);
612
613/// Global interner for datatype IRIs
614pub static DATATYPE_INTERNER: once_cell::sync::Lazy<StringInterner> =
615    once_cell::sync::Lazy::new(StringInterner::new);
616
617/// Global interner for language tags
618pub static LANGUAGE_INTERNER: once_cell::sync::Lazy<StringInterner> =
619    once_cell::sync::Lazy::new(StringInterner::new);
620
621/// Global interner for general strings (JSON-LD processing)
622pub static STRING_INTERNER: once_cell::sync::Lazy<StringInterner> =
623    once_cell::sync::Lazy::new(StringInterner::new);
624
625/// An interned string that supports efficient comparison and hashing
626#[derive(Debug, Clone)]
627pub struct InternedString {
628    inner: Arc<str>,
629}
630
631impl InternedString {
632    /// Create a new interned string using the default IRI interner
633    pub fn new(s: &str) -> Self {
634        InternedString {
635            inner: IRI_INTERNER.intern(s),
636        }
637    }
638
639    /// Create a new interned string using a specific interner
640    pub fn new_with_interner(s: &str, interner: &StringInterner) -> Self {
641        InternedString {
642            inner: interner.intern(s),
643        }
644    }
645
646    /// Create an interned datatype string
647    pub fn new_datatype(s: &str) -> Self {
648        InternedString {
649            inner: DATATYPE_INTERNER.intern(s),
650        }
651    }
652
653    /// Create an interned language tag string
654    pub fn new_language(s: &str) -> Self {
655        InternedString {
656            inner: LANGUAGE_INTERNER.intern(s),
657        }
658    }
659
660    /// Get the string content
661    pub fn as_str(&self) -> &str {
662        &self.inner
663    }
664
665    /// Get the inner `Arc<str>` for zero-copy operations
666    pub fn as_arc_str(&self) -> &Arc<str> {
667        &self.inner
668    }
669
670    /// Convert into the inner `Arc<str>`
671    pub fn into_arc_str(self) -> Arc<str> {
672        self.inner
673    }
674}
675
676impl std::fmt::Display for InternedString {
677    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
678        write!(f, "{}", self.inner)
679    }
680}
681
682impl std::ops::Deref for InternedString {
683    type Target = str;
684
685    fn deref(&self) -> &Self::Target {
686        &self.inner
687    }
688}
689
690impl AsRef<str> for InternedString {
691    fn as_ref(&self) -> &str {
692        &self.inner
693    }
694}
695
696impl PartialEq for InternedString {
697    fn eq(&self, other: &Self) -> bool {
698        // Fast pointer comparison first
699        Arc::ptr_eq(&self.inner, &other.inner) || self.inner == other.inner
700    }
701}
702
703impl Eq for InternedString {}
704
705impl Hash for InternedString {
706    fn hash<H: Hasher>(&self, state: &mut H) {
707        // Hash the string content, not the pointer
708        self.inner.hash(state);
709    }
710}
711
712impl PartialOrd for InternedString {
713    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
714        Some(self.cmp(other))
715    }
716}
717
718impl Ord for InternedString {
719    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
720        self.inner.cmp(&other.inner)
721    }
722}
723
724impl From<&str> for InternedString {
725    fn from(s: &str) -> Self {
726        InternedString::new(s)
727    }
728}
729
730impl From<String> for InternedString {
731    fn from(s: String) -> Self {
732        InternedString::new(&s)
733    }
734}
735
736/// Extension trait for string interning common RDF vocabulary
737pub trait RdfVocabulary {
738    /// Common XSD namespace
739    const XSD_NS: &'static str = "http://www.w3.org/2001/XMLSchema#";
740    /// Common RDF namespace
741    const RDF_NS: &'static str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
742    /// Common RDFS namespace
743    const RDFS_NS: &'static str = "http://www.w3.org/2000/01/rdf-schema#";
744    /// Common OWL namespace  
745    const OWL_NS: &'static str = "http://www.w3.org/2002/07/owl#";
746
747    fn xsd_string() -> InternedString {
748        InternedString::new_datatype(&format!("{}string", Self::XSD_NS))
749    }
750
751    fn xsd_integer() -> InternedString {
752        InternedString::new_datatype(&format!("{}integer", Self::XSD_NS))
753    }
754
755    fn xsd_decimal() -> InternedString {
756        InternedString::new_datatype(&format!("{}decimal", Self::XSD_NS))
757    }
758
759    fn xsd_boolean() -> InternedString {
760        InternedString::new_datatype(&format!("{}boolean", Self::XSD_NS))
761    }
762
763    fn xsd_double() -> InternedString {
764        InternedString::new_datatype(&format!("{}double", Self::XSD_NS))
765    }
766
767    fn xsd_float() -> InternedString {
768        InternedString::new_datatype(&format!("{}float", Self::XSD_NS))
769    }
770
771    fn xsd_date_time() -> InternedString {
772        InternedString::new_datatype(&format!("{}dateTime", Self::XSD_NS))
773    }
774
775    fn rdf_type() -> InternedString {
776        InternedString::new(&format!("{}type", Self::RDF_NS))
777    }
778
779    fn rdfs_label() -> InternedString {
780        InternedString::new(&format!("{}label", Self::RDFS_NS))
781    }
782
783    fn rdfs_comment() -> InternedString {
784        InternedString::new(&format!("{}comment", Self::RDFS_NS))
785    }
786}
787
788/// Implement RdfVocabulary for InternedString to provide easy access to common terms
789impl RdfVocabulary for InternedString {}
790
791#[cfg(test)]
792mod tests {
793    use super::*;
794
795    #[test]
796    fn test_string_interner_survives_lock_poisoning() {
797        // Regression test: a panic while holding one of the interner's
798        // internal locks from another thread must not permanently disable
799        // it - intern/stats should recover via into_inner() rather than
800        // propagating the poison via `.expect()`.
801        let interner = Arc::new(StringInterner::new());
802
803        let poisoning_interner = interner.clone();
804        let handle = std::thread::spawn(move || {
805            let _guard = poisoning_interner.strings.write().unwrap();
806            panic!("intentionally poison the strings lock");
807        });
808        let _ = handle.join();
809
810        let s = interner.intern("http://example.org/after-poison");
811        assert_eq!(s.as_ref(), "http://example.org/after-poison");
812        let stats = interner.stats();
813        assert!(stats.total_requests >= 1);
814    }
815
816    #[test]
817    fn test_string_interner_basic() {
818        let interner = StringInterner::new();
819
820        let s1 = interner.intern("http://example.org/test");
821        let s2 = interner.intern("http://example.org/test");
822        let s3 = interner.intern("http://example.org/different");
823
824        // Same string should return same Arc (pointer equality)
825        assert!(Arc::ptr_eq(&s1, &s2));
826        assert!(!Arc::ptr_eq(&s1, &s3));
827
828        // Content should be equal
829        assert_eq!(s1.as_ref(), "http://example.org/test");
830        assert_eq!(s2.as_ref(), "http://example.org/test");
831        assert_eq!(s3.as_ref(), "http://example.org/different");
832    }
833
834    #[test]
835    fn test_string_interner_stats() {
836        let interner = StringInterner::new();
837
838        // First request - cache miss
839        let _s1 = interner.intern("test");
840        let stats = interner.stats();
841        assert_eq!(stats.total_requests, 1);
842        assert_eq!(stats.cache_misses, 1);
843        assert_eq!(stats.cache_hits, 0);
844
845        // Second request for same string - cache hit
846        let _s2 = interner.intern("test");
847        let stats = interner.stats();
848        assert_eq!(stats.total_requests, 2);
849        assert_eq!(stats.cache_misses, 1);
850        assert_eq!(stats.cache_hits, 1);
851        assert_eq!(stats.hit_ratio(), 0.5);
852    }
853
854    #[test]
855    fn test_string_interner_cleanup() {
856        let interner = StringInterner::new();
857
858        {
859            let _s1 = interner.intern("temporary");
860            assert_eq!(interner.len(), 1);
861        } // s1 goes out of scope
862
863        interner.cleanup();
864        assert_eq!(interner.len(), 0);
865    }
866
867    #[test]
868    fn test_interned_string_creation() {
869        let s1 = InternedString::new("http://example.org/test");
870        let s2 = InternedString::new("http://example.org/test");
871        let s3 = InternedString::new("http://example.org/different");
872
873        assert_eq!(s1, s2);
874        assert_ne!(s1, s3);
875        assert_eq!(s1.as_str(), "http://example.org/test");
876    }
877
878    #[test]
879    fn test_interned_string_ordering() {
880        let s1 = InternedString::new("apple");
881        let s2 = InternedString::new("banana");
882        let s3 = InternedString::new("apple");
883
884        assert!(s1 < s2);
885        assert!(s2 > s1);
886        assert_eq!(s1, s3);
887
888        // Test that ordering is consistent
889        let mut strings = vec![s2.clone(), s1.clone(), s3.clone()];
890        strings.sort();
891        assert_eq!(strings, vec![s1, s3, s2]);
892    }
893
894    #[test]
895    fn test_interned_string_hashing() {
896        use std::collections::HashMap;
897
898        let s1 = InternedString::new("test");
899        let s2 = InternedString::new("test");
900        let s3 = InternedString::new("different");
901
902        let mut map = HashMap::new();
903        map.insert(s1.clone(), "value1");
904        map.insert(s3.clone(), "value2");
905
906        // s2 should map to the same value as s1 since they're equal
907        assert_eq!(map.get(&s2), Some(&"value1"));
908        assert_eq!(map.get(&s3), Some(&"value2"));
909        assert_eq!(map.len(), 2);
910    }
911
912    #[test]
913    fn test_global_interners() {
914        let iri1 = InternedString::new("http://example.org/test");
915        let iri2 = InternedString::new("http://example.org/test");
916
917        let datatype1 = InternedString::new_datatype("http://www.w3.org/2001/XMLSchema#string");
918        let datatype2 = InternedString::new_datatype("http://www.w3.org/2001/XMLSchema#string");
919
920        let lang1 = InternedString::new_language("en");
921        let lang2 = InternedString::new_language("en");
922
923        // Verify that equal strings are interned
924        assert_eq!(iri1, iri2);
925        assert_eq!(datatype1, datatype2);
926        assert_eq!(lang1, lang2);
927    }
928
929    #[test]
930    fn test_rdf_vocabulary() {
931        let string_type = InternedString::xsd_string();
932        let integer_type = InternedString::xsd_integer();
933        let rdf_type = InternedString::rdf_type();
934
935        assert_eq!(
936            string_type.as_str(),
937            "http://www.w3.org/2001/XMLSchema#string"
938        );
939        assert_eq!(
940            integer_type.as_str(),
941            "http://www.w3.org/2001/XMLSchema#integer"
942        );
943        assert_eq!(
944            rdf_type.as_str(),
945            "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"
946        );
947
948        // Test that repeated calls return interned strings
949        let string_type2 = InternedString::xsd_string();
950        assert_eq!(string_type, string_type2);
951    }
952
953    #[test]
954    fn test_interned_string_display() {
955        let s = InternedString::new("http://example.org/test");
956        assert_eq!(format!("{s}"), "http://example.org/test");
957    }
958
959    #[test]
960    fn test_interned_string_deref() {
961        let s = InternedString::new("test");
962        assert_eq!(&*s, "test");
963        assert_eq!(s.len(), 4);
964        assert!(s.starts_with("te"));
965    }
966
967    #[test]
968    fn test_interned_string_conversions() {
969        let s1 = InternedString::from("test");
970        let s2 = InternedString::from("test".to_string());
971
972        assert_eq!(s1, s2);
973        assert_eq!(s1.as_str(), "test");
974    }
975
976    #[test]
977    fn test_concurrent_interning() {
978        use std::sync::Arc;
979        use std::thread;
980
981        let interner = Arc::new(StringInterner::new());
982        let handles: Vec<_> = (0..10)
983            .map(|i| {
984                let interner = Arc::clone(&interner);
985                thread::spawn(move || {
986                    let s = format!("http://example.org/test{}", i % 3);
987                    (0..100).map(|_| interner.intern(&s)).collect::<Vec<_>>()
988                })
989            })
990            .collect();
991
992        let results: Vec<Vec<Arc<str>>> = handles
993            .into_iter()
994            .map(|h| h.join().expect("thread should not panic"))
995            .collect();
996
997        // Verify that all equal strings are the same Arc
998        for result_set in &results {
999            for (i, s1) in result_set.iter().enumerate() {
1000                for s2 in &result_set[i + 1..] {
1001                    if s1.as_ref() == s2.as_ref() {
1002                        assert!(Arc::ptr_eq(s1, s2));
1003                    }
1004                }
1005            }
1006        }
1007
1008        // Should have at most 3 unique strings (test0, test1, test2)
1009        assert!(interner.len() <= 3);
1010    }
1011
1012    #[test]
1013    fn test_term_id_mapping() {
1014        let interner = StringInterner::new();
1015
1016        // Test interning with ID
1017        let (arc1, id1) = interner.intern_with_id("test_string");
1018        let (arc2, id2) = interner.intern_with_id("test_string");
1019
1020        // Same string should get same ID
1021        assert_eq!(id1, id2);
1022        assert!(Arc::ptr_eq(&arc1, &arc2));
1023
1024        // Different strings should get different IDs
1025        let (arc3, id3) = interner.intern_with_id("different_string");
1026        assert_ne!(id1, id3);
1027        assert!(!Arc::ptr_eq(&arc1, &arc3));
1028
1029        // Test ID lookup
1030        assert_eq!(interner.get_id("test_string"), Some(id1));
1031        assert_eq!(interner.get_id("different_string"), Some(id3));
1032        assert_eq!(interner.get_id("nonexistent"), None);
1033
1034        // Test string lookup
1035        assert_eq!(
1036            interner
1037                .get_string(id1)
1038                .expect("operation should succeed")
1039                .as_ref(),
1040            "test_string"
1041        );
1042        assert_eq!(
1043            interner
1044                .get_string(id3)
1045                .expect("operation should succeed")
1046                .as_ref(),
1047            "different_string"
1048        );
1049        assert_eq!(interner.get_string(999), None);
1050    }
1051
1052    #[test]
1053    fn test_id_mapping_stats() {
1054        let interner = StringInterner::new();
1055
1056        assert_eq!(interner.id_mapping_count(), 0);
1057
1058        interner.intern_with_id("string1");
1059        assert_eq!(interner.id_mapping_count(), 1);
1060
1061        interner.intern_with_id("string2");
1062        assert_eq!(interner.id_mapping_count(), 2);
1063
1064        // Interning same string again shouldn't increase count
1065        interner.intern_with_id("string1");
1066        assert_eq!(interner.id_mapping_count(), 2);
1067    }
1068
1069    #[test]
1070    fn test_get_all_mappings() {
1071        let interner = StringInterner::new();
1072
1073        let (_, id1) = interner.intern_with_id("first");
1074        let (_, id2) = interner.intern_with_id("second");
1075        let (_, id3) = interner.intern_with_id("third");
1076
1077        let mappings = interner.get_all_mappings();
1078        assert_eq!(mappings.len(), 3);
1079
1080        // Verify all mappings are present
1081        let mut found_ids = [false; 3];
1082        for (id, string) in mappings {
1083            match string.as_ref() {
1084                "first" => {
1085                    assert_eq!(id, id1);
1086                    found_ids[0] = true;
1087                }
1088                "second" => {
1089                    assert_eq!(id, id2);
1090                    found_ids[1] = true;
1091                }
1092                "third" => {
1093                    assert_eq!(id, id3);
1094                    found_ids[2] = true;
1095                }
1096                _ => panic!("Unexpected string in mappings"),
1097            }
1098        }
1099        assert!(found_ids.iter().all(|&found| found));
1100    }
1101
1102    #[test]
1103    fn test_mixed_interning_modes() {
1104        let interner = StringInterner::new();
1105
1106        // Mix regular interning and ID interning
1107        let arc1 = interner.intern("regular");
1108        let (_arc2, id2) = interner.intern_with_id("with_id");
1109        let arc3 = interner.intern("regular"); // Same as first
1110
1111        // Regular interning should still work
1112        assert!(Arc::ptr_eq(&arc1, &arc3));
1113
1114        // ID interning should work independently
1115        assert_eq!(
1116            interner
1117                .get_string(id2)
1118                .expect("operation should succeed")
1119                .as_ref(),
1120            "with_id"
1121        );
1122
1123        // Mixed mode length reporting should work
1124        assert!(interner.len() >= 2);
1125    }
1126}