1use arrow::array::{Array, DictionaryArray, StringArray, UInt32Array};
4use arrow::datatypes::UInt32Type;
5use std::collections::HashMap;
6use std::sync::{Arc, OnceLock, RwLock};
7
8static REGISTRY: OnceLock<RwLock<SymbolRegistry>> = OnceLock::new();
9
10struct SymbolRegistry {
11 to_id: HashMap<String, u32>,
12 to_string: Vec<String>,
13}
14
15impl SymbolRegistry {
16 fn new() -> Self {
17 Self {
18 to_id: HashMap::new(),
19 to_string: Vec::new(),
20 }
21 }
22}
23
24fn registry() -> &'static RwLock<SymbolRegistry> {
25 REGISTRY.get_or_init(|| RwLock::new(SymbolRegistry::new()))
26}
27
28pub fn intern(s: &str) -> u32 {
31 {
33 let reg = registry().read().unwrap();
34 if let Some(&id) = reg.to_id.get(s) {
35 return id;
36 }
37 }
38 let mut reg = registry().write().unwrap();
40 if let Some(&id) = reg.to_id.get(s) {
42 return id;
43 }
44 let id = reg.to_string.len() as u32;
45 let owned = s.to_string();
46 reg.to_id.insert(owned.clone(), id);
47 reg.to_string.push(owned);
48 id
49}
50
51pub fn resolve(id: u32) -> String {
53 resolve_checked(id).expect("invalid symbol ID: this is a bug")
54}
55
56pub fn resolve_checked(id: u32) -> Option<String> {
58 let reg = registry().read().unwrap();
59 reg.to_string.get(id as usize).cloned()
60}
61
62#[derive(Clone, Debug, Eq, PartialEq)]
68pub struct SymbolSnapshot {
69 entries: Box<[(u32, Box<str>)]>,
70}
71
72impl SymbolSnapshot {
73 pub fn entries(&self) -> &[(u32, Box<str>)] {
75 &self.entries
76 }
77}
78
79#[derive(Clone, Debug, Eq, PartialEq)]
81pub enum SymbolSnapshotError {
82 MissingSymbol {
84 id: u32,
86 },
87 EntryLimit {
89 requested: usize,
91 limit: usize,
93 },
94 ByteLimit {
96 limit: usize,
98 },
99 RegistryPoisoned,
101}
102
103impl std::fmt::Display for SymbolSnapshotError {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 match self {
106 Self::MissingSymbol { id } => write!(f, "symbol ID {id} is not registered"),
107 Self::EntryLimit { requested, limit } => {
108 write!(
109 f,
110 "symbol snapshot has {requested} entries, exceeding {limit}"
111 )
112 }
113 Self::ByteLimit { limit } => {
114 write!(f, "symbol snapshot UTF-8 bytes exceed {limit}")
115 }
116 Self::RegistryPoisoned => write!(f, "symbol registry lock is poisoned"),
117 }
118 }
119}
120
121impl std::error::Error for SymbolSnapshotError {}
122
123pub fn snapshot_checked(
128 ids: &[u32],
129 max_entries: usize,
130 max_utf8_bytes: usize,
131) -> Result<SymbolSnapshot, SymbolSnapshotError> {
132 if ids.len() > max_entries {
133 return Err(SymbolSnapshotError::EntryLimit {
134 requested: ids.len(),
135 limit: max_entries,
136 });
137 }
138 let reg = registry()
139 .read()
140 .map_err(|_| SymbolSnapshotError::RegistryPoisoned)?;
141 let mut remaining = max_utf8_bytes;
142 for &id in ids {
143 let text = reg
144 .to_string
145 .get(id as usize)
146 .ok_or(SymbolSnapshotError::MissingSymbol { id })?;
147 remaining = remaining
148 .checked_sub(text.len())
149 .ok_or(SymbolSnapshotError::ByteLimit {
150 limit: max_utf8_bytes,
151 })?;
152 }
153 let entries = ids
154 .iter()
155 .map(|&id| (id, reg.to_string[id as usize].clone().into_boxed_str()))
156 .collect();
157 Ok(SymbolSnapshot { entries })
158}
159
160pub fn clear() {
163 let mut reg = registry().write().unwrap();
164 reg.to_id.clear();
165 reg.to_string.clear();
166}
167
168pub fn count() -> usize {
170 registry().read().unwrap().to_string.len()
171}
172
173pub fn memory_usage() -> usize {
175 let reg = registry().read().unwrap();
176 let string_bytes: usize = reg.to_string.iter().map(|s| s.len()).sum();
177 let map_overhead =
178 reg.to_id.len() * (std::mem::size_of::<String>() + std::mem::size_of::<u32>());
179 string_bytes + map_overhead
180}
181
182pub fn to_arrow(ids: &[u32]) -> DictionaryArray<UInt32Type> {
184 use std::collections::HashSet;
185
186 let mut seen = HashSet::new();
188 let unique_ids: Vec<u32> = ids.iter().filter(|id| seen.insert(**id)).copied().collect();
189
190 let dict_strings: Vec<String> = unique_ids.iter().map(|&id| resolve(id)).collect();
192 let dictionary = StringArray::from(dict_strings);
193
194 let id_to_index: HashMap<u32, u32> = unique_ids
196 .iter()
197 .enumerate()
198 .map(|(i, &id)| (id, i as u32))
199 .collect();
200
201 let keys: Vec<u32> = ids.iter().map(|id| *id_to_index.get(id).unwrap()).collect();
202 let keys_array = UInt32Array::from(keys);
203
204 DictionaryArray::try_new(keys_array, Arc::new(dictionary)).unwrap()
205}
206
207pub fn from_arrow(arr: &DictionaryArray<UInt32Type>) -> Vec<u32> {
209 let dict = arr
210 .values()
211 .as_any()
212 .downcast_ref::<StringArray>()
213 .expect("dictionary values must be StringArray");
214
215 let dict_to_symbol: Vec<u32> = dict
217 .iter()
218 .map(|s| intern(s.expect("null not supported in symbols")))
219 .collect();
220
221 arr.keys()
223 .iter()
224 .map(|k| {
225 let idx = k.expect("null keys not supported") as usize;
226 dict_to_symbol[idx]
227 })
228 .collect()
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use serial_test::serial;
235
236 fn setup() {
238 clear();
239 }
240
241 #[test]
242 #[serial]
243 fn snapshot_preserves_order_duplicates_and_utf8_limits() {
244 setup();
245 let empty = intern("");
246 let unicode = intern("é🎉");
247 let ids = [unicode, empty, unicode];
248 let snapshot = snapshot_checked(&ids, 3, 12).unwrap();
249 let entries: Vec<_> = snapshot
250 .entries()
251 .iter()
252 .map(|(id, text)| (*id, text.as_ref()))
253 .collect();
254 assert_eq!(entries, [(unicode, "é🎉"), (empty, ""), (unicode, "é🎉")]);
255 assert_eq!(
256 snapshot_checked(&ids, 2, 12),
257 Err(SymbolSnapshotError::EntryLimit {
258 requested: 3,
259 limit: 2
260 })
261 );
262 assert_eq!(
263 snapshot_checked(&ids, 3, 11),
264 Err(SymbolSnapshotError::ByteLimit { limit: 11 })
265 );
266 assert!(snapshot_checked(&[], 0, 0).unwrap().entries().is_empty());
267 }
268
269 #[test]
270 #[serial]
271 fn snapshot_missing_symbol_rejects_whole_request_without_mutation() {
272 setup();
273 let valid = intern("present");
274 assert_eq!(
275 snapshot_checked(&[valid, 9, valid], 3, 100),
276 Err(SymbolSnapshotError::MissingSymbol { id: 9 })
277 );
278 assert_eq!(count(), 1);
279 assert_eq!(resolve(valid), "present");
280 }
281
282 #[test]
283 #[serial]
284 fn snapshot_survives_clear_and_reintern_without_reinterpreting_ids() {
285 setup();
286 let id = intern("old");
287 let accepted = snapshot_checked(&[id], 1, 3).unwrap();
288 clear();
289 assert_eq!(intern("new"), id);
290 let current = snapshot_checked(&[id], 1, 3).unwrap();
291 assert_eq!(accepted.entries()[0].1.as_ref(), "old");
292 assert_eq!(current.entries()[0].1.as_ref(), "new");
293 assert_ne!(accepted, current);
294 }
295
296 #[test]
297 #[serial]
298 fn snapshot_never_tears_across_concurrent_registry_replacement() {
299 setup();
300 intern("old-left");
301 intern("old-right");
302 let start = std::sync::Barrier::new(2);
303 std::thread::scope(|scope| {
304 let writer = scope.spawn(|| {
305 start.wait();
306 for index in 0..1000 {
307 clear();
308 let (left, right) = if index % 2 == 0 {
309 ("new-left", "new-right")
310 } else {
311 ("old-left", "old-right")
312 };
313 intern(left);
314 intern(right);
315 }
316 });
317 start.wait();
318 for _ in 0..1000 {
319 match snapshot_checked(&[0, 1], 2, 18) {
320 Ok(snapshot) => {
321 let entries = snapshot.entries();
322 let pair = (entries[0].1.as_ref(), entries[1].1.as_ref());
323 assert!(
324 pair == ("old-left", "old-right") || pair == ("new-left", "new-right"),
325 "torn snapshot: {pair:?}"
326 );
327 }
328 Err(SymbolSnapshotError::MissingSymbol { .. }) => {}
329 other => panic!("unexpected snapshot result: {other:?}"),
330 }
331 }
332 writer.join().unwrap();
333 let final_snapshot = snapshot_checked(&[0, 1], 2, 18).unwrap();
334 assert_eq!(final_snapshot.entries()[0].1.as_ref(), "old-left");
335 assert_eq!(final_snapshot.entries()[1].1.as_ref(), "old-right");
336 });
337 }
338
339 #[test]
340 #[serial]
341 fn test_intern_sequential() {
342 setup();
343 assert_eq!(intern("foo"), 0);
344 assert_eq!(intern("bar"), 1);
345 assert_eq!(intern("baz"), 2);
346 }
347
348 #[test]
349 #[serial]
350 fn test_intern_idempotent() {
351 setup();
352 let id1 = intern("hello");
353 let id2 = intern("hello");
354 assert_eq!(id1, id2);
355 assert_eq!(count(), 1); }
357
358 #[test]
359 #[serial]
360 fn test_resolve_roundtrip() {
361 setup();
362 let id = intern("world");
363 assert_eq!(resolve(id), "world");
364 }
365
366 #[test]
367 #[serial]
368 #[should_panic(expected = "invalid symbol ID")]
369 fn test_resolve_invalid() {
370 setup();
371 resolve(9999); }
373
374 #[test]
375 #[serial]
376 fn test_clear() {
377 setup();
378 intern("a");
379 intern("b");
380 assert_eq!(count(), 2);
381 clear();
382 assert_eq!(count(), 0);
383 assert_eq!(intern("a"), 0); }
385
386 #[test]
387 #[serial]
388 fn test_empty_string() {
389 setup();
390 let id = intern("");
391 assert_eq!(resolve(id), "");
392 }
393
394 #[test]
395 #[serial]
396 fn test_unicode() {
397 setup();
398 let id = intern("日本語");
399 assert_eq!(resolve(id), "日本語");
400
401 let id2 = intern("émoji🎉");
402 assert_eq!(resolve(id2), "émoji🎉");
403 }
404
405 #[test]
406 #[serial]
407 fn test_concurrent_intern() {
408 setup();
409 use std::collections::HashSet;
410 use std::thread;
411
412 let handles: Vec<_> = (0..10)
413 .map(|i| {
414 thread::spawn(move || {
415 let mut ids = Vec::new();
416 for j in 0..100 {
417 let s = format!("thread{}_{}", i, j);
418 let id = intern(&s);
419 ids.push((s, id));
420 }
421 ids
422 })
423 })
424 .collect();
425
426 let mut all_results = Vec::new();
427 for h in handles {
428 all_results.extend(h.join().unwrap());
429 }
430
431 for (s, id) in &all_results {
433 assert_eq!(&resolve(*id), s);
434 }
435
436 assert_eq!(count(), 1000);
438
439 let unique_ids: HashSet<u32> = all_results.iter().map(|(_, id)| *id).collect();
441 assert_eq!(unique_ids.len(), 1000);
442 }
443
444 #[test]
445 #[serial]
446 fn test_large_scale() {
447 setup();
448 use std::time::Instant;
449
450 let start = Instant::now();
451
452 for i in 0..100_000 {
454 let s = format!("symbol_{:06}", i);
455 let id = intern(&s);
456 assert_eq!(id, i as u32);
457 }
458
459 let intern_time = start.elapsed();
460
461 let start = Instant::now();
463 for i in 0..100_000 {
464 let expected = format!("symbol_{:06}", i);
465 assert_eq!(resolve(i as u32), expected);
466 }
467 let resolve_time = start.elapsed();
468
469 assert_eq!(count(), 100_000);
471
472 println!(
474 "100K intern: {:?}, 100K resolve: {:?}",
475 intern_time, resolve_time
476 );
477
478 let mem = memory_usage();
480 assert!(mem < 10_000_000, "memory usage {} exceeds 10MB", mem);
481 }
482
483 #[test]
484 #[serial]
485 fn test_arrow_roundtrip() {
486 setup();
487 let ids = vec![
488 intern("apple"),
489 intern("banana"),
490 intern("apple"),
491 intern("cherry"),
492 intern("banana"),
493 ];
494
495 let arrow = to_arrow(&ids);
496 let back = from_arrow(&arrow);
497
498 assert_eq!(ids, back);
499 }
500}