velesdb_core/column_store/string_table.rs
1//! String interning table for fast string comparisons.
2//!
3//! # Safety (EPIC-032/US-010)
4//!
5//! `StringId` uses u32 internally, limiting the table to ~4 billion strings.
6//! The `intern()` method debug-asserts this limit (physically unreachable:
7//! 4 billion interned strings would require terabytes of RAM).
8
9use std::sync::Arc;
10
11use rustc_hash::FxHashMap;
12
13use super::types::StringId;
14
15/// String interning table for fast string comparisons.
16///
17/// Both maps share one `Arc<str>` per interned string — an interning table
18/// that stored every string twice (map key + vec entry) would defeat half
19/// its own point. `Arc<str>` rather than `Rc<str>` because `ColumnStore`
20/// must stay `Send`/`Sync`.
21#[derive(Debug, Default)]
22pub struct StringTable {
23 /// String to ID mapping
24 string_to_id: FxHashMap<Arc<str>, StringId>,
25 /// ID to string mapping (for retrieval)
26 id_to_string: Vec<Arc<str>>,
27}
28
29impl StringTable {
30 /// Creates a new empty string table.
31 #[must_use]
32 pub fn new() -> Self {
33 Self::default()
34 }
35
36 /// Interns a string, returning its ID.
37 ///
38 /// If the string already exists, returns the existing ID.
39 pub fn intern(&mut self, s: &str) -> StringId {
40 if let Some(&id) = self.string_to_id.get(s) {
41 return id;
42 }
43
44 // EPIC-032/US-010: physically unreachable (4B strings = terabytes of RAM)
45 let len = self.id_to_string.len();
46 debug_assert!(
47 len < u32::MAX as usize,
48 "StringTable overflow: cannot intern more than {} strings",
49 u32::MAX
50 );
51 #[allow(clippy::cast_possible_truncation)] // Bounds checked above
52 let id = StringId(len as u32);
53 let shared: Arc<str> = Arc::from(s);
54 self.id_to_string.push(Arc::clone(&shared));
55 self.string_to_id.insert(shared, id);
56 id
57 }
58
59 /// Gets the string for an ID.
60 #[must_use]
61 pub fn get(&self, id: StringId) -> Option<&str> {
62 self.id_to_string.get(id.0 as usize).map(AsRef::as_ref)
63 }
64
65 /// Gets the ID for a string without interning.
66 #[must_use]
67 pub fn get_id(&self, s: &str) -> Option<StringId> {
68 self.string_to_id.get(s).copied()
69 }
70
71 /// Returns the number of interned strings.
72 #[must_use]
73 pub fn len(&self) -> usize {
74 self.id_to_string.len()
75 }
76
77 /// Returns true if the table is empty.
78 #[must_use]
79 pub fn is_empty(&self) -> bool {
80 self.id_to_string.is_empty()
81 }
82}