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. Interning the string itself is
243        // already race-safe and dedups the `Arc<str>` allocation, so do that
244        // first; then take the `string_to_id` write lock and re-check under
245        // it before minting a new id -- mirroring `intern()`'s
246        // double-checked-locking pattern. Without this re-check, two
247        // concurrent callers could both miss the fast-path read-lock check
248        // above, each `fetch_add` a *distinct* id for the same string, and
249        // both insert into the maps (last writer wins), leaving
250        // `string_to_id` pointing at one id while `id_to_string` retains a
251        // stale duplicate entry for the other -- violating the interner's
252        // same-string-implies-same-id invariant.
253        let arc_str = self.intern(s); // This will handle the string interning
254
255        let mut string_to_id = self
256            .string_to_id
257            .write()
258            .unwrap_or_else(|poisoned| poisoned.into_inner());
259
260        // Double-check: another thread may have raced us and already
261        // assigned an id to this string while we were interning it / waiting
262        // for this lock.
263        if let Some(&existing_id) = string_to_id.get(s) {
264            return (arc_str, existing_id);
265        }
266
267        let id = self
268            .next_id
269            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
270        string_to_id.insert(s.to_string(), id);
271        drop(string_to_id);
272
273        {
274            let mut id_to_string = self
275                .id_to_string
276                .write()
277                .unwrap_or_else(|poisoned| poisoned.into_inner());
278            id_to_string.insert(id, arc_str.clone());
279        }
280
281        (arc_str, id)
282    }
283
284    /// Get the ID for a string if it's already interned
285    pub fn get_id(&self, s: &str) -> Option<u32> {
286        let string_to_id = self
287            .string_to_id
288            .read()
289            .unwrap_or_else(|poisoned| poisoned.into_inner());
290        string_to_id.get(s).copied()
291    }
292
293    /// Get the string for an ID if it exists
294    pub fn get_string(&self, id: u32) -> Option<Arc<str>> {
295        let id_to_string = self
296            .id_to_string
297            .read()
298            .unwrap_or_else(|poisoned| poisoned.into_inner());
299        id_to_string.get(&id).cloned()
300    }
301
302    /// Get all ID mappings (useful for serialization/debugging)
303    pub fn get_all_mappings(&self) -> Vec<(u32, Arc<str>)> {
304        let id_to_string = self
305            .id_to_string
306            .read()
307            .unwrap_or_else(|poisoned| poisoned.into_inner());
308        id_to_string
309            .iter()
310            .map(|(&id, s)| (id, s.clone()))
311            .collect()
312    }
313
314    /// Clean up expired weak references to save memory
315    ///
316    /// Returns the number of entries cleaned up
317    pub fn cleanup(&self) -> usize {
318        let mut strings = self
319            .strings
320            .write()
321            .unwrap_or_else(|poisoned| poisoned.into_inner());
322        let before = strings.len();
323        strings.retain(|_, weak_ref| weak_ref.strong_count() > 0);
324        let after = strings.len();
325        before - after
326    }
327
328    /// Get current statistics
329    pub fn stats(&self) -> InternerStats {
330        self.stats
331            .read()
332            .unwrap_or_else(|poisoned| poisoned.into_inner())
333            .clone()
334    }
335
336    /// Get the number of unique strings currently stored
337    pub fn len(&self) -> usize {
338        // Return the maximum of both counts to handle mixed usage
339        let id_count = self
340            .string_to_id
341            .read()
342            .unwrap_or_else(|poisoned| poisoned.into_inner())
343            .len();
344        let string_count = self
345            .strings
346            .read()
347            .unwrap_or_else(|poisoned| poisoned.into_inner())
348            .len();
349        std::cmp::max(id_count, string_count)
350    }
351
352    /// Get the number of strings with ID mappings
353    pub fn id_mapping_count(&self) -> usize {
354        self.string_to_id
355            .read()
356            .unwrap_or_else(|poisoned| poisoned.into_inner())
357            .len()
358    }
359
360    /// Check if the interner is empty
361    pub fn is_empty(&self) -> bool {
362        self.strings
363            .read()
364            .unwrap_or_else(|poisoned| poisoned.into_inner())
365            .is_empty()
366    }
367
368    /// Batch intern multiple strings for improved performance
369    /// Returns a Vec of `Arc<str>` in the same order as input
370    pub fn intern_batch(&self, strings: &[&str]) -> Vec<Arc<str>> {
371        let mut result = Vec::with_capacity(strings.len());
372        let mut to_create = Vec::new();
373
374        // First pass: collect existing strings with read lock
375        {
376            let string_map = self
377                .strings
378                .read()
379                .unwrap_or_else(|poisoned| poisoned.into_inner());
380            for &s in strings {
381                if let Some(weak_ref) = string_map.get(s) {
382                    if let Some(arc_str) = weak_ref.upgrade() {
383                        result.push(arc_str);
384                        continue;
385                    }
386                }
387                to_create.push((result.len(), s));
388                result.push(Arc::from("")); // Placeholder
389            }
390        }
391
392        // Second pass: create missing strings with write lock
393        if !to_create.is_empty() {
394            let mut string_map = self
395                .strings
396                .write()
397                .unwrap_or_else(|poisoned| poisoned.into_inner());
398            let mut stats = self
399                .stats
400                .write()
401                .unwrap_or_else(|poisoned| poisoned.into_inner());
402
403            for (index, s) in to_create {
404                // Double-check in case another thread added it
405                if let Some(weak_ref) = string_map.get(s) {
406                    if let Some(arc_str) = weak_ref.upgrade() {
407                        result[index] = arc_str;
408                        stats.cache_hits += 1;
409                        continue;
410                    }
411                }
412
413                // Create new interned string
414                let arc_str: Arc<str> = Arc::from(s);
415                let weak_ref = Arc::downgrade(&arc_str);
416                string_map.insert(s.to_string(), weak_ref);
417                result[index] = arc_str;
418
419                stats.cache_misses += 1;
420                stats.total_strings_stored += 1;
421                stats.memory_saved_bytes += s.len();
422            }
423
424            stats.total_requests += strings.len();
425        }
426
427        result
428    }
429
430    /// Prefetch strings into the interner cache for improved performance
431    /// This is useful when you know you'll need certain strings soon
432    pub fn prefetch(&self, strings: &[&str]) {
433        let _ = self.intern_batch(strings);
434    }
435
436    /// Get memory usage statistics for performance monitoring
437    pub fn memory_usage(&self) -> MemoryUsage {
438        let string_map_size = self
439            .strings
440            .read()
441            .unwrap_or_else(|poisoned| poisoned.into_inner())
442            .len();
443        let id_map_size = self
444            .string_to_id
445            .read()
446            .unwrap_or_else(|poisoned| poisoned.into_inner())
447            .len();
448        let stats = self
449            .stats
450            .read()
451            .unwrap_or_else(|poisoned| poisoned.into_inner());
452
453        MemoryUsage {
454            interned_strings: string_map_size,
455            id_mappings: id_map_size,
456            estimated_memory_bytes: string_map_size * 64 + id_map_size * 8, // Rough estimate
457            memory_saved_bytes: stats.memory_saved_bytes,
458            compression_ratio: if stats.memory_saved_bytes > 0 {
459                stats.memory_saved_bytes as f64
460                    / (stats.memory_saved_bytes + string_map_size * 32) as f64
461            } else {
462                0.0
463            },
464        }
465    }
466
467    /// Get comprehensive performance metrics from SciRS2
468    ///
469    /// Returns detailed statistics including:
470    /// - Cache hit/miss counts and ratios
471    /// - Operation timing statistics
472    /// - String length distribution
473    /// - Memory usage patterns
474    pub fn get_metrics(&self) -> InternerMetrics {
475        let cache_hits = self.cache_hit_counter.get();
476        let cache_misses = self.cache_miss_counter.get();
477        let total_requests = cache_hits + cache_misses;
478        let hit_ratio = if total_requests > 0 {
479            cache_hits as f64 / total_requests as f64
480        } else {
481            0.0
482        };
483
484        let timer_stats = self.intern_timer.get_stats();
485        let string_length_stats = self.string_length_histogram.get_stats();
486        let memory_stats = self.memory_usage_histogram.get_stats();
487
488        InternerMetrics {
489            cache_hits,
490            cache_misses,
491            total_requests,
492            hit_ratio,
493            avg_intern_time_secs: timer_stats.mean,
494            total_intern_observations: timer_stats.count,
495            avg_string_length: string_length_stats.mean,
496            total_memory_tracked_bytes: memory_stats.sum as u64,
497        }
498    }
499
500    /// Optimize the interner by cleaning up and compacting data structures
501    ///
502    /// Performs comprehensive optimization:
503    /// - Cleans up expired weak references
504    /// - Rehashes HashMaps with optimal capacity
505    /// - Updates memory usage statistics
506    /// - Tracks optimization impact with SciRS2 metrics
507    pub fn optimize(&self) {
508        let start = std::time::Instant::now();
509
510        // Clean up expired weak references
511        let cleaned_count = self.cleanup();
512
513        // Get current sizes
514        let current_size = {
515            let strings = self
516                .strings
517                .read()
518                .unwrap_or_else(|poisoned| poisoned.into_inner());
519            strings.len()
520        };
521
522        // Rehash with optimal capacity (1.3x current size to reduce future reallocations)
523        let optimal_capacity = ((current_size as f64 * 1.3) as usize).max(1024);
524
525        {
526            let mut strings = self
527                .strings
528                .write()
529                .unwrap_or_else(|poisoned| poisoned.into_inner());
530            let mut string_to_id = self
531                .string_to_id
532                .write()
533                .unwrap_or_else(|poisoned| poisoned.into_inner());
534            let mut id_to_string = self
535                .id_to_string
536                .write()
537                .unwrap_or_else(|poisoned| poisoned.into_inner());
538
539            // Create new maps with optimal capacity
540            let mut new_strings = HashMap::with_capacity(optimal_capacity);
541            let mut new_string_to_id = HashMap::with_capacity(optimal_capacity);
542            let mut new_id_to_string = HashMap::with_capacity(optimal_capacity);
543
544            // Move data to new maps (rehashing in the process)
545            for (key, value) in strings.drain() {
546                new_strings.insert(key, value);
547            }
548            for (key, value) in string_to_id.drain() {
549                new_string_to_id.insert(key, value);
550            }
551            for (key, value) in id_to_string.drain() {
552                new_id_to_string.insert(key, value);
553            }
554
555            // Replace with optimized maps
556            *strings = new_strings;
557            *string_to_id = new_string_to_id;
558            *id_to_string = new_id_to_string;
559        }
560
561        // Calculate and track memory usage
562        let mem_usage = self.memory_usage();
563        self.memory_usage_histogram
564            .observe(mem_usage.estimated_memory_bytes as f64);
565
566        // Update stats
567        {
568            let mut stats = self
569                .stats
570                .write()
571                .unwrap_or_else(|poisoned| poisoned.into_inner());
572            stats.total_strings_stored = current_size;
573        }
574
575        let duration = start.elapsed();
576        tracing::debug!(
577            "Interner optimized: cleaned {} entries, rehashed to capacity {}, took {:?}",
578            cleaned_count,
579            optimal_capacity,
580            duration
581        );
582    }
583}
584
585impl Default for StringInterner {
586    fn default() -> Self {
587        Self::new()
588    }
589}
590
591impl std::fmt::Debug for StringInterner {
592    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
593        f.debug_struct("StringInterner")
594            .field(
595                "strings_count",
596                &self
597                    .strings
598                    .read()
599                    .unwrap_or_else(|poisoned| poisoned.into_inner())
600                    .len(),
601            )
602            .field(
603                "id_mappings_count",
604                &self
605                    .string_to_id
606                    .read()
607                    .unwrap_or_else(|poisoned| poisoned.into_inner())
608                    .len(),
609            )
610            .field(
611                "next_id",
612                &self.next_id.load(std::sync::atomic::Ordering::Relaxed),
613            )
614            .field(
615                "stats",
616                &self
617                    .stats
618                    .read()
619                    .unwrap_or_else(|poisoned| poisoned.into_inner()),
620            )
621            .finish()
622    }
623}
624
625// Global interner instances for common string types
626/// Global interner for IRI strings
627pub static IRI_INTERNER: once_cell::sync::Lazy<StringInterner> =
628    once_cell::sync::Lazy::new(StringInterner::new);
629
630/// Global interner for datatype IRIs
631pub static DATATYPE_INTERNER: once_cell::sync::Lazy<StringInterner> =
632    once_cell::sync::Lazy::new(StringInterner::new);
633
634/// Global interner for language tags
635pub static LANGUAGE_INTERNER: once_cell::sync::Lazy<StringInterner> =
636    once_cell::sync::Lazy::new(StringInterner::new);
637
638/// Global interner for general strings (JSON-LD processing)
639pub static STRING_INTERNER: once_cell::sync::Lazy<StringInterner> =
640    once_cell::sync::Lazy::new(StringInterner::new);
641
642/// An interned string that supports efficient comparison and hashing
643#[derive(Debug, Clone)]
644pub struct InternedString {
645    inner: Arc<str>,
646}
647
648impl InternedString {
649    /// Create a new interned string using the default IRI interner
650    pub fn new(s: &str) -> Self {
651        InternedString {
652            inner: IRI_INTERNER.intern(s),
653        }
654    }
655
656    /// Create a new interned string using a specific interner
657    pub fn new_with_interner(s: &str, interner: &StringInterner) -> Self {
658        InternedString {
659            inner: interner.intern(s),
660        }
661    }
662
663    /// Create an interned datatype string
664    pub fn new_datatype(s: &str) -> Self {
665        InternedString {
666            inner: DATATYPE_INTERNER.intern(s),
667        }
668    }
669
670    /// Create an interned language tag string
671    pub fn new_language(s: &str) -> Self {
672        InternedString {
673            inner: LANGUAGE_INTERNER.intern(s),
674        }
675    }
676
677    /// Get the string content
678    pub fn as_str(&self) -> &str {
679        &self.inner
680    }
681
682    /// Get the inner `Arc<str>` for zero-copy operations
683    pub fn as_arc_str(&self) -> &Arc<str> {
684        &self.inner
685    }
686
687    /// Convert into the inner `Arc<str>`
688    pub fn into_arc_str(self) -> Arc<str> {
689        self.inner
690    }
691}
692
693impl std::fmt::Display for InternedString {
694    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
695        write!(f, "{}", self.inner)
696    }
697}
698
699impl std::ops::Deref for InternedString {
700    type Target = str;
701
702    fn deref(&self) -> &Self::Target {
703        &self.inner
704    }
705}
706
707impl AsRef<str> for InternedString {
708    fn as_ref(&self) -> &str {
709        &self.inner
710    }
711}
712
713impl PartialEq for InternedString {
714    fn eq(&self, other: &Self) -> bool {
715        // Fast pointer comparison first
716        Arc::ptr_eq(&self.inner, &other.inner) || self.inner == other.inner
717    }
718}
719
720impl Eq for InternedString {}
721
722impl Hash for InternedString {
723    fn hash<H: Hasher>(&self, state: &mut H) {
724        // Hash the string content, not the pointer
725        self.inner.hash(state);
726    }
727}
728
729impl PartialOrd for InternedString {
730    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
731        Some(self.cmp(other))
732    }
733}
734
735impl Ord for InternedString {
736    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
737        self.inner.cmp(&other.inner)
738    }
739}
740
741impl From<&str> for InternedString {
742    fn from(s: &str) -> Self {
743        InternedString::new(s)
744    }
745}
746
747impl From<String> for InternedString {
748    fn from(s: String) -> Self {
749        InternedString::new(&s)
750    }
751}
752
753/// Extension trait for string interning common RDF vocabulary
754pub trait RdfVocabulary {
755    /// Common XSD namespace
756    const XSD_NS: &'static str = "http://www.w3.org/2001/XMLSchema#";
757    /// Common RDF namespace
758    const RDF_NS: &'static str = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
759    /// Common RDFS namespace
760    const RDFS_NS: &'static str = "http://www.w3.org/2000/01/rdf-schema#";
761    /// Common OWL namespace  
762    const OWL_NS: &'static str = "http://www.w3.org/2002/07/owl#";
763
764    fn xsd_string() -> InternedString {
765        InternedString::new_datatype(&format!("{}string", Self::XSD_NS))
766    }
767
768    fn xsd_integer() -> InternedString {
769        InternedString::new_datatype(&format!("{}integer", Self::XSD_NS))
770    }
771
772    fn xsd_decimal() -> InternedString {
773        InternedString::new_datatype(&format!("{}decimal", Self::XSD_NS))
774    }
775
776    fn xsd_boolean() -> InternedString {
777        InternedString::new_datatype(&format!("{}boolean", Self::XSD_NS))
778    }
779
780    fn xsd_double() -> InternedString {
781        InternedString::new_datatype(&format!("{}double", Self::XSD_NS))
782    }
783
784    fn xsd_float() -> InternedString {
785        InternedString::new_datatype(&format!("{}float", Self::XSD_NS))
786    }
787
788    fn xsd_date_time() -> InternedString {
789        InternedString::new_datatype(&format!("{}dateTime", Self::XSD_NS))
790    }
791
792    fn rdf_type() -> InternedString {
793        InternedString::new(&format!("{}type", Self::RDF_NS))
794    }
795
796    fn rdfs_label() -> InternedString {
797        InternedString::new(&format!("{}label", Self::RDFS_NS))
798    }
799
800    fn rdfs_comment() -> InternedString {
801        InternedString::new(&format!("{}comment", Self::RDFS_NS))
802    }
803}
804
805/// Implement RdfVocabulary for InternedString to provide easy access to common terms
806impl RdfVocabulary for InternedString {}
807
808#[cfg(test)]
809mod tests {
810    use super::*;
811
812    #[test]
813    fn test_string_interner_survives_lock_poisoning() {
814        // Regression test: a panic while holding one of the interner's
815        // internal locks from another thread must not permanently disable
816        // it - intern/stats should recover via into_inner() rather than
817        // propagating the poison via `.expect()`.
818        let interner = Arc::new(StringInterner::new());
819
820        let poisoning_interner = interner.clone();
821        let handle = std::thread::spawn(move || {
822            let _guard = poisoning_interner.strings.write().unwrap();
823            panic!("intentionally poison the strings lock");
824        });
825        let _ = handle.join();
826
827        let s = interner.intern("http://example.org/after-poison");
828        assert_eq!(s.as_ref(), "http://example.org/after-poison");
829        let stats = interner.stats();
830        assert!(stats.total_requests >= 1);
831    }
832
833    #[test]
834    fn test_string_interner_basic() {
835        let interner = StringInterner::new();
836
837        let s1 = interner.intern("http://example.org/test");
838        let s2 = interner.intern("http://example.org/test");
839        let s3 = interner.intern("http://example.org/different");
840
841        // Same string should return same Arc (pointer equality)
842        assert!(Arc::ptr_eq(&s1, &s2));
843        assert!(!Arc::ptr_eq(&s1, &s3));
844
845        // Content should be equal
846        assert_eq!(s1.as_ref(), "http://example.org/test");
847        assert_eq!(s2.as_ref(), "http://example.org/test");
848        assert_eq!(s3.as_ref(), "http://example.org/different");
849    }
850
851    #[test]
852    fn test_string_interner_stats() {
853        let interner = StringInterner::new();
854
855        // First request - cache miss
856        let _s1 = interner.intern("test");
857        let stats = interner.stats();
858        assert_eq!(stats.total_requests, 1);
859        assert_eq!(stats.cache_misses, 1);
860        assert_eq!(stats.cache_hits, 0);
861
862        // Second request for same string - cache hit
863        let _s2 = interner.intern("test");
864        let stats = interner.stats();
865        assert_eq!(stats.total_requests, 2);
866        assert_eq!(stats.cache_misses, 1);
867        assert_eq!(stats.cache_hits, 1);
868        assert_eq!(stats.hit_ratio(), 0.5);
869    }
870
871    #[test]
872    fn test_string_interner_cleanup() {
873        let interner = StringInterner::new();
874
875        {
876            let _s1 = interner.intern("temporary");
877            assert_eq!(interner.len(), 1);
878        } // s1 goes out of scope
879
880        interner.cleanup();
881        assert_eq!(interner.len(), 0);
882    }
883
884    #[test]
885    fn test_interned_string_creation() {
886        let s1 = InternedString::new("http://example.org/test");
887        let s2 = InternedString::new("http://example.org/test");
888        let s3 = InternedString::new("http://example.org/different");
889
890        assert_eq!(s1, s2);
891        assert_ne!(s1, s3);
892        assert_eq!(s1.as_str(), "http://example.org/test");
893    }
894
895    #[test]
896    fn test_interned_string_ordering() {
897        let s1 = InternedString::new("apple");
898        let s2 = InternedString::new("banana");
899        let s3 = InternedString::new("apple");
900
901        assert!(s1 < s2);
902        assert!(s2 > s1);
903        assert_eq!(s1, s3);
904
905        // Test that ordering is consistent
906        let mut strings = vec![s2.clone(), s1.clone(), s3.clone()];
907        strings.sort();
908        assert_eq!(strings, vec![s1, s3, s2]);
909    }
910
911    #[test]
912    fn test_interned_string_hashing() {
913        use std::collections::HashMap;
914
915        let s1 = InternedString::new("test");
916        let s2 = InternedString::new("test");
917        let s3 = InternedString::new("different");
918
919        let mut map = HashMap::new();
920        map.insert(s1.clone(), "value1");
921        map.insert(s3.clone(), "value2");
922
923        // s2 should map to the same value as s1 since they're equal
924        assert_eq!(map.get(&s2), Some(&"value1"));
925        assert_eq!(map.get(&s3), Some(&"value2"));
926        assert_eq!(map.len(), 2);
927    }
928
929    #[test]
930    fn test_global_interners() {
931        let iri1 = InternedString::new("http://example.org/test");
932        let iri2 = InternedString::new("http://example.org/test");
933
934        let datatype1 = InternedString::new_datatype("http://www.w3.org/2001/XMLSchema#string");
935        let datatype2 = InternedString::new_datatype("http://www.w3.org/2001/XMLSchema#string");
936
937        let lang1 = InternedString::new_language("en");
938        let lang2 = InternedString::new_language("en");
939
940        // Verify that equal strings are interned
941        assert_eq!(iri1, iri2);
942        assert_eq!(datatype1, datatype2);
943        assert_eq!(lang1, lang2);
944    }
945
946    #[test]
947    fn test_rdf_vocabulary() {
948        let string_type = InternedString::xsd_string();
949        let integer_type = InternedString::xsd_integer();
950        let rdf_type = InternedString::rdf_type();
951
952        assert_eq!(
953            string_type.as_str(),
954            "http://www.w3.org/2001/XMLSchema#string"
955        );
956        assert_eq!(
957            integer_type.as_str(),
958            "http://www.w3.org/2001/XMLSchema#integer"
959        );
960        assert_eq!(
961            rdf_type.as_str(),
962            "http://www.w3.org/1999/02/22-rdf-syntax-ns#type"
963        );
964
965        // Test that repeated calls return interned strings
966        let string_type2 = InternedString::xsd_string();
967        assert_eq!(string_type, string_type2);
968    }
969
970    #[test]
971    fn test_interned_string_display() {
972        let s = InternedString::new("http://example.org/test");
973        assert_eq!(format!("{s}"), "http://example.org/test");
974    }
975
976    #[test]
977    fn test_interned_string_deref() {
978        let s = InternedString::new("test");
979        assert_eq!(&*s, "test");
980        assert_eq!(s.len(), 4);
981        assert!(s.starts_with("te"));
982    }
983
984    #[test]
985    fn test_interned_string_conversions() {
986        let s1 = InternedString::from("test");
987        let s2 = InternedString::from("test".to_string());
988
989        assert_eq!(s1, s2);
990        assert_eq!(s1.as_str(), "test");
991    }
992
993    #[test]
994    fn test_concurrent_interning() {
995        use std::sync::Arc;
996        use std::thread;
997
998        let interner = Arc::new(StringInterner::new());
999        let handles: Vec<_> = (0..10)
1000            .map(|i| {
1001                let interner = Arc::clone(&interner);
1002                thread::spawn(move || {
1003                    let s = format!("http://example.org/test{}", i % 3);
1004                    (0..100).map(|_| interner.intern(&s)).collect::<Vec<_>>()
1005                })
1006            })
1007            .collect();
1008
1009        let results: Vec<Vec<Arc<str>>> = handles
1010            .into_iter()
1011            .map(|h| h.join().expect("thread should not panic"))
1012            .collect();
1013
1014        // Verify that all equal strings are the same Arc
1015        for result_set in &results {
1016            for (i, s1) in result_set.iter().enumerate() {
1017                for s2 in &result_set[i + 1..] {
1018                    if s1.as_ref() == s2.as_ref() {
1019                        assert!(Arc::ptr_eq(s1, s2));
1020                    }
1021                }
1022            }
1023        }
1024
1025        // Should have at most 3 unique strings (test0, test1, test2)
1026        assert!(interner.len() <= 3);
1027    }
1028
1029    #[test]
1030    fn test_term_id_mapping() {
1031        let interner = StringInterner::new();
1032
1033        // Test interning with ID
1034        let (arc1, id1) = interner.intern_with_id("test_string");
1035        let (arc2, id2) = interner.intern_with_id("test_string");
1036
1037        // Same string should get same ID
1038        assert_eq!(id1, id2);
1039        assert!(Arc::ptr_eq(&arc1, &arc2));
1040
1041        // Different strings should get different IDs
1042        let (arc3, id3) = interner.intern_with_id("different_string");
1043        assert_ne!(id1, id3);
1044        assert!(!Arc::ptr_eq(&arc1, &arc3));
1045
1046        // Test ID lookup
1047        assert_eq!(interner.get_id("test_string"), Some(id1));
1048        assert_eq!(interner.get_id("different_string"), Some(id3));
1049        assert_eq!(interner.get_id("nonexistent"), None);
1050
1051        // Test string lookup
1052        assert_eq!(
1053            interner
1054                .get_string(id1)
1055                .expect("operation should succeed")
1056                .as_ref(),
1057            "test_string"
1058        );
1059        assert_eq!(
1060            interner
1061                .get_string(id3)
1062                .expect("operation should succeed")
1063                .as_ref(),
1064            "different_string"
1065        );
1066        assert_eq!(interner.get_string(999), None);
1067    }
1068
1069    #[test]
1070    fn test_id_mapping_stats() {
1071        let interner = StringInterner::new();
1072
1073        assert_eq!(interner.id_mapping_count(), 0);
1074
1075        interner.intern_with_id("string1");
1076        assert_eq!(interner.id_mapping_count(), 1);
1077
1078        interner.intern_with_id("string2");
1079        assert_eq!(interner.id_mapping_count(), 2);
1080
1081        // Interning same string again shouldn't increase count
1082        interner.intern_with_id("string1");
1083        assert_eq!(interner.id_mapping_count(), 2);
1084    }
1085
1086    #[test]
1087    fn test_get_all_mappings() {
1088        let interner = StringInterner::new();
1089
1090        let (_, id1) = interner.intern_with_id("first");
1091        let (_, id2) = interner.intern_with_id("second");
1092        let (_, id3) = interner.intern_with_id("third");
1093
1094        let mappings = interner.get_all_mappings();
1095        assert_eq!(mappings.len(), 3);
1096
1097        // Verify all mappings are present
1098        let mut found_ids = [false; 3];
1099        for (id, string) in mappings {
1100            match string.as_ref() {
1101                "first" => {
1102                    assert_eq!(id, id1);
1103                    found_ids[0] = true;
1104                }
1105                "second" => {
1106                    assert_eq!(id, id2);
1107                    found_ids[1] = true;
1108                }
1109                "third" => {
1110                    assert_eq!(id, id3);
1111                    found_ids[2] = true;
1112                }
1113                _ => panic!("Unexpected string in mappings"),
1114            }
1115        }
1116        assert!(found_ids.iter().all(|&found| found));
1117    }
1118
1119    #[test]
1120    fn test_mixed_interning_modes() {
1121        let interner = StringInterner::new();
1122
1123        // Mix regular interning and ID interning
1124        let arc1 = interner.intern("regular");
1125        let (_arc2, id2) = interner.intern_with_id("with_id");
1126        let arc3 = interner.intern("regular"); // Same as first
1127
1128        // Regular interning should still work
1129        assert!(Arc::ptr_eq(&arc1, &arc3));
1130
1131        // ID interning should work independently
1132        assert_eq!(
1133            interner
1134                .get_string(id2)
1135                .expect("operation should succeed")
1136                .as_ref(),
1137            "with_id"
1138        );
1139
1140        // Mixed mode length reporting should work
1141        assert!(interner.len() >= 2);
1142    }
1143
1144    /// Regression test: concurrent `intern_with_id` calls for the *same*
1145    /// string must never assign more than one id to it. Before the
1146    /// double-checked-locking fix, two threads that both missed the
1147    /// fast-path read-lock check could each `fetch_add` a distinct id and
1148    /// both write it into the maps, breaking the same-string-implies-
1149    /// same-id invariant.
1150    #[test]
1151    fn regression_intern_with_id_concurrent_same_string_single_id() {
1152        use std::collections::HashSet;
1153        use std::sync::Arc;
1154        use std::thread;
1155
1156        for _ in 0..20 {
1157            let interner = Arc::new(StringInterner::new());
1158            let num_threads = 16;
1159
1160            let handles: Vec<_> = (0..num_threads)
1161                .map(|_| {
1162                    let interner = Arc::clone(&interner);
1163                    thread::spawn(move || interner.intern_with_id("http://example.org/dup"))
1164                })
1165                .collect();
1166
1167            let results: Vec<(Arc<str>, u32)> = handles
1168                .into_iter()
1169                .map(|h| h.join().expect("thread should not panic"))
1170                .collect();
1171
1172            let unique_ids: HashSet<u32> = results.iter().map(|(_, id)| *id).collect();
1173            assert_eq!(
1174                unique_ids.len(),
1175                1,
1176                "all concurrent intern_with_id calls for the same string must return the same id"
1177            );
1178
1179            let id = *unique_ids.iter().next().expect("one id was collected");
1180            assert_eq!(interner.get_id("http://example.org/dup"), Some(id));
1181            assert_eq!(
1182                interner
1183                    .get_string(id)
1184                    .expect("id should resolve back to the string")
1185                    .as_ref(),
1186                "http://example.org/dup"
1187            );
1188        }
1189    }
1190}