1use std::collections::{HashMap, HashSet};
8use std::marker::PhantomData;
9
10use serde::Serialize;
11use serde::de::DeserializeOwned;
12
13use crate::config::EncodingConfig;
14use crate::types::symbol::ObjectId;
15
16#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
22pub enum MigrationMode {
23 TraditionalOnly,
25 #[default]
27 PreferTraditional,
28 Adaptive,
30 PreferSymbolNative,
32 SymbolNativeOnly,
34}
35
36impl MigrationMode {
37 #[must_use]
42 pub fn should_use_raptorq(&self, hint: Option<bool>, data_size: usize) -> bool {
43 match (self, hint) {
44 (_, Some(true)) | (Self::SymbolNativeOnly | Self::PreferSymbolNative, None) => true,
46 (_, Some(false)) | (Self::TraditionalOnly | Self::PreferTraditional, None) => false,
47 (Self::Adaptive, None) => data_size > 1024,
49 }
50 }
51}
52
53#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)]
59pub enum MigrationFeature {
60 JoinEncoding,
62 RaceEncoding,
64 DistributedRegions,
66 SymbolCancellation,
68 SymbolTracing,
70 EpochBarriers,
72}
73
74impl MigrationFeature {
75 pub fn all() -> impl Iterator<Item = Self> {
77 [
78 Self::JoinEncoding,
79 Self::RaceEncoding,
80 Self::DistributedRegions,
81 Self::SymbolCancellation,
82 Self::SymbolTracing,
83 Self::EpochBarriers,
84 ]
85 .into_iter()
86 }
87}
88
89#[derive(Debug, Clone, Default)]
95pub struct MigrationConfig {
96 features: HashSet<MigrationFeature>,
98 mode: MigrationMode,
100 overrides: HashMap<String, MigrationMode>,
102}
103
104impl MigrationConfig {
105 #[must_use]
107 pub fn is_enabled(&self, feature: MigrationFeature) -> bool {
108 self.features.contains(&feature)
109 }
110
111 #[must_use]
113 pub fn mode(&self) -> MigrationMode {
114 self.mode
115 }
116
117 #[must_use]
119 pub fn enabled_features(&self) -> &HashSet<MigrationFeature> {
120 &self.features
121 }
122
123 #[must_use]
125 pub fn mode_for(&self, operation: &str) -> MigrationMode {
126 self.overrides.get(operation).copied().unwrap_or(self.mode)
127 }
128}
129
130#[derive(Debug, Default)]
136pub struct MigrationBuilder {
137 features: HashSet<MigrationFeature>,
139 mode: MigrationMode,
141 overrides: HashMap<String, MigrationMode>,
143}
144
145impl MigrationBuilder {
146 #[must_use]
148 pub fn new() -> Self {
149 Self::default()
150 }
151
152 #[must_use]
154 pub fn enable(mut self, feature: MigrationFeature) -> Self {
155 self.features.insert(feature);
156 self
157 }
158
159 #[must_use]
161 pub fn disable(mut self, feature: MigrationFeature) -> Self {
162 self.features.remove(&feature);
163 self
164 }
165
166 #[must_use]
168 pub fn with_mode(mut self, mode: MigrationMode) -> Self {
169 self.mode = mode;
170 self
171 }
172
173 #[must_use]
175 pub fn override_operation(mut self, operation: impl Into<String>, mode: MigrationMode) -> Self {
176 self.overrides.insert(operation.into(), mode);
177 self
178 }
179
180 #[must_use]
182 pub fn full_raptorq(mut self) -> Self {
183 self.features = MigrationFeature::all().collect();
184 self.mode = MigrationMode::SymbolNativeOnly;
185 self
186 }
187
188 #[must_use]
190 pub fn build(self) -> MigrationConfig {
191 MigrationConfig {
192 features: self.features,
193 mode: self.mode,
194 overrides: self.overrides,
195 }
196 }
197}
198
199#[must_use]
201pub fn configure_migration() -> MigrationBuilder {
202 MigrationBuilder::new()
203}
204
205#[derive(Debug)]
211pub enum DualValueError {
212 SerializationFailed(String),
214 DeserializationFailed(String),
216}
217
218impl std::fmt::Display for DualValueError {
219 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220 match self {
221 Self::SerializationFailed(msg) => write!(f, "serialization failed: {msg}"),
222 Self::DeserializationFailed(msg) => write!(f, "deserialization failed: {msg}"),
223 }
224 }
225}
226
227impl std::error::Error for DualValueError {}
228
229pub enum DualValue<T> {
239 Traditional(T),
241 SymbolNative {
243 serialized: Vec<u8>,
245 object_id: ObjectId,
247 _phantom: PhantomData<T>,
249 },
250}
251
252impl<T> DualValue<T> {
253 #[must_use]
255 pub fn uses_raptorq(&self) -> bool {
256 matches!(self, Self::SymbolNative { .. })
257 }
258
259 #[must_use]
261 pub fn is_traditional(&self) -> bool {
262 matches!(self, Self::Traditional(_))
263 }
264}
265
266impl<T: Clone + Serialize + DeserializeOwned> DualValue<T> {
267 pub fn get(&self) -> Result<T, DualValueError> {
269 match self {
270 Self::Traditional(v) => Ok(v.clone()),
271 Self::SymbolNative { serialized, .. } => serde_json::from_slice(serialized)
272 .map_err(|e| DualValueError::DeserializationFailed(e.to_string())),
273 }
274 }
275
276 pub fn ensure_symbols(&mut self, _config: &EncodingConfig) {
281 if let Self::Traditional(v) = self {
282 let serialized =
283 serde_json::to_vec(v).expect("serialization of existing value should succeed");
284 let object_id = ObjectId::new_for_test(0);
285 *self = Self::SymbolNative {
286 serialized,
287 object_id,
288 _phantom: PhantomData,
289 };
290 }
291 }
292}
293
294impl<T: std::fmt::Debug> std::fmt::Debug for DualValue<T> {
295 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
296 match self {
297 Self::Traditional(v) => f.debug_tuple("Traditional").field(v).finish(),
298 Self::SymbolNative {
299 serialized,
300 object_id,
301 ..
302 } => f
303 .debug_struct("SymbolNative")
304 .field("bytes", &serialized.len())
305 .field("object_id", object_id)
306 .finish(),
307 }
308 }
309}
310
311#[cfg(test)]
316mod tests {
317 use super::*;
318
319 #[test]
320 fn test_dual_value_traditional() {
321 let value = DualValue::Traditional(42i32);
322 assert_eq!(value.get().unwrap(), 42);
323 assert!(!value.uses_raptorq());
324 }
325
326 #[test]
327 fn test_dual_value_conversion() {
328 let mut value = DualValue::Traditional("hello".to_string());
329 let config = EncodingConfig::default();
330
331 value.ensure_symbols(&config);
333 assert!(matches!(value, DualValue::SymbolNative { .. }));
334
335 assert_eq!(value.get().unwrap(), "hello".to_string());
337 }
338
339 #[test]
340 fn test_migration_mode_decisions() {
341 assert!(!MigrationMode::TraditionalOnly.should_use_raptorq(None, 10000));
343
344 assert!(MigrationMode::SymbolNativeOnly.should_use_raptorq(None, 10));
346
347 assert!(MigrationMode::TraditionalOnly.should_use_raptorq(Some(true), 10));
349 assert!(!MigrationMode::SymbolNativeOnly.should_use_raptorq(Some(false), 10));
350
351 assert!(!MigrationMode::Adaptive.should_use_raptorq(None, 100));
353 assert!(MigrationMode::Adaptive.should_use_raptorq(None, 10000));
354 }
355
356 #[test]
357 fn test_migration_builder() {
358 let config = configure_migration()
359 .enable(MigrationFeature::JoinEncoding)
360 .enable(MigrationFeature::RaceEncoding)
361 .build();
362
363 assert!(config.is_enabled(MigrationFeature::JoinEncoding));
364 assert!(config.is_enabled(MigrationFeature::RaceEncoding));
365 assert!(!config.is_enabled(MigrationFeature::DistributedRegions));
366 }
367
368 #[test]
369 fn test_full_raptorq_mode() {
370 let config = configure_migration().full_raptorq().build();
371
372 for feature in MigrationFeature::all() {
373 assert!(config.is_enabled(feature));
374 }
375 }
376
377 #[test]
378 fn test_migration_mode_default() {
379 let mode = MigrationMode::default();
380 assert_eq!(mode, MigrationMode::PreferTraditional);
381 }
382
383 #[test]
384 fn test_migration_builder_disable() {
385 let config = configure_migration()
386 .full_raptorq()
387 .disable(MigrationFeature::SymbolTracing)
388 .build();
389
390 assert!(config.is_enabled(MigrationFeature::JoinEncoding));
391 assert!(!config.is_enabled(MigrationFeature::SymbolTracing));
392 }
393
394 #[test]
395 fn test_per_operation_override() {
396 let config = configure_migration()
397 .with_mode(MigrationMode::PreferTraditional)
398 .override_operation("heavy_join", MigrationMode::PreferSymbolNative)
399 .build();
400
401 assert_eq!(config.mode(), MigrationMode::PreferTraditional);
402 assert_eq!(
403 config.mode_for("heavy_join"),
404 MigrationMode::PreferSymbolNative
405 );
406 assert_eq!(
407 config.mode_for("other_op"),
408 MigrationMode::PreferTraditional
409 );
410 }
411
412 #[test]
415 fn dual_value_error_display_serialization() {
416 let err = DualValueError::SerializationFailed("bad input".into());
417 assert_eq!(err.to_string(), "serialization failed: bad input");
418 }
419
420 #[test]
421 fn dual_value_error_display_deserialization() {
422 let err = DualValueError::DeserializationFailed("unexpected EOF".into());
423 assert_eq!(err.to_string(), "deserialization failed: unexpected EOF");
424 }
425
426 #[test]
427 fn dual_value_error_source_is_none() {
428 use std::error::Error;
429 let err = DualValueError::SerializationFailed("x".into());
430 assert!(err.source().is_none());
431 }
432
433 #[test]
436 fn dual_value_is_traditional() {
437 let val = DualValue::Traditional(100u32);
438 assert!(val.is_traditional());
439 assert!(!val.uses_raptorq());
440 }
441
442 #[test]
443 fn dual_value_uses_raptorq_after_ensure_symbols() {
444 let mut val = DualValue::Traditional(42u32);
445 let config = EncodingConfig::default();
446 val.ensure_symbols(&config);
447 assert!(val.uses_raptorq());
448 assert!(!val.is_traditional());
449 }
450
451 #[test]
452 fn dual_value_ensure_symbols_idempotent() {
453 let mut val = DualValue::Traditional(42u32);
454 let config = EncodingConfig::default();
455 val.ensure_symbols(&config);
456 assert!(val.uses_raptorq());
457 val.ensure_symbols(&config);
459 assert!(val.uses_raptorq());
460 assert_eq!(val.get().unwrap(), 42u32);
461 }
462
463 #[test]
464 fn dual_value_get_deserialization_failure() {
465 let bad = DualValue::<u32>::SymbolNative {
467 serialized: b"not valid json".to_vec(),
468 object_id: ObjectId::new_for_test(0),
469 _phantom: PhantomData,
470 };
471 let err = bad.get().unwrap_err();
472 assert!(matches!(err, DualValueError::DeserializationFailed(_)));
473 }
474
475 #[test]
476 fn dual_value_debug_traditional() {
477 let val = DualValue::Traditional(99i32);
478 let dbg = format!("{val:?}");
479 assert!(dbg.contains("Traditional"), "{dbg}");
480 assert!(dbg.contains("99"), "{dbg}");
481 }
482
483 #[test]
484 fn dual_value_debug_symbol_native() {
485 let mut val = DualValue::Traditional("hello".to_string());
486 let config = EncodingConfig::default();
487 val.ensure_symbols(&config);
488 let dbg = format!("{val:?}");
489 assert!(dbg.contains("SymbolNative"), "{dbg}");
490 assert!(dbg.contains("bytes"), "{dbg}");
491 }
492
493 #[test]
496 fn migration_config_enabled_features_returns_set() {
497 let config = configure_migration()
498 .enable(MigrationFeature::EpochBarriers)
499 .enable(MigrationFeature::SymbolCancellation)
500 .build();
501
502 let features = config.enabled_features();
503 assert_eq!(features.len(), 2);
504 assert!(features.contains(&MigrationFeature::EpochBarriers));
505 assert!(features.contains(&MigrationFeature::SymbolCancellation));
506 }
507
508 #[test]
509 fn migration_config_default_has_no_features() {
510 let config = MigrationConfig::default();
511 assert!(config.enabled_features().is_empty());
512 assert_eq!(config.mode(), MigrationMode::PreferTraditional);
513 }
514
515 #[test]
518 fn adaptive_mode_boundary_at_1024() {
519 assert!(!MigrationMode::Adaptive.should_use_raptorq(None, 1024));
521 assert!(MigrationMode::Adaptive.should_use_raptorq(None, 1025));
523 }
524
525 #[test]
526 fn prefer_symbol_native_without_hint() {
527 assert!(MigrationMode::PreferSymbolNative.should_use_raptorq(None, 0));
528 assert!(MigrationMode::PreferSymbolNative.should_use_raptorq(None, 9999));
529 }
530
531 #[test]
534 fn migration_feature_all_has_six_items() {
535 assert_eq!(MigrationFeature::all().count(), 6);
536 }
537
538 #[test]
539 fn migration_feature_all_roundtrip_via_full_raptorq() {
540 let config = configure_migration().full_raptorq().build();
541 assert_eq!(config.mode(), MigrationMode::SymbolNativeOnly);
542 for feature in MigrationFeature::all() {
543 assert!(
544 config.is_enabled(feature),
545 "full_raptorq should enable {feature:?}"
546 );
547 }
548 }
549
550 #[test]
553 fn migration_builder_with_mode() {
554 let config = MigrationBuilder::new()
555 .with_mode(MigrationMode::Adaptive)
556 .build();
557 assert_eq!(config.mode(), MigrationMode::Adaptive);
558 }
559
560 #[test]
561 fn migration_builder_multiple_overrides() {
562 let config = configure_migration()
563 .override_operation("op_a", MigrationMode::SymbolNativeOnly)
564 .override_operation("op_b", MigrationMode::TraditionalOnly)
565 .build();
566 assert_eq!(config.mode_for("op_a"), MigrationMode::SymbolNativeOnly);
567 assert_eq!(config.mode_for("op_b"), MigrationMode::TraditionalOnly);
568 assert_eq!(config.mode_for("op_c"), MigrationMode::PreferTraditional);
570 }
571
572 #[test]
573 fn migration_mode_debug_clone_copy_default_eq() {
574 let m = MigrationMode::Adaptive;
575 let dbg = format!("{m:?}");
576 assert!(dbg.contains("Adaptive"), "{dbg}");
577 let copied: MigrationMode = m;
578 let cloned = m;
579 assert_eq!(copied, cloned);
580 assert_eq!(MigrationMode::default(), MigrationMode::PreferTraditional);
581 assert_ne!(m, MigrationMode::TraditionalOnly);
582 }
583
584 #[test]
585 fn migration_feature_debug_clone_copy_hash_eq() {
586 use std::collections::HashSet;
587 let f = MigrationFeature::JoinEncoding;
588 let dbg = format!("{f:?}");
589 assert!(dbg.contains("JoinEncoding"), "{dbg}");
590 let copied: MigrationFeature = f;
591 let cloned = f;
592 assert_eq!(copied, cloned);
593
594 let mut set = HashSet::new();
595 set.insert(MigrationFeature::JoinEncoding);
596 set.insert(MigrationFeature::RaceEncoding);
597 set.insert(MigrationFeature::DistributedRegions);
598 assert_eq!(set.len(), 3);
599 }
600
601 #[test]
602 fn migration_config_debug_clone_default() {
603 let c = MigrationConfig::default();
604 let dbg = format!("{c:?}");
605 assert!(dbg.contains("MigrationConfig"), "{dbg}");
606 let cloned = c;
607 assert_eq!(format!("{cloned:?}"), dbg);
608 }
609}